演算法分析:XCTF 4th-WHCTF-2017

極安御信發表於2022-04-13

1.下載附件是一個32位無殼console的exe,執行一遍發現基本邏輯就是提示輸入一個字串,輸入以後透過一個判斷來提示不同內容

2.既然是exe,那先用OD跑一邊

檢視是否有關鍵字串

發現關鍵字元“Wrong!”,這個字元就在我們輸入錯誤後提示的,可以看第一大步的圖,雙擊找到這個字串的引用(如下圖)

大概分析引用“Wrong!”的程式碼段

發現跳轉到輸出“Wrong!”函式的jnz在00401341,跳轉條件就是eax不等於1,那說明只要eax只要等於1則就不跳轉,執行輸出“Right!flag is your input”,那現在我們需要做的就是逆推EAX的來源(如上圖)

逆推EAX的來源

00401337  |.  8B4424 08     mov eax,dword ptr ss:[esp+0x8]           ;  kernel32.BaseThreadInitThunk

0040133B  |.  83C4 08       add esp,0x8

0040133E  |.  83F8 01       cmp eax,0x1

00401341  |.  75 07         jnz short 1c40a4a4.0040134A

00401343  |.  68 90A04000   push 1c40a4a4.0040A090                   ;  Right!flag is your input\n

00401348  |.  EB 05         jmp short 1c40a4a4.0040134F

0040134A  |>  68 C0A04000   push 1c40a4a4.0040A0C0                   ;  Wrong!\n

0040134F  |>  E8 1C000000   call 1c40a4a4.00401370

閱讀程式碼可以看到第一步跟蹤到的eax是在00401337處被ss:[esp+0x8]賦值,那我們斷點到00401337檢視ss:[esp+0x8]的值,但是斷點執行後居然沒停下來而直接判斷輸出了“Wrong!”,(如下圖)

往上瀏覽程式碼,發現在004012D4出還有一個Wrong字串和提示我們輸入的資訊,那說明這裡有有一個初步的對我們輸入的判斷(如下圖)

004012A4  |.  68 D0A04000   push 1c40a4a4.0040A0D0                   ;  Please input flag:

004012A9  |.  E8 C2000000   call 1c40a4a4.00401370

004012AE  |.  8D4424 0C     lea eax,dword ptr ss:[esp+0xC]

004012B2  |.  50            push eax

004012B3  |.  68 C8A04000   push 1c40a4a4.0040A0C8                   ;  %31s

004012B8  |.  E8 7A010000   call 1c40a4a4.00401437

004012BD  |.  8D7C24 14     lea edi,dword ptr ss:[esp+0x14]

004012C1  |.  83C9 FF       or ecx,0xFFFFFFFF

004012C4  |.  33C0          xor eax,eax

004012C6  |.  83C4 0C       add esp,0xC

004012C9  |.  F2:AE         repne scas byte ptr es:[edi]

004012CB  |.  F7D1          not ecx

004012CD  |.  49            dec ecx

004012CE  |.  5F            pop edi

004012CF  |.  83F9 13       cmp ecx,0x13

004012D2  |.  74 1D         je short 1c40a4a4.004012F1

004012D4  |.  68 C0A04000   push 1c40a4a4.0040A0C0                   ;  Wrong!\n

004012D9  |.  E8 92000000   call 1c40a4a4.00401370

004012DE  |.  68 B8A04000   push 1c40a4a4.0040A0B8                   ;  pause

004012E3  |.  E8 B9000000   call 1c40a4a4.004013A1

 

閱讀上面程式碼可以知道要執行輸出“Wrong!”則使得004012D2處不跳轉,那我們的目的是要讓他不執行,所以要使得ecx等於0x13(19),經過我對004012D2處斷點測試得到這個0x13(19)就是要求輸入的長度等於19

那我們繼續往下走,發現004012D2處的跳轉跳到了004012FA1(如下圖)

那我們斷點來到這個004012D2函式(記得輸入19個字元)(如下圖)

跳轉成功以後執行了CreatfeFileA函式,第一個引數便是檔名“Your_input”,那我們執行完CreateFileA後exe就會在它所在目錄建立一個叫作Your_input的檔案(如果檔案在則不建立),最後CreatfeFileA函式返回Your_input檔案控制程式碼值,返回的值放在eax中(通常函式返回值都是存在eax中,這裡就是)

繼續往下走(如下圖)

程式繼續執行WriteFile函式,第一個函式便是待寫入檔案的檔案的控制程式碼值(hfile),第二個函式便是待寫入的資料儲存地址(Buffer),第三個便是要寫入的位元組數(nBytesToWrite ),那就可以知道exe是想把0019FF14處的內容寫19個位元組到剛才生成的Your_input檔案中,檢視0019FF14地址便可以看到寫入的字元就是我們輸入的19個字元

 

那我們先執行完WriteFile函式去看看是否存在Your_input檔案並且寫入成功了(如下圖)

開啟Your_input檔案以後發現裡面的內容根本就不是我們寫入的1234567890123456789啊,而是一些其他字元但是WriteFile寫入的字串就是1234567890123456789,所以WriteFile指定出現了問題,我們跟進WriteFile看下函式(如下圖)

 

 

跟進WriteFile內發現居然是個jmp!!!,那說明這個函式被做過手腳啊,通常這種寫jmp的都是對這個函式進行了HOOK,所以,我們跳到00401080去看看到底發生了啥?(如下圖)

果然是對WriteFile函式進行了HOOK,在執行了401000、401140函式後才執行了

WriteFile函式,那決定最終寫入Your_input檔案的字串重點就是在這兩個函式了,所以我們就跟進401000、401140兩個函式進行分析,此時為了方便分析我們採用IDA來分析

拖進IDA pro 直接按G跳轉到401000(如下圖)

401000函式程式碼

int __cdecl sub_401000(int a1, int a2)

{

  char i; // al

  char v3; // bl

  char v4; // cl

  int v5; // eax

 

  for ( i = 0; i < a2; ++i )

  {

    if ( i == 18 )

    {

      *(_BYTE *)(a1 + 18) ^= 0x13u;

    }

    else

    {

      if ( i % 2 )

        v3 = *(_BYTE *)(i + a1) - i;

      else

        v3 = *(_BYTE *)(i + a1 + 2);

      *(_BYTE *)(i + a1) = i ^ v3;

    }

  }

  v4 = 0;

  if ( a2 <= 0 )

    return 1;

  v5 = 0;

  while ( byte_40A030[v5] == *(_BYTE *)(v5 + a1) )

  {

    v5 = ++v4;

    if ( v4 >= a2 )

      return 1;

  }

  return 0;

}

401000兩個引數a1,a2是什麼呢?如下圖

可以看到esi是我們輸入字元的長度,edi是我們輸入的字元長度,但是這裡有個入棧細節

0040108C   .  56                   push esi

0040108D   .  57                   push edi

0040108E   .  E8 6DFFFFFF          call 1c40a4a4.00401000

這裡的推入引數入棧是從引數的左邊開始向右邊推入,例如:

//c呼叫約定

void add(a,b){}

//對應彙編

push b

push a

call add

所以esi是ida中虛擬碼的a2,也就是我們輸入字元的長度,edi是ida中虛擬碼的a1,也就是我們輸入字元所在的地址,那麼此時我們閱讀401000函式虛擬碼,就可以得到邏輯:

1.迴圈a2(0x13)次,迴圈體內:判斷此時的迴圈次數是否為19次,如果是第19次迴圈的話則將我們輸入的字串第19位字元a[18]與0x13異或再返回第19位,如果迴圈次數取模2不為0的話則將a[i+a1]-i的值賦值給v3,否則將a[i+a1+2]賦值給v3,最後,無論當前迴圈次數是否取模2等於0都將v3異或迴圈次數i的值賦值給a[i]
2.迴圈結束後判斷a2(我們輸入字元的長度)是否小於等於0,滿足的話則推出函式返回1,但是看來這個判斷是毫無意義的
3.判斷我們輸入的字串的每一位字元是否等於byte_40A030陣列中的每一個對應的值,,等於的話就一直迴圈,知道迴圈了strlen(byte_40A030)後退出函式返回1,那我們檢視一下byte_40A030陣列是多少?(如圖)

 

strlen(byte_40A030)就是等於我們輸入字串的正確長度(0x13),其實這裡可以有很大程度可以確定byte_40A030就是flag最後的加密結果,但是為了嚴謹我們還是透過函式呼叫來具體分析

如上圖,找到了401000函式的呼叫者401080,我們分析一下401080的邏輯:
定義整數變數v5接受401000的返回值(剛才分析過,只有0或1),接下來執行401140函式,跟進去看以後就只是一個HOOK相關的功能,不影響資料
最下面有一個判斷,如果v5不為0則將lpNumberOfBytesWritten指向的值賦值為1

if ( v5 )

*lpNumberOfBytesWritten = 1;

 

往上一看lpNumberOfBytesWritten就是WriteFile中的第三個引數,也就是我們設定寫入檔案的位元組數,從這裡就得不到更多資訊了,所以我們現在需要找到WriteFile函式的呼叫者,如下圖

主函式呼叫了WriteFile函式,我們先理一下主函式的邏輯是什麼?

if ( NumberOfBytesWritten == 1 )

      sub_401370(aRightFlagIsYou);

    else

      sub_401370(aWrong);

 

透過跟入sub_401370函式發現該函式就是一個printf函式,引數就是輸出的字串變數,aRightFlagIsYou變數是提示我們輸入的flag正確的字串,aWrong字串是提示我們輸入的flag是錯誤的字串,而要想執行提示我們輸入正確則需要使得NumberOfBytesWritten == 1,這個NumberOfBytesWritten 就是HOOK了WriteFile的函式中做出的賦值更改,但是在主函式中查案程式碼發現NumberOfBytesWritten 還參與了sub_401240函式,而且還是在hook之後,也就是說有可能這個sub_401240函式更改了NumberOfBytesWritten ,那到底sub_401240函式是否對NumberOfBytesWritten 做出更改還是得跟進去才知道如下圖

a1是我們輸入得字串所在的地址,a2是NumberOfBytesWritten所在的地址,閱讀程式碼邏輯:
將字串v4="This_is_not_the_flag"中v4[a1 - v4 + result]元素與v4[result]對比,如果相等則迴圈以下程式碼:

if ( ++result >= (int)(v3 - 1) )

      {

        if ( result == 21 )

        {

          result = (int)a2;

          *a2 = 1;

        }

        return result;

      }

從上面程式碼中可以看出唯一對NumberOfBytesWritten操作且等於1的地方只有for中的if滿足條件執行,那我們就想辦法去執行這個*a2 = 1,但是再仔細讀程式碼後發現這個函式中我們輸入的字串未參與任何運算,也得不到一箇中繼的加密值,所以我們無法從這個函式中獲得任何切確的資訊,而且一看v4字串是"This_is_not_the_flag",這很有可能說明作者寫這個函式是用來誤導我們的,所以綜上所述我們只能去找NumberOfBytesWritten的另一個函式,也就是之前我們分析過的sub_401000函式,因為剛才我們上面已經分析過了,所以先透過sub_401000中的寫出byte_40A030變數來反推出我們輸入的flag因該是多少,解密指令碼如下:

#include

#include

 

int main()

{

        int v3 = 0;

        unsigned char a1[] =

        {

                0x61, 0x6A, 0x79, 0x67, 0x6B, 0x46, 0x6D, 0x2E, 0x7F, 0x5F,

                0x7E, 0x2D, 0x53, 0x56, 0x7B, 0x38, 0x6D, 0x4C, 0x6E, 0x00

        };

        for (size_t i = 19; i >0; i--)

        {

                if (i == 18)

                {

                        *(BYTE *)(a1 + 18) ^=  0x13;

 

                }

                else

                {

                        v3 = *(BYTE *)(i + a1) ^ i;

                        if (i%2)

                        {

                                *(BYTE *)(i + a1) = v3 + i;

                        }

                        else

                        {

                                *(BYTE *)(i + a1 + 2) = v3;

                        }

                }

        }

        printf("%s",a1);

        system("pause");

        return 0;

}

 

此時我們得到了我們應該輸入的flag,此時NumberOfBytesWritten的值已經是1了,那我們繼續帶入下面的sub_401240中看這個函式是否會將NumberOfBytesWritten改為0就可以了,經過測試,sub_401240帶入我們推出的flag也是使得NumberOfBytesWritten變為1,所以main函式執行了輸出提示正確的功能。


相關文章