PwnCollege - Program Security - Complex Corruption
Canary Conundrum (Easy)
1 | hacker@program-security~canary-conundrum-easy:~$ /challenge/canary-conundrum-easy |
Offsets: buf -> canary = 136, buf -> ret = 152
1 | hacker@program-security~canary-conundrum-easy:~$ r2 -A -q -c "pdf @ sym.challenge" /challenge/canary-conundrum-easy |
1 | #!/usr/bin/env python3 |
Canary Conundrum (Hard)
1 | hacker@program-security~canary-conundrum-hard:~$ /challenge/canary-conundrum-hard |
1 | # buf = rbp - 0x70 |
1 | #!/usr/bin/env python3 |
1 | hacker@program-security~canary-conundrum-hard:~$ python a.py |
A Crafty Clobber (Easy)
Write a full exploit involving injecting shellcode, and a method of tricking the challenge into executing your payload.
1 | hacker@program-security~a-crafty-clobber-easy:~$ /challenge/crafty-clobber-easy |
1 | hacker@program-security~a-crafty-clobber-easy:~$ r2 -A -q -c "pdf @ sym.challenge" /challenge/crafty-clobber-easy |
Payload 结构必须是这样:
[104 字节] 的填充数据(比如 b"A" * 104),这正好填满 buf 到 var_18h 之间的空隙。
[8 字节] value:p64(0x855cc253c479598b),覆盖 [var_18h],绕过 exit()。
[8 字节] 的填充数据(0x18 - 0x8 = 0x10,也就是 16 字节距离,减去刚刚写入的 8 字节魔法值,还需要 8 字节),用来走到 Canary 面前。
[8 字节] 的 Canary:按原值写回,防止触发 __stack_chk_fail。
[8 字节] 的 Saved RBP:可任意填入,或填入题干给出的 saved frame pointer。
[8 字节] 的 Saved RIP:填入题干给出的 input buffer starts at 的地址。
[N 字节] 的 Shellcode(既然题目说栈是可执行的,直接把 Shellcode 放在开头或者紧接着 RIP 后面跳转执行)。
1 | #!/usr/bin/env python3 |
pwn.college{*******************************************}
A Crafty Clobber (Hard)
1 | # buf -> rbp - 0x60 |
1 | #!/usr/bin/env python3 |
pwn.college{******************************************}
Can It Fizz?
Exploit the binary to obtain the flag!
1 | hacker@program-security~can-it-fizz:~$ checksec /challenge/can-it-fizz |
1 | 0x0000126c imul rax, rax, 0xffffffff88888889 |
% 15 == 0(0x88888889 乘法逆元) -> 对应FizzBuzz\n% 3 == 0(0x55555556 乘法逆元) -> 对应Fizz\n% 5 == 0(0x66666667 乘法逆元) -> 对应buf + 0x44(即事先写好的"Buzz")- 都不满足 -> 对应
\n
1 | # check buf? |
[buf]位于rbp - 0x60- 输入起点
buf + 0x14=rbp - 0x4c - 循环计数器
var_14h=rbp - 0x14 - 源字符串指针
src=rbp - 0x10 - 目标字符串指针
dest=rbp - 0x8 - Saved RBP =
rbp - Saved RIP (Return Address) =
rbp + 0x8
从输入起点 (rbp - 0x4c) 到循环计数器
(rbp - 0x14),距离正好是
0x4c - 0x14 = 0x38,56 bytes
1 | #!/usr/bin/env python3 |
1. 利用 Buzz 泄露栈地址
1 | # 推进到第 5 次循环 (var_14h == 5) |
程序内置了 FizzBuzz 逻辑。当 var_14h == 5 时,满足
% 5 == 0,触发 Buzz 分支。此时,二进制文件会将
src 指针指向 buf + 0x44(这里提前写入了
"Buzz")。
1 | payload_leak_stack = b"A" * 56 + b"\xff\xff\xff\xff" |
- 填充 56 字节的
A,恰好抵达var_14h。 - 用
\xff\xff\xff\xff(-1) 覆盖var_14h。 read()函数不会自动补\x00(Null byte),而printf("You entered: %s")遇到\x00才会停。- 由于
var_14h已被改为非零的-1,printf会继续向后读取,直接将紧邻其后的src指针(位于rbp - 0x10)打印出来。 - 同时,由于
-1 + 1 = 0,循环变量更新后变成了 0,满足0 < 16,程序继续下一次循环,不会崩溃。
2. 利用 FizzBuzz 泄露程序基址
1 | payload_leak_pie = b"A" * 56 + b"\xff\xff\xff\xff" |
因为上一步把计数器改成了 -1,这一轮循环加一后
var_14h 变成了 0。
0 % 15 == 0,所以这一轮触发的是 FizzBuzz
分支。 此时,程序会将 src
指向只读数据段(.rodata)中的 "FizzBuzz\n"
字符串(地址为 PIE 偏移 0x4018)。
再次用 56 字节 + -1 覆盖,把此时的 src
指针打印出来。减去偏移
0x4018,就拿到了程序的真实基址(pie_base)
3. Code Execution
至此,栈的绝对地址(用于放置 Shellcode)与 PIE 基址均已获得。
1 | dest_addr = rbp_addr - 0x200 |
既然可以直接覆盖 RIP,此处仍需构造如此复杂的 payload 并修复
src 和 dest: 回看反汇编,在
printf 之后,程序执行了一个操作:
call sym.imp.strcpy (把 src 复制到
dest)
如果只是用垃圾数据往后覆盖,把 src 和 dest
覆写成了无效地址(比如全 A),strcpy 就会触发
SIGSEGV (段错误)
56: p32(16): 把循环计数器改成 16。等这一轮结束16 + 1 = 17,不满足< 16的条件,程序会立刻跳出循环,执行leave; ret。60: p64(src_addr): 填入刚泄露的有效可读地址(只读段的"FizzBuzz")。68: p64(dest_addr): 填入栈上一个远离 shellcode 的空闲区(rbp_addr - 0x200)76: p64(rbp_addr): 还原 RBP。84: p64(shellcode_addr): 将 Saved RIP 劫持为栈上的 Shellcode 地址。92: asm(...): 注入 shellcode。
pwn.college{********************************************}
Does It Buzz?
1 | hacker@program-security~does-it-buzz:~$ r2 -A -q -c "pdf @ sym.challenge; pdf @ sym.win" /challenge/does-it-buzz |
查看汇编中的栈分配和 read() 调用:
0x000013d8 sub rsp, 0x90:开辟了 144 字节空间。canary位于rbp - 0x8。var_24h(循环计数器) 位于rbp - 0x24。src指针位于rbp - 0x20。dest指针位于rbp - 0x18。read()的输入起点在buf + 0x14,通过计算可以得出它位于rbp - 0x5c。
最关键的变化在 read() 的参数:
1 | 0x00001540 mov edx, 0x50 ; size_t nbyte = 80 |
现在的缓冲区溢出最多只能写 80 个字节。 buffer
address (rbp - 0x5c) 到 Canary (rbp - 0x8)
的距离是 0x5c - 0x8 = 0x54 (84 字节)。
最多只能写到 rbp - 0xc。
既然无法触及返回地址
(rbp + 0x8),劫持控制流的切入点在于被覆盖的局部变量:
距离起点 60 字节的地方是 src,68 字节的地方是
dest。这两个指针完全在 80 字节范围内。
1 | hacker@program-security~does-it-buzz:~$ checksec /challenge/does-it-buzz |
- SHSTK: Enabled (Hardware Shadow Stack / 硬件影子栈)
- IBT: Enabled (Indirect Branch Tracking / 间接分支追踪)
用 strcpy 将后门地址写入 saved_rip
后,函数执行到末尾的 ret 指令。 现代系统中,CPU
内部维护了一个仅硬件可访问的影子栈 (Shadow Stack)。执行
call 时,返回地址会同时压入普通栈和影子栈;执行
ret 时,CPU
会将普通栈上的地址与影子栈中的地址进行对比。对比失败时,CPU 硬件直接抛出
Control Protection Exception (#CP)
- RELRO: Partial RELRO
Partial RELRO 意味着全局偏移表 (GOT) 是可写的 看一下
strcpy 之后程序干了什么:
1 | 0x00001580 call sym.imp.strcpy ; 我们控制的任意写入 |
如果在执行 strcpy 时,将目标地址 (dest)
设置为 printf 的 GOT 表地址,将写入内容设为
sym.win,那么下一条指令 call printf
就会直接跳入 win 函数
1 | #!/usr/bin/env python3 |
1 | hacker@program-security~does-it-buzz:~$ python a.py |
Make It FizzBuzz
1 | hacker@program-security~make-it-fizzbuzz:~$ r2 -A -q -c "pdf @ sym.challenge" /challenge/make-it-fizbuzz |
1 | hacker@program-security~make-it-fizzbuzz:~$ checksec /challenge/make-it-fizbuzz |
- 泄露 Stack & PIE:利用 FizzBuzz 机制泄露栈指针和程序基址。
- 泄露 Libc :将
src指向printf的 GOT 表,将dest指向栈上的安全区(比如buf)。strcpy会把 libc 地址复制过来,紧接着的printf("Correct answer: %s\n", dest)就会把 libc 地址泄露给终端 - 劫持 GOT 表:有了 libc 基址,算出
execve的真实地址。再次利用strcpy,把execve的地址写进strcpy自己的 GOT 表里
汇编代码里调用 strcpy 的传参过程,rsi 和 rdx 都被赋予了 src 的值。
这意味着,如果将 dest 指向字符串 "/bin/sh",将 src 指向一个指针数组 ["/bin/sh", "-p", NULL]:
rdi (可执行文件路径) = "/bin/sh"
rsi (命令行参数 argv) = ["/bin/sh", "-p", NULL]
rdx (环境变量 envp) = ["/bin/sh", "-p", NULL]
最核心的优势:使用 execve 并通过 argv 传入 -p 参数,可以直接让 /bin/sh 保持 SUID 的 Root 权限
1 | #!/usr/bin/env python3 |
1 | hacker@program-security~make-it-fizzbuzz:~$ python a.py |