附录E:
为什么要将开关语句拆分成三个以上?
用下面这个例子很容易解释这个问题,假设有如下这么一个函数:
01.int Dummy( int arg1 )
02.{
03. int ret =0;
04.
05. switch( arg1 ) {
06. case 1: ret = 1; break;
07. case 2: ret = 2; break;
08. case 3: ret = 3; break;
09. case 4: ret = 0xA0B0; break;
10. }
11. return ret;
12.}
编译后变成下面这个样子:
01.地址 操作码/参数 解释后的指令
02.--------------------------------------------------
03. ; arg1 -> ECX
04.:00401000 8B4C2404 mov ecx, dword ptr [esp+04]
05.:00401004 33C0 xor eax, eax ; EAX = 0
06.:00401006 49 dec ecx ; ECX --
07.:00401007 83F903 cmp ecx, 00000003
08.:0040100A 771E ja 0040102A
09.
10.; JMP 到表***中的地址之一
11.; 注意 ECX 包含的偏移
12.:0040100C FF248D2C104000 jmp dword ptr [4*ecx+0040102C]
13.
14.:00401013 B801000000 mov eax, 00000001 ; case 1: eax = 1;
15.:00401018 C3 ret
16.:00401019 B802000000 mov eax, 00000002 ; case 2: eax = 2;
17.:0040101E C3 ret
18.:0040101F B803000000 mov eax, 00000003 ; case 3: eax = 3;
19.:00401024 C3 ret
20.:00401025 B8B0A00000 mov eax, 0000A0B0 ; case 4: eax = 0xA0B0;
21.:0040102A C3 ret
22.:0040102B 90 nop
23.
24.; 地址表***
25.:0040102C 13104000 DWORD 00401013 ; jump to case 1
26.:00401030 19104000 DWORD 00401019 ; jump to case 2
27.:00401034 1F104000 DWORD 0040101F ; jump to case 3
28.:00401038 25104000 DWORD 00401025 ; jump to case 4
注意如何实现这个开关语句?
与其单独检查每个CASE语句,不如创建一个地址表,然后通过简单地计算地址表的偏移量而跳转到正确的CASE语句。这实际上是一种改进。假设你有50个CASE语句。如果不使用上述的技巧,你得执行50次 CMP和JMP指令来达到最后一个CASE。相反,有了地址表后,你可以通过表查询跳转到任何CASE语句,从计算机算法角度和时间复杂度看,我们用O(5)代替了O(2n)算法。其中:
1.O表示最坏的时间复杂度;
2.我们假设需要5条指令来进行表查询计算偏移量,最终跳到相应的地址;
现在,你也许认为出现上述情况只是因为CASE常量被有意选择为连续的(1,2,3,4)。幸运的是,它的这个方案可以应用于大多数现实例子中,只有偏移量的计算稍微有些复杂。但有两个例外:
如果CASE语句少于等于三个;
如果CASE 常量完全互不相关(如:“"case 1” ,“case 13” ,“case 50” , 和“case 1000” );
显然,单独判断每个的CASE常量的话,结果代码繁琐耗时,但使用CMP和JMP指令则使得结果代码的执行就像普通的if-else 语句。
有趣的地方:如果你不明白CASE语句使用常量表达式的理由,那么现在应该弄明白了吧。为了创建地址表,显然在编译时就应该知道相关地址。
现在回到问题!
注意到地址 0040100C 处的JMP指令了吗?我们来看看Intel关于十六进制操作码 FF 的文档是怎么说的:
1.操作码 指令 描述
2.FF /4 JMP r/m32 Jump near, absolute indirect,
3. address given in r/m32
原来JMP 使用了一种绝对寻址方式,也就是说,它的操作数(CASE语句中的 0040102C)表示一个绝对地址。还用我说什么吗?远程 ThreadFunc 会盲目地认为地址表中开关地址是 0040102C,JMP到一个错误的地方,造成远程进程崩溃。
附录F:
为什么远程进程会崩溃呢?
当远程进程崩溃时,它总是会因为下面这些原因:
1.在ThreadFunc 中引用了一个不存在的串;
2.在在ThreadFunc 中 中一个或多个指令使用绝对寻址(参见附录E);
3.ThreadFunc 调用某个不存在的函数(该调用可能是编译器或链接器添加的)。你在反汇编器中可以看到这样的情形:
1.:004014C0 push EBP ; ThreadFunc 的入口点
2.:004014C1 mov EBP, ESP
3. ...
4.:004014C5 call 0041550 ; 这里将使远程进程崩溃
5. ...
6.:00401502 ret
如果 CALL 是由编译器添加的指令(因为某些“禁忌” 开关如/GZ是打开的),它将被定位在 ThreadFunc 的开始的某个地方或者结尾处。
不管哪种情况,你都要小心翼翼地使用 CreateRemoteThread 和 WriteProcessMemory 技术。尤其要注意你的编译器/链接器选项,一不小心它们就会在 ThreadFunc 添加内容。