最近看了不少关于花指令的博文,感觉大致了解插入花指令的方法,此处加以总结记录。
本文参考资料:
1).<Secrets of Reverse Engineering> Chap10_2 看雪论坛译
2).<加密与解密 第三版>
3).<矛与盾的较量(1) -花指令> 作者 罗聪 http://www.luocong.com/articles/show_article.asp?Article_ID=14
4).还有一些无法记录出处的博文
花指令属于静态反调试技术,只是通过加入烟幕弹扰乱代码可读性从而磨光调试者的耐心,本身并不影响程序执行逻辑。下面以一个简单的MessageBoxA程序为小白鼠,一步步与花指令相识相知~
#include <windows.h>
int main(int argc, char* argv[])
{
char* str1="message";
char* str2="caption";
HMODULE u32dll = LoadLibrary("user32.dll");
DWORD MsgBoxAddr = (DWORD)GetProcAddress(u32dll,"MessageBoxA");
__asm
{
push 0; //MB_OK
lea ebx,str2;
push [ebx];
lea ebx,str1;
push [ebx];
push NULL;
mov eax,MsgBoxAddr;
call eax;
}
return 0;
}
__asm{}块中的指令将被加入花指令,影响调试器静态的分析。柿子先捡软的捏,先拿“linear sweep”式的反汇编器开刀,然后再考虑怎么对付“Recursive traversal”式的反汇编器
1.首先祭出W32DASM分析, Linear Sweep反汇编算法。
W32DASM只是用str2的栈地址:[ebp+FFFFFFF8]代替了lea ebx,str2。(ebp+FFFFFFF8=ebp-8,vc debug版的程序用[ebp-偏移N]的形式来获取局部变量)。其他的输出跟代码一样,简直就是开源项目了,这当然不是想要的结果。来修改一下程序!
__asm
{
push 0; //MB_OK
lea ebx,str2;
push [ebx];
lea ebx,str1;
push [ebx];
push NULL;
mov eax,MsgBoxAddr;
/
jmp Lab1
_emit 0xB8 //mov的opcode
Lab1:
/
call eax;
}
分隔线之间的指令是新加的代码,就是最简单的花指令。jmp指令直接跳转到call eax,看着好像没什么作用(极端份子会说影响执行效率),还是来看下W32DASM的输出。
shit,没被欺骗,看来升级版的W32DASM还是能识别这类花指令。换个调试器,就windbg了,也是Linear Sweep反汇编算法
00401037 6a00 push 0
00401039 8d9df8ffffff lea ebx,[ebp-8]
0040103f ff33 push dword ptr [ebx]
00401041 8d9dfcffffff lea ebx,[ebp-4]
00401047 ff33 push dword ptr [ebx]
00401049 6a00 push 0
0040104b 8b45f0 mov eax,dword ptr [ebp-10h]
0040104e eb01 jmp Msg!main+0x51 (00401051)
00401050 b8ffd033c0 mov eax,0C033D0FFh <=========================此处,跟W32DASM的输出不同了
00401055 5f pop edi
00401056 5e pop esi
00401057 5b pop ebx
00401058 8be5 mov esp,ebp
0040105a 5d pop ebp
0040105b c3 ret
从windbg输出看,00401050处被编译成mov指令,而不是无效数据BYTE B8。同时,仔细看mov上一条指令,jmp 00401051,跳转到mov指令中间,而不是一条指令的开始处,由此可见windbg被绕晕了。好吧,windbg主要用来动态调试,这样的花指令意义也不大,改进改进先饶过W32DASM
__asm
{
push 0; //MB_OK
lea ebx,str2;
push [ebx];
lea ebx,str1;
push [ebx];
push NULL;
mov eax,MsgBoxAddr;
mov ecx,0;
//比较1==2,Lab2分支永远不会达到
cmp ecx,1;
jne Lab1;
je Lab2;
Lab2:
_emit 0xB8
Lab1:
call eax;
}
这次,W32DASM真跪了:
顺便看下Od的结果:
看来,在401056处和401058处的jnz/je指令没有成功饶过Od,有点可惜。又要修改。
再继续后面的尝试前先打住一下,为什么用cmp/jnz/je代替jmp指令能饶过W32DASM?
据说W32DASM是Linear Sweep反汇编算法,在这种算法下反汇编器只是依次逐个地将整个模块中的每一条指令都反汇编成汇编指令,如果代码中的插入一个无效字节Linear Sweep会将无效字节和后续的字节合在一起错误的解释为错误的指令,于是产生了上梁不正下梁歪的连锁反映。当然,我们的目的不是执行错误的流程,因此要用jmp跳过无效字节,于是就有了jmp+thunkcode这种简单的花指令组合方式。
可是,我的W32DASM目测是进击版的,采用了Recursive traversal算法,这种算法预测每个分支的地址并继续反汇编。对此用了所谓的“opaque predicate"方式欺骗反汇编器。这种方式就是在程序中安上一个伪分支。正如正常的分支语句那样,“opaque predicate”的也分成两个部分,一部分是跳转到真正需要执行的代码,另一部分则跳转到能玩晕反汇编器的代码,而能跳转到这一部分的分支条件需要设成永远为假。伪分支中的代码类似欺骗Linear Sweep反汇编算法,在分支的入口处加如无效字节,如0xB8/0xE9等。
就是这种“opaque predicate”方式成功的迷惑了W32DASM。
__asm
{
push 0; //MB_OK
lea ebx,str2;
push ebx;
lea ebx,str1;
push ebx;
push NULL;
mov eax,MsgBoxAddr;
xor ebx,0x5a;
cmp ebx,0x5a;
jz Mali;
jnz CallMsg;
Mali:
_emit 0xb8;
CallMsg:
call eax;
}