两天做两题,最菜的一集。希望明年能进线下赛。
expect_number
1 | expect_number (56 solves) [134pt] |
程序初始化时调用了 srand(1)
,所以运算符并不随机。
选项 3 要求 &unk_5400+12+n
的值等于 0xA2,和题意不符。如果相等则执行 cat gift
。(然而并没有什么用)
初步想法是直接暴力求解出符合要求的序列,但执行运算前有一步 movsx eax, al
的操作,当 al 最高位为 1 的时候,被视为负数,于是 eax 高位被填充 1,导致求解困难,同时 gift 不是 flag,pwn 也不是 misc,需要其他利用方法。
在程序开始处,菜单输入时通过 try-catch 隐藏了一个后门。
同时在动态调试时发现,&unk_5400+0x120
处存在一个指针,指向指向退出函数的指针,会在选项 4 退出时解引用后调用。可以发现,这个指针在可写范围之内,只要输入序列长度等于 0x114,并控制好最终运算结果就能部分覆写。
该指针指向一个函数表。
其中 sub_2984
存在栈溢出,恰好能够劫持返回地址,同时在 read
时若字节数大于 8 则抛出异常,可以跳转到之前发现的后门。于是,利用思路就很明显了。
- 计算出长度恰好为 0x114,运算结果为 0x60 的输入序列,覆写指针的最低位。
- 通过
show
函数获取 ELF 泄露。 - 输入选项 4 退出,调用
sub_2984
,覆写 rbp 为 ELF 上的有效地址,覆写返回地址为后门 try-catch 的地址(0x2516
)。
最后成功 get shell。
1 | from pwn import * |
参考输入序列如下。
1 | nums = [1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 2, 2, 2, 2, 0, 0, 2, 2, 2, 2, 2, 1, 1, 2] |
prpr
1 | prpr (8 solves) [371pt] |
通过 __printf_chk
函数实现的 VM。
本文将各个寄存器分别记为 %r0-%r6,每次函数调用时分配的空间记为 frame,当前指令的指针记为 ip。根据逆向结果,可知每次调用函数时 frame 大小固定为 0x100,函数返回地址保存在 frame+0x100
处。另外,每次运行代码时会检查栈和 frame 地址的合法性,但 ip 没有检查。
sub_1240
处定义了所有 opcode,其中部分需要留意。
%D
/%F
/%H
- 出栈一个元素,并将当前 frame 与该元素逐字节进行逻辑与/或/与非运算,直到遇到 \x00,终止条件显然有问题,可以篡改返回地址。%#V
- 出栈一个元素 N,再将frame+N
上的元素入栈。这里 N 没有检查,可以越界读。%#X
- 出栈一个元素 N,再出栈一个元素置于frame+N
。这里 N 没有检查,可以越界写。
从 ELF 从提取出主程序如下。
1 | 0x0 %x 0x0 ; exit |
主函数实现了一个菜单,可以调用 6 个函数。根据之前的分析,可以得到以下利用流程。
- 调用
func_6
,填满 frame_0 和 frame_1。 - 调用
func_2
,借助 0x3c 处的函数,在 frame_1 上填充利用代码。同时设置 %r4 为 63,函数结束时 %r5 应达到 64。 - 调用
func_3
,对 frame_0 进行异或操作,修改返回地址为 0x32,借此跳转到 0x3c,由于此时 %r5 为 64,可以任意写返回地址,计算偏移返回到第二步中填充的代码处。
此时可以执行任意 VM 代码。由于设置在堆上,我们首先通过 %#V
越界读,泄露出 ELF 的地址;由于寄存器基址也位于堆上,通过 %#X
越界写,可以将其进行修改为 ELF 的 GOT 表的地址,最后通过 output
将 puts
和 write
地址输出。(代码中的 misaka
用于方便 exp 定位;实际运行时有几率获取到错误基址,我也不知道为什么,但问题不大)
1 | ; leak libc |
实际情况下,由于远程与本地环境不同,需要找到新的偏移量,可以另写一段代码用于遍历输出内存。
获得 puts
和 write
地址后,通过 libc-database 搜索得到 libc 版本。然后通过 _environ
泄露栈地址,并将寄存器基址指向要插入 ROP 链的地址。由于程序正常退出时也会莫名其妙出现 Bad system call,需要在 __printf_chk
的某个内部函数的栈帧后插入 ROP 链。
1 | ; point stack |
然后持续读取 ROP 链插入。由于程序会在 ip 大于 0x250 时返回退出,这里在输入完成后跳转到 0x1000 处。
1 | ; rop chain |
最终 exp 如下。
1 | from pwn import * |
(代码转换留给读者自行完成。)