yonkies_keygenme_4 代码混淆

这是一道代码混淆的题。Dennis Yurichev的开源大作《Reverse Engineering for Beginners》里介绍代码混淆技术的时候提到“made a little attempt to hack Tiny C compiler's codegenerator”。这道题就是这个魔改的 C 编译器,编译出来的。 读者可以在 https://yurichev.com/blog/58/ 这个地址找到介绍和示例。示例代码展示的代码混淆技术,主要是:

1常数替换成一系列的算术求值值令

2插入大量随机指令,将少量有效代码淹没在大量无效指令中 ==针对这种混淆技术,其实人工分析只需要抓住关键“字段/数据”(比如函数参数)在内存/寄存器之间流动的方向,进行追踪,就可以较快的识别出正确的代码逻辑。== 这题是一个比较好的学习理解代码混淆技术逆向工程的基础题。 当然就这个题目而言,在示例代码所用的混淆技术上,还有三种混淆技术(或者说技巧更确切一些),本质上可以归为控制流混淆:

3call指令等效替换

4函数 ret 指令的替换

5无效跳转指令(永恒跳转和永恒不跳转)

控制流混淆可以干扰IDA等静态分析工具对代码块的识别,不能自动生成函数。特别是无效跳转指令,跳转地址会破坏顺序分析的某个指令边界,给静态分析工具造成困扰。 随机干扰指令会静态分析工具对栈分配或寻址的分析,阻碍生成更高级别的伪代码。

一、验证逻辑分析

经过混淆的程序,要分析其逻辑,首先要去混淆。由于混淆存在多个模式,需要一一识别,一层一层去混淆。好比剥茧抽丝。下面来看看这个程序在IDA里长啥样。

functions_1.png
图片加载失败


IDA的自动分析只分析出很少的东西,没有分析出main函数。start函数是程序的入口点。Tiny C 编译器很简单,看看start函数:

func_start.png
图片加载失败


main函数有统一标准:argc、argv、env 三个参数。可以看到sub_416DA7正好三个参数,前面还有__getmainargs的调用。可以直接将此函数修改为main。IDA会自动更新参数。

func_main.png


这个main函数似乎有点不对,这么短,也没有ret。这就是控制流混淆后IDA自动分析出错。来看一下TEXT View模式下的情况:

func_main_textview.png


可以清晰看到IDA分析的mian endp后面还是有很长的代码的。

1、函数调用混淆

图中似乎是对call printf指令替换混淆技巧。

call $+5这个指令通常在恶意软件里面用来获取当前EIP的值。

其原理是$表示当前指令的地址,这里就是call指令的地址0x416DB8。执行call $+5指令时,会将其下一条指令pop eax的地址0x416DBD压栈(作为函数调用的返回地址),而call $+5指令的长度正好是5个字节,下一条指令的地址就等于$+5,这也是call指令跳转到的目标地址。通过这个操作当前EIP值就保存在了栈顶。

后面对eax的操作,相当于修改函数的返回地址:0x416DBD + 0x0A = 0x416DC7,然后跳转到printf

0x416DC7add esp, 4,根据C调用规范,就是printf调用结束后的堆栈清理。这里只有一个参数,所以+4。

这个从0x416DB80x416DC7之间的代码,实际上就是call printf指令的混淆模式。分析后续代码可以发现,程序里对函数的调用,基本都采用了这种混淆模式。

去混淆:只需要分析混淆代码的二进制模式,替换成call xxx形式的二进制代码就可以。

先来看看混淆代码的二进制情况:

obfuscated_call_bytes.png


可以看到混淆代码的模式:10个字节(0x0A)都是一模一样的\xE8\x00\x00\x00\x00\x58\x83\xC0\x0A\x50

* 只需要把这10个字节都NOP(\x90)掉。

* 然后把接下来的一个字节jmp(\xE9)改成call(\xE8)

直接上IDA脚本

patch后的函数调用长这样:

patch_call_defuscation.png


2、函数返回指令的混淆

由于有着明确的printf函数的字符串,跟踪错误提示字符串分支,可以发现main函数没有ret指令,导致IDA无法判断函数结尾。

ret_obfuscation.png


leave指令一般都是后面紧跟ret(\xC3)指令。这里ret(\xC3)被替换成了3个字节的pop ebx; jmp ebx

手动修改此处后,IDA还是不能create function,查看后main函数里面还有多块没有识别为代码的数据块。此处应该还藏着某种混淆方式。

3、无意义jnz混淆

由于IDA等反编译工具在分析指令流时,对于基本块(basic code block)都是顺序分析的。用jnz等条件跳转指令,指令一个破坏顺序分析指令边界的地址,就能让反编译工具产生错误。而条件跳转指令的跳转条件可以永远为真或者永远为假。

nomean_jnz_obfuscation_1.png
nomean_jnz_obfuscation_2.png




这两张图可以看出,xor ebx, ebx; jnzsub ebx, ebx; jnz这两个条件跳转是永远不会执行的。放这里就是干扰IDA的分析的,可以看到0x417214处的真实跳转指令就分析断档了,正确的跳转基本块应该从0x4174E6开始分析。

经过多处类似无效jnz指令的分析,可以总结出若干模式。直接上IDC脚本(连同上一个函数返回混淆的模式一并处理):

应用以上两个脚本处理三种容易识别的混淆模式后,IDA可以手动定义函数了。虽然存在大量junk code的干扰,但字符串没有加密,控制流已经恢复,main函数的整体逻辑已经可以看清楚了。

4、常数混淆和junk codes

const_obfuscation.png


程序的关键常数,比如循环的初始值等,都使用了上图的方式隐藏起来了。虽然源头都是从一个0x41C1B4附近的初始数据块中取出数据进行计算。但计算方法多样,很难识别一个统一模式。还存在类似的无用代码干扰。不知道计算出来的结果,是不是真的是有效的,实际代码会用到。

junk_codes.png


junk codes会出现一些比较大的数据,让IDA的栈分析失效。

对于这两种混淆方式,目前的水平没有好的自动化方式处理。只能硬着头皮看代码了。

好在junk codes的写入地址都是寄存器,不往内存写入。紧跟参数和关键内存的读写,就可以找到原始的代码流程了。

5、汇总

经过针对1、2、3混淆模式的脚本处理,IDA可以手动Create Function。优先观察函数的输出(不观察函数的输出,跟进get_key系列函数,就会绕在里面浪费大量时间),以及上层函数的代码逻辑,可以得到下图的函数列表:

functions_2.png


基本的程序逻辑其实很简单:

(1)输入的字符串序列号(128个字符)转成对应的DWORD数组(16个):

(2)16个get_key函数获取对应的16个DWORD解密密钥

(3)解密逻辑:

(4)校验:

二、keygen实现

算法其实挺简单的,直接可逆,比yonkie的前面两题简单很多。需要注意的是DWORD生成字符串的字节顺序,这个在C代码中无所谓,但用python写的话,需要注意一下。给一个C代码的keygen吧:

这段代码中sum_crc32的算法是还原自题目本身。

当尝试使用yonkies_keygen_me_3里面的CRC32算法时,发现怎么算都不对。经过比对才发现,问题出现在单字节扩展成DWORD的时候是不是带符号扩展。



0 条评论
某人
表情
可输入 255