題目來自於以下網址:算法
第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)