这道题使用了unicorn引擎来对输入的shellcode进行模拟,我们可以直接通过unicorn的文档来查看程序中涉及到的函数的作用和调用方式。
程序分析
首先来对程序的整体功能进行一个简要的分析:
其中一些函数的名字经过了修改,其中INIT函数作用是开沙箱
然后读入32位shellcode
接下来是一系列的unicorn引擎中的函数,我们根据unicorn的用户手册简要介绍一下:
uc_open:创建新的Unicorn实例,这里只需要知道是32位x86架构即可
uc_mem_map:从名字和参数也能看出来,开辟空间,当做正常map即可
uc_mem_write:将一段字节码写入指定内存地址
uc_mem_read:从内存中读取字节
uc_reg_read:读取寄存器的值,rsi为寄存器id,rdx为指向保存寄存器值的指针
uc_reg_write:将值写入寄存器,rsi为寄存器id,rdx指向寄存器将被修改成的值的指针
uc_hook_add:注册hook事件的回调,当hook事件被触发将会进行回调
现在再来理解这段代码,其实就是开个沙盒,映射两个内存空间,然后读入一段shellcode,注册一个hook函数,最后执行这段shellcode
我们来看看这段hook函数:
我们可以看到,这段hook函数实现了一个菜单,有四个函数,名字已经改好了。
当我们调用syscall指令的时候,会根据rax来执行不同的函数
然后我们重点关注一下open函数
uc_reg_read和uc_reg_write的参数识别不出来,可以直接观察汇编代码,这里我们其实可以选择不去过多关注它,其作用我们直接理解成是在识别shellcode并为寄存器赋值,然后关注类似uc_mem_read以及uc_mem_write这类函数的参数即可,这里我在注释中写了uc_mem_read的参数都是什么。
可以看出来,有两个reg_read,意味着open函数在执行的时候我们要设置rdi和rsi两个寄存器的值,通过观察汇编代码可以知道rdi是filename所在地址,rsi是size
我们进入mem_read后的那个函数中接着看
这个函数创建了一个大小为0x48的结构体,作为自定义的一个简单的FILE结构,其中包含什么东西呢:
这样的一个结构体创建出来之后,程序的功能就非常清晰了,open函数,其实就是指定一个name和一个size,然后创建一个结构体,将name用strcpy函数复制到结构体中的name字段,按顺序分配一个fd,对size以及三个自定义的函数指针赋值。
在创建之前还会比较,如果指定的name已经出现过则直接返回。默认已经有了三个文件,即stdin,stdout和stderr,这三个文件的函数指针是libc中的实际的read,write和close,而剩下的通过open函数创建的file,三个函数的函数指针指向的都是自定义的函数。
而除了open函数之外的三个函数,作用也很简单,就是通过这三个函数指针调用对应的函数,所以这里不展开分析。
这三个自定义的函数其功能也很简单,在实现的时候其实也都是依据真实的read,write来设计的,而close的参数也没变,仍是fd指针,只不过功能变成了释放fd对应file中的chunk。
所以最后我们可以发现,实际上就是给了我们执行shellcode的权限,但是开了沙箱,同时open函数不是真的open服务器上的文件,而是创建一个指定大小的chunk,否则直接orw就结束了hhhh
大致分析清楚了之后我们开始做这道题
准备工作
首先做一些准备工作,由于我们会大量的使用到那四个函数,并且需要写汇编代码,所以提前将他们封装起来以便后续使用
def sysopen(nameaddr, size):
return '''
mov rax, 2
mov rdi, {name}
mov rsi, {size}
syscall
'''.format(name=hex(nameaddr), size=hex(size))
def sysclose(fd):
return '''
mov rax, 3
mov rdi, {fd}
syscall
'''.format(fd=fd)
def sysread(fd, addr, size):
return '''
mov rax, 0
mov rdi, {fd}
mov rsi, {addr}
mov rdx, {size}
syscall
'''.format(fd=fd, addr=hex(addr), size=hex(size))
def syswrite(fd, addr, size):
return '''
mov rax, 1
mov rdi, {fd}
mov rsi, {addr}
mov rdx, {size}
syscall
'''.format(fd=fd, addr=hex(addr), size=hex(size))
漏洞分析
那么这道题的漏洞出在哪里呢,虽然我们可以执行四个syscall,但是由于open函数不真实,导致无法拿到flag,所以要寻找程序新的漏洞点。
关注一下这里,可以看到程序使用了strcpy将指定地址的name赋值到file结构体中,虽然说a1限定了最多24字节,但是由于strcpy在赋值的时候会自动在结尾补一个\x00,所以其实当name恰好等于24字节的时候,会造成一个off-by-null,我们来看看name下面存的是什么,如果造成了off-by-null会有什么后果:
可以看到是堆地址,这就意味着我们可以通过修改堆地址来进行后续一系列操作了,让我们大致的梳理一下思路,首先通过修改堆地址造成堆块重叠,然后修改fd为rwx段上的地址,申请过来,写入orw的shellcode,然后同样的手法将free_hook申请过来,写上shellcode的地址,就结束了。
这里有几个难点,一个是合适的堆块不是很好找,要自己慢慢调,第二个就是不能先打free_hook,因为修改完free_hook之后会立刻free,所以必须先布置好shellcode。
最后提一嘴,学长用的tcg打的,exp比我的简洁很多,属实牛逼。
附上exp:
from re import L
from pwn import *
from ctypes im