main 函数反编译失败,原因是撞到了 __stack_chk_fail,而这个函数是 __noreturn 的
但是调试可以发现,__stack_chk_fail 的 got 项被劫持为 sub_403608,这个函数就是异或 0xdeadbeef

补丁打法:mov eax, xxx; xor eax, 0xdeadbeef,正好也是 8 字节
F5 可以看到平坦化,每次状态转换后都将状态变量异或 0xdeadbeef
除了平坦化之外这题还有一个 trick:从一个偏移表中动态加载函数指针,偏移是 0x13371337

可以观察到函数指针都是预先计算出来存储的,打补丁比较容易
唯一需要注意的是函数指针最终存储的地点,有时是寄存器,有时是栈上,这里将 add 全部换成 mov imm,忽略加载表项的 mov,存回栈上的 mov 予以保留
| |
选中需要打补丁的地方,运行上述脚本,现在 main 函数可以 F5 了

sub_403EF0 会对输入进行初步检查,输入的要求是 52 字节的大写 HEX
可以在字符串中找到迷宫,从左上角走到右下角的 * 处正好 26 步
...#....#.#.
.######...#.
..#..##.#.#.
#.##.##.#.#.
#....##.#...
...#.#..#.##
##.#.#.##.##
.#.#.#.##.##
.#.#.#.#####
.###...#...#
.....#.#.#.#
.#####...#.*
DDRDDRRRDDDDDRRDDRRUURRDDR
重复输入 26 个 41 可以得到 ‘game init’ 和 ‘start!’,而重复 26 个 61 可以得到 ‘game init’ 和 ‘invalid input’
这里有两个思路:第一是通过爆破确定合理的输入,第二则是找到 game 类的虚表,从而定位走迷宫的函数
爆破的结果是 0x00 ~ 0x1a, 0x40 ~ 0x5a, 0x80 ~ 0x9a, 0xc0 ~ 0xda,显然,输入字节的高 3 位代表上下左右四个方向,但是低 5 位依然不确定
边调试边观察输出可以推断出 game 类的构造函数位于 sub_405FC0,虚表位于 0x6E0738

虚表 func2 是游戏真正的入口,func3 检测是否通关并输出结果
这些成员函数中的指针加密使用了另一个偏移表 0x6e6e70
实际上,func3 的判断逻辑在 func5,除了要达到终点之外,还需要一个在 +26 的变量积攒到 26,这里也可以推断出 maze 的 vector 指针在 +32,行号在 +48,列号在 +52

再看 func2 是很明显的循环取命令的结构,其中 func10 的输入 v7 调试易得就是命令解码后的行列变化值,0x00 对应向下 (0, 1),0x40 对应向右 (1, 0),0x80 对应向上 (0, -1),0xc0 对应向左 (-1, 0)

为了得到低 5 位的具体内容,还需要分析 func10

可以看到除了走迷宫之外,func10 执行一些算法进行检测,检测通过之后就自增上面见过的关键变量
单步检测可以单步爆破,用 libdebug 在图中所示位置 (+6b60) 下断点,检测返回值是否是 1 即可,要注意确定了多少输入,就 cont 多少次
| |