0x1 switch-case分支
switch-case其实就是if-else语句的另一种体现形式。但大于3之后的switchc-case。编译器会对代码进行优化。
1.1 简单switch-case分支识别技巧
C源代码:
int _tmain(int argc, _TCHAR* argv[])
{
int nNum = 2;
switch (nNum)
{
case 0:
printf("nNum=0", nNum);
break;
case 1:
printf("nNum=%d", nNum);
break;
case 2:
printf("nNum=%d", nNum);
break;
default:
printf("nNum=%d,error!", nNum);
}
return 0;
}
Debug版反汇编代码
Debug版的反汇编代码与源代码是很相似的。并不会剪掉不可达分支。
- 以常量、变量为判断条件的优化效果与if-else有多大区别?
011A70C0 push ebp
011A70C1 mov ebp,esp
011A70C3 sub esp,0xD0
011A70C9 push ebx
011A70CA push esi
011A70CB push edi
011A70CC lea edi,[local.52]
011A70D2 mov ecx,0x34
011A70D7 mov eax,0xCCCCCCCC
011A70DC rep stos dword ptr es:[edi]
011A70DE mov [local.2],0x2 ; 将2赋值到变量 nNum = 2
011A70E5 mov eax,[local.2] ; 将nNum变量赋值到eax
011A70E8 mov [local.52],eax ; 赋值到局部变量中
011A70EE cmp [local.52],0x0 ; 对比0的情况
011A70F5 je X9-25.011A710B
011A70F7 cmp [local.52],0x1 ; 对比1的情况
011A70FE je X9-25.011A711E
011A7100 cmp [local.52],0x2 ; 对比2的情况
011A7107 je X9-25.011A7131
011A7109 jmp X9-25.011A7144 ; 默认情况就无条件跳转到最后一个分支处
011A710B mov eax,[local.2]
011A710E push eax
011A710F push 9-25.01235E50 ; nNum=%d
011A7114 call 9-25.011A3CE7
011A7119 add esp,0x8
011A711C jmp X9-25.011A7155 ; 执行完就无条件跳转到结束的位置
011A711E mov eax,[local.2]
011A7121 push eax
011A7122 push 9-25.01235E50 ; nNum=%d
011A7127 call 9-25.011A3CE7
011A712C add esp,0x8
011A712F jmp X9-25.011A7155 ; 执行完就无条件跳转到结束的位置
011A7131 mov eax,[local.2]
011A7134 push eax
011A7135 push 9-25.01235E50 ; nNum=%d
011A713A call 9-25.011A3CE7
011A713F add esp,0x8
011A7142 jmp X9-25.011A7155 ; 执行完就无条件跳转到结束的位置
011A7144 mov eax,[local.2]
011A7147 push eax
011A7148 push 9-25.01235E5C ; nNum=%d,error!
011A714D call 9-25.011A3CE7
011A7152 add esp,0x8
011A7155 xor eax,eax
011A7157 pop edi
011A7158 pop esi ; 9-25.<ModuleEntryPoint>
011A7159 pop ebx
011A715A add esp,0xD0
011A7160 cmp ebp,esp
011A7162 call 9-25.011A231A
011A7167 mov esp,ebp
011A7169 pop ebp
011A716A retn
通过Debug反汇编代码,总结以下特点:
cmp xxx,xxx
jXX CASE1_TAG
cmp xxx,xxx
jXX CASE2_TAG
cmp xxx,xxx
jXX CASE3_TAG
JMP SWITCH_END_TAG
CASE1_TAG:
.....分支1执行代码
JMP SWITCH_END_TAG
CASE2_TAG:
.....分支2执行代码
JMP SWITCH_END_TAG
CASE3_TAG:
.....分支3执行代码
JMP SWITCH_END_TAG
DEFAULT:
..DEFAULT代码执行代码
SWITCH_END_TAG
Release版反汇编代码:
Release版反汇编则是把代码进行了优化,由此可见switch-case语句与if-else语句一样,不可达分支都会被编译器优化掉。
001B1000 push 0x2 0x2
001B1002 push 9-25.001C6448 nNum=%d
001B1007 call 9-25.printf printf
001B100C add esp,0x8
001B100F xor eax,eax
001B1011 retn
那么尝试以变量为判断条件的优化效果与if-else有多大区别?
C源代码:
int _tmain(int argc, _TCHAR* argv[])
{
switch (argc)
{
case 0:
printf("argc=0",argc);
break;
case 1:
printf("argc=%d",argc);
break;
case 2:
printf("argc=%d",argc);
break;
default:
printf("argc=%d,error!",argc);
}
return 0;
}
按照if-else的优化逻辑,case 1与case 2 会指向一处。
00B11000 push ebp
00B11001 mov ebp,esp
00B11003 mov ecx,dword ptr ss:[ebp+0x8];argc传参给ecx寄存器中
00B11006 mov eax,ecx ;ecx寄存器又把值传给eax
00B11008 sub eax,0x0 ;switch()开始的地方,对比0
00B1100B je X9-25.00B1104F
00B1100D sub eax,0x1 ;注意这里用的是减法,非常巧妙
00B11010 je X9-25.00B1103C ;当值等于0的时候跳转
00B11012 sub eax,0x1
00B11015 je X9-25.00B11029
00B11017 push ecx ;Default case
00B11018 push 9-25.00B26458 ;argc=%d,error!
00B11022 add esp,0x8
00B11025 xor eax,eax
00B11027 pop ebp
00B11028 retn
00B11029 push 0x2 ;argc为2的情况
00B1102B push 9-25.00B26450 ;argc=%d
00B11030 call 9-25.printf ;printf
00B11035 add esp,0x8
00B11038 xor eax,eax
00B1103A pop ebp
00B1103B retn
00B1103C push 0x1 ;argc为1的情况
00B1103E push 9-25.00B26450 ;argc=%d
00B11043 call 9-25.printf ;printf
00B11048 add esp,0x8
00B1104B xor eax,eax
00B1104D pop ebp
00B1104E retn
00B1104F push 0x0 ;argc为0的情况
00B11051 push 9-25.00B26448 ;argc=0
00B11056 call 9-25.printf ;printf
00B1105B add esp,0x8
00B1105E xor eax,eax
00B11060 pop ebp
00B11061 retn
可以发现release在条件跳转前用的不再是cmp,而是sub。通过阅读这块代码可知,程序先将main()函数的参数1传递给EAX,然后减0。je的跳转条件是ZF=1,因此当EAX为0时,那么将其减0肯定会使用ZF位置1,而其实就是一个变形的CMP指令。
假设EAX等于2,因此按照上面代码的流程走会先将其减0,此时ZF位不变;
接着下面又对EAX减1,此时ZF位仍然没变化;
而当走到第三步时,此时EAX的值为1,又将其减1后肯定就等于0了,ZF位置为1,后面的JZ跳转生效。
小结:上诉操作就是做了一连串的减法,到哪等于0后,就证明这个值原先为多少。
1.2 复杂分支的switch-case识别
C源代码:
int _tmain(int argc, _TCHAR* argv[])
{
switch (argc)
{
case 0:
printf("argc=0",argc);
break;
case 1:
printf("argc=%d",argc);
break;
case 6:
printf("argc=%d",argc);
break;
case 7:
printf("argc=%d",argc);
break;
case 9:
printf("argc=%d",argc);
break;
default:
printf("nNum=%d,error!",argc);
}
return 0;
}
Release版反汇编代码:
00D71000 push ebp
00D71001 mov ebp,esp
00D71003 mov eax,[arg.1] ; 取得局部变量后传递给EAX
00D71006 cmp eax,0x9 ; Switch (cases 0..9)
00D71009 ja short 9-29.00D71071 ; 如果大于最大值9,直接跳转到Default处
00D7100B jmp dword ptr ds:[eax*4+0xD71084] ; 这里采用了一个比较特殊的方式
00D71012 push 0x0 ; Case 0 of switch 00D71006
00D71014 push 9-29.00D86448 ; argc=0
00D71019 call 9-29.printf_initialize_sse2_sse2_fr>
00D7101E add esp,0x8 ; argc=0
00D71021 xor eax,eax
00D71023 pop ebp ; kernel32.770B62C4
00D71024 retn
00D71025 push 0x1 ; Case 1 of switch 00D71006
00D71027 push 9-29.00D86450 ; argc=%d
00D7102C call 9-29.printf_initialize_sse2_sse2_fr>
00D71031 add esp,0x8 ; argc=%d
00D71034 xor eax,eax
00D71036 pop ebp ; kernel32.770B62C4
00D71037 retn
00D71038 push 0x6 ; Case 6 of switch 00D71006
00D7103A push 9-29.00D86450 ; argc=%d
00D7103F call 9-29.printf_initialize_sse2_sse2_fr>
00D71044 add esp,0x8 ; argc=%d
00D71047 xor eax,eax
00D71049 pop ebp ; kernel32.770B62C4
00D7104A retn
00D7104B push 0x7 ; Case 7 of switch 00D71006
00D7104D push 9-29.00D86450 ; argc=%d
00D71052 call 9-29.printf_initialize_sse2_sse2_fr>
00D71057 add esp,0x8 ; argc=%d
00D7105A xor eax,eax
00D7105C pop ebp ; kernel32.770B62C4
00D7105D retn
00D7105E push 0x9 ; Case 9 of switch 00D71006
00D71060 push 9-29.00D86450 ; argc=%d
00D71065 call 9-29.printf_initialize_sse2_sse2_fr>
00D7106A add esp,0x8
00D7106D xor eax,eax ; argc=%d
00D7106F pop ebp ; kernel32.770B62C4
00D71070 retn
00D71071 push eax ; Default case of switch 00D71006
00D71072 push 9-29.00D86458 ; nNum=%d,error!
00D71077 call 9-29.printf_initialize_sse2_sse2_fr>
00D7107C add esp,0x8
00D7107F xor eax,eax ; nNum=%d,error!
00D71081 pop ebp ; kernel32.770B62C4
00D71082 retn
位于第4行的jmp指令以及前几条反汇编指令的含义:
mov eax, [ebp+arg_0] ; 取得局部变量后传递给EAX
cmp eax, 9 ; 与9比较,我们通过源代码可知这是switch-case分支的最大值
; 因此如果传入的值大于9就肯定会执行Default处代码
ja short loc_401071 ; jumptable 0040100B default case
jmp ds:off_401084[eax*4] ; EAX此时保存的是输入的值,将其乘以4后再加上一个地址,这其实就是一个典型的数组寻址。
; 是一个读取int型数组的操作,数组里保存的是地址指针
OD上-数据窗口中跟随-地址常量。就可以看懂0xD71084存储的到底是啥了。里面保存的内容就是各个case块的地址,我们将之称为“跳转表”。跳转表的作用就是用数组的寻址运算代替复杂的if-else分支,可以大大提高程序的执行效率。
跳转表的基本原理是建立一张表格,里面保存着从case1到caseN的所有分支应该到达的地址。从数据窗口中可以看到代码中的case1到caseN地址都保存在了跳转表里。
00D71084 12 10 D7 00 25 10 D7 00 71 10 D7 00 71 10 D7 00 ဒ×ဥ×ၱ×ၱ×
00D71094 71 10 D7 00 71 10 D7 00 38 10 D7 00 4B 10 D7 00 ၱ×ၱ×း×။×
00D710A4 71 10 D7 00 5E 10 D7 00 ၱ×ၞ×
case2至case5里保存的地址都是Default分支的地址,这就证明这几个case在程序的源代码中是属于未处理的状态。
图:跳转表
1.3 switch-case分支结构与稀疏矩阵
Switch-case除了转成if-else与利用跳转表两种优化模式外。当switch-case分支两个数之差大于50甚至更多的时候,编译器会采用更加优化的方式进行处理。由此引出稀疏矩阵的知识点。
1)什么是稀疏矩阵?
稀疏矩阵就是零元素或同一元素占全部元素的百分比很小(例如5%以下)的矩阵
2)什么时候应用稀疏矩阵?
由于每个编译器所使用的策略不一样,因此其“体积-效率比”权值的设定也不尽相同。如果使用跳转表生成的代码的体积大于使用稀疏矩阵的体积,那么一般情况下编译器就会选择使用稀疏矩阵来优化此段代码。
C源代码:
int _tmain(int argc, _TCHAR* argv[])
{
switch (argc)
{
case 0:
printf("argc=0",argc);
break;
case 1:
printf("argc=%d",argc);
break;
case 6:
printf("argc=%d",argc);
break;
case 7:
printf("argc=%d",argc);
break;
case 199: // 注意这里,想想如果还使用跳转表的话会是什么后果……
printf("argc=%d",argc);
break;
default:
printf("nNum=%d,error!",argc);
}
return 0;
}
以上代码编译之后,拖进IDA里,旁边的函数名caseN是我改掉的。在VS系列编译器中,针对switch-case分支结构的稀疏矩阵(间接表)都是用1字节表示的,因此其最小索引值与最大索引值之差不得大于256,否则此优化方法便不再适用。
其次这个稀疏矩阵,又可称为间接表的数组需要与跳转表相呼应才能保证程序流程最终执行到正确的地方。IDA解析后的反汇编代码如下:
.text:00401000 sub_401000 proc near
.text:00401000
.text:00401000 arg_0 = dword ptr 8
.text:00401000
.text:00401000 push ebp
.text:00401001 mov ebp, esp
.text:00401003 mov ecx, [ebp+arg_0]
.text:00401006 cmp ecx, 0C7h ; switch 200 cases
.text:0040100C ja short caseDefault ; jumptable 00401015 default case
.text:0040100E movzx eax, ds:byte_4010A8[ecx]
.text:00401015 jmp ds:off_401090[eax*4] ; switch jump
.text:0040101C ; -----------------------------------------------------------------
.text:0040101C
.text:0040101C case0:
.text:0040101C push 0 ; jumptable 00401015 case 0
.text:0040101E push offset aArgc0 ; "argc=0"
.text:00401023 call sub_401180
.text:00401028 add esp, 8
.text:0040102B xor eax, eax
.text:0040102D pop ebp
.text:0040102E retn
.text:0040102F ; ---------------------------------------------------------------------------
.text:0040102F
.text:0040102F case1:
.text:0040102F push 1 ; jumptable 00401015 case 1
.text:00401031 push offset aArgcD ; "argc=%d"
.text:00401036 call sub_401180
.text:0040103B add esp, 8
.text:0040103E xor eax, eax
.text:00401040 pop ebp
.text:00401041 retn
.text:00401042 ; ---------------------------------------------------------------------------
.text:00401042
.text:00401042 case6:
.text:00401042 push 6 ; jumptable 00401015 case 6
.text:00401044 push offset aArgcD ; "argc=%d"
.text:00401049 call sub_401180
.text:0040104E add esp, 8
.text:00401051 xor eax, eax
.text:00401053 pop ebp
.text:00401054 retn
.text:00401055 ; ---------------------------------------------------------------------------
.text:00401055
.text:00401055 case7:
.text:00401055 push 7 ; jumptable 00401015 case 7
.text:00401057 push offset aArgcD ; "argc=%d"
.text:0040105C call sub_401180
.text:00401061 add esp, 8
.text:00401064 xor eax, eax
.text:00401066 pop ebp
.text:00401067 retn
.text:00401068 ; ---------------------------------------------------------------------------
.text:00401068
.text:00401068 case199:
.text:00401068 push 0C7h ; jumptable 00401015 case 199
.text:0040106D push offset aArgcD ; "argc=%d"
.text:00401072 call sub_401180
.text:00401077 add esp, 8
.text:0040107A xor eax, eax
.text:0040107C pop ebp
.text:0040107D retn
.text:0040107E ; ---------------------------------------------------------------------------
.text:0040107E
.text:0040107E caseDefault:
.text:0040107E push ecx ; jumptable 00401015 default case
.text:0040107F push offset aNnumDError ; "nNum=%d,error!"
.text:00401084 call sub_401180
.text:00401089 add esp, 8
.text:0040108C xor eax, eax
.text:0040108E pop ebp
.text:0040108F retn
.text:0040108F sub_401000 endp
通过上面的代码分析出主要是以下两句代码在控制其流程:
.text:0040100E movzx eax, ds:byte_4010A8[ecx] ; Move with Zero-Extend
.text:00401015 jmp ds:off_401090[eax*4] ; switch jump
由于代码中的switch-case分支结构拥有6个分支,因此间接表里保存的索引内容都是在0-5之间的,然后根据索引来确定跳转到第几个分支上去。
图:间接表(稀疏矩阵)
1.4 switch-case分支结构与平衡二叉树
间接表的应用只限于小于等于256的情况,如果超过256这种与跳转表相配合的间接表就会失效,取而代之的便是著名的二叉树了。
二叉树查找法我们也可以暂时将其理解为折半查找法。
C源代码:
int _tmain(int argc, _TCHAR* argv[])
{
switch (argc)
{
case 1:
printf("argc1=0",argc);
break;
case 92:
printf("argc12=%d",argc);
break;
case 262:
printf("argc1=%d",argc);
break;
case 118:
printf("argc118=%d",argc);
break;
case 25:
printf("argc25=%d",argc);
break;
case 456:
printf("argc456=%d",argc);
break;
case 588:
printf("argc588=%d",argc);
break;
case 896:
printf("argc896=0",argc);
break;
case 1000:
printf("argc1000=%d",argc);
break;
case 1090:
printf("argc1090=%d",argc);
break;
case 2100:
printf("argc2100=%d",argc);
break;
default:
printf("default nNum=%d,error!",argc);
}
return 0;
}
Release版本反汇编代码:
.text:00401000 _wmain proc near ; CODE XREF: ___tmainCRTStartup+F5p
.text:00401000
.text:00401000 arg_0 = dword ptr 4
.text:00401000
.text:00401000 mov eax, [esp+arg_0]
.text:00401004 cmp eax, 1C8h ; 1C8H == 456
.text:00401009 jg loc_4010AA ;大于456时转移
.text:0040100F jz loc_401095 ; zf == 0 也就是刚好是456时也跳转
.text:00401015 cmp eax, 5Ch ; 5CH == 92
.text:00401018 jg short loc_401062
.text:0040101A jz short loc_401050
.text:0040101C mov ecx, eax
.text:0040101E sub ecx, 1
.text:00401021 jz short loc_40103E
.text:00401023 sub ecx, 18h ;
.text:00401026 jnz loc_40110A
.text:0040102C push 19h ;
.text:0040102E push offset aArgc25D ; "argc25=%d"
.text:00401033 call _printf
.text:00401038 add esp, 8
.text:0040103B xor eax, eax
.text:0040103D retn
.text:0040103E ; ---------------------------------------------------------------------------
.text:0040103E
.text:0040103E loc_40103E: ; CODE XREF: _wmain+21j
.text:0040103E push 1
.text:00401040 push offset aArgc10 ; "argc1=0"
.text:00401045 call _printf
.text:0040104A add esp, 8
.text:0040104D xor eax, eax
.text:0040104F retn
.text:00401050 ; ---------------------------------------------------------------------------
.text:00401050
.text:00401050 loc_401050: ; CODE XREF: _wmain+1Aj
.text:00401050 push 5Ch ; 5CH == 92
.text:00401052 push offset aArgc12D ; "argc12=%d"
.text:00401057 call _printf
.text:0040105C add esp, 8
.text:0040105F xor eax, eax
.text:00401061 retn
.text:00401062 ; ---------------------------------------------------------------------------
.text:00401062
.text:00401062 loc_401062: ; CODE XREF: _wmain+18j
.text:00401062 cmp eax, 76h ; 76h == 118
.text:00401065 jz short loc_401083
.text:00401067 cmp eax, 106h ; 106h == 262
.text:0040106C jnz loc_40110A
.text:00401072 push eax
.text:00401073 push offset aArgc1D ; "argc1=%d"
.text:00401078 call _printf
.text:0040107D add esp, 8
.text:00401080 xor eax, eax
.text:00401082 retn
.text:00401083 ; ---------------------------------------------------------------------------
.text:00401083
.text:00401083 loc_401083: ; CODE XREF: _wmain+65j
.text:00401083 push 76h
.text:00401085 push offset aArgc118D ; "argc118=%d"
.text:0040108A call _printf
.text:0040108F add esp, 8
.text:00401092 xor eax, eax
.text:00401094 retn
.text:00401095 ; ---------------------------------------------------------------------------
.text:00401095
.text:00401095 loc_401095: ; CODE XREF: _wmain+Fj
.text:00401095 push 1C8h ; 1C8h == 456
.text:0040109A push offset aArgc456D ; "argc456=%d"
.text:0040109F call _printf
.text:004010A4 add esp, 8
.text:004010A7 xor eax, eax
.text:004010A9 retn
.text:004010AA ; ---------------------------------------------------------------------------
.text:004010AA
.text:004010AA loc_4010AA: ; CODE XREF: _wmain+9j
.text:004010AA cmp eax, 3E8h ; 3E8h == 1000
.text:004010AF jg short loc_4010FC
.text:004010B1 jz short loc_4010E7
.text:004010B3 cmp eax, 24Ch ; 24Ch == 588
.text:004010B8 jz short loc_4010D2
.text:004010BA cmp eax, 380h ; 389h == 896
.text:004010BF jnz short loc_40110A
.text:004010C1 push eax
.text:004010C2 push offset aArgc8960 ; "argc896=0"
.text:004010C7 call _printf
.text:004010CC add esp, 8
.text:004010CF xor eax, eax
.text:004010D1 retn
.text:004010D2 ; ---------------------------------------------------------------------------
.text:004010D2
.text:004010D2 loc_4010D2: ; CODE XREF: _wmain+B8j
.text:004010D2 push 24Ch
.text:004010D7 push offset aArgc588D ; "argc588=%d"
.text:004010DC call _printf
.text:004010E1 add esp, 8
.text:004010E4 xor eax, eax
.text:004010E6 retn
.text:004010E7 ; ---------------------------------------------------------------------------
.text:004010E7
.text:004010E7 loc_4010E7: ; CODE XREF: _wmain+B1j
.text:004010E7 push 3E8h
.text:004010EC push offset aArgc1000D ; "argc1000=%d"
.text:004010F1 call _printf
.text:004010F6 add esp, 8
.text:004010F9 xor eax, eax
.text:004010FB retn
.text:004010FC ; ---------------------------------------------------------------------------
.text:004010FC
.text:004010FC loc_4010FC: ; CODE XREF: _wmain+AFj
.text:004010FC cmp eax, 442h ; 442h == 1090
.text:00401101 jz short loc_401130
.text:00401103 cmp eax, 834h ; 834h == 2100
.text:00401108 jz short loc_40111B
.text:0040110A
.text:0040110A loc_40110A: ; CODE XREF: _wmain+26j
.text:0040110A ; _wmain+6Cj ...
.text:0040110A push eax
.text:0040110B push offset aDefaultNnumDEr ; "default nNum=%d,error!"
.text:00401110 call _printf
.text:00401115 add esp, 8
.text:00401118 xor eax, eax
.text:0040111A retn
.text:0040111B ; ---------------------------------------------------------------------------
.text:0040111B
.text:0040111B loc_40111B: ; CODE XREF: _wmain+108j
.text:0040111B push 834h
.text:00401120 push offset aArgc2100D ; "argc2100=%d"
.text:00401125 call _printf
.text:0040112A add esp, 8
.text:0040112D xor eax, eax
.text:0040112F retn
.text:00401130 ; ---------------------------------------------------------------------------
.text:00401130
.text:00401130 loc_401130: ; CODE XREF: _wmain+101j
.text:00401130 push 442h
.text:00401135 push offset aArgc1090D ; "argc1090=%d"
.text:0040113A call _printf
.text:0040113F add esp, 8
.text:00401142 xor eax, eax
.text:00401144 retn
.text:00401144 _wmain endp
.text:00401144
对于二叉树结构的识别,如果其每次跳转所对比的值都是其后面分支跳转的中间值之一,那么这就有可能是一个二叉树。
.text:00401004 cmp eax, 1C8h ; 1C8H == 456
.text:00401009 jg loc_4010AA ; 大于456时转移
.text:00401015 cmp eax, 5Ch ; 5CH == 92
.text:00401018 jg short loc_401062 ; 分支1
.text:0040101A jz short loc_401050
... ...
.text:004010AA loc_4010AA: ; CODE XREF: _wmain+9j
.text:004010AA cmp eax, 3E8h ; 3E8h == 1000
.text:004010AF jg short loc_4010FC
.text:004010B1 jz short loc_4010E7
.text:004010B3 cmp eax, 24Ch ; 24Ch == 588
图:二叉树结构
0x2 参考:
-【专题策划】《恶意样本分析手册》合辑
http://blog.nsfocus.net/malware-sample-analysis-summary/?from=timeline&isappinstalled=0