羽夏逆向指引——破解第一個程式

寂靜的羽夏發表於2021-11-20

寫在前面

  此係列是本人一個字一個字碼出來的,包括示例和實驗截圖。可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閒錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並宣告我的個人資訊和本人部落格地址即可,但必須事先通知我

你如果是從中間插過來看的,請仔細閱讀 羽夏逆向指引——序 ,方便學習本教程。

小白鼠

  本次示例使用C++,也就是編譯型語言,為了方便檢視彙編程式碼和編碼故使用VS 2022,其他IDE即可,程式碼如下:

#include <iostream>
using namespace std;

int main(int argc, char* argv[])
{
    int x = 0;

    cout << "請輸入金鑰:" << endl;
    cin >> x;
    if (x == 1234)
    {
        cout << "成功,By.寂靜的羽夏,CNBLOG Only!!!" << endl;
    }
    else
    {
        cout << "失敗,By.寂靜的羽夏,CNBLOG Only!!!" << endl;
    }

    system("pause");
    return 0;
}

  是不是程式碼很簡單?判斷你輸入的是不是1234,是的話就是成功,反之失敗。這是最簡單的校驗示例了。

分析

  程式碼的流程十分簡單,我們可以用下面的流程圖進行表示:

graph TD; A("程式開始")-->B("顯示資訊,提示輸入")-->C[/"獲取輸入"/]-->D{"判斷是否和 1234 相等"}; D--YES-->E("輸出正確資訊")-->F("程式結束"); D-.NO.->G("輸出錯誤資訊")-->F;

  由於是初次分析編譯型程式,我們直接在IDE先看看它的反彙編:

#include <iostream>
using namespace std;

int main(int argc, char* argv[])
{
00E42550  push        ebp
00E42551  mov         ebp,esp
00E42553  sub         esp,0D0h
00E42559  push        ebx
00E4255A  push        esi
00E4255B  push        edi
00E4255C  lea         edi,[ebp-10h]
00E4255F  mov         ecx,4
00E42564  mov         eax,0CCCCCCCCh
00E42569  rep stos    dword ptr es:[edi]
00E4256B  mov         eax,dword ptr [__security_cookie (0E4C004h)]
00E42570  xor         eax,ebp
00E42572  mov         dword ptr [ebp-4],eax
00E42575  mov         ecx,offset _3226632D_ConsoleApplication3@cpp (0E4F066h)
00E4257A  call        @__CheckForDebuggerJustMyCode@4 (0E41384h)
    int x = 0;
00E4257F  mov         dword ptr [x],0

    cout << "請輸入金鑰:" << endl;
00E42586  mov         esi,esp
00E42588  push        offset std::endl<char,std::char_traits<char> > (0E4103Ch)
00E4258D  push        offset string "\xc7\xeb\xca\xe4\xc8\xeb\xc3\xdc\xd4\xbf\xa3\xba" (0E49B30h)
00E42592  mov         eax,dword ptr [__imp_std::cout (0E4D0DCh)]
00E42597  push        eax
00E42598  call        std::operator<<<std::char_traits<char> > (0E411A9h)
00E4259D  add         esp,8
00E425A0  mov         ecx,eax
00E425A2  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0E4D0A8h)]
00E425A8  cmp         esi,esp
00E425AA  call        __RTC_CheckEsp (0E4128Fh)
    cin >> x;
00E425AF  mov         esi,esp
00E425B1  lea         eax,[x]
00E425B4  push        eax
00E425B5  mov         ecx,dword ptr [__imp_std::cin (0E4D098h)]
00E425BB  call        dword ptr [__imp_std::basic_istream<char,std::char_traits<char> >::operator>> (0E4D09Ch)]
00E425C1  cmp         esi,esp
00E425C3  call        __RTC_CheckEsp (0E4128Fh)
    if (x == 1234)
00E425C8  cmp         dword ptr [x],4D2h
00E425CF  jne         __$EncStackInitStart+0A0h (0E425FCh)
    {
        cout << "成功,By.寂靜的羽夏,CNBLOG Only!!!" << endl;
00E425D1  mov         esi,esp
00E425D3  push        offset std::endl<char,std::char_traits<char> > (0E4103Ch)
00E425D8  push        offset string "\xb3\xc9\xb9\xa6\xa3\xacBy.\xbc\xc5\xbe\xb2\xb5\xc4\xd3\xf0\xcf\xc4\xa3\xacCNBLOG Onl@"... (0E49B64h)
00E425DD  mov         eax,dword ptr [__imp_std::cout (0E4D0DCh)]
00E425E2  push        eax
00E425E3  call        std::operator<<<std::char_traits<char> > (0E411A9h)
00E425E8  add         esp,8
00E425EB  mov         ecx,eax
00E425ED  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0E4D0A8h)]
00E425F3  cmp         esi,esp
00E425F5  call        __RTC_CheckEsp (0E4128Fh)
    }
00E425FA  jmp         __$EncStackInitStart+0C9h (0E42625h)
    else
    {
        cout << "失敗,By.寂靜的羽夏,CNBLOG Only!!!" << endl;
00E425FC  mov         esi,esp
00E425FE  push        offset std::endl<char,std::char_traits<char> > (0E4103Ch)  
00E42603  push        offset string "\xca\xa7\xb0\xdc\xa3\xacBy.\xbc\xc5\xbe\xb2\xb5\xc4\xd3\xf0\xcf\xc4\xa3\xacCNBLOG Onl@"... (0E49D40h)
00E42608  mov         eax,dword ptr [__imp_std::cout (0E4D0DCh)]
00E4260D  push        eax
00E4260E  call        std::operator<<<std::char_traits<char> > (0E411A9h)
00E42613  add         esp,8
00E42616  mov         ecx,eax
00E42618  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0E4D0A8h)]
00E4261E  cmp         esi,esp
00E42620  call        __RTC_CheckEsp (0E4128Fh)
    }

    system("pause");
00E42625  mov         esi,esp
00E42627  push        offset string "pause" (0E49B40h)
00E4262C  call        dword ptr [__imp__system (0E4D1D4h)]
00E42632  add         esp,4
00E42635  cmp         esi,esp
00E42637  call        __RTC_CheckEsp (0E4128Fh)
    return 0;
00E4263C  xor         eax,eax
}

  看不懂彙編的意思的,請自學彙編。看不懂整體組合語言的作用,請學習我的教程 羽夏看C語言 即可看懂。
  輸出資訊最關鍵的當然是判斷了,也就是下面這個部分:

    if (x == 1234)
00E425C8  cmp         dword ptr [x],4D2h
00E425CF  jne         __$EncStackInitStart+0A0h (0E425FCh)

  如果我把jne給幹掉,無論我輸入什麼,都會輸出正確。也就是我們把判斷失敗的鏈條給砍斷了,只能走這條路。

graph TD; A("程式開始")-->B("顯示資訊,提示輸入")-->C[/"獲取輸入"/]-->D{"判斷是否和 1234 相等"}; D--->E("輸出正確資訊")-->F("程式結束"); G("輸出錯誤資訊")-->F("程式結束");

  上面只改彙編程式碼的,通過修改彙編以修改程式碼執行流程實現自己的目的,就是我們所謂的暴力破解,也就是patch,俗稱打補丁。如果我逆向分析得到金鑰就是1234,直接輸入,這東西也就是所謂的key。當然,現實中驗證不可能這麼簡單,它們往往是通過某種演算法,通過獲取計算機獨特的資訊生成註冊碼(如果我們能夠知道演算法是什麼,根據演算法來寫出算key的軟體,俗稱序號產生器),然後根據校驗函式進行校驗,大大增大了分析難度。這就是說為什麼爆破比序號產生器簡單很多。
  但是爆破也不是那麼輕鬆的,如果在校驗函式中生成了大量的中間結果,如果它再拿來這東西校驗的話,被破解的難度就會提高一個層次。發現被破解,直接退出程式,這往往是付費軟體防破解的一個手段。
  當然,我們破解軟體的時候不可能是拿到別人的原始碼按照本篇文章開頭那樣進行分析。我們來真槍實戰一下。假設這個軟體是我們未知的,如何分析這個程式。

小試牛刀

  本小節是按照常規套路進行分析,分析不同型別的軟體可能流程不太一樣,比如分析病毒你肯定不能直接在真機上跑,有些病毒還有反虛擬機器的模組,甚至變形多型;編譯型程式碼軟體分析和解釋型程式碼軟體分析也不太一樣;只是分支操作有所不同。接下來是編譯型軟體的常規操作:

  查殼,看看是啥軟體編寫的:

羽夏逆向指引——破解第一個程式

  直接拖到IDA瞅一瞅,如果沒報錯,說明軟體可以直接分析。如果報錯,說明軟體被加殼或者加密。當然這軟體沒有加殼,所以正常,由於軟體短小精悍,我們很快定位到主函式部分,下面是它的流程:

羽夏逆向指引——破解第一個程式

  看到彙編程式碼後,IDA會有一些註釋,看下圖:

羽夏逆向指引——破解第一個程式

  這個明明是字串,但IDA沒有識別出來。這個是經常會遇到的情況。有時候IDA識別不出Unicode字串,甚至識別錯誤以為是函式偏移,不要僅依賴這個軟體。那麼我們如何處理呢?把游標放到上面字串的首地址,按下ALT + A,將會彈出下面的窗體:

羽夏逆向指引——破解第一個程式

  這個就是將資料轉化為字串,由於這個是普通的ASCII字串,直接選C-style即可。

羽夏逆向指引——破解第一個程式

  轉換成功後結果如下:

羽夏逆向指引——破解第一個程式

  聽說IDA有一個F5的功能,的確,它是IDA的一個外掛。如果沒有選擇偵錯程式的情況下,它會預設呼叫Hex-ray這個外掛翻譯成偽C程式碼。但是不要過度依賴這個外掛,因為翻譯出來的東西很多是不準確的,甚至是錯誤的。比如函式呼叫的引數個數、多個變數其實就是一個變數、型別不對等等。這些東西都需要依賴自身的開發經驗和分析進行調整。經過分析和重新命名後,如下所示:

羽夏逆向指引——破解第一個程式

  通過分析,我們既得到了Key,也分析透了函式流程。

暴力破解

  IDA其實可以patch軟體的,怎麼搞自行學習,比較麻煩,一旦patch就會導致原來的分析流程變化,自認為不太適用,如果用於去除花指令的話另當別論。在動態偵錯程式裡修改是最快捷的,最舒服的方式。如下的動圖進行演示:

羽夏逆向指引——破解第一個程式

結語

  學習本篇指引,如果IDA不會使用的話,請自行找教程進行練習。本系列教程只是個指引,故不提供IDA使用教程。IDA有如下基本操作:函式和變數的重新命名、跳轉、型別轉化、搜尋字串、查詢引用是最常用的操作了,請自行學習。

下一篇

  羽夏逆向指引——補丁

相關文章