我对混淆代码的粗浅认识
首先,我只知道这个东西来源于病毒领域,没有真正学习过,更没有用过病毒引擎,完全是脱壳过程
中自己的感受,认识很肤浅,甚至完全错误。请大家多指点。
混淆代码与花指令是否可以划等号?我觉得尽管二者不能完全划清界限,但还是有区别。花指令似乎大多是
完全的垃圾代码,一般通过在编程时插入宏来实现,只要能识别出来,大多可以直接拿掉(不考虑校验),
如使用dejunk插件。
混淆代码却难以简单地剥除。比如下面这几句代码,来自ExeCryptor。
_2fimh7mp:0058E96C 68 96 3C BD 79 push 79BD3C96h
_2fimh7mp:0058E971 58 pop eax
_2fimh7mp:0058E972 81 C8 28 23 76 C3 or eax, 0C3762328h
_2fimh7mp:0058E978 81 E8 18 61 46 D9 sub eax, 0D9466118h
_2fimh7mp:0058E97E 81 E0 FD 23 0B 83 and eax, 830B23FDh
_2fimh7mp:0058E984 E9 C3 3B FF FF jmp loc_58254C
显然难以通过定义Search/Replace字符串来加以清理。调试时逐句跟很耗费精力,而计算后的结果却往往
是有意义的,需要加以注意的值。我猜测这样的代码并不是编程时实现的,可能是用单独的混淆引擎生成的。
不知道dejunk是如何实现的。我自己写过一个IDA插件,模仿dejunk的文件格式,在IDA内去掉花
指令。用字符串比较的方式来判断,比如Themida壳中的花指令(到达oep前基本就只有这1种):
[Type 1]
; push 0
; push reg32
; call loc_1
; _THREE_BYTES_JUNKCODE
;loc_1
; pop reg32
; mov [esp+4],reg32
; add [esp+4],imm32 ; 这个值可变
; inc reg32
; push reg32
; ret
; _TWO_BYTES_JUNKCODE ; bytes数可变
[imm32 = 0x14]
S = 6A00??E803000000????C3??89??24048144240414??????????C3??
R = 90909090909090909090909090909090909090909090909090909090
[imm32 = 0x15]
S = 6A00??E803000000????C3??89??24048144240415??????????C3????
R = 9090909090909090909090909090909090909090909090909090909090
[imm32 = 0x16]
S = 6A00??E803000000????C3??89??24048144240416??????????C3??????
R = 909090909090909090909090909090909090909090909090909090909090
[imm32 = 0x17]
S = 6A00??E803000000????C3??89??24048144240417??????????C3????????
R = 90909090909090909090909090909090909090909090909090909090909090
[imm32 = 0x18]
S = 6A00??E803000000????C3??89??24048144240418??????????C3??????????
R = 9090909090909090909090909090909090909090909090909090909090909090
[imm32 = 0x19]
S = 6A00??E803000000????C3??89??24048144240419??????????C3????????????
R = 909090909090909090909090909090909090909090909090909090909090909090
[imm32 = 0x1A]
S = 6A00??E803000000????C3??89??2404814424041A??????????C3??????????????
R = 90909090909090909090909090909090909090909090909090909090909090909090
[imm32 = 0x1B]
S = 6A00??E803000000????C3??89??2404814424041B??????????C3????????????????
R = 9090909090909090909090909090909090909090909090909090909090909090909090
[imm32 = 0x1C]
S = 6A00??E803000000????C3??89??2404814424041C??????????C3??????????????????
R = 909090909090909090909090909090909090909090909090909090909090909090909090
[imm32 = 0x1D]
S = 6A00??E803000000????C3??89??2404814424041D??????????C3????????????????????
R = 90909090909090909090909090909090909090909090909090909090909090909090909090
显然,这里只有一种套路,但用字符串匹配,代码有任何变化,都需要新的S/R对,非常笨拙。到了
Themida的虚拟机部分,完全对付不了混淆代码。不知道用正则表达式能否做得灵活些(那个我也没
用过;-)。
为了读Themida的VM代码,重新写了个插件,希望能比上面的稍微"聪明"一点。程序大致分几步:
1. 清除由单条指令实现的垃圾代码
指完全可以用nop替代的指令,基本上只有 lea r32,[r32]类型
另外,把变形的jmp替换,如:
_11d0000:011D173E 68 59 0F 2D 27 push 272D0F59h
_11d0000:011D1743 81 2C 24 52 FA 0F 26 sub dword ptr [esp], 260FFA52h
_11d0000:011D174A C3 retn ; 这里等于jmp loc_11D1507
2. 清除垃圾指令序列
指可以用nop替换的指令序列,如:
_11d0000:011D001E F7 D8 neg eax
_11d0000:011D0020 F7 D8 neg eax
_11d0000:011D261C 4B dec ebx
_11d0000:011D261D F7 D3 not ebx
_11d0000:011D261F F7 DB neg ebx
_11d0000:011D80CD 57 push edi
_11d0000:011D80CE F7 1C 24 neg dword ptr [esp]
_11d0000:011D80D1 5F pop edi
_11d0000:011D80D2 F7 DF neg edi
需要注意的是,从第2步起,所有的代码模式中间都可能夹杂着其他的垃圾指令,如lea r32,[r32]
或jmp。这也是第1步先行替换的原因(以简化判断)。如:
_11d0000:011D1736 68 00 00 00 00 push 0
_11d0000:011D173B 29 2C 24 sub [esp], ebp
_11d0000:011D173E 68 59 0F 2D 27 push 272D0F59h
_11d0000:011D1743 81 2C 24 52 FA 0F 26 sub dword ptr [esp], 260FFA52h
_11d0000:011D174A C3 retn ; 这里等于jmp loc_11D1507
_11d0000:011D1507 5D pop ebp
_11d0000:011D1508 F7 DD neg ebp
这里垃圾代码被jmp分成了2段。
由于对各种代码序列的识别顺序难以安排,一些模式可能要在别的代码被处理后才能识别,对给定的地址范围,
插件需要运行3次才能清理干净.
3. 对可以按模式匹配的指令加以简化
比如:
_11d0000:011D01F6 68 76 5E 00 00 push 5E76h
_11d0000:011D01FB 89 04 24 mov [esp], eax
等价于push eax
_11d0000:011D3563 31 F7 xor edi, esi
_11d0000:011D3565 31 FE xor esi, edi
_11d0000:011D3567 31 F7 xor edi, esi
等价于xchg esi,edi
4. 简化push/pop指令对
这个可以归入3,但数量众多,所以单独处理,如:
_11d0000:011D5547 52 push edx
_11d0000:011D5548 BA C5 42 00 00 mov edx, 42C5h
_11d0000:011D554D 01 57 08 add [edi+8], edx
_11d0000:011D5550 5A pop edx
等价于 add [edi+8],42C5h
5. 简化mov r8/r32,imm指令
如:
_11d0000:011D55EC BA BC 55 00 00 mov edx, 55BCh
_11d0000:011D55F1 C1 EA 09 shr edx, 9
_11d0000:011D55F4 4A dec edx
_11d0000:011D55F5 E9 40 FC FF FF jmp loc_11D523A ; 夹杂了jmp
_11d0000:011D523A 81 CA D6 2B 00 00 or edx, 2BD6h
_11d0000:011D5240 81 F2 3A 24 00 00 xor edx, 243Ah
_11d0000:011D5246 81 F2 5E 73 00 00 xor edx, 735Eh
_11d0000:011D524C 81 F2 64 83 FF FF xor edx, 0FFFF8364h
等价于mov edx, 0FFFFFFFFh
6. 简化mov mem32,imm32指令
如:
_11d0000:011D0054 C7 47 18 F9 28 00 00 mov dword ptr [edi+18h], 28F9h
_11d0000:011D005B F7 57 18 not dword ptr [edi+18h]
_11d0000:011D005E 31 FB xor ebx, edi ; 垃圾
_11d0000:011D0060 31 FB xor ebx, edi ; 垃圾
_11d0000:011D0062 F7 5F 18 neg dword ptr [edi+18h]
_11d0000:011D0065 81 67 18 C0 3B 00 00 and dword ptr [edi+18h], 3BC0h
_11d0000:011D006C 81 6F 18 EB 64 00 00 sub dword ptr [edi+18h], 64EBh
_11d0000:011D0073 8D 36 lea esi, [esi] ; 垃圾
_11d0000:011D0075 81 6F 18 D5 C3 FF FF sub dword ptr [edi+18h], 0FFFFC3D5h
等价于mov dword ptr [edi+18h], 0
暂时就只做了这些,Themida虚拟机里的混淆代码实在太多,要想清理干净恐怕是不容易。这只
是个尝试,希望把代码搞得可读性强些。另外,处理后的代码与原始代码看起来差别较大,调试起
来也难看,可以考虑做个OD插件(不会;-),或着把IDA的数据贴过去。现在bug不少,贴过去肯定是
跑不起来,只是个试验品。
编码中一个突出问题是对代码的正确识别。IDA SDK中提供的isCode,isData,isHead,个人感觉
不大好用,经常出现误判或漏掉数据。在代码识别时我直接使用了z0mbie写的XDE反汇编引擎。但
仍有问题,主要是对8为寄存器访问代码,不能正确给出src_set和dst_set,不得已自己判断指令。
对代码进行识别时,和以前用字符串匹配没有太多区别,只是增强了读入代码数据的过程(跳过
nop,及在jmp的目的地址继续)。这样就限制了仍然不能做得足够灵活。尤其是同样的助记符可能
对应不只一个opcode,使代码更加冗长。
下面是一个bug例子。
_11d0000:011D0451 30 6F 20 xor [edi+20h], ch
_11d0000:011D0454 66 8B 0C 24 mov cx, [esp]
_11d0000:011D0458 83 C4 02 add esp, 2 ; DeObfuscated code
_11d0000:011D045B 90 90 90 90 90 90 90 90+ db 0Dh dup(90h)
_11d0000:011D0468 E9 B8 07 00 00 jmp loc_11D0C25
_11d0000:011D046D 90 90 90 90 90 90 90 90 db 8 dup(90h)
_11d0000:011D0475 90 90 90 90 90 90 90 90+byte_11D0475 db 0Bh dup(90h) ; CODE XREF: _11d0000:011D037Dj
_11d0000:011D0480 C1 E8 02 shr eax, 2
_11d0000:011D0483 C1 E0 03 shl eax, 3
_11d0000:011D0486 09 C0 or eax, eax
_11d0000:011D0488 0F 84 73 FD FF FF jz loc_11D0201
已经经过前4步处理。011D0468的jmp是正确的。但第5步错误地将011D0469的B8 07 00 00 90
误判为mov eax,xxxxxxxx,而后续的指令也恰好满足判断条件:
_11d0000:011D0480 C1 E8 02 shr eax, 2
_11d0000:011D0483 C1 E0 03 shl eax, 3
_11d0000:011D0486 09 C0 or eax, eax
这3句都是在对同一寄存器操作,且源对象集不包括内存数据或别的寄存器,于是判别为
符合条件的指令序列,进行合并。最后暂且改为连续判断4条指令是否符合条件。少清理
一些总比弄错了好。
怎样才能更加灵活? 也许可以考虑定义一套类似脚本的东西,用来描述混淆代码模式。
然后对描述字符串进行分析,类似于编译过程。另外再用一个汇编引擎,从得到的结果汇编
出所有可能的opcode组合用于判断。这些东西已经超出我的能力了,写这篇东西,希望抛砖
引玉,哪位高人开发出能用于实战的工具。
插件在IDA 4.7下使用,没有处理的混淆代码还有很多(我怀疑如果榨干全部水份,有
用的代码不组三分之一)。源码过分丑陋,就不拿出来献眼了;-)
附件:themida.rar