前面把一些基本的運算都記錄完了,後面開始流程結構,其流程結構主要分爲分支結構和循環結構。對於流程結構的逆向識別,主要在於首先需要識別是什麼,然後識別語句體(花括號上下界位置),因爲確定好了這兩步後,其裏面的主體內容就是前面的基本運算或者又是流程結構(遞歸進行逐步分解)。
對於流程結構主要就以debug版爲主了,因爲debug版更加利於研究其結構如何,而release中除了結構以外,還混有一些的優化,對於優化的情況,會再額外對其release進行分析。
先看分支語句,其大體上可分爲if和switch,因爲其兩者的實現機制有稍許的不同,所以這裏打算分開來記錄,這一篇先記錄if語句。
對於if語句主要分以下三類
單分支
雙分支
多分支
下面先來看一下單分支的情況
int main(int argc, char* argv[])
{
if(argc)
{
printf("if argc");
}
printf("hello world\r\n");
return 0;
}
對應的彙編代碼,這裏是Release版代碼,因爲debug差不多所以就選擇一個觀察
9: if(argc)
00401028 83 7D 08 00 cmp dword ptr [ebp+8],0
0040102C 74 0D je main+2Bh (0040103b)//爲零則跳轉,跳轉的位置是if結束
10: {
11: printf("if argc");
0040102E 68 2C 20 42 00 push offset string "if argc" (0042202c)
00401033 E8 38 00 00 00 call printf (00401070)
00401038 83 C4 04 add esp,4
12: }
13: printf("hello world\r\n");
0040103B 68 1C 20 42 00 push offset string "hello world\r\n" (0042201c)
對於上面的彙編代碼,如果argc爲零則進行跳轉,其跳轉的地址就是if語句的結束(下花括號),而上花括號就是je跳轉的下一行代碼開始,這樣子我們就可以確定了if語句塊的上下界位置了。
然後對於跳轉邏輯,可以發現此處的彙編邏輯代碼與我們的高級代碼的邏輯是相反的,因爲按照if語句的規定,滿足if判定的表達式才能執行if的語句塊,而彙編語言的條件跳轉卻是滿足某條件則跳轉(繞過語句塊不執行),這一點是和C語言是相反的。
那麼如果C語言編譯可以把if和else的語句塊進行相互的調換,那麼是不是就能和C語言的邏輯一致呢(調換後if的執行塊在下面,那麼條件符合就會跳轉到下面執行)。
是的,這樣子理論上是可行的,只是因爲C語言的根據代碼行的位置來決定編譯後的二進制代碼的地址高低的(有時會使用標號相減來得到代碼段的長度),所以C編譯器不能隨意改變代碼行在內存中的順序。
下面簡化一下單分支的彙編情況
jxx IF_END
IF_BEGIN:
....
IF_END:
結構特性
條件跳轉是增量跳轉 - 向下跳
跳轉的目標標號上面沒有jmp
確定好if語句的上下界後,然後對其條件的反條件進行還原即可。
下面來看雙分支結構的情況
int main(int argc, char* argv[])
{
if(argc)
{
printf("if if");
}
else
{
printf("else else");
}
return 0;
}
對應的彙編代碼講解
9: if(argc)
0040FB98 83 7D 08 00 cmp dword ptr [ebp+8],0
0040FB9C 74 0F je main+2Dh (0040fbad)
10: {
11: printf("if if");
0040FB9E 68 28 50 42 00 push offset string "if if" (00425028)
0040FBA3 E8 F8 16 FF FF call printf (004012a0)
0040FBA8 83 C4 04 add esp,4
12: }
13: else
0040FBAB EB 0D jmp main+3Ah (0040fbba) //這裏跳轉到else的結尾
14: {
15: printf("else else");
//上面的if je跳轉到這裏,上面有一jmp,說明這裏是一個else結構,否則是一個單分支
0040FBAD 68 8C 61 42 00 push offset string "else else" (0042618c)
0040FBB2 E8 E9 16 FF FF call printf (004012a0)
0040FBB7 83 C4 04 add esp,4
16: }
17: return 0;
0040FBBA 33 C0 xor eax,eax
這裏雙分支的情況,對比之前的單分支而言,就是jxx目標跳轉的地址的上方會多一個jmp,那麼此時可以確定是一個else結構,那麼對於else結構的結束位置(else下花括號),就是該jmp的目標地址。
jxx IF_END
IF_BEGIN:
....
jmp ELSE_END
IF_END:
ELSE_BEGIN:
...
ELSE_END:
結構特性
條件跳轉是增量跳轉 - 向下跳
跳轉的目標標號上面有jmp
OK,下面需要先說一下編譯器的O1和O2優化,因爲這兩種優化都可能會出現在Release版本中,並且對於後面的流程結構都可能會有該優化哦。
速度優先-O2方案
對於同樣的流程搞多套 -減少彙總,增加節點 (速度快,都爲單分支)
體積最小-O1方案
編譯器會使用圖結構展示流程,然後根據圖算法歸併掉一些公共節點 (分支彙總,減少節點)
對於上面的優化方案,在vc6.0的項目選項中可選擇,對於Release而言默認一般是O2方案
C/C++
Optimizations
Maximize Speed - O2方案
Minimize Size - O1方案
所以我們直接來看一下上例代碼中的release版情況。
.text:00401000 mov eax, [esp+argc]
.text:00401004 test eax, eax
.text:00401006 jz short IF_END //跳轉到IF的結束處
.text:00401008 push offset aIfIf ; "if if"
.text:0040100D call sub_401030
.text:00401012 add esp, 4
.text:00401015 xor eax, eax
.text:00401017 retn
.text:00401018 ; ---------------------------------------------------------------------------
.text:00401018 //這裏的上方沒有jmp,說明是一個單分支語句
.text:00401018 IF_END: ; CODE XREF: _main+6↑j
.text:00401018 push offset aElseElse ; "else else"
.text:0040101D call sub_401030
.text:00401022 add esp, 4
.text:00401025 xor eax, eax
.text:00401027 retn
可以發現,在Release版本中,其雙分支被優化爲單分支了,那麼我們來還原下對於的高級語言代碼,就能明白O2方案了。
int main(int argc, char* argv[])
{
if(argc)
{
printf("if if");
return 0; //return 0語句作爲公共語句塊部分,可以對if語句體和else語句體都添加該代碼
}
printf("else else");
return 0;
}
看完高級語言代碼後應該就明白了,其實對於O2來說,可以將公共塊都塞到if和else的語句塊內,這樣子是等價的,因爲這裏剛好是一個return,所以可以省略else,也就是說該release版本對於debug版本來說,其實少掉的是一行jmp
0040FBAB EB 0D jmp main+3Ah (0040fbba) //這裏跳轉到else的結尾
將jmp的目標地址處的全部代碼替換了該jmp指令。
那麼大家可能就會有疑問了,假如公共的語句內容比較多的時候,還會這麼處理麼?假如公共語句塊比較多,那麼其實這麼子就化不來了,因爲相當於代碼有了兩份,比較冗餘。所以當公共語句塊的代碼量較多時,此時編譯器又自然會使用O1方案了。
下面我們只需來改動一下源碼,使其進行O1方案進行編譯(外面外提優化)
int main(int argc, char* argv[])
{
if(argc)
{
printf("if if");
}
else
{
printf("else else");
}
//此時下面公共語句塊內容較多,防止O2優化
printf("hello world");
printf("hello world");
printf("hello world");
return 0;
}
Release版本
.text:00401000 mov eax, [esp+argc]
.text:00401004 test eax, eax
.text:00401006 jz short loc_40100F
.text:00401008 push offset aIfIf ; "if if"
.text:0040100D jmp short loc_401014
.text:0040100F ; ---------------------------------------------------------------------------
//這裏可以看出來 else 結構出來了,因爲上面有一jmp
.text:0040100F
.text:0040100F loc_40100F: ; CODE XREF: _main+6↑j
.text:0040100F push offset aElseElse ; "else else"
.text:00401014
.text:00401014 loc_401014: ; CODE XREF: _main+D↑j
.text:00401014 call sub_401040
.text:00401019 add esp, 4
.text:0040101C push offset aHelloWorld ; "hello world"
.text:00401021 call sub_401040
//.......省略後面printf打印
看完上面的彙編代碼,其實如果對其進行高級代碼的還原,這裏C語言是模擬不出來的,因爲上面的語句體中只使用了一行push,爲什麼呢?因爲對於if和else的語句體而言,都只是一個printf,而其兩者最大的區別就是參數不同,所以O1優化就將語句體的公共部分放到了最後的公共語句塊中,這樣子很明顯就會減少了體積。
所以在還原的時候,我們需要將這部分公共的再給他還原回去,這樣子就能還原回高級代碼了。
.text:00401000 mov eax, [esp+argc]
.text:00401004 test eax, eax
.text:00401006 jz short loc_40100F
.text:00401008 push offset aIfIf ; "if if"
.text:00401014 call sub_401040 //粘貼到一處
.text:00401019 add esp, 4
.text:0040100D jmp short loc_401014
.text:0040100F ; ---------------------------------------------------------------------------
.text:0040100F
.text:0040100F loc_40100F: ; CODE XREF: _main+6↑j
.text:0040100F push offset aElseElse ; "else else"
.text:00401014 call sub_401040 //粘貼到二處
.text:00401019 add esp, 4
.text:00401014
.text:00401014 loc_401014: ; CODE XREF: _main+D↑j
//將這兩行代碼複製走,還回上面的語句中
.text:0040101C push offset aHelloWorld ; "hello world"
.text:00401021 call sub_401040
//.......省略後面printf打印
按照這裏子處理後,可以發現其模樣就和debug很像了,可以按其指令還原。所以有時候在遇到一些單獨指令時無法還原時,考慮是不是使用了該種優化,如果使用了這種優化,那麼需要把缺的這部分代碼添回去即可。
到了這裏,應該能明白O1和O2的優化了,對於體積優先的O1方案,也就是做了一個語句體內公共代碼外提的工作,而對於速度優先的O2方案,就是做了一個語句體內添加公共代碼的動作。
最後再來看一下多分支的情況,其實對於多分支而言,其實就是多個雙分支而已,理解完上面的雙分支,這個就很好理解了。
int main(int argc, char* argv[])
{
if(argc == 0)
{
printf("argc == 0");
}
else if (argc == 1)
{
printf("argc == 1");
}
else if (argc == 2)
{
printf("argc == 2");
}
return 0;
}
對應的彙編代碼
9: if(argc == 0)
0040D718 83 7D 08 00 cmp dword ptr [ebp+8],0
0040D71C 75 0F jne main+2Dh (0040d72d) //跳轉到if結束
10: {
11: printf("argc == 0");
0040D71E 68 BC 2F 42 00 push offset string "argc == 0" (00422fbc)
0040D723 E8 48 39 FF FF call printf (00401070)
0040D728 83 C4 04 add esp,4
12: }
13: else if (argc == 1)
0040D72B EB 28 jmp main+55h (0040d755) //0040d755 爲else結束
這裏if結束處的上方有一jmp,說明這裏應該爲 else 結構
0040D72D 83 7D 08 01 cmp dword ptr [ebp+8],1
0040D731 75 0F jne main+42h (0040d742)
14: {
15: printf("argc == 1");
0040D733 68 2C 20 42 00 push offset string "if argc" (0042202c)
0040D738 E8 33 39 FF FF call printf (00401070)
0040D73D 83 C4 04 add esp,4
16: }
17: else if (argc == 2)
0040D740 EB 13 jmp main+55h (0040d755)
//這裏同理 應該爲 else 結構
0040D742 83 7D 08 02 cmp dword ptr [ebp+8],2
0040D746 75 0D jne main+55h (0040d755)
18: {
19: printf("argc == 2");
0040D748 68 1C 20 42 00 push offset string "else else" (0042201c)
0040D74D E8 1E 39 FF FF call printf (00401070)
0040D752 83 C4 04 add esp,4
20: }
21: return 0;
// IF0 else結束位置,IF1 else結束位置,也爲IF2結束位置
0040D755 33 C0 xor eax,eax
其實在結構上而已,和雙分支是沒有什麼區別的,其實我們直接按雙分支的套路也能還原出對應的等價高級代碼
int main(int argc, char* argv[])
{
if(argc == 0)
{
printf("argc == 0");
}
else
{
if (argc == 1)
{
printf("argc == 1");
}
else
{
if (argc == 2)
{
printf("argc == 2");
}
}
}
return 0;
}
這裏還原的高級代碼是和上面是等價的。好了既然多到了多分支,那麼總有些不同吧,其實對於多分支的結構特點而言,就是jxx目標上一行有jmp,高亮後發現不止一個分支是同樣的目標,所以在上面的彙編代碼中,可以發現其jmp的地址都爲0040D755,是一致的。
這裏也很好理解,因爲對於多分支而言,最終只能執行一個語句塊,不管執行到哪個語句塊,其最終都要去同一個地址處執行後面的流程代碼,所以其jmp的地址會是一致的。
下面整理下多分支的結構情況
jxx IF_END1
IF_BEGIN1:
...
jmp ELSE_END //jmp目標一致
IF_END1:
ELSE_BEGIN:
IF_BEGIN2:
...
jmp ELSE_END //jmp目標一致
IF_END2:
結構特性
jxx目標上一行有jmp,並不止一個分支是同樣的目標
在release版中,可能會存在上面說的O1優化,那麼此時會有代碼外提的動作,所以可能會導致其jmp的目標地址不一致,在還原時我們需要將外提的代碼塞回去,此時多個jmp處的標號會重合,表示其相當於是跳到同一目標。