TCTF 2021 0bf - 利用Unicorn模拟被混淆的代码
文章目录
Notice
本文首发于安全客,欢迎各位捧场:链接
简介
这次的TCTF 2021 Final有队友们的鼎力相助,拿了Rising Star的冠军,十分刺激。其中收获比较大的是0bf
这道题目,这是一个10轮的加密算法,代码有大量混淆,加密逻辑没法直观地看出来。赛后官方的writeup是去混淆再解,但在比赛时没有足够的时间来研究混淆的规律,只能硬着头皮分析。这道题目也是我第一次用Unicorn来模拟函数逻辑,收获还是很大。
程序逻辑
由于代码经过了混淆,函数严重膨胀,首先要修改IDA的配置文件,将函数大小的上限调高,IDA反编译插件的配置文件在IDA安装目录\cfg\hexrays.cfg
,打开找到MAX_FUNCSIZE
,改成一个大数,再重新分析程序的加密函数sub_11C9
,等上十几分钟,反编译完成后把代码复制到一个文件中,重新格式化成每个赋值语句一行的形式,方便查看。我上传了一份反编译后的代码,有需要可以下载。
这时从VSCode的代码缩略图中就可以看出,该加密函数有明显的重复部分,例如函数入口的两行:
|
|
和第二轮开始的部分:
|
|
以及第三轮开始的部分:
|
|
仔细寻找类似的代码,可以将整个加密函数分成10轮,每轮之间传递两个32位的值。第一轮的输入是a1
开始的8字节,第二轮输入是第一轮计算产生的v103
和v105
两个值,依次类推,第10轮完成后计算结果作为加密后的8字节。
第一轮代码大致结构如下:
|
|
从上面的代码可以将一轮的加密过程总结如下:
1、将本轮输入*a1
,a1[1]
经过短暂的处理,变为两个新的32位数v2 = s(*a1)
和v10 = t(a[1])
,再用这两个数计算第三个值v12 = r(v2, v10)
。
2、此后的计算仅依赖于第三个值v12
(注意计算出v12
后,下面的代码对v2
和v10
的使用形式与v12
完全相同,均为21 * (v10 & ~v2 & 0xEE1B0FD2) + ...
,实际上这个表达式还是v12
)。
3、将第二步的计算结果记为f(v12)
,则两个输出32位值v103
和v105
分别为g(v2, f(v12))
和h(v10, f(v12))
。
r
,s
,t
,f
,g
,h
的具体操作未知。
分析
在没法去除混淆的情况下,只能将各段代码分别看作不同的黑盒,通过改变黑盒的输入,观察黑盒的输出,来猜测黑盒中的逻辑。
IDA的反汇编代码中用到的_DWORD
,LOWORD
,__CFADD__
等宏定义可以从IDA安装目录\plugins\defs.h
中找到,以分析v2 = s(*a1)
中的s
函数为例,将IDA反汇编重新改为C代码:
|
|
输入0-255的所有值,再输入一些随机的值,可以观察出s
函数实际为s(x) = x + 0x6e62d8bf
,通过类似的方法还可以确定r(x, y) = g(x, y) = h(x, y) = x ^ y
,t(x) = (x*0x9d267e07-((x*0x9d267e07)>>32))&0xFFFFFFFF
。剩余f
过于复杂,没法直接观察出结果。单轮加密可以总结如下:
|
|
解法
不去混淆的情况下,程序中唯一无法求逆的是f
函数,但观察得知f
函数的可以通过异或来消去。给定加密结果x
,y
,若取v1'=x^y, v2'=0
,则v3'=x^y
,x'=v1'^f(v3')=x^y^f(x^y)
,y'=v2'^f(v3')=f(x^y)
,可以发现新的加密中的y'
即原先的加密时f
函数的输出,再将y'
分别与x
,y
异或,可以求出原先加密时的v1
和v2
。
再取a'=0, b'=1
,则v1'=C1, v2'=C2
,加上上一步解出的v1
和v2
,就可以推出最初的a
和b
。这样就可以在不对f
函数求逆的情况下进行解密。
利用Unicorn可以方便地对代码片段进行部分模拟。模拟执行的关键在于找到一个合适的起点和终点,将二者之间的代码视为一个黑盒,并且使该黑盒对外部信息的依赖尽可能地小,方便模拟。
加密函数sub_11C9
的开头部分如下:
|
|
可以将0x11F4
作为模拟的起点,此时r12
和r13
寄存器中分别存放了首轮的输入值。第一轮的结束位于0xF7AD
,此时加密结果也存放在r12
和r13
寄存器中。我们同时还需要中途控制v3
并检查v1
和v2
的值,因此还需要找到一个中间点,可以选在0x241A
,此时r12
和r13
寄存器中分就是v1
和v2
。代码如下:
|
|
其中do_emulation
是用Unicorn模拟执行代码片段,第一次我们从起点开始,a
和b
分别取1
和0
,运行到中间点,根据上面的分析,此时r12=C2, r13=C1
,将结果保存为s
和t
。第一次我们从中间点开始,v1
和v2
分别取x^y
和0
,运行到终点,根据上面的分析,此时r12=x^y^f(x^y), r13=f(x^y)
,将结果保存为z
和w
。再通过v1=w^x, v2=w^y
继续逆推出v1
和v2
。b=v2-C2, a=(v1*n)%0x100000001
,其中n
是C1
对0x100000001
的乘法逆元。
Unicorn的使用
接下来是do_emulation
的实现:
|
|
题目是PIE程序,为了省去重定位过程,直接将.text段映射到0x1000
地址处。再分配一个空的栈段,将rsp和rbp都指向栈段,r12和r13填入输入值,调用uc_emu_start
开始运行即可。
将Unicorn下载下来并编译,一般不建议将自己编译的东西用make install
安装到系统目录,避免和已有的东西冲突。网上很多教程编译OpenSSL都敢直接安装到/usr/lib,很多依赖OpenSSL的系统程序都会因此载入错误版本的libssl而无法运行。
采用静态链接+局部包含路径的方式可以很方便地使用自编译的第三方库,同时不污染环境,适合编译仅运行一次的程序,链接到Unicorn时可以使用如下指令:
|
|
-I选项指定头文件的搜索目录,同时把静态库libunicorn.a直接作为链接器输入,再链接到libm和libpthread即可编译出一个可独立运行的模拟程序。
完整代码如下:
|
|
flag: flag{0o0..MBA_0bF_1s_S0_1ntEresT1ng!!}
总结
这道题目因为不懂去混淆还是费了很大的力气,最后还是靠加密函数中的异或结构巧妙地重用了已有的二进制代码,还是要提高自己的知识水平,学习去混淆。
文章作者 Nagi
上次更新 2021-10-10
许可协议 本文已投稿安全客,转载请参考安全客相关规定