逆向-分支結構上

前面把一些基本的運算都記錄完了,後面開始流程結構,其流程結構主要分爲分支結構和循環結構。對於流程結構的逆向識別,主要在於首先需要識別是什麼,然後識別語句體(花括號上下界位置),因爲確定好了這兩步後,其裏面的主體內容就是前面的基本運算或者又是流程結構(遞歸進行逐步分解)。

對於流程結構主要就以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處的標號會重合,表示其相當於是跳到同一目標。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章