关闭
Hit
enter
to search or
ESC
to close
May I Suggest ?
#leanote #leanote blog #code #hello world
Mutepig's Blog
Home
Archives
Tags
Search
About Me
RealWorldCTF vmware escape
无
307
0
0
mut3p1g
https://zhuanlan.zhihu.com/p/52140921 # 0x01 pre-know ## 1. backdoor机制 在`vmware`中存在`vmtools`这样的东西,它能在宿主机和虚拟机之间完成包括传递文件等一系列操作的通信和交互,该通信机制就叫做`backdoor`。 其基本命令格式如下。可以看到里面用到了`IN`指令,在正常情况下该指令应当是特权指令,然而这里是可以用用户态就可以使用的。 ![](https://leanote.com/api/file/getImage?fileId=5cf8bfadab644172990060c7) `backdoor`机制所有的命令和调用都类似上面的格式,先设置寄存器,然后调用`IN`或者`OUT`指令来完成操作。 ## 2. backdoor完成RPC ### 1. all 使用`backdoor`机制来完成`RPC`调用的流程如下: ``` +------------------+ | Open RPC channel | +---------+--------+ | +------------v-----------+ | Send RPC command length| +------------+-----------+ | +------------v-----------+ | Send RPC command data | +------------+-----------+ | +-------------v------------+ | Recieve RPC reply length | +-------------+------------+ | +------------v-----------+ | Receive RPC reply data | +------------+-----------+ | +--------------v-------------+ | Finish receiving RPC reply | +--------------+-------------+ | +---------v---------+ | Close RPC channel | +-------------------+ ``` ### 2. 各个操作 根据文档可以看到,`cx=1e`的时候就是对`rpc`进行操作了:https://sites.google.com/site/chitchatvmback/backdoor#cmd1eh,具体分类是通过`cx`的高位决定的,总体格式如下: ``` CALL EAX = 564D5868h - magic number EBX = subcommand specific parameter ECX(HI) = RPC subcommand ECX(LO) = 001Eh - command number EDX(HI) = only 0 dont care, others is channel number EDX(LO) = 5658h - port number ``` 对于`加强版RPC`来说,还多了`cookie`这个参数,这是一个8字节的随机字符串,低位记作`cookie1`,高位记作`cookie2`,对应到寄存器如下: ``` ESI = cookie#1 EDI = cookie#2 ``` 接着看一下各个操作的具体参数,不变的参数就不单独写出来了 * Open RPC channel 该操作可以打开一个`RPC`的`channel`,成功后会通过`EDX`返回一个`channel number`,之后通信都通过该`channel number`进行操作。需要注意的是,在单个虚拟机中最多只能打开8个`channel`(#0~#7);当尝试打开第9个`channel`的时候,会检查其他`channel`的打开时间,如果时间过了某一个值,会将超时的`channel`关闭,再把这个`channel`的编号返回;如果都没有超时则失败。 ``` CALL EAX = 564D5868h - magic number EBX = C9435052h - enhanced RPC open magic number ('RPCI' | 80000000h) ECX(HI) = 0000h - subcommand number ECX(LO) = 001Eh - command number EDX(HI) = dont care EDX(LO) = 5658h - port number ESI = dont care EDI = dont care EBP = dont care RETURN EAX = ? EBX = unchanged ECX = 00010000h: success / 00000000h: failure EDX(HI) = RPC channel number EDX(LO) = 0000h ESI = cookie#1 EDI = cookie#2 EBP = unchanged ``` * Send RPC command length 该功能用于发送`RPC command`的长度,需要注意的是,`channel`必须处于打开状态。 ``` CALL EAX = 564D5868h - magic number EBX = command length (not including the terminating NULL) ECX(HI) = 0001h - subcommand number ECX(LO) = 001Eh - command number EDX(HI) = channel number EDX(LO) = 5658h - port number ESI = cookie#1 EDI = cookie#2 EBP = dont care RETURN EAX = ? EBX = unchanged ECX = 00810000h: success / 00000000h: failure EDX = unchanged ESI = unchanged EDI = unchanged EBP = unchanged ``` * Send RPC command data 在发送数据的时候,每次最多只能发送4个字节的数据,例如若要发送machine.id.get,则必须得调用四次: ``` EBX set to 6863616Dh ("mach") EBX set to 2E656E69h ("ine.") EBX set to 672E6469h ("id.g") EBX set to 00007465h ("et\x00\x00") ``` ``` CALL EAX = 564D5868h - magic number EBX = 00010000h - data transfer magic number? ECX = command data length (must be the same as the length sent with subcommand 02h) EDX(HI) = channel number EDX(LO) = 5659h - port number (Not the usual backdoor port!!) ESI = pointer to the data buffer EDI = cookie#2 EBP = cookie#1 RETURN EAX = unchanged EBX = 00010000h: success / 00000000h: failure ECX = 0 EDX = unchanged ESI = points to the end of data buffer EDI = unchanged EBP = unchanged ``` * Recieve RPC reply length 返回数据的长度。需要注意的是,所有命令都至少会返回2个字节的数据。 ``` CALL EAX = 564D5868h - magic number EBX = dont care ECX(HI) = 0003h - subcommand number ECX(LO) = 001Eh - command number EDX(HI) = channel number EDX(LO) = 5658h - port number ESI = cookie#1 EDI = cookie#2 EBP = dont care RETURN EAX = ? EBX = reply length (not including the terminating NULL) ECX = 00830000h: success / 00000000h: failure EDX(HI) = reply id EDX(LO) = 0000h ESI = unchanged EDI = unchanged EBP = unchanged ``` * Receive RPC reply data 接受返回的数据。和发送数据一样,每次只能接受4个字节的数据。 ``` CALL EAX = 564D5868h - magic number EBX = 00010000h - data transfer magic number? ECX = reply data length (must be the same as the length received with the subcommand 03h) EDX(HI) = channel number EDX(LO) = 5659h - port number (Not the usual backdoor port!!) ESI = cookie#1 EDI = pointer to the data buffer EBP = cookie#2 RETURN EAX = unchanged EBX = 00010000h: success / 00000000h: failure ECX = 0 EDX = unchanged ESI = unchanged EDI = points to the end of data buffer EBP = unchanged ``` * Finish receiving RPC reply 完成通信。 ``` CALL EAX = 564D5868h - magic number EBX = reply id from subcommand 03h ECX(HI) = 0005h - subcommand number ECX(LO) = 001Eh - command number EDX(HI) = channel number EDX(LO) = 5658h - port number ESI = cookie#1 EDI = cookie#2 EBP = dont care RETURN EAX = ? EBX = unchanged ECX = 00010000h: success / 00000000h: failure EDX = unchanged ESI = unchanged EDI = unchanged EBP = unchanged ``` * Close RPC channel 关闭该`channel` ``` CALL EAX = 564D5868h - magic number EBX = dont care ECX(HI) = 0006h - subcommand number ECX(LO) = 001Eh - command number EDX(HI) = channel number EDX(LO) = 5658h - port number ESI = cookie#1 EDI = cookie#2 EBP = dont care RETURN EAX = ? EBX = unchanged ECX = 00010000h: success / 00000000h: failure EDX = unchanged ESI = unchanged EDI = unchanged EBP = unchanged ``` # 0x02 逆向分析 ## 1. 漏洞定位 首先使用010editor对两个二进制进行比较,原文件在`/usr/lib/vmware/bin`中。查看`Tools->Compare Files`: ![](https://leanote.com/api/file/getImage?fileId=5cf8d68eab64417299006583) 那么可以看到,问题主要出在这里: 原版: ![](https://leanote.com/api/file/getImage?fileId=5cf8d765ab644172990065a8) patch版: ![](https://leanote.com/api/file/getImage?fileId=5cf8d765ab644172990065a9) 主要修改的就是`v4+7=0`这一句,以及最后参数由1变成了0x21。 所以首先得逆向这个函数,可以先从`https://github.com/unamer/vmware_escape`下载一些头文件,并导入到IDA中。 由于这里正好有6个`case`,所以应该就是上面说到的`RPC`的操作函数,那么开头的这个函数应该就是获取寄存器的值: ![](https://leanote.com/api/file/getImage?fileId=5cfb1955ab644128b5004eb6) 根据后面的分析,可以知道`get_reg`数字与寄存器的对应关系: ``` 1: ecx 2: edx 3: ebx 6: esi 7: edi ``` 逆向完成之后可以知道,原本是把`chall->output_buf`给置0的,然而patch过后就把这句话给删掉了。 ## 2. 动态调试 我识别出来的结构体,由于在后面读代码时候可以经常看到它下标+96这样的操作,所以可以猜测结构体大小为96. ``` struct struct_channel { unsigned int state;//0 int some_idx; char flag1;//1 char field_9; char field_A; char field_B; char field_C; char field_D; char field_E; char field_F; __int64 VirtualTime;//2 int msg_size;//3 int input_left; unsigned __int8 *in_msg_buf;//4 int buf_size;//5 unsigned int output_size; unsigned int outputleft;//6 _BYTE gap34[1]; char field_35; unsigned __int8 *out_msg_buf;//7 __int64 finish_recv; // 8 __int64 somestruct; // 9 _BYTE inused_g; // 10 __attribute__((packed)) __attribute__((aligned(1))) _DWORD cookie1; __attribute__((packed)) __attribute__((aligned(1))) _DWORD cookie2; __attribute__((packed)) //11 __attribute__((aligned(1))) int field_59; __attribute__((packed)) __attribute__((aligned(1))) __int16 field_5D; char field_5F; }; ``` 可以使用`ART+Q`将全局`channel`设置为我们识别出来的结构体,这样就能将它声明为一个结构体数组了。 然后需要动态调试,看一下这个调用的函数是什么。在虚拟机1号装好gdb,然后运行起来虚拟机,接着attach上去: ``` sudo gdb attach `ps aux |grep vmware-vmx | awk '{print $2}' | head -n 1` ``` 接着ssh上去运行编译好的exp就可以了 ``` #!/bin/bash ip="192.168.146.130" cmd="/tmp/exp" gcc -O0 -static exp.c -o exp scp exp rwctf@$ip:/tmp/ ssh rwctf@$ip $cmd ``` 那么定位到该函数,地址为0x177700。之后会把`somestruct`给清0,如果是堆上的则直接释放掉。 ## 3. 功能分析 下面把分析的结果记录一下: ### 1) open 先获取传入的`magic number`,然后判断是否是合法的。 接着会遍历全局的`channel`数组,看有没有`state`为0的,若有则直接用这个`channel number`。若没有找到,则重新遍历一遍,判断打开的时间是否超时,若找不到条件返回失败。 接着会将该`channel`进行各种属性的清空和释放。 然后会进行初始化,如获取`cookie`,设置使用中的标志设置为真,当前`state`设置为0等。 最终将`cookie`和`channel number`返回。 ### 2) sendsize 先获取`channel`,然后判断`state`是否是0。然后通过ebx获取`size`,并对输入的`size`校验合法性,主要是不能超过0x10000。 接着判断输入的`size`是否超过`buf_size`,若是则重新给`input_buf`分配一块区域,分配成功则将`state`设置为2,返回成功。 ### 3) sendpayload 先获取`channel`,然后判断`state`是否是2。接着判断输入的`size`是否已经读完,若还有空余则判断是否超过4,若超过则读4个字节后直接返回成功;否则将剩下的读完最后一位置0,之后会通过函数指针调用`run_cmd`函数来运行该命令,并在这个时候将`output_buf`给`malloc`出来,并将结果存进去。运行完后设置`state`为1返回成功,否则返回失败。 ### 4) recvsize 先获取`channel`,然后判断`state`是否是1。设置`state`为`4-chan->outputleft >= 1`,即如果`outputleft`为0则设置为4,否则设置为3。之后返回输出的`size`以及成功。 ### 5) recvpayload 先获取`channel`,然后判断`state`是否是3。之后与`sendpayload`类似,最多返回4个字节的内容。最后设置`state`为4。 ### 6) recvstatus 先获取`channel`,然后判断`state`是否是4。之后会判断`inished`表示是否为1,若是则将`state`设置为1,`finished`设置为0,之后返回成功。 否则判断`output_buf`是否有值,若没有则返回失败;若有值则从`rbx`获取`reply_id`,之后将`state`设置为1,正常应该要把`output_buf`置0但这里没有,同时调用函数`finish_recv`,正常参数是`reply&1`但这里的参数变为了`replay&0x21`。之后返回成功。 而`finish_recv`的第一个参数是一个结构体`str`,而`str+8`正好是`output_buf`,跟进去后会发现,如果`replyid&0x20`为真的话,便直接将它`free`掉返回,否则如果`replyid&0x10`为假的话,也会将它`free`掉返回,但同时也会将该结构体全部元素清0,也许这就是为什么把`&0x1`改为`&0x20`的原因吧。 ### 7) close 先获取`channel`,如果`state`大于0则清理`channel`,尤其是会释放`input_buf`,最后返回成功。 # 0x03 漏洞利用 现在我们拥有了一个`use after free`,而且对于每个打开的`channel`都可以随意进行上述的操作,那么后面就是如何利用这个漏洞了。 ## 1. 堆的操作 首先我们需要知道,如何申请一个指定大小的`chunk`,以及什么时候该chunk会被释放掉,并且这个`chunk`所在的位置是`output_buf`。 首先是堆的分配,由于我们需要控制的是`output_buf`,所以需要可以控制返回的内容的长度,这样就能控制返回的大小了。具体实现为利用对`guestinfo`的属性进行自定义设置,然后再将值取出来。操作如下: ``` rwctf@ubuntu:~$ vmtoolsd --cmd "info-set guestinfo.mutepig aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" rwctf@ubuntu:~$ vmtoolsd --cmd "info-get guestinfo.mutepig" aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ``` 我们先设置为0x100个'A',然后可以调试一下看看`output_buf`: ``` gdb-peda$ x /10xg 0x55e0ba43d6c0 0x55e0ba43d6c0: 0x0000000000000001 0x0000000000000000 0x55e0ba43d6d0: 0x000000028daeec0d 0x000000000000001a 0x55e0ba43d6e0: 0x00007f838408cd30 0x000001020000001b 0x55e0ba43d6f0: 0x0000000000000102 0x00007f83841bcbe0[output_buf] => gdb-peda$ x /10xg 0x00007f83841bcbe0-0x10 0x7f83841bcbd0: 0x0000000000000000 0x0000000000000115 0x7f83841bcbe0: 0x4141414141412031 0x4141414141414141 0x7f83841bcbf0: 0x4141414141414141 0x4141414141414141 0x7f83841bcc00: 0x4141414141414141 0x4141414141414141 ``` 可以看到,这里的大小应当是`0x100+2`,内容也是一致的。根据调试,在将`payload`发送完毕后,`output_buf`就被`malloc`出来了。 而释放的时机也如上面说的,在`finish`的时候会调用`finish_recv`函数,完成`output_buf`的释放。 最后获得的输出也是通过的`output_buf`中的内容获得的。 ## 2. leak 我们需要泄露的是程序地址,因为利用程序地址我们就足够做到很多事情了。 实现泄露的步骤如下: 1. 打开两个`channel`,分别记作A和B 2. 将A的`output_buf`申请为一个大小为size的块,之后释放掉 3. B和A做相同的操作,这样能使得B的`output_buf`和A是同一个块,但不获取其内容 4. 由于现在`A`的`outputleft`为0,所以再来一次`recvsize`会将`state`设置为4,那么再调用一次`finish`会将A再次释放 5. 用A进行操作,使得`vmware`会申请一个大小为size的块存储某数据结构,其中需要包含`vtable` 6. 获取B的`output_buf`的内容,得到`vtable`的地址,减去偏移即可得到程序的基址 那么这里问题的关键就在于运行什么命令能让`vmware`申请一个结构体,结论就是使用`vmx.capability.dnd_version`,下面我们来跟踪一下它申请结构体这个块的过程: 1. 在`send_data`结束后,调用`run_cmd`运行该命令 2. 在`run_cmd`中,调用命令对应的函数,这里是`get_dnd_version`: ``` 0x17800C: v27 = (*(v26 + 2))(*(v26 + 3), &v38, v22, v24, &n[4], n); ``` 3. 在`get_dnd_version`中,若`qword_FE5FE0[v1]`为0,则申请一个0x60的块放进去,之后会调用`init_object`,这里会`malloc(0x100)` 4. 之后在`0x198B30`处设置`vtable for'CopyPasteRpcV4'` 可以向上追踪,可以发现有一个命令和函数对应的关系,在函数0x1147F0中声明。 不过这里有个问题,就是只能用一次,因为一次之后就使得`qword_FE5FE0[v1]`有值了,就不会再去`malloc`了。 ## 3. getshell 先看一下`senddata`这里,当发送的size为0时会走到这里: ``` v19 = *(&unk_FE95C0 + v25); v26 = (*(&unk_FE95B0 + v25 + 8))(v19, ret_value, tmp_chan->in_msg_buf, tmp_chan->msg_size); ``` `v25`一般都为0,那么这里就相当于是调用了`*(0xFE95B8)(*(FE95C0))`,而这都是在`bss`段上,所以我们把前者写成`system`,后者写作`cmd`就可以了。 当然,用`close`中的这个也是可以的: ``` (*(&unk_FE95C0 + 8 * v12->some_idx + 1))( *(&unk_FE95D0 + 8 * v12->some_idx), (0xAAAB * ((v12 - global_channel) >> 5)), 0LL); ``` 由于系统的`libc`是2.27,所以直接改写`tcache`的`fd`就可以了。 实现命令执行的步骤如下: 1. 打开两个`channel`,分别记作A和B 2. 将A的`output_buf`申请为一个大小为size的块,之后释放掉 3. 将A的`output_buf`再次释放,这样就构成了`tcache->A's_output_buf->A's_output_buf` 4. 将B的`input_buf`申请出来,使得和A的`output_buf`是同一块地址,之后通过B修改其`fd`为0xFE95B8 5. 通过C将其重新申请出来,此时`tcache`就指向了0xFE95B8 6. 通过D将0xFE95B8申请出来,并修改其值为`system`的地址,后面设置为`FE95C0`,然后后面写入要执行的命令就可以了 # 注意 1. 比赛的时候虚拟机打开两份,一份用来调exp,一份用来最终完成exp后打 2. 获得虚拟机后,两个虚拟机分别装上ssh,外面的装好gdb和插件,里面的设置为桥接网卡,固定好ip。最后做个镜像 3. 查看堆的情况不能直接用getchar作断点,必须进到RPC函数时才是当时的堆现场。
觉得不错,点个赞?
提交评论
Sign in
to leave a comment.
No Leanote account ?
Sign up now
.
0
条评论
More...
文章目录
No Leanote account ? Sign up now.