周末了,稍微有一点时间,尽量详细的写了一下自己解题的过程。看着长,但实际上并不复杂,希望大家不要被吓到。 当然,我猜其他人并不是用解混淆的方法做的,他们的方法可能更值得追求省心省力的各位学习 :) 为了照顾行文逻辑,下面贴出代码可能不完整,完整代码见附件。同时代码带有不少调试痕迹,比较乱,凑合看一下吧。
题目(又²)给出了一个 32 位控制台 Windows 应用程序 transformer.exe
,其有 824 KB。 运行一下:
看起来还是一个经典的输入序列号,输出对错的题目。注意到输入到输出之间有一个约1秒的停顿。
在 IDA Pro 中加载该可执行文件之后,到处乱点点,乱标标,很容易定位到 00401AF0 处为 main 函数:
其中调用的 CheckSerial 函数在 00401150,其 (表面上的) 逻辑大致为:
表面上看起来这是一个分两部分的问题,第一部分的入口在 00401C70 处,是一个约 400K 大的明显加了混淆的函数。用调试器大概观察一下其行为,可以发现其是将输入进行了变换,并且写了 mat0 的前 4 个值,输入了几组值“感受”了一下,大抵是个 block cipher 吧。第二部分是把第一部分的输入拆开之后填进四个 4x4 的矩阵的固定位置,再乘起来,然后验证结果。注意到里面还有一个疑似 typo (explode4[14]
用了两次,而 [15]
没用到),这就非常可疑,一开始觉得是题目没有出好,这个问题的规模又比较小,总之先瞎找一组解试试呗,将 mat0 中的两个由混淆过的函数填充的值也设为变量。掏出一个 SMT Solver 问问它怎么看(相关代码见附件):
结果它并不想理我,并丢出来一个说这问题没解的证明。仔细盯着自己的代码看了一会儿确认没写错。
………………………………………………
也就是说它真的没解,那这是怎么回事呢?看来这个程序中还隐藏着更多的秘密。仔细观察可以发现那个很不自然的 if (IsDebuggerPresent())
之前有两条长的很奇怪的废指令:
难道程序中有奇怪的地方带自修改?拿 x32dbg 简单看了一下(被混淆的函数中有读 PEB 的反调试,x32dbg 自带的隐藏调试器可过),这里似乎并没有神秘问题,可以走到。难道是有奇怪的地方用其他方式做了检测不到 调试器才会进行的自修改?程序结尾有一个 system("pause")
会停下,正好提供了一个附加上去的时机,于是在这个时候附加上去看一下:
果然变了,也就是说实际上第二部分执行的检查逻辑是 block[0] == 0x87654321 && block[1] == 0x12345678
的那个。
本节充分说明了我有多菜,这种简单的反调试 trick 想必各路高手们都是秒过吧。
第二部分的谜题看起来像是解开了(还不能石锤,因为不知道混淆过的函数里到底做了什么,也没有直接目击自修改现场,万一它执行过之后又把自己改回去了呢?),接下来处理第一部分被混淆的函数。我们先观察一下这个混淆是咋回事,大致翻一翻,可以发现其中有很多个这样的 pattern,有趣的地方直接原地用注释标出了:
有趣的是,正常代码执行完之后没有将其再反变换回去,也没有在任何地方记录变换是否发生过,也就是说这些代码只能从头到尾被执行一次。 也说明这段被混淆的代码里面很可能没有复杂的循环之类的控制流逻辑 :) 否则生成混淆的时候就要精确分析出后继是否已经被变换过了,而这至少是困难的。因此我们的解混淆代码不妨就先假设里面没什么 if for,胡乱写一写试试。
总结一下这里面的混淆方式:
首先明确目标:我们希望将这段代码反混淆到可以用 Hex-Rays 分析的程度。
其中 1、2 只要我们掏一个模拟器或者最好是带符号执行引擎的静态分析框架出来,对我们的反混淆就不会有影响。而 Hex-Rays 自带十分强大的常量折叠和死代码消除,因此对我们最终的分析也不会有影响。 3 可能就是拿来克我们的,但它(可能是故意)加的比较弱,所有的空循环形式都一样,结尾一定是一个连起来的 jb + call .+5
,中间无垃圾指令,因此 72??E800000000
就成为了一个很好的特征,直接查找替换成 9090E800000000
即可,存为 transformer-noloop.exe
。 而我们注意到,3、4、5 总是在一起出现,且乍一看总是出现在真正干活的代码之前,解密这段真正干活的代码。并且这一块总是以三个连续的 pop ecx; pop ebx; pop eax
结尾,中间没有垃圾指令。这成为了很好的一个特征。因此我们考虑监控程序的执行,做以下事情:
考虑到 1 中需要监控所有的内存写,最方便的 instrument 方法可能还是直接拿起一个模拟器来跑这个程序,于是选择用 Unicorn Engine 开干。(当然考虑到这里面的自修改的指令形式都比较单一,直接弄个 OllyScript 之类的东西应该也行。)
本来做好了要手调处理一下里面之前没有暴露出的问题(比如条件跳指令)之类的准备,但运行一下,出乎意料的是,看起来被混淆的函数中真的没有什么正常代码中的跳转指令,至少直到返回之前都没有。 在调试器里观察一下被解混淆的函数的输出,发现和解混淆之前是一样的,说明这一步做的OK。
遗憾的是,这里做完之后,我们发现代码还是难以直接在 Hex-Rays 中看懂,由于栈帧分析不正确,Hex-Rays 基本没有办法识别函数参数、局部变量和在此基础上进行合理的死代码消除。稍微观察之后发现这个函数是使用 ebp 作为 frame pointer 的,我们在 IDA Pro 里按 Alt+P 编辑函数,选上 BP based frame试试:
很遗憾,由于 Hex-Rays 依赖 IDA Pro 本体进行的栈帧分析,而 IDA Pro 本体并不是一个 decompiler,没有常量折叠等功能,形如如下的带 constant blinding 的访问函数参数的代码成功被识别错了:
看来我们必须手动解除这样的指令中的混淆。注意到这样的指令的形式都比较单一,既然我们已经模拟了这个程序,不妨糙一把,在执行到这样的指令的时候手动将发现是常数的寄存器的值换进去:(要是一开始用了个带符号执行的静态分析框架就好了,这里可以写的更鲁棒,可以直接看符号执行的结果判断ebp加上的值是不是常数)
除函数结尾处 0045BFC5 的一个手动 ret 没有被识别出来需要手动修复一下以外,这样得到的程序 IDA Pro 终于可以正确分析出栈帧了。Hex-Rays 也可以正常得出结果:
然而我们惊讶的发现,Hex-Rays 的优化器居然没有化简连续的 ror / rol 的功能,导致这个结果十分难看。解决这个问题的正常方法当然是利用 7.1 版本以上新加入的 microcode API 给 Hex-Rays 写一个新的优化 pass,化简掉这些东西。但人总是懒惰的,Hex-Rays 输出的代码又是几乎可编译的 C 代码,所以我们不妨将结果修改到 GCC 可编译,然后丢进 GCC 里试试,看看 O2 能给优化成什么样子,顺便可以把这里参数传进去的常数都写死。(morenicer.cpp 见附件)
使用 Hex-Rays 分析得到的 a.out,结果令人惊讶的好:
已经能看出明显的 Feistel cipher 的结构了。
Feistel cipher 是一种只要看出了结构,并不需要把每一个操作逆回去就可以解密的东西,将上面的代码再复制出来,稍作整理,然后念一念咒语:
即可解密。
仔细看看,可以发现该程序修改了初始化 stack cookie 有关的函数,在其中加入了对反调试函数 00490F70 的调用。这个函数里有一些花指令混淆,但全都是同一种固定 pattern,可直接替换掉。 其检测调试器的方法是调用 NtQueryInformationProcess(GetCurrentProcess(), ProcessDebugObjectHandle, ...)
。也就是说,其实那一条 LoadLibrary + GetProcAddress ntdll.dll 里的所有函数的长链大概是为了隐藏 NtQueryInformationProcess
这个字符串,使其不那么扎眼,从而让反调试代码不那么显眼吧……
Unicorn Engine 的文档基本等于没有,用起来真头疼,早知道换一个用了……
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2018-12-9 02:10
被Riatre编辑
,原因:
上传的附件: