签名:水泊梁山.zZhouQing
日期:2024年8月30日
今天正式成年,开心。想起之前答应坛友 mb_mgodlfyn
说在赛期结束前会提供一份反混淆代码,其实我这俩天一直没关注这个问题,怕他被气到,于是有了本篇随笔,希望各位看的愉快。O(∩_∩)O
首先,这道题内容一般,所以我会结合《加密与解密(第四版)》的内容讲一些基础的东西。帮助没有基础的朋友过渡一下。幸运地是,我也正好忘记掉这道题是如何编写的了。(相信你会学明白的,因为我也是个小菜)
可以看到,程序是添加混淆了的,有着俩个区段,分别为 obf
和 obf
。(其中一个用于存储数据,一个用于执行代码)
可以看到,程序的混淆与栈相勾连。但是,我们的经验可以告诉我们,这个混淆似乎仅仅只是做了指令变形,而无代码乱序的功能。这对于反混淆来说,是一个非常重要的消息。
我们已经知道了混淆并无代码乱序的功能(代码乱序会使线性扫描算法出现错误),于是,我们对应的反汇编算法既可以是 线性扫描算法
,也可以是 递归行进算法
。出于方便,这里使用线性扫描算法。
这一技术,应该常见于早期的 花指令
对抗,混淆通过添加基于固定模式生成的花指令达到抵御静态分析的效果,而反混淆(机器码层的特征码匹配)则将混淆中的固定模式转化为字节形式的模板,通过特征匹配,将对应的代码删除,达到反混淆的目的。
在这里,由于技术过于古老,反混淆效果不太好,我仅作一个示例。发现程序具有如下特征,提取特征码,编写脚本进行反混淆。
由于使用的是 memset
命令来去除花指令,而该命令作用后,并不会将修改结果保存到 补丁
当中。所以,这里使用 scylla
(x64dbg 配套插件)来保存修改后的程序。
Dbg 将程序停在 EP
(EntryPoint)的位置,scylla 依次点击 Dump
-> PE Rebuild
。
可以看到,经过修改后的程序能够正常运行,同时程序的硬盘占用大大减小。
由于这里仅出于演示目的,我并未考虑将那样特征的汇编删去有何后果,很有可能是不会正常运行的。但,反混淆,就应该多尝试,甚至不需要程序能够正常运行,毕竟时间有限,能将关键代码显现出来即可。
这一技术,常见于早期 VM 对抗的 handle 匹配或是常见密码学的特征匹配。我看到坛友 tacesrever
就是通过这一方式来反混淆的,但是效果不佳。
这里举个例子,我们将这样的特征匹配为 VADD_Q。
当然,这一技术不仅仅是化简程序的流程,还可以用来还原本题的 CF
(控制流:control flow)。IDA 打开经过第一次修改的程序 kctf_crackme_sbls_dump.exe
。
可以看到,有个立即数 sub_A41F0D
,IDA 转过去发现,正是下一个基本块。而 dword_4AA610
与 0Ah
进行异或,值是 0042A9E0
,IDA 转过去发现,是 _alloca_probe
函数。
看样子我们可以知道了,混淆对 Call Imm
的指令做了变形,导致 IDA 的控制流分析失败了。利用 python
编写个简单的脚本还原即可。还记得上文提到的 线性扫描算法
吗?就是从一个 base
开始死循环反汇编,脚本自己会异常打印日志的。
由于生成的去混淆脚本内容较长,这里不贴出了。
这一个脚本还不足以还原完整的 CF
,因为 Call
指令分为四种类型。(这里并未使用 intel 指令语法)
提取三次汇编特征,编写三个脚本就能还原,有了第一次的经验,复制粘贴一下就行了。经过三次修复,IDA 已经能够正常显示 CFG 了。
本想写到这里便停下的,不过,看到 tacesrever
通过汇编匹配来还原原始的汇编指令,我也写个脚本来做个简单的化简好了。IDA 将程序 main
函数反编译后,可以发现这样形式的全局变量。这个全局变量有俩次读,一次写的操作。
而读操作却是这样的,这样的汇编指令可以直接化简为 push eax
。看样子。这样的全局变量可以直接删除了。
又浏览了几个全局变量,发现还有这样形式的,这样的全局变量似乎是参与运算了,保留即可。
编写脚本,做第六次反混淆操作,依旧是套用第二次的模板。(第一次为机器码匹配)
这次生成的反混淆脚本巨大,X64Dbg 的补丁窗口会卡死,所以通过 scylla
这一插件进行保存修改。
测试下修改结果,发现程序正常执行,继续玩下去。
IDAPython
提供了操作 AST
的接口,这里的 AST
对应结构为 ctree
,可以在 idapython_docs
当中查看相关信息。
ctree
中的节点称为 citem_t
,而 citem_t
这一抽象结构可以具体分化为 cinsn_t
与 cexpr_t
。(cinsn_t 的结构中包含 cexpr
结构)
同时,IDAPython 提供了 ctree_visitor_t
类,通过继承该类,重写特定函数(visit_insn
与 visit_expr
)达到对 ctree
的 curd
操作。
接下来做几个基本的演示。
通过观察反编译结果,发现如下特征代码。这些代码对于我们分析程序来说,是没有用处的,毕竟谁会去在意 eflags
呢?
我们使用 HRDevHelper
提取对应表达式的结构,这个结构用于判断是否是垃圾代码。
编写如下代码,即可去除 ctree
中特定类型的指令。
IDA 执行脚本后,发现如下报错,这是没有及时更新函数内容导致的。
解决方案:修改下函数名,让 IDA 更新函数内容。
可以看到,IDA 中的反汇编结果已经没有了那串垃圾代码。
在上图当中,我们发现,有太多全局变量的操作了,影响我们分析,干脆将 表达式中第一个操作数为全局变量
的指令全部删掉。它们的值无需关心,因为在这样情况下的静态分析,你已无法清晰地知道程序的动态结果。(若要用,跑个 Trace 补上就好)
在 isjunk
函数中添加如下逻辑即可:
运行脚本,并手动更新下函数。怎样,程序逻辑是不是一下子就明晰了?
我印象里我的程序里并未定义啥全局变量,所以没有判断全局变量的地址是否在混淆范围内。(应该是有定义的,我想说些闲话)
至此,我们可以开始愉快地做题啦,考虑到实际做题时间的问题,懒得继续优化下去了。(从反混淆的第一步到现在的第八步,不会花多长时间的,预计不超过三小时)
Dbg 创建程序运行起来,输入如下内容(不要着急按下回车开始验证身份)。
Dbg 转到内存窗口,给最后一个段下 内存执行
断点。
程序断在这个位置,IDA 转过去。
发现程序最后调用了 sub_A6322D
函数,这个函数内容有点大,如出现 function is too big
的问题,请查阅附录中的 IDA问题解决->function is too big
。
在我观察 main 函数的时候,发现程序调用了 std::cout
,扫下它的引用看看。
发下这段代码很可疑,X64Dbg 转过去看看,注意下 &unk_42C4DE
是什么。
哈,看样子我们很幸运,这下子我们知道程序是如何判断 flag 的了。
由于 IDA 并未将 *(_DWORD *)(a1 + 116)
与 *(_DWORD *)(*(_DWORD *)(a1 + 68) + 8)
识别为变量,故采取复制内容到 VsCode
进行分析的方案。Ctrl+F
搜索 *(_DWORD *)(a1 + 116)
发现其有俩个引用,好消息,这意味着 *(_DWORD *)(*(_DWORD *)(a1 + 68) + 8)
很有可能为 const_flag
。(经过搜索,发现其的确只引用了一次)
寻找将这条语句包裹起来的代码块,即寻找 if 语句。
途中发现这样的垃圾代码,编写脚本删掉这个 citem_t
即可。
发现其被包裹在这样的语句中,在这些代码中寻找我们的 特殊性
,这是我们思考问题的方法。这 PAIR64 函数未免太特殊了,得多注意。
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2024-9-8 15:41
被kanxue编辑
,原因: