「金山杯2007逆向分析挑戰賽」第一階段第一題分析

  題目來自於以下網址:算法

  http://www.pediy.com/kssd/編程

  第13篇 論壇活動 \ 金山杯2007逆向分析挑戰賽 \ 第一階段 \ 第一題 \ 題目 \ [第一階段 第一題];數組

 

  現將此題目概述粘貼以下:
 app

  CrackMe.exe 是一個簡單的註冊程序,見附件,請寫一個註冊機,要求:編程語言

  1. 註冊機是KeyGen,不是內存註冊機或文件Patch;函數

  2. 註冊機可使用 ASM,VC,BC,VB,Delphi等語言書寫,其餘謝絕使用;oop

  3. 註冊機必須能夠運行在 Windows 系統上;ui

  ......spa

 

  昨天我偶然瀏覽到這裏,看到這個題目,因而看了下,題目早就已是過去時了,但今天依然能夠看看,大概用了半天時間求解此題目(原本覺得挺簡單,但實際上仍是少有點難度的)。把附件下載下來能夠看到,CrackMe.exe 是一個僅有 1.85 KB 的 Windows 對話框程序。界面也很是簡單,只有兩個文本框用於獲取用戶名,註冊碼,和一個註冊按鈕,當點擊註冊按鈕時,若是註冊碼正確,會彈出 MessageBox 顯示 "OK!!",不然會顯示 "Fail!" 。翻譯

 

  

 

  很顯然,若是咱們直接修改這個 exe,跳過其註冊碼檢測或者修改裏面的跳轉邏輯,修改調用 DecryptText 的參數,修改棧上的密文數據等等,有 無數種方法能夠直接令這個程序顯示 「OK!!」 消息框。可是,這顯然不是這個題目的目的(由於這太簡單了,也無需理解那些驗證用的彙編代碼)。根據題目要求,能夠看出出題者的要求是,原程序是不能修改的,解題的人經過閱讀和分析驗證註冊碼的彙編代碼,得出其驗證思路,而後根據此寫出註冊機(KeyGen),實現給出任意的 UserName,得出 SerialNo = f (UserName) ,也就是要求找出 f 關係,並用編程語言實現註冊機。

 

  用 IDA 反彙編這個極小的程序,能夠看到它的組成很是簡單,下面給出一些主要的彙編代碼,並部分的翻譯成 C 語言。

  首先是,當點擊「註冊"按鈕,程序將會檢測註冊碼是否正確。在對話框的窗口過程[參考補充說明2]中,能夠找到一段重要代碼,如今把它提取出來做爲一個註冊機要用到的重要函數,即根據用戶名字符串獲得一個整數特徵值,彙編代碼省略,這裏把這個函數翻譯爲 C 語言以下:

 

//根據用戶名,計算出用戶特徵值
int GetUserValue(const char* pUserName)
{
    int nUserVal= 0x13572468;
    int i;
    int len = strlen(pUserName);
    for(i = 0; i < len; i++)
    {
        nUserVal = nUserVal + pUserName[i];
        nUserVal = nUserVal * 0x03721273;
        nUserVal = nUserVal + 0x24681357;
        nUserVal = (nUserVal << 25) | (nUserVal >> 7);
    }
    return nUserVal;
}

 

  接下來,程序將會調用一個校驗註冊碼的函數,在這個函數中同時會彈出 MessageBox,這個函數由幾塊功能組成,也是此題目必需要分析的重點,這個函數的原型能夠推測出爲形如:

 

  void CheckSerialNo(int nUserVal, char* pSerialNo);

 

  經過這個函數,咱們能夠很容易看到它是如何彈出顯示着 「Fail!!" 的消息框的,該函數首先在棧上放置成功和失敗的文本密文,而後經過檢測結果,把相應的密文地址傳送到解密函數(這裏稱之爲 DecryptText),解密函數把解密後的明文放入棧上的緩衝區,而後以此調用 MessageBox,很是明顯,這是模擬軟件的常規保護方法,在實際軟件中都會將關鍵和敏感信息進行隱藏,固然這也不是這道題目的重點,由於很容易就找到密文的位置,爲緊靠 EBP 附近的兩個字節數組。

 

  在代碼段(.text) 的第一個函數就是解密函數,其原型形如:void DecrptText(const BYTE* pSecret, char* pPlainTextBuffer);  這個加解密很是簡單,只是把一個數組用另外一個事先擬定好的 key 數組線性的異或了一下而已,因此這不是本題重點,在此暫時省略不提(其 C 語言代碼版本見本文結尾 [1] )。

 

  下面給出的是這個程序的關鍵彙編代碼,也就是 CheckSerialNo 的完整代碼,此題目的本意正是要求讀懂這個函數的邏輯,並找出註冊機算法。這個函數較長,但分開割裂不太好,因此完整粘貼以下(注:主體來自於 IDA 反彙編結果,爲了更好的顯示和更好的可讀性,我整理和調整了反彙編結果的部分細節,以及對部分地址標號進行了重命名)(前面有一大段花裏胡哨的稀奇古怪的指令,一些變量賦值操做也經過隔開少量的 PUSH / POP 來完成,最惡劣的是 DWORD 指針居然地址不對齊,彷彿是人爲故意設置的障礙):

 

.text:004002CC CheckSerialNo   proc near
.text:004002CC
.text:004002CC Text            = byte ptr -128h       ; char Text[260];
.text:004002CC var_24          = byte ptr -24h        ; BYTE var_24[12]; //nUserBits
.text:004002CC var_18          = byte ptr -18h        ; BYTE var_18[12]; //Secret_OK
.text:004002CC var_C           = byte ptr -0Ch        ; BYTE var_C[12]; //Secret_Fail
.text:004002CC CurSerialChar   = byte ptr -1
.text:004002CC nUserValue      = dword ptr  8         ; (uint nUserVal,
.text:004002CC szSerialNo      = dword ptr  0Ch       ; const char *pSerialNo)
.text:004002CC
.text:004002CC                 push    ebp
.text:004002CD                 mov     ebp, esp
.text:004002CF                 sub     esp, 128h       ; alloc 296 bytes on stack
.text:004002D5                 and     byte ptr [ebp+var_24], 0 ; var_24[0] = 0;
.text:004002D9                 push    ebx
.text:004002DA                 push    esi
.text:004002DB                 push    edi
.text:004002DC                 xor     eax, eax
.text:004002DE                 lea     edi, [ebp+var_24+1]
.text:004002E1                 stosd ;var_24[1~4]=0
.text:004002E2                 and     [ebp+Text], 0   ; Text[0] = 0;
.text:004002E9                 push    40h
.text:004002EB                 stosd   ;var_24[5~8]=0
.text:004002EC                 stosb   ;var_24[9]=0
.text:004002ED                 pop     ecx
.text:004002EE                 xor     eax, eax
.text:004002F0                 lea     edi, [ebp-127h] ; 很是古怪,edi未對齊到 DWORD
.text:004002F6                 or      [ebp+var_C], 0FFh ; Secret_Fail[0] = 0xFF
.text:004002FA                 rep stosd              ;Text[1~256]=0
.text:004002FC                 or      [ebp+var_18], 0FFh ; Secret_OK[0] = 0xFF
.text:00400300                 or      ecx, 0FFFFFFFFh ; 計算序列號長度
.text:00400303                 stosw
.text:00400305                 stosb
.text:00400306                 mov     edi, [ebp+szSerialNo]
.text:00400309                 xor     eax, eax
.text:0040030B                 repne scasb
.text:0040030D                 not     ecx
.text:0040030F                 push    1
.text:00400311                 dec     ecx             ; ecx = strlen(szSerialNo)
.text:00400312                 pop     ebx             ; ebx = 1 (此函數中 EBX 恆爲 1)
.text:00400313                 mov     byte ptr [ebp-0Bh], 63h ; Init Secret_Fail[1~10]
.text:00400317                 mov     [ebp-0Ah], 0FBh ; "Fail!"的密文: var_C
.text:0040031B                 mov     [ebp-09h], 9Ah
.text:0040031F                 mov     [ebp-08h], 3
.text:00400323                 mov     [ebp-07h], 0A3h
.text:00400327                 mov     [ebp-06h], 0DAh
.text:0040032B                 mov     [ebp-05h], 72h
.text:0040032F                 mov     [ebp-04h], 0FEh
.text:00400333                 mov     [ebp-03h], 0C9h
.text:00400337                 mov     [ebp-02h], 0B7h
.text:0040033B                 mov     byte ptr [ebp-17h], 6Ah ; Init Secret_OK[1~10]
.text:0040033F                 mov     [ebp-16h], 0D1h ; "OK!!"的密文: var_18
.text:00400343                 mov     [ebp-15h], 0D2h
.text:00400347                 mov     [ebp-14h], 4Eh
.text:0040034B                 mov     [ebp-13h], 82h
.text:0040034F                 mov     [ebp-12h], 0DAh
.text:00400353                 mov     [ebp-11h], 72h
.text:00400357                 mov     [ebp-10h], 0FEh
.text:0040035B                 mov     [ebp-0Fh], 0C9h
.text:0040035F                 mov     [ebp-0Eh], 0B7h
.text:00400363                 mov     esi, ecx        ; esi = strlen(szSerialNo)
.text:00400365                 mov     edi, ebx        ; EDI = 1; 循環起始值
.text:00400367
.text:00400367 loc_400367:
.text:00400367                 mov     eax, [ebp+nUserValue] ; ----初始化 var_24[] 內容-----
.text:00400367                                         ; for (edi = 1; edi < 9; ++edi) {
.text:0040036A                 mov     ecx, edi        ; ecx = edi;
.text:0040036C                 shr     eax, cl         ; eax = ((uint)nUserVal >> edi);
.text:0040036E                 and     al, bl          ; al = al & 1
.text:00400370                 mov     byte ptr [ebp+edi+var_24], al ; var_24[edi] = (nUserVal >> edi) & 1;
.text:00400374                 inc     edi
.text:00400375                 cmp     edi, 9
.text:00400378                 jl      short loc_400367 ; }---循環尾部----
.text:0040037A                 xor     edi, edi         ; edi = 0
.text:0040037C                 mov     [ebp+var_1B], bl ; var_24[9] = 1
.text:0040037F                 test    esi, esi         ; esi = strlen(szSerial)
.text:00400381                 jle     short loc_4003EE ; for(edi=0; edi<strlen(szSerialNo);edi++) {
.text:00400383
.text:00400383 loc_400383:
.text:00400383                 mov     eax, [ebp+szSerialNo]
.text:00400386                 mov     al, [edi+eax]  ;al=szSerialNo[edi];
.text:00400389                 cmp     al, 30h
.text:0040038B                 mov     [ebp+CurSerialChar], al
.text:0040038E                 jl      AlertFail
.text:00400394                 cmp     al, 39h
.text:00400396                 jg      AlertFail       ; if(szSerial[edi] < '0' || szSerial[edi] > '9')
.text:00400396                                         ;    goto AlertFail;
.text:0040039C                 mov     eax, edi        ; 準備計算 edi % 31
.text:0040039E                 push    1Fh
.text:004003A0                 cdq
.text:004003A1                 pop     ecx             ;
.text:004003A2                 idiv    ecx             ; edx = edi % 31;
.text:004003A4                 mov     eax, [ebp+nUserValue]
.text:004003A7                 push    0Ah
.text:004003A9                 mov     ecx, edx        ; ecx = edi % 31;
.text:004003AB                 xor     edx, edx
.text:004003AD                 shr     eax, cl         ; eax = nUserVal >> (EDI % 31);
.text:004003AF                 pop     ecx             ; ecx = 10;
.text:004003B0                 div     ecx             ; edx = (nUserVal >> (EDI % 31)) % 10;
.text:004003B2                 movsx   eax, [ebp+CurSerialChar] ; eax = szSerial[edi]
.text:004003B6                 lea     eax, [edx+eax-30h] ; eax = EDX + (szSerial[i] - '0');
.text:004003BA                 xor     edx, edx
.text:004003BC                 div     ecx             ; edx = (edx + szSerial[i] - '0') % 10;
.text:004003BE                 cmp     edx, ebx        ; if(ebx == 1) {
.text:004003C0                 jnz     short EdxIsNotOne_ ; else goto ...
.text:004003C2                 xor     byte ptr [ebp+var_24+1], bl ; var_24[1] ^ = 1; switching var_24[1]
.text:004003C5                 jmp     short ContinueLoop ; continue; }
.text:004003C7 ; --------------------
.text:004003C7
.text:004003C7 EdxIsNotOne_:
.text:004003C7                 cmp     [ebp+edx+var_24-1], bl ; else {
.text:004003C7                                         ;    if(var_24[edx-1] != 1)
.text:004003C7                                         ;        goto AlertFail;
.text:004003C7                                         ; var_24[edx-1] 必須爲1
.text:004003CB                 jnz     short AlertFail
.text:004003CD                 lea     eax, [edx-2]    ; //檢查 [1, edx-2] 元素是否都是 0.
.text:004003D0                 mov     ecx, ebx        ; for(ecx = 1;
.text:004003D2                 cmp     eax, ebx        ;     ecx <= edx-2; ecx++) {
.text:004003D4                 jl      short SwitchingBit_
.text:004003D6
.text:004003D6 loc_4003D6:
.text:004003D6                 cmp     byte ptr [ebp+ecx+var_24], bl
.text:004003D6                                         ;     if(var_24[ecx] == 1)
.text:004003D6                                         ;         goto AlertFail;
.text:004003DA                 jz      short AlertFail 
.text:004003DC                 inc     ecx             ; }
.text:004003DD                 cmp     ecx, eax
.text:004003DF                 jle     short loc_4003D6
.text:004003E1
.text:004003E1 SwitchingBit_:
.text:004003E1                 xor     byte ptr [ebp+edx+var_24], bl ; var_24[edx]^=1; switching var_24[edx]
.text:004003E5                 lea     eax, [ebp+edx+var_24] ;
.text:004003E9
.text:004003E9 ContinueLoop:
.text:004003E9                 inc     edi             ; for(_;_; ++edi)
.text:004003EA                 cmp     edi, esi
.text:004003EC                 jl      short loc_400383 ; } --序列號循環的尾部--
.text:004003EE
.text:004003EE loc_4003EE:
.text:004003EE                 mov     eax, ebx        ; for(eax = 1; eax < 10; eax++) {
.text:004003F0
.text:004003F0 loc_4003F0:
.text:004003F0                 cmp     byte ptr [ebp+eax+var_24], bl
.text:004003F0                                         ;    if(var_24[eax] == 1)
.text:004003F4                 jz      short AlertFail ; goto AlertFail;
.text:004003F6                 inc     eax
.text:004003F7                 cmp     eax, 0Ah
.text:004003FA                 jl      short loc_4003F0 ; } --檢測序列號是否正確
.text:004003FC                 lea     eax, [ebp+Text]
.text:00400402                 push    eax
.text:00400403                 lea     eax, [ebp+var_18] ; DecryptText(Secret_OK, Text);
.text:00400406
.text:00400406 loc_400406:
.text:00400406                 push    eax
.text:00400407                 call    DecryptText     ; 把密文解密存放到 Text 中
.text:0040040C                 pop     ecx
.text:0040040D                 lea     eax, [ebp+Text]
.text:00400413                 pop     ecx
.text:00400414                 push    0               ; uType
.text:00400416                 push    offset Caption  ; lpCaption
.text:0040041B                 push    eax             ; lpText
.text:0040041C                 push    0               ; hWnd
.text:0040041E                 call    MessageBoxA     ; MessageBox(NULL, Text, "", MB_OK);
.text:00400424                 pop     edi
.text:00400425                 mov     eax, ebx        ; return 1;
.text:00400427                 pop     esi
.text:00400428                 pop     ebx
.text:00400429                 leave
.text:0040042A                 retn
.text:0040042B ;--------------------
.text:0040042B
.text:0040042B AlertFail:
.text:0040042B
.text:0040042B                 lea     eax, [ebp+Text]
.text:00400431                 push    eax
.text:00400432                 lea     eax, [ebp+var_C] ; DecryptText(Secret_Fail, Text);
.text:00400435                 jmp     short loc_400406
.text:00400435 CheckSerialNo   endp

 

  在彙編代碼右側,我已經大體寫了必定的註釋,下面把這個校驗註冊碼的函數翻譯到 C 語言以下(爲了使代碼的文本緊湊,部分使用了起始大括號不換行的代碼風格),而後根據代碼推斷註冊機算法。

 

int CheckSerialNo(UINT uintUserVal, char* pSerialNo)
{
    char CurrentSerialChar; //Secret_Fail[11]: 當前字符
    BYTE Secret_Fail[] = {
        0xFF, 0x63, 0xFB, 0x9A,
        0x03, 0xA3, 0xDA, 0x72, 
        0xFE, 0xC9, 0xB7
    }; //"Fail!" 密文 (EBP-0Ch)

    BYTE Secret_OK[] = {
        0xFF, 0x6A, 0xD1, 0xD2,
        0x4E, 0x82, 0xDA, 0x72,
        0xFE, 0xC9, 0xB7
    }; //"OK!!" 密文 (EBP-18h)

    BYTE nUserBits[10] = { 0 }; //即上面彙編代碼中的 var_24 char Text[260] = { 0 }; //存放解密後的明文,送給 MessageBox

    int i; //EDI: 序列號索引
    int k; //ECX / EAX: nUserBits 索引 int nWhichBit; // EDX: 要修改的 nUserBits 索引
    nUserBits[0] = 0; //初始化 BYTE nUserBits[10] for(i = 1; i < 9; i++)
    {
        nUserBits[i] = (uintUserVal >> i ) & 1;
    }
    nUserBits[9] = 1; //最高位手工設1,保證序列號具備較大長度

    BOOL bShowFail = FALSE; // 是否顯示 "Fail!" int SerialLen = strlen(pSerialNo);

    //線性遍歷註冊碼字符串
    for(i = 0; i < SerialLen; i++)
    {
        CurrentSerialChar = pSerialNo[i];

        //註冊碼必須是數字
        if(CurrentSerialChar < '0' || CurrentSerialChar > '9')
        {
            bShowFail = TRUE;
            break;
        } //最初發表時此處遺漏了 break,2014-4-29 補充
        
        nWhichBit = ((uintUserVal >> (i % 31)) + CurrentSerialChar - '0') % 10;

        //修改最低位時,無須作任何校驗。不然需檢測是否知足可修改條件
        if(nWhichBit == 1)
        {
            nUserBits[1] ^= 1;
        }
        else
        {
            //緊鄰的低位是 1 嗎?若是不是,則註冊失敗
            if(nUserBits[nWhichBit - 1] != 1)
            {
                bShowFail = TRUE;
                break;
            }

            //其他低位都是 0 嗎?若是不是,則註冊失敗
            for(k = 1; k <= nWhichBit - 2; k++)
            {
                if(nUserBits[k] != 0)
                {
                    bShowFail = TRUE;
                    break;
                }
            }

            if(bShowFail) break;

            //對當前位取反(0-1 切換)
            nUserBits[ nWhichBit ] ^= 1;
        }
    }

    //查驗 nUserBits 是否都爲 0,是則成功,不然失敗
    if( !bShowFail ) {
        for(k = 1; k < 10; k++) {
            if(nUserBits[k] == 1) {
                bShowFail = TRUE;
                break;
            }
        }
    }

    //解密密文到 Text 中
    DecryptText(bShowFail? Secret_Fail : Secret_OK, Text);
    MessageBox(NULL, Text, "", MB_OK);
    return 1;
}

 

  在高級語言版本中,我使用了一些語言方法,來避免在高級語言代碼把彙編中的那些相對跳轉直譯爲 goto,大致上將是等效的。從上面的代碼中能夠看出檢查註冊碼的重要標準,若是咱們把註冊碼看作輸入,實際上在掃描註冊碼的過程當中,就是 nUserBits 這個元素爲二元的數組變化的過程,nUserBits 是根據用戶填寫的用戶名計算獲得的數組,因此它的元素和用戶名相關,是不肯定的,註冊碼掃描結束後,這個數組必須全部元素都爲 0。所以,至關於以註冊碼爲驅動。

 

  從上面的代碼邏輯中咱們還能看到很重要的一點,要修改 nUserBits 的某一位(假設爲 nWhichBit),那麼必須知足如下條件,nUserBits 必須處於以下狀態:

 

Index: 1 2 3 ... 5 6 7 8 9 ...
Value: 0 0 0 ... 0 1 nWhichBit x x ...

 

  也就是說,當咱們要切換某一位的狀態時,從這一位向低位方向(左側)看去,應該是 【0】* N + 【1】 的組合(緊鄰的低位爲 1, 其他均爲 0)。最低位(索引 1 )能夠隨時修改,由於左邊已經沒有位了,固然就不必向左看了。這裏把上述邏輯用數學語言表述以下:

 

  有一個數組x,能夠把它理解爲一個二進制數,每一個數組元素表示一個bit。x = f (UserName) 。可以切換 x[i] (i = 1, 2, 3, ..., n )的准許條件是:

 

  (1)對任何 1 <= j <= i - 2,有 x [ j ] = 0; 而且

  (2)若是 i > 1, 有 x [ i - 1 ] = 1;

 

  這時能夠執行對 x [ i ] 的切換動做(對該位取反)。註冊碼正確的標準是,根據註冊碼執行一系列動做(註冊碼的每一位對應於切換 x 的哪一位)後,x 的全部元素爲0。在 x 初始化時,已經將 x 的最高位固定設置爲1,以防止 x 的初始值剛好都是 0 的特殊狀況。

 

  因此這樣就提示註冊機算法,能夠用遞歸函數 ( 下面代碼中的 SetBit 函數 ) 來求解。註冊碼是由一系列的 nWhichBit (要切換狀態的位索引)組成,這個索引是根據註冊碼的當前位獲得的。換句話說,求註冊碼至關於找出這樣一個有序序列,{ b1, b2, b3, ...,  } ,每一步都是合法切換,處理後 nUserBits 全部位都爲 0。

 

  這樣註冊機算法就算呼之欲出了,還有一個簡單問題是,咱們的目標是把  nUserBits 中的每一位都變爲 0,那麼應該先從哪一位入手呢,應該按照從左(低位)向右(高位)的順序依次清零,仍是從右(高位)向左(低位)?不可貴出答案應該採用後者,由於在設置某一位爲 0 時,其全部低位都會動態變化,而高位則不受影響能夠保持靜態不動。因此咱們應該先把高位清零,而後在逐一貫低位方向推動。程序中顯式的把最高位(索引爲9)設置爲 1,也暗示了這一求解順序。

 

  這裏給出一個例子來講明,以校驗的索引範圍爲從 1 到 4 ,假設 nUserBits 的初始狀態爲 { 0, 0, 0, 1 } (索引從 1 開始),則如何通過一系列上述規則容許的元素切換,把它變爲 { 0, 0, 0, 0 },參考下表(整個過程讓我想起了漢諾塔,能夠很容易看出,總體解中包含了形式相同的規模更小的子問題的解):

 

Index (1-based) [1] [2] [3] [4] nWhichBit to be switched
nUserBits[]: 0 0 0 1 Initial State
1 0 0 1 [1]
1 1 0 1 [2]
0 1 0 1 [1]
0 1 1 1 [3]
1 1 1 1 [1]
1 0 1 1 [2]
0 0 1 1 [1]
0 0 1 0 [4]
1 0 1 0 [1]
1 1 1 0 [2]
0 1 1 0 [1]
0 1 0 0 [3]
1 1 0 0 [1]
1 0 0 0 [2]
0 0 0 0 [1]

 

  把最右側一列的索引值連在一塊兒,就是一個咱們須要設計的位置序列 { 121312141213121 },依次對這些位置進行開關切換(把 nUserBits 每個元素想象成一個開關)後,就可讓 nUserBits 數組變爲全爲 0 的狀態(註冊碼正確的條件)。若是不須要和用戶特徵值關聯到一塊兒的話,那這這個序列就是註冊碼。但原程序中是經過註冊碼和用戶特徵值(nUserVal)混編後獲得這個調整序列,因此咱們只須要對這個調整序列作個逆運算,「剔除」其中的用戶特徵值成分,便可獲得實際註冊碼。

 

  爲此咱們給出註冊機的以下多個函數,即爲題目要求的註冊機,顯然,註冊碼若是不限長度,能夠有無數多個,但咱們固然選擇生成簡短的。

//用戶特徵值(注意有時候須要使用其無符號形式,根據CrackMe的指令而定)
int g_UserVal;

//用戶特徵位數組,由9個位組成 [1,... 9] 僅索引1~9有效
int g_UserBits[10];

//序列號
TCHAR g_SerialNo[4096];

////若是把序列號看着stack,這是棧頂
int g_Top;

//註冊機算法:
void Push(int index);
BOOL CanModify(int index);
void SetBit(int index, int nDesiredVal);
int GetUserValue(LPCTSTR szUser);
void InitUserBits();

//index 即須要修改的 UserBits 索引。這個索引變換後,追加到序列號
void Push(int index)
{
    //注意彙編中的右移指令是 SHR,因此須要轉爲無符號數字(不然爲 SAR)
    UINT uintUserVal = g_UserVal;
    
    int nMapped =  index - (uintUserVal >> (g_Top % 31)) % 10;

    if(nMapped < 0)
        nMapped += 10;

    //把數字轉換成字符,這樣 SerialNo 就是字符串。
    g_SerialNo[g_Top] = nMapped + _T('0');
    ++g_Top;
}

//是否能夠直接修改 Bits[ index ]
//要求必須知足 [0 0 0 ... 0 1 Index...

BOOL CanModify(int index)
{
    int i;
    if(index > 1)
    {
        if(g_UserBits[index - 1] != 1)
            return FALSE;
    }
    for(i = 1; i <= index - 2; i++)
    {
        if(g_UserBits[i] != 0)
            return FALSE;
    }
    return TRUE;
}

//遞歸函數,把索引爲index的位設置爲 nDesiredVal。

void SetBit(int index, int nDesiredVal)
{
    int i;
    if(g_UserBits[index] != nDesiredVal)
    {
        //能當即修改嗎?若是不能,調整低位。
        if(!CanModify(index))
        {
            SetBit(index - 1, 1);
            for(i = index - 2; i >= 1; i--)
            {
                SetBit(i, 0);
            }
        }

        //如今能夠修改這一位了!
        g_UserBits[index] = nDesiredVal;
        Push(index);
    }
}

//根據用戶名,計算出用戶特徵值
int GetUserValue(LPCTSTR szUser)
{
    int nUserVal= 0x13572468;
    int i;
    int len = _tcslen(szUser);
    for(i = 0; i < len; i++)
    {
        nUserVal = nUserVal + szUser[i];
        nUserVal = nUserVal * 0x03721273;
        nUserVal = nUserVal + 0x24681357;
        nUserVal = (nUserVal << 25) | (nUserVal >> 7);
    }
    return nUserVal;
}

//
void InitUserBits()
{
    //下面指令中用到 SHR (邏輯右移),因此須要無符號數

    UINT uintUserVal = g_UserVal;
    int i;
    for(i = 1; i < 9; i++)
    {
        g_UserBits[i] = (uintUserVal >> i) & 1;
    }

    //最高位被置爲1,保證註冊碼必然具備至關長度。
    g_UserBits[9] = 1;
}

BOOL GetSerialNo(LPCTSTR szUserName)
{
    int i;

    if(_tcslen(szUserName) == 0)
        return FALSE;

    g_UserVal = GetUserValue(szUserName);
    memset(g_UserBits, 0, sizeof(g_UserBits));
    InitUserBits();

    g_Top = 0;

    for(i = 9; i >= 1; i--)
        SetBit(i, 0);

    //字符串的 null-terminator;
    g_SerialNo[g_Top] = 0;
    ++g_Top;
}

 

  這裏,附上原題目程序,我寫的註冊機的源碼以及可執行文件的壓縮包下載:

  http://files.cnblogs.com/hoodlum1980/CrackMe_1_1.zip

 

  註冊機截圖,用 VC 寫的一個對話框程序(寫成 Console 程序更容易,但 Windows 程序對用戶來講更熟悉):

 

  

 

  最後,隨便給出一個註冊機計算出的註冊碼做爲結束(因爲註冊碼太長,因此插入了換行和縮進):

 

  User:   hoodlum1980  Serial:

  3299167863423793371021417400742237107415

  4242840289212146582658628926588332475377

  0031219490849267008495623294423911312678

  1668321916686352376337402131741074023730

  7405425284528981213658365842894658733257

  5347006121849094924700049552320442991171

  2668167832991688634237733710216174007412

  3710742542428462892121865826585289265893

  3247535700312114908492570084957232944209

  1131260816683209166863623763372021317430

  740237207405426284528931213658

 


 

  【1】關於 DecryptText 函數,用於加/解密字符串(對稱),其等效 C 語言代碼以下:

 

void DecryptText(const BYTE *pSecret, char *pTextBuf)
{
    int i;
    BYTE key[] = 
    {
        0x25, 0x9A, 0xF3, 0x6F,
        0x82, 0xDA, 0x72, 0xFE,
        0xC9, 0xB7
    };

    if(pSecret[0] == 0xFF)
    {
        for(i = 0; i < 10; i++)
            pTextBuf[i] = pSecret[i + 1] ^ key[i];
    }
    else
    {
        strcpy(pTextBuf, (const char*)pSecret);
    }
}

 

  【2】關於對話框的窗口過程,是很是簡單的。但它在棧上爲臨時變量分配空間使用的不是常規的( sub esp, ....) 語句,而是調用了函數(loc_400540)來完成分配臨時空間,這個函數能夠總結爲等效於( sub esp, eax ),只須要把這個函數理解成分配棧上空間便可。(2014年5月4日 補充 --hoodlum1980)

相關文章
相關標籤/搜索