ZJCTF 2021 Triple Language Writeup
比赛时遇到这道题目,花了两个多小时才做出来。赛后又仔细看了看题,觉得比较有意思,因此来分享一下我的解题过程,方便大家复现赛题。
题目分为 check1 和 check2 两个部分,要求完成两个部分的解密即可得到 flag,其中两个部分内还分为几个小问题,我这里全部使用 Z3 来解决。
第一部分
约束一
这部分提供了一个约束条件,首先是要求第一部分输入的长度为 22,并且给出了以下约束
|
|
约束二
这部分内容就来自于 unicorn 的模拟执行
这部分内容的逻辑比较清楚,但是其中一些常数我们无法得知含义,而且此比赛要求离线,所以我考虑从我本地安装的 python 版 unicorn 库中找到这些常数。
我之前正好写过一题 unicorn 的题(感谢 TCTF),找到启动内容大致如下,猜测这个启动和 C 实现中的 uc_open 类似,所以常数应该也是对应的。
在代码中分别传入的常数是 3 和 4,我们在常数定义中分别找到这两个内容。
3 代表的就是 MIPS 架构
4 代表的是 32 位
所以我们知道接下来要模拟的代码就是 32 位的 MIPS 代码,这便于我们后续分析。
后续可以看到在 0x10000 处写入了 MIPS 汇编代码,根据写入长度,定义字节长度为 0x110,再用 IDA 的 Ctrl + E 将其提取
将提取得到的内容用 IDA32 再次打开,这里多次试验后,发现指令集应该是 MIPS 小端
IDA 打开后,可以根据原来代码中的调用信息来修复段信息。
uc_mem_map 在 0x10000 位置定义了一个长度为 0x200000 的可读可写可执行段,我们在 IDA 选择以下内容
进行之后在代码内容中按 Alt +S 修改段信息,将其按照代码中的,勾选右下角三个勾
再到代码中按 C 来转为 Code
结合之前的信息,我们很容易得知这三个位置的内容分别是
0x11000 对应一个常量,0x12000 对应从 Input[6] ~ Input[13],0x13000 对应从 Input[14] ~ Input[21],我们可以修改其命名来方便阅读,但是其中的汇编指令不太懂,并且离线无法查询,这时候可以打开 IDA 的 Auto comments
以上都修改后,代码内容为
做完以上事情,是时候去看看程序 Hook 了那些内容了。
结合 Hook 的位置来对应其代码内容不难看出,实际上在验证汇编执行 mul (乘法)之后的结果,而这几个乘法的结果,取决于 t1 – t6 这六个寄存器,而这六个寄存器在启动前被赋值,值来源于输入内容的前六字节
也就是我们得到了一个关于前六字节的约束,而且这个约束是有唯一确定的结果的。
约束三
续上文,我们可以看到程序在 unicorn 结束执行后还对几个寄存器进行了比对,如果比对一致则返回 1。
结合上面的代码和注释,不难看出,这几个寄存器的加载的内容就是 part1 和 part2 中对应内容相加的值。结合上面的约束一,已知 part1 + part2 和 part1 – part2,就不难得到 part1 和 part2 的值。
Check1 解题脚本
结合以上三个约束,我们可以编写如下脚本
|
|
这个问题解决后,就准备迎来更加复杂的第二部分
第二部分
前四字节
要求输入长度为 20,并且通过 part1 检测
不难看出,这部分内容实际上只检验了前四个字节,我这里给出两种做法,分别对应不同情况:
- 爆破解法:因为只有四个字节,所以在比赛过程中我考虑直接使用爆破的方法来求解,计算的时间虽然长一些,但是计算的同时我可以去看下面的代码内容。
- 如果字节数更多,那么爆破的方法就不再可行,所以赛后我又写了一份使用 Z3 来进行求解的脚本。
爆破解法
这个解法的好处在于,相比 Z3 的写法更快一些,但是要更加的注意类型约束,因为在 python 中默认是高精度的数字。
|
|
Z3 解法
这个解法速度快,效率高,但是编写过程中需要一些思考,对 Z3 能力的考察也更多一些。
|
|
这个解法速度快,效率高,但是编写过程中需要一些思考,对 Z3 能力的考察也更多一些。
其余字节
结合上面的分析方法,得知这个代码架构是 ARM
导出代码然后用 IDA 打开
打开后设置
根据代码中 uc_mem_map 的内容可以添加几个段,名称可以随意取
[1]0x10000 开始,长度为 0x1000
[2]0x20000 开始,长度为 0x10000
再分别给上面三个段开 RWX,便于 IDA 识别
这时候,在 ROM 段框选全部的代码内容,再按 P 可以生成函数,按 F5 也可以查看伪代码,但是由于没有给变量位置定义,IDA 也没能自动识别出,这时的伪代码是不易理解的。
我们知道 0x21024 指针指向的就是我们输入内容的其余字节,所以我们可以去这个地址来定义一个变量。
按 G 可以跳转到这个地址,再按 N 可以定义一个名字
按小键盘上的 * 可以设置数组长度,这里设置为 0x1D
按 Y 可以设置变量类型,设置为 unsigned char
这时候再重新定义(点击函数头部,先按 U 取消定义,再选择整个函数的代码内容按 P 定义函数)这个函数,效果就完全不同了
代码的含义已经是比较清晰了
接下来来看看 hook 函数的代码,它对于 R3(0x45 常量对应)寄存器的内容进行了修改,使得最终结果不同,反应到伪代码上的内容就是上图中的 v11 变量的内容。
所以我们可以考虑先把这些变化依次在汇编中注释,然后再还原。
Z3 解题代码
|
|
GetFlag
总结
我认为这题非常值得大家学习,我认为以后也会出现类似的赛题,如果提前熟悉 unicorn,那么做题速度一定会大大的增加,但是这道题目对于这个比赛实在是不太友好,而且在比赛进行到一半时间才把附件放出,导致大多数选手没能完成本题,且在比赛过程中不能使用外网,对于一些并没有接触过 unicorn 的同学来说,这题里的一些函数调用,包括一些常数的定义,以及 MIPS 汇编、ARM 汇编的含义也难以理解。