逆向初學者CrakeMe(1)
在《加密與解密》(第三版)第三章 3.2.16 小節中,使用 Delphi5.exe 這個小 CrakeMe 簡單地講解了一下 FLIRT 的知識,但沒有對該程式做深入的討論,這裡用 OD 對其進行破解。
從程式名字可以知道它是用 Delphi 語言編寫的,用 PEID 檢視證實也的確如此。開啟程式,隨便輸入註冊資訊,根據不同情況彈出以下幾種錯誤提示視窗:
記住以上的提示資訊,大概瞭解了一下程式的功能,但程式內部的正確的註冊資訊是已經常量存在的還是根據輸入的註冊資訊來計算生成的,我們還不得而知。直接用 OD 載入,入口處如下:
可以看見,入口點後面跟著幾個 call ,但它們的作用是什麼,我們不需要知道,我們只需要分析關鍵部分。直接搜尋字串,找到剛剛彈出的錯誤提示資訊,並雙擊 "請輸入使用者名稱和註冊碼“一行,轉到該處指令處
在反彙編視窗註釋一欄,我們可以找到所有的提示資訊,並可以推知,當輸入正確的註冊資訊時,程式將提示 “恭喜你,註冊成功!“。接下來,我們要確定破解的大體思路。由於這是一個小程式,猜想功能並不複雜。因此猜想程式在接收使用者輸入的註冊資訊後,程式執行將執行到我們現在所處的地方,根據輸入的註冊資訊,判斷並選擇一條相應的提示資訊並輸出。因此我們可以在現在所處的地址處下個斷點,按 F9 讓程式跑起來,然後隨便輸入註冊資訊,程式將在我們設立的斷點處停下,接著我們單步執行,分析正確的註冊資訊是如何誕生的。
選擇在地址 00441693 處下斷,輸入的註冊資訊如下,按以上步驟執行後,程式中斷在我們設立的斷點處
接下來 F8 單步執行,當執行到 004416AD 行時,程式碰到一個 cmp 和 jnz 指令,觀察堆疊視窗中 local.3 處的資料,也就是 ebp-3*4 處的資料,發現是我們輸入的使用者名稱(實質上是使用者名稱在記憶體中的存放地址,可在資料視窗中跟隨觀察)。
因為我們輸入的使用者名稱並不為空, jnz 指令將跳過輸出 “請輸入使用者名稱和註冊碼”的 MessageBoxA 函式呼叫指令。
接著分析接下來的指令
同樣可以發現一條 cmp 指令和 jge 指令。單步步過 004416DF 行,觀察 eax 暫存器,存放著使用者名稱的地址,緊接著執行 call 指令,再觀察 eax 暫存器,發現其值為 6。聯想該處的提示資訊“使用者名稱至少四個字元”以及我們輸入的使用者名稱 “ABCEEF”,推知該 call 指令的返回值是使用者名稱的長度(跟蹤進去證實的確如此),用 eax 存放。將使用者名稱的長度與 4 比較後,若 大於等於4 ,則跳過輸出 ”使用者名稱至少四個字元!“ 的MessageBoxA 函式呼叫指令。
當然,不要忘了在我們分析過的 call 指令後面加上註釋。
接下來我們面對一段沒有註釋的指令,過了這一段,將是提示輸入註冊資訊正確與否的提示資訊,因此這一段指令應該比較重要。不急,慢慢分析。一路 F8 單步步過,到地址 00441750 處,發現掉進一個迴圈中
仔細分析,容易得知,該處迴圈的作用是把使用者名稱的 ASCII 碼生成一個字串,並壓棧到堆疊 local.1 處,ebi 作為使用者名稱相對地址偏移量,ebx 初始存放著使用者名稱長度,用作迴圈計數器。輸入的使用者名稱 “ABCDEF" 的 ASCII 碼對應生成的字串即為 ”414243444546"。為了方便,接下來都把這個由使用者名稱的 ASCII 碼生成的字串稱作 使用者名稱生成串。
記住這個生成的字串,繼續往下看。當執行到地址 0044176B 行時,觀看堆疊視窗 local.7 處,出現了我們輸入的註冊碼,說明該行指令上面的那個 call 的作用是把我們輸入的註冊碼壓棧到 local.7 處
繼續往下分析,先不執行程式,用滑鼠單擊地址 00441776 這一行,看見有一個跳轉,這個跳轉反映了我們輸入的註冊資訊的正確與否。很明顯,在該跳轉前面的那個 call 指令裡面,進行了註冊資訊正確與否的判斷。爆破沒有意義,我們在那個 call 指令上下個斷點,方便重新除錯,並加上註釋,然後 F7 跟進去。進去以後,上個全圖
可以看見,接下來的指令充滿了各種跳轉。慢慢分析,前面幾條語句沒有太大價值,一直到地址 004403C6F 行。這裡實現了一個功能,判斷出註冊碼和使用者名稱生成串兩者中長度較短的長度,把這個長度壓棧後,再把該長度右移 0x2,存放到 ebx 中。這裡右移 0x2,也就是除以 4,為什麼要這樣做?這個下面再說
此時 edx 的值為 1。繼續分析,在地址 004403C81 行碰到一個迴圈。分析得知,該迴圈以 ebx 為迴圈計數器,每一次迴圈,都將從註冊碼和使用者名稱生成串中依次取出四個位元組長度的十六進位制 ASCII碼 資料,作為 DWORD 型別的整型資料進行比較,若不相等,程式將跳轉到後面的指令,並影響 ZF 標誌位,跳出該 call 後,程式將輸出 ”註冊碼不對呀,請再試試!“ 的錯誤提示資訊。否則,在迴圈終止後,繼續比較註冊碼和使用者名稱生成串中還沒比較過的部分,並且這部分將小於或等於 3 個位元組。
由此可知,將較短的長度除以 4 的作用是,求得註冊碼和使用者名稱生成串在迴圈中需要比較的次數,每次比較四個位元組。同樣可得,如果迴圈是正常終止的話,說明註冊碼和使用者名稱生成串中已比較過的部分必然是相同的。
很明顯,我們輸入的註冊資訊中,註冊碼和使用者名稱生成串並不相同,若繼續執行迴圈,程式將輸出錯誤提示資訊。為了繼續分析接下來的指令,我們在迴圈判斷出註冊資訊不正確時,修改 ZF 標誌位的值,使程式能夠繼續執行下去。
OK,到這裡我們已經能夠大概猜測出程式判斷註冊資訊正確與否的演算法是這個樣子的,將使用者名稱的 十六進位制ASCII 碼生成一個字串,再把這個字串與註冊碼的十六進位制 ASCII 碼比較,若相等,則註冊成功,否則失敗。也就是說,當我們輸入的註冊碼,與使用者名稱的 十六進位制 ASCII 碼相同,並且使用者名稱不少於 4 個字元時,程式註冊成功。繼續分析下面的指令,發現與我們的猜測相符,這一部分就不分析了。
當然還沒結束,離最終結論還差一點,當我們的使用者名稱是 “ABCD” 而註冊碼是 “41424344” 時,程式將註冊成功。但是當用戶名是 “ABCD" 而註冊碼是 ”4142434445“ 時,也就是說,保證使用者名稱生成串是註冊碼的子串,或者倒過來時,程式能註冊成功嗎?並不能,僅當我們的註冊碼和使用者名稱生成串完全相等時,程式才會註冊成功。那麼是程式的哪一部分剔除了這種可能呢?讓我們想想,在我們的關鍵 call 後面,緊跟著的就是輸出註冊成功與否資訊的 jnz 指令,這是一條條件跳轉指令,依賴於 ZF 標誌位的值進行判斷,ZF = 0,則跳轉。好,我們以使用者名稱是 “ABCD" 而註冊碼是 ”4142434445“ 為例,去到 call 指令裡面最後一條能夠影響 ZF 標誌位的指令分析,也就是下面這條指令
這是一條 add 指令,“ add eax,eax ”,操作物件是 eax 暫存器,eax 的值最後一次改變,是在找出使用者名稱生成串和註冊碼兩者長度中較短的長度那裡
分析一下,如果使用者名稱生成串和註冊碼長度不相同,則 eax 不為 0,則 add eax,eax 指令執行後,ZF 標誌位置 0,跳出 call 後,jnz 跳轉成功,輸出註冊失敗資訊。否則,若兩者長度相同,eax 為 0,則最後 ZF 標誌位置 1,jnz 跳轉不成功,輸出註冊成功資訊。
弄明白程式判斷註冊資訊的演算法後,要註冊該程式就很容易了。要注意的是,從使用者名稱生成使用者名稱生成串的時候,使用的是十六進位制 ASCII 碼,並且字母是大寫的,所以註冊碼中也需要大寫的字母
END.
程式下載:http://pan.baidu.com/s/1b6X1me 密碼:lrcn
題外話:
博主目前還是一個逆向的初學者,在逆向這個領域,幾乎什麼都還不會,還有很長的路要走。
這篇部落格是我的第一篇部落格,因為沒寫過部落格,不知道怎麼寫,都是截圖了事。也許其中還有許多錯誤,或者用詞不恰當,說的不夠好的地方,希望讀者多多包涵,也歡迎直接指明或交流。當然,這是我很用心寫的一篇部落格。
這篇部落格,沒有什麼技術可言,面向的讀者群體自然很小,只是希望像我一樣的逆向初學者,在閱讀了這篇部落格之後,增添一點搞逆向的興趣。
同時,在計劃中,這篇部落格將是我的【逆向初學者CrakeMe】系列的第一篇文章。接下來的,就一邊學一邊寫吧。