演算法分析:看雪CTF2019的一道逆向題目

極安御信發表於2022-03-24

1.檢視程式的基本執行邏輯

出現一個視窗,有一個提示標籤password、一個輸入框,一個驗證按鈕

當再輸入框內輸入一些字元按下驗證按鈕後出現錯誤提示資訊框

其執行邏輯大概可視為下圖

2.查殼初探

MFC封裝vc寫的沒殼

3.使用IDA Pro進行靜態分析

拖入程式,先開啟函式呼叫情況視窗(方便函式跟蹤)

shift+F12檢視引用字元

看到了剛才錯誤的提示字元“錯了”,還有嫌疑字元“pass!”,那我們找到pass字元所在的位置檢視引用情況

檢視引用,找到引用函式
發現pass!字元的引用的函式是sub_401770,跟進去

跟進來看到的資訊可以直接判斷sub_401770這個函式就是輸入提示我們輸入的password是正確的函式

跟蹤函式呼叫情況,直至找到最終呼叫者
(1)做一個小提示:在ida中我們看到的函式都是一個以sub_函式地址值來命名這個函式,不便於我們分析,所以我們只用將游標放在函式名字上按下n鍵就可以更改為我們便於理解和記住的名字(不可以用中文)
先將sub_401770改名為Tip_Success,透過函式呼叫表點選Caller字元函式名就會跳轉Instruction欄位函式的對應的呼叫者

(2)跟進發現sub_4017F0函式中有關鍵邏輯

將Str1與"KanXueCTF2019JustForhappy"作對比,如果相等則執行了提示成功的函式Tip_Success,那我們可以直接推測Str1就是Flag最後的變換值
(3)查詢虛擬碼中的關於Str1的邏輯語句
單機Str1看到了Str1是一個字元型陣列區域性變數,大小為28,經歷了一個while中的計算

程式碼中涉及了v4和a1,v4可以看到是一個初始化值為0的整型變數,a1則是一個該函式的形參,那說明此時我們要重點尋找a1.
分析迴圈體

 while ( *(_DWORD *)(a1 + 4 * v4) < 62 && *(_DWORD *)(a1 + 4 * v4) >= 0 )

  {

    Str1[v4] = aAbcdefghiabcde[*(_DWORD *)(a1 + 4 * v4)];

    ++v4;

  }

第二步我提到過Str1變數已經可以確定是Flag最後的變換值
檢視迴圈體內,其意思便是隻要a1 + 4 v4這個地址裡面的內容小於62並且大於等於0則將a1 + 4 v4地址的值作為aAbcdefghiabcde陣列的下標,然後將對應的aAbcdefghiabcde陣列內容賦值給Str1
那麼我們去檢視一下aAbcdefghiabcde陣列的元素是什麼?算擊aAbcdefghiabcde陣列名字檢視內容
原來就是一串字元

此時分析到這裡基本這個函式就分析完了,我們如果要繼續分析則必須先找出a1到底是什麼?
繼續透過函式呼叫表找到sub_4017F0(我們改名為Check_Encryption)函式的呼叫者
跟進來以後看到了Check_Encryption函式中的形參就是此時函式sub_401890中的一個整型陣列v5的首地址(該陣列大小為26)

那我們此時就重點跟蹤v5的值了

紅框框住的就是一些mfc元件的程式碼,不用管,可以看到,v5的變化初始化和變化來自於Str陣列,
那麼Str又是什麼呢,仔細看從上往下第一個箭頭指向的地方GetBuffer函式,這個函式會返回一個緩衝區指標,Str來接收了,說明Str很有可能就是我們輸入的password所在的地址,再往GetBuffer下看一行遍看到了一個if語句
判斷Str長度是否為0,也就是判斷是否空,空的話就執行了return語句,不為空的話就進行了一系列關於v5和Str的計算,那麼此時可以直接肯定Str就是我們輸入的字元所在地址

透過程式碼又可以看出我們輸入的password存到Str後進行了一系列if判斷後計算賦值給了v5,而v5的值又作為了Check_Encryption函式的唯一實參

 if ( strlen(Str) )

  {

    for ( i = 0; Str[i]; ++i )

    {

      if ( Str[i] > 57 || Str[i] < 48 )

      {

        if ( Str[i] > 122 || Str[i] < 97 )

        {

          if ( Str[i] > 90 || Str[i] < 65 )

            sub_4017B0();

          else

            v5[i] = Str[i] - 29;

        }

        else

        {

          v5[i] = Str[i] - 87;

        }

      }

      else

      {

        v5[i] = Str[i] - 48;

      }

    }

    result = Check_Encryption((int)v5);

  }

  else

  {

    result = CWnd::MessageBoxA(v8, "請輸入pass!", 0, 0);

  }

(4)梳理大概邏輯

5)我們按照從頭到尾的順序來分析這個程式
既然我們知道了我們輸入的password是被Str指向(可以理解為Str是個字元陣列存了我們輸入的password),那麼我們試試是否可以從sub_401890這個函式推出Str

閱讀程式碼,可以得到Str[i]在此時可以有三種情況,所以此時我們不可以確定Str的唯一性,也就是我們不可以在這裡得到Str(我們輸入的)具體是多少,只能得到範圍,那麼就繼續分析後面一步,跟進Check_Encryption函式
(6)分析Check_Encryption關鍵程式碼

  v4 = 0;

  v3 = 0;

  while ( *(_DWORD *)(a1 + 4 * v4) < 62 && *(_DWORD *)(a1 + 4 * v4) >= 0 )

  {

    Str1[v4] = aAbcdefghiabcde[*(_DWORD *)(a1 + 4 * v4)];

    ++v4;

  }

閱讀程式碼可知,(_DWORD )(a1 + 4 * v4)的作用便是遍歷上一個函式v5的每一個元素,若每一個元素滿足小於62並且大於0的話則將v5對應的每一個元素作為aAbcdefghiabcde陣列的下角標,將對應的aAbcdefghiabcde元素賦值給Str1,之前提過Str1是最後的Flag變換結果

while ( *(_DWORD *)(a1 + 4 * v4) < 62 && *(_DWORD *)(a1 + 4 * v4) >= 0 )

  {

    Str1[v4] = aAbcdefghiabcde[*(_DWORD *)(a1 + 4 * v4)];

    ++v4;

  }

  Str1[v4] = 0;

  if ( !strcmp(Str1, "KanXueCTF2019JustForhappy") )

    result = Tip_Success();

  else

result = sub_4017B0();

作者設計,如果最後的Flag遍結果等於“KanXueCTF2019JustForhappy”則提示成功,那由上我們就可以寫指令碼得到v5的值

char encryption[] = "KanXueCTF2019JustForhappy";

        char text[] = "abcdefghiABCDEFGHIJKLMNjklmn0123456789opqrstuvwxyzOPQRSTUVWXYZ";

        for (int i = 0; i <= strlen(encryption); i++)

        {

                for (int j = 0; j <= strlen(text); j++)

                {

                        if (encryption[i] == text[j])

                        {

                                //printf("%d--", j);

                                v5[i] = j;

                        }

                }

此時我們既然得到了v5的值那麼就可以去分析上一個函式sub_401890得到Str(我們輸入的password)了

 for ( i = 0; Str[i]; ++i )

    {

      if ( Str[i] > 57 || Str[i] < 48 )

      {

        if ( Str[i] > 122 || Str[i] < 97 )

        {

          if ( Str[i] > 90 || Str[i] < 65 )

            sub_4017B0();

          else

            v5[i] = Str[i] - 29;

        }

        else

        {

          v5[i] = Str[i] - 87;

        }

      }

      else

      {

        v5[i] = Str[i] - 48;

      }

    }

這個迴圈結構就是判斷Strp[i]是否在一個規定的範圍,透過Str[i]所屬的範圍來進行加密或者推=退出程式,因為v5是Str[i]-常數得到的,那麼我們就可以用v5+常數來判斷Str[i]的範圍以及確卻數值了,反推程式碼如下,input陣列就是Str(使用者輸入)

for (int i = 0; i <= 26; i++)

        {

                if (48 <= (v5[i] + 48) && (v5[i] + 48) <= 57)

                {

                        input[i] = v5[i]+48;

                }

                if (97 <= (v5[i] + 87) && (v5[i] + 87) <= 122)

                {

                        input[i] = v5[i]+87;

                }

                if (65 <= (v5[i] + 29) && (v5[i] + 29) <= 90)

                {

                        input[i] = v5[i] + 29;

                }

        }

最後寫出第一輪和第二輪的解密程式碼,即可得到flag

#include

#include

int main()

{

        int input[26] = { 1 };

        int v5[26] = {1};

        int Str[26] = { 1 };

        char encryption[] = "KanXueCTF2019JustForhappy";

        char text[] = "abcdefghiABCDEFGHIJKLMNjklmn0123456789opqrstuvwxyzOPQRSTUVWXYZ";

        for (int i = 0; i <= strlen(encryption); i++)

        {

                for (int j = 0; j <= strlen(text); j++)

                {

                        if (encryption[i] == text[j])

                        {

                                //printf("%d--", j);

                                v5[i] = j;

                        }

                }

        }

        for (int i = 0; i <= 26; i++)

        {

                if (48 <= (v5[i] + 48) && (v5[i] + 48) <= 57)

                {

                        input[i] = v5[i]+48;

                }

                if (97 <= (v5[i] + 87) && (v5[i] + 87) <= 122)

                {

                        input[i] = v5[i]+87;

                }

                if (65 <= (v5[i] + 29) && (v5[i] + 29) <= 90)

                {

                        input[i] = v5[i] + 29;

                }

        }

        for (int i = 0; i < 26; i++)

        {

                printf("%c",input[i]);

        }

        return 0;


相關文章