附录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语句。这实际上是一种改进。假设你有50CASE语句。如果不使用上述的技巧,你得执行50CMPJMP指令来达到最后一个CASE。相反,有了地址表后,你可以通过表查询跳转到任何CASE语句,从计算机算法角度和时间复杂度看,我们用O(5)代替了O(2n)算法。其中:

1.O表示最坏的时间复杂度;

2.我们假设需要5条指令来进行表查询计算偏移量,最终跳到相应的地址;

现在,你也许认为出现上述情况只是因为CASE常量被有意选择为连续的(1234)。幸运的是,它的这个方案可以应用于大多数现实例子中,只有偏移量的计算稍微有些复杂。但有两个例外:

如果CASE语句少于等于三个;

如果CASE 常量完全互不相关(如:“"case 1” ,“case 13” ,“case 50” , 和“case 1000” );

显然,单独判断每个的CASE常量的话,结果代码繁琐耗时,但使用CMPJMP指令则使得结果代码的执行就像普通的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 会盲目地认为地址表中开关地址是 0040102CJMP到一个错误的地方,造成远程进程崩溃。

附录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 添加内容。