我們前面爲各位讀者分別介紹了轉成if-esle與利用跳轉表兩種優化模式,但是在最後我隱含着提出了一個問題,既如果我們的switch-case分支兩個數之差大於50甚至更多的時候,那麼我們此時是否仍需要利用跳轉表來解決問題呢?很顯然我們不能這樣做,假如我們遇到如下這段代碼:
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;
}
我們通過上面這個稍顯極端的例子可以發現,如果此時編譯器仍以跳轉表的方式來優化的話,那麼會出現什麼情況?這會使得編譯出來的代碼具有多達788字節的冗餘數據,至少會使其體積變爲用Debug模式生成代碼體積的2.7倍以上!
通過與編譯器打這麼長市間的交道,我們猜也能猜得出編譯器肯定不會使用這麼笨的方法,由此便引出的“稀疏矩陣”。
“稀疏矩陣”這名字起的很好,正可謂閱名知意,通過名字我們就可以猜到這應該是一個用來表達稀疏數據的矩陣,正好可以用於我們剛剛所面對的這種情況。
那麼我們的switch-case分支結構生麼時候纔會用到稀疏矩陣,而稀疏矩陣又是怎麼回事呢?
下面就由筆者一一爲各位解答……
(1、)什麼時候應用稀疏矩陣
由於每個編譯器所使用的策略不一樣,因此其“體積-效率比”權值的設定也不盡相同,筆者在這裏只能告訴大家,如果使用跳轉表所生成代碼的體積大於使用稀疏矩陣的體積,那麼一般情況下編譯器就會選擇使用稀疏矩陣來優化此段代碼。
(2、)什麼是稀疏矩陣
單從數學上講,假設我們有一個m行乘以n列的二維陣列(既二維數組),那麼如果此矩陣中非零值數量N小於等於m*n的話,那麼我們就將這個矩陣稱之爲稀疏矩陣。
當然稀疏矩陣的具體定義與其它特點還有很多,這裏我們不再一一討論,現在我們着重講解一下編譯器優化時所使用的稀疏矩陣是個什麼情況。
其實當我們深入的接觸這種優化方式之後可以發現,編譯器所用的優化方案僅僅是思路上借鑑了稀疏矩陣,但是其實際使用中並不符合稀疏矩陣的相關定義,因此筆者認爲將其稱之爲“間接表”更爲合適一些,現在就讓我們共同看一看switch-case分支結構的間接表是怎麼回事。
在VC系列編譯器中,其針對switch-case分支結構的間接表都是用一字節表示的,因此其最小索引值與最大索引值之差不得大於256,否則此優化方法便不再適用。
其次,這個擁有256個byte型元素的數組(間接表)需要與跳轉表相呼應最終才能保證程序流程執行到正確的地方上去。下面我就帶領各位讀者深入的瞭解一下間接表是怎樣被體現出來的。
鑑於以後知識的複雜性,從現在開始,我們將不再使用OllyDbg,因此也請各位讀者跟隨本文一起轉變到IDA這個最專業的逆向工作平臺上去(有關於IDA的使用請參考相關文章)。
我們下面以一個例子來證明,以前面那個源碼爲藍本,看看IDA生成的反彙編代碼是不是更已讀易懂:
.text:00401000 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00401000 _main proc near ; CODE XREF: __tmainCRTStartup+10Ap
.text:00401000
.text:00401000 argc= dword ptr 4
.text:00401000 argv= dword ptr 8
.text:00401000 envp= dword ptr 0Ch
.text:00401000
.text:00401000 mov eax, [esp+argc]
.text:00401004 cmp eax, 0C7h ; switch 200 cases
.text:00401009 ja short loc_40107B ; default
.text:00401009 ; jumptable 00401012 cases 2-5,8-198
.text:0040100B movzx ecx, ds:byte_4010A8[eax]
.text:00401012 jmp ds:off_401090[ecx*4] ; switch jump
.text:00401019
.text:00401019 $LN6: ; DATA XREF: _main:off_401090o
.text:00401019 push 0 ; jumptable 00401012 case 0
.text:0040101B push offset Format ; "argc=0"
.text:00401020 call ds:__imp__printf
.text:00401026 add esp, 8
.text:00401029 xor eax, eax
.text:0040102B retn
.text:0040102C ; ---------------------------------------------------------------------------
.text:0040102C
.text:0040102C $LN5: ; CODE XREF: _main+12j
.text:0040102C ; DATA XREF: _main:off_401090o
.text:0040102C push 1 ; jumptable 00401012 case 1
.text:0040102E push offset aArgcD ; "argc=%d"
.text:00401033 call ds:__imp__printf
.text:00401039 add esp, 8
.text:0040103C xor eax, eax
.text:0040103E retn
.text:0040103F ; ---------------------------------------------------------------------------
.text:0040103F
.text:0040103F $LN4: ; CODE XREF: _main+12j
.text:0040103F ; DATA XREF: _main:off_401090o
.text:0040103F push 6 ; jumptable 00401012 case 6
.text:00401041 push offset aArgcD ; "argc=%d"
.text:00401046 call ds:__imp__printf
.text:0040104C add esp, 8
.text:0040104F xor eax, eax
.text:00401051 retn
.text:00401052 ; ---------------------------------------------------------------------------
.text:00401052
.text:00401052 $LN3: ; CODE XREF: _main+12j
.text:00401052 ; DATA XREF: _main:off_401090o
.text:00401052 push 7 ; jumptable 00401012 case 7
.text:00401054 push offset aArgcD ; "argc=%d"
.text:00401059 call ds:__imp__printf
.text:0040105F add esp, 8
.text:00401062 xor eax, eax
.text:00401064 retn
.text:00401065 ; ---------------------------------------------------------------------------
.text:00401065
.text:00401065 $LN2: ; CODE XREF: _main+12j
.text:00401065 ; DATA XREF: _main:off_401090o
.text:00401065 push 0C7h ; jumptable 00401012 case 199
.text:0040106A push offset aArgcD ; "argc=%d"
.text:0040106F call ds:__imp__printf
.text:00401075 add esp, 8
.text:00401078 xor eax, eax
.text:0040107A retn
.text:0040107B ; ---------------------------------------------------------------------------
.text:0040107B
.text:0040107B loc_40107B: ; CODE XREF: _main+9j
.text:0040107B ; _main+12j
.text:0040107B ; DATA XREF: ...
.text:0040107B push eax ; default
.text:0040107B ; jumptable 00401012 cases 2-5,8-198
.text:0040107C push offset aNnumDError ; "nNum=%d,error!"
.text:00401081 call ds:__imp__printf
.text:00401087 add esp, 8
.text:0040108A xor eax, eax
.text:0040108C retn
.text:0040108C ; ---------------------------------------------------------------------------
我想通過上面的反彙編代碼已經足以能表明IDA的強大之處了,很明顯它已經自動識別出了這就是一個switch-case語句,併爲我們生成了清晰明瞭的註釋,看看這是多麼愜意呀!
但是筆者在這裏要提醒各位讀者注意,IDA也並不是每次都能靈驗的(在實戰時大多數情況都是如此),因此在學習逆向時一定要注意學會忽略IDA的註釋,否則總有後悔的那一天的。
通過上面的代碼我們可以分析出主要是以下兩句代碼在控制其流程:
.text:0040100B movzx ecx, ds:byte_4010A8[eax]
.text:00401012 jmp ds:off_401090[ecx*4] ; switch jump
爲了更好的理解第一句彙編指令的意思,我們需要看看4010A8處保存了些什麼信息:
.text:004010A8 byte_4010A8 db 0 ; DATA XREF: _wmain+Br
.text:004010A8 ; indirect table for switch statement
.text:004010A9 db 1
.text:004010AA db 5
…………
.text:004010AD db 5
.text:004010AE db 2
.text:004010AF db 3
.text:004010B0 db 5
…………
.text:0040116E db 5
.text:0040116F db 4
由於我們的switch-case分支結構擁有6個分支,因此間接表裏保存的內容都是在0-5之間的,然後便根據此索引來確定調轉到第幾個分支上去。下面我們來人工模擬一下,假如此時switch-case分支結構接收到的判斷變量爲166,那麼首先會通過執行“mov eax, [esp+argc]”將值傳遞給eax,進行簡單的對比檢查之後,已確定其值未超過switch-case分支的最大值,然後就通過執行“movzx ecx, ds:byte_4010A8[eax]”指令,以eax爲索引到地址爲4010A8h的間接表中尋取相對應的值爲跳轉表索引,並將此索引保存在ecx裏,最後在以ecx爲索引執行“jmp ds:off_401090[ecx*4]”指令跳轉到正確定分支上去,整體流程如下圖:
argc = 166 (假設傳入的數值爲166)
┃
┃
▽
mov eax, [esp+argc]
eax = 166
┃
┃
┃
▽ byte_4010A8
movzx ecx, ds:byte_4010A8[eax] *---*
ecx = 5 000| 0 |
┃ 001| 1 |
┃ 002| 2 |
┃ ...| . |
┃ 165| 5 |
┃ eax--> 166| 5 |
┃ ...| . |
┃ *---*
┃
▽ off_401090
jmp ds:off_401090[ecx*4] *---------------------*
000| 401019h | cases_0 |
004| 40102Ch | cases_1 |
008| 40103Fh | cases_6 |
012| 401052h | cases_7 |
016| 401065h | cases_199 |
ecx*4--> 020| 40107Bh | default |
*---------------------*
通過上面這幅流程圖,相信各位讀者已經明白間接表是怎麼回事了,那麼下面我們就開始來點有挑戰的。
1.6.1、switch-case分支結構與平衡二叉樹
雖然我們前面所學的知識已經足夠對付大多數情況,但是畢竟大多數不代表所有,而當我們學完本小節之後,那麼各位讀者的switch-case分支結構學習之旅纔算是真正的告一段落。
通過上面的中轉表的數據類型我們就能判斷出他所能應付的只限於分支小於等於256的情況,如果超過256之後這種與跳轉表相配合的中轉表肯定會隨之失效,取而代之的便是我們著名的二叉樹了(準確的說應該叫平衡二叉樹),有關於什麼是二叉樹或平衡二叉樹筆者在這裏很難以一個小節的容量將其介紹清楚,因此希望各位讀者參考一下百度百科相關的內容,畢竟數據結構不是本文討論的主題。
爲了是各位讀者可是順利的閱讀下去,筆者在這裏爲各位簡單的爲大家介紹一下二叉樹。
所謂的二叉樹查找法我們也可以暫時將其理解爲折半查找法,例如我們想快速在1-7幾個數字中找到某個數值,那麼我們肯定是現將其與1-7的中間數4作比對看看它是比4小還是比4大,如果比4大的話那麼就在4-7之間取一箇中間數6與其比對,如果大於6那麼這個數就是7,如果小於6那麼這個數就是5了。如果將所有的可能性化成一個流程的話,那麼這幅圖大概就是以下這個樣子:
4
/ /
2 6
/ / / /
1 3 5 7
由此可知當比對次數(深度)爲k時,則最多可以查找2^(k)-1個結點,例如如果用此算法查找419萬條數據的某一條的話,那麼其比對次數不會超過20次。
大致瞭解了二叉樹的原理與優勢之後,我們先看一段代碼:
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 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00401000 _main proc near ; CODE XREF: __tmainCRTStartup+10A p
.text:00401000
.text:00401000 argc = dword ptr 4
.text:00401000 argv = dword ptr 8
.text:00401000 envp = dword ptr 0Ch
.text:00401000
.text:00401000 mov eax, [esp+argc]
.text:00401004 cmp eax, 1C8h
.text:00401009 jg loc_4010B0
.text:0040100F jz loc_40109A
.text:00401015 cmp eax, 5Ch
.text:00401018 jg short loc_401065
.text:0040101A jz short loc_401052
.text:0040101C mov ecx, eax
.text:0040101E sub ecx, 1
.text:00401021 jz short loc_40103F
.text:00401023 sub ecx, 18h
.text:00401026 jnz loc_401113
.text:0040102C push 19h
.text:0040102E push offset Format ; "argc25=%d"
...... ...... ......
.text:0040103F ; ---------------------------------------------------------------------------
.text:0040103F
.text:0040103F loc_40103F: ; CODE XREF: _main+21j
.text:0040103F push 1
.text:00401041 push offset aArgc10 ; "argc1=0"
...... ...... ......
.text:00401052 ; ---------------------------------------------------------------------------
.text:00401052
.text:00401052 loc_401052: ; CODE XREF: _main+1Aj
.text:00401052 push 5Ch
.text:00401054 push offset aArgc12D ; "argc12=%d"
...... ...... ......
.text:00401065 ; ---------------------------------------------------------------------------
.text:00401065
.text:00401065 loc_401065: ; CODE XREF: _main+18j
.text:00401065 cmp eax, 76h
.text:00401068 jz short loc_401087
.text:0040106A cmp eax, 106h
.text:0040106F jnz loc_401113
.text:00401075 push eax
.text:00401076 push offset aArgc1D ; "argc1=%d"
...... ...... ......
.text:00401087 ; ---------------------------------------------------------------------------
.text:00401087
.text:00401087 loc_401087: ; CODE XREF: _main+68j
.text:00401087 push 76h
.text:00401089 push offset aArgc118D ; "argc118=%d"
...... ...... ......
.text:0040109A ; ---------------------------------------------------------------------------
.text:0040109A
.text:0040109A loc_40109A: ; CODE XREF: _main+Fj
.text:0040109A push 1C8h
.text:0040109F push offset aArgc456D ; "argc456=%d"
...... ...... ......
.text:004010B0 ; ---------------------------------------------------------------------------
.text:004010B0
.text:004010B0 loc_4010B0: ; CODE XREF: _main+9j
.text:004010B0 cmp eax, 3E8h
.text:004010B5 jg short loc_401105
.text:004010B7 jz short loc_4010EF
.text:004010B9 cmp eax, 24Ch
.text:004010BE jz short loc_4010D9
.text:004010C0 cmp eax, 380h
.text:004010C5 jnz short loc_401113
.text:004010C7 push eax
.text:004010C8 push offset aArgc8960 ; "argc896=0"
...... ...... ......
.text:004010D9 ; ---------------------------------------------------------------------------
.text:004010D9
.text:004010D9 loc_4010D9: ; CODE XREF: _main+BEj
.text:004010D9 push 24Ch
.text:004010DE push offset aArgc588D ; "argc588=%d"
...... ...... ......
.text:004010EF ; ---------------------------------------------------------------------------
.text:004010EF
.text:004010EF loc_4010EF: ; CODE XREF: _main+B7j
.text:004010EF push 3E8h
.text:004010F4 push offset aArgc1000D ; "argc1000=%d"
...... ...... ......
.text:00401105 ; ---------------------------------------------------------------------------
.text:00401105
.text:00401105 loc_401105: ; CODE XREF: _main+B5j
.text:00401105 cmp eax, 442h
.text:0040110A jz short loc_40113B
.text:0040110C cmp eax, 834h
.text:00401111 jz short loc_401125
.text:00401113
.text:00401113 loc_401113: ; CODE XREF: _main+26j
.text:00401113 ; _main+6Fj ...
.text:00401113 push eax
.text:00401114 push offset aDefaultNnumDEr ; "default nNum=%d,error!"
...... ...... ......
.text:00401125 ; ---------------------------------------------------------------------------
.text:00401125
.text:00401125 loc_401125: ; CODE XREF: _main+111j
.text:00401125 push 834h
.text:0040112A push offset aArgc2100D ; "argc2100=%d"
...... ...... ......
.text:0040113B ; ---------------------------------------------------------------------------
.text:0040113B
.text:0040113B loc_40113B: ; CODE XREF: _main+10Aj
.text:0040113B push 442h
.text:00401140 push offset aArgc1090D ; "argc1090=%d"
...... ...... ......
.text:00401150 _main endp
對於這種二叉樹結構的識別,一般情況下只需要看兩步跳轉即可,如果其每次跳轉所對比的值都是其後面分支跳轉的中間值之一,那麼這就有可能是一個二叉樹,我們以本程序爲例:
第一次跳轉及其後面的分支:
.text:00401004 cmp eax, 1C8h ; 跳轉
.text:00401009 jg loc_4010B0
...... ......
.text:00401015 cmp eax, 5Ch
.text:00401018 jg short loc_401065 ; 分支1
...... ......
.text:004010B0 loc_4010B0: ; CODE XREF: _main+9 j
.text:004010B0 cmp eax, 3E8h
.text:004010B5 jg short loc_401105 ; 分支2
我們不難發現上面的第一個跳轉所對比的值位於其後面兩個分支對比值的區域中,我們在跟進分支1看看:
.text:00401015 cmp eax, 5Ch
.text:00401018 jg short loc_401065 ; 分支1
...... ......
.text:0040101C mov ecx, eax
.text:0040101E sub ecx, 1
.text:00401021 jz short loc_40103F ; 子分支1
...... ......
.text:00401065 loc_401065: ; CODE XREF: _main+18 j
.text:00401065 cmp eax, 76h
.text:00401068 jz short loc_401087 ; 子分支2
同樣的,分支1也是位於其兩個子分支比對值的區域內,其實現在我們至少就有60%的把握可以確定這是一個二叉樹結構了,當然,如果想要更爲精準的結果,我們還是要把大部分流程跟一遍的,下面就是本二叉樹的結構圖:
456
/ /
/ /
/ /
/ /
/ /
92 1000
/ / / /
1 118 588 1090
/ / / / / / / /
def 25 def 262 def 896 def 2100
細心的讀者應該發現了,其實上面的代碼是經過筆者細心修剪的,所以看起來結構清晰明瞭,當我們實際做逆向時也應該如此,在一開始要去其枝蔓留其骨幹,這樣才能更爲順利的識別類似於二叉樹這樣的較複雜數據結構。
到這裏本小節已經近尾聲了,不知道讀者們是否有所發覺,其實本小節都是在講解兩種數據結構而已,因此其實對於switch-case分支結構的識別就是對這兩種數據結構的識別,但是我們怎樣才能知道這種數據結構是編譯器生成的,而並非是別人寫的代碼呢?這個問題很難回答,我們的逆
向工程越是複雜,越是靠後的內容,理論上其不確定性也在不斷增加,就像是上面的代碼,如果我們將所有分支按照二叉樹的規則排好序,並用if-else分支來實現它的話,那麼其生成的代碼與以上反彙編代碼不會相差多少,有興趣的讀者可以自己試驗一下。
代碼逆向(五)——switch-case識別技巧提高
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.