switch語句逆向分析

风花赏秋月發表於2024-10-18

switch語句逆向分析

有序

小於3時

  • 程式碼:

#include "stdafx.h"

void MySwitch(int x){

switch(x) {

case 1:

printf("num is 1\n");

break;

case 2:

printf("num is 2\n");

break;

case 3:

printf("num is 3\n");

break;

default:

printf("no cases match\n");

break;

}

}

int main(int argc, char* argv[])

{

MySwitch(2);

return 0;

}

  • 反彙編:

10: switch(x) {

0040D7A8 mov eax,dword ptr [ebp+8]

0040D7AB mov dword ptr [ebp-4],eax

0040D7AE cmp dword ptr [ebp-4],1

0040D7B2 je MySwitch+32h (0040d7c2)

0040D7B4 cmp dword ptr [ebp-4],2

0040D7B8 je MySwitch+41h (0040d7d1)

0040D7BA cmp dword ptr [ebp-4],3

0040D7BE je MySwitch+50h (0040d7e0)

0040D7C0 jmp MySwitch+5Fh (0040d7ef)

11: case 1:

12: printf("num is 1\n");

0040D7C2 push offset string "num is 1\n" (00422fc4)

0040D7C7 call printf (00401060)

0040D7CC add esp,4

13: break;

0040D7CF jmp MySwitch+6Ch (0040d7fc)

14: case 2:

15: printf("num is 2\n");

0040D7D1 push offset string "num is 2\n" (00422fb8)

0040D7D6 call printf (00401060)

0040D7DB add esp,4

16: break;

0040D7DE jmp MySwitch+6Ch (0040d7fc)

17: case 3:

18: printf("num is 3\n");

0040D7E0 push offset string "num is 3\n" (00422fac)

0040D7E5 call printf (00401060)

0040D7EA add esp,4

19: break;

0040D7ED jmp MySwitch+6Ch (0040d7fc)

20: default:

21: printf("no cases match\n");

0040D7EF push offset string "Hello World!\n" (0042201c)

0040D7F4 call printf (00401060)

0040D7F9 add esp,4

22: break;

23: }

24: }

  • 反彙編分析:
  • switch語句逆向分析

1.反彙編程式碼為將引數x的值賦給eax

2.將eax的值放入堆疊中

3.將前面放入堆疊中的eax拿出來和第1個case中的條件進行比較(也就是比較引數x和case)

4.判斷是否要跳轉,je:jump equal,前面比較的兩個數相同則跳轉,跳轉的地址為case 1對應的地址

5.如果沒有跳轉則繼續將引數和第2個case中的條件進行比較

6.依舊是根據比較的結果判斷是否要跳轉,跳轉的地址為case 2對應的地址

7.如果沒有跳轉則繼續將引數和第3個case中的條件進行比較

8.依舊是根據比較的結果判斷是否要跳轉,跳轉的地址為case 3對應的地址

9.如果沒有跳轉則絕對跳轉到default:

下面的內容就是 case 1,case 2,case 3了

可以注意到,case裡面的break都對應為跳出switch,而default裡的break因為下面就已經是退出switch所以沒有生成對應的彙編程式碼

case1裡的break

case2裡的break

case3裡的break

透過上面的分析,發現此時(switch 中的case數量≤3時)的反彙編程式碼和if else並無本質上的區別,都是要依次比較判斷條件

大於三時:(同理簡化)

ja指令:jump above,大於時跳轉(無符號),也就是比較引數x-1和3(case中的最大差值),最大差值就是最大值減最小值,此案例中就是4-1=3

如果x-1>3則跳轉,如果前面引數沒有減1的話,就變成了直接判斷x>3,如果此時x=4也會產生跳轉,不符合程式的邏輯(原本x=4應該對應跳轉到case 4)

注意到這裡採用的是無符號比較,而不採用有符號比較指令jg:jump greater,大於時跳轉(有符號),為什麼?

這裡的比較程式碼其實就是判斷引數是否在(case中的最小值,case中的最大值)這個區間內

當引數小於case中的最小值時,前面的sub ecx,case中的最小值就後就會產生下溢,此時將其看作無符號數就會相當大,一定會大於case中的最大差值,舉個簡單的例子,假如此時的引數為0,0-1 = -1對應的是十六進位制為FFFF FFFF,將其看作無符號數就是4294967295

0040D7BB ja $L539+0Fh (0040d803)

跳轉的地址為:0040d803,對應為default的地址

23: default:

24: printf("no cases match\n");

0040D803 push offset string "Hello World!\n" (0042201c)

0040D808 call printf (00401060)

0040D80D add esp,4

25: break;

如果前面沒有跳轉,這裡則又將前面儲存的引數-1的值取了出來,並賦值給edx

0040D7BD mov edx,dword ptr [ebp-4]

過上面的分析,發現此時(switch 中的case數量>3時)的反彙編程式碼和if else的差別就體現出來了,但到達一定條件後一般都會採用到case地址表首地址+偏移的方法,此時是將引數的值減case中的最小值,然後判斷這個減完的數值是否大於case中的最大差值,如果大於則直接跳轉到default,如果小於或等於則透過jmp [儲存case地址表的首地址+偏移×4]的方式直接跳轉到對應case的地址,而不再像if else中那樣依次比較來判斷是否要跳轉

無序

  • 程式碼

switch(x) {

case 2:

printf("num is 2\n");

break;

case 3:

printf("num is 3\n");

break;

case 4:

printf("num is 4\n");

break;

case 1:

printf("num is 1\n");

break;

default:

printf("no cases match\n");

break;

}

簡單地調換了一下case語句的順序,觀察其反彙編

  • 反彙編程式碼

10: switch(x) {

0040D7A8 mov eax,dword ptr [ebp+8]

0040D7AB mov dword ptr [ebp-4],eax

0040D7AE mov ecx,dword ptr [ebp-4]

0040D7B1 sub ecx,1

0040D7B4 mov dword ptr [ebp-4],ecx

0040D7B7 cmp dword ptr [ebp-4],3

0040D7BB ja $L539+0Fh (0040d803)

0040D7BD mov edx,dword ptr [ebp-4]

0040D7C0 jmp dword ptr [edx*4+40D821h]

11: case 2:

12: printf("num is 2\n");

0040D7C7 push offset string "num is 1\n" (00422fd0)

0040D7CC call printf (00401060)

0040D7D1 add esp,4

13: break;

0040D7D4 jmp $L539+1Ch (0040d810)

14: case 3:

15: printf("num is 3\n");

0040D7D6 push offset string "num is 2\n" (00422fc4)

0040D7DB call printf (00401060)

0040D7E0 add esp,4

16: break;

0040D7E3 jmp $L539+1Ch (0040d810)

17: case 4:

18: printf("num is 4\n");

0040D7E5 push offset string "num is 3\n" (00422fb8)

0040D7EA call printf (00401060)

0040D7EF add esp,4

19: break;

0040D7F2 jmp $L539+1Ch (0040d810)

20: case 1:

21: printf("num is 1\n");

0040D7F4 push offset string "num is 271\n" (00422fac)

0040D7F9 call printf (00401060)

0040D7FE add esp,4

22: break;

0040D801 jmp $L539+1Ch (0040d810)

23: default:

24: printf("no cases match\n");

0040D803 push offset string "no cases match\n" (0042201c)

0040D808 call printf (00401060)

0040D80D add esp,4

25: break;

26: }

27: }

switch語句逆向分析

EAX = 00000002 EBX = 7FFDB000

ECX = 00000001 EDX = 00000001

ESI = 00000000 EDI = 0012FF28

EIP = 0040D4C0 ESP = 0012FED8

EBP = 0012FF28 EFL = 00000293

記憶體

0040D521 D6 D4 40 00 衷@.

0040D525 C7 D4 40 00 竊@.

0040D529 F4 D4 40 00 粼@.

0040D52D E5 D4 40 00 逶@.

在值連續的情況下,順序並不會影響生成大表

空缺地址透過填充default語句塊地址解決,但會造成記憶體浪費

;大表

0040D8A6 1F D8 40 00 .谸.

0040D8AA 2E D8 40 00 .谸.

0040D8AE 3D D8 40 00 =谸.

0040D8B2 4C D8 40 00 L谸.

0040D8B6 5B D8 40 00 [谸.

0040D8BA 6A D8 40 00 j谸.

0040D8BE 79 D8 40 00 y谸.

0040D8C2 88 D8 40 00 堌@.

;小表

0040D8C6 00 01 02 07 ....

0040D8CA 07 07 03 07 ....

0040D8CE 04 07 07 07 ....

0040D8D2 05 07 07 06 ....

小表的解釋:

當空缺值太多時記憶體的浪費也會變多,編譯器當然知道這樣不是辦法,所以利用小表來解決這個問題。小表可以看作是一個智慧蹦床,對於不同的玩家會給出不同的力,遇到沒有付費的玩家(空缺值)直接將他丟擲場外(給出引數,使其跳轉到default的語句塊),遇到付費玩家(存在的值)則按照他的等級給出不同的力(給出引數,使其跳轉到其對應的語句塊)

可以看出,在小表中所有的空缺值都是07(因為在這個樣例中,當edx=7時[edx*4+40D8A6h]的地址為default語句塊的地址),而存在的值的對應值從0遞增。

相關文章