獨樹一幟
題幹
題目地址:https://ctf.pediy.com/itembank.htm
是一個驗證器,也可以叫做註冊機。
題意是讓我們拿到用戶 :CTFHUB的Code
0x1 查殼
0x2 OD調試
我第一想法就是下消息斷點,斷在按鈕類上
點擊 “check” 後斷下,但是消息斷點的知識掌握的還不是很好,我沒能追溯到消息處理後跳回的地址。
所以換了思路
有 EditText
,所以想到了之前使用過的方法
分析:
- 編輯框中的內容存在內存中
- 點擊check後訪問內存取出值
通過取值操作對內存的訪問,定位函數的位置
果然成功的追到了函數,同時看到了win32API GetDialogTextA()函數,這裏也給自己擴展了知識點,以後想斷獲取編輯框內容的斷點可使用API斷點 GetDialogTextA
我首先對函數頭下斷點,發現這個函數很大可能是一個窗口消息的回調函數,裏面是個switch,對消息進行分類操作。
call .00401000 這個函數前 push了兩個參數
壓入參數分別爲:
- Name
- KeyCode
隨着 cmp eax,0x1,然後jnz switch的default分支,不再繼續執行
所以可以猜測如果驗證成功那麼 eax中存放的應該是 0x1
這時候我開始耍小聰明,然後以爲是隻要彈出信息框就過了,在jnz的時候把ZFLAG改一下即可,或者把eax改爲0x1,後來仔細去看題目,是讓我提交一個Code
0x3 獲取Code分析
首先要分析註冊機的原理,我們輸入一個註冊碼,程序生成一個正確的註冊碼,兩者進行對比,從而判斷是否正確。
那麼獲取Code的方式就有2種:
- 解析生成Code的算法
- 通過動態調試去找到生成後的Code直接拿來用
0x4 分析彙編
00401000 /$ 53 push ebx ; 獨樹一幟.00406A30
00401001 |. 8B5C24 0C mov ebx,dword ptr ss:[esp+0xC] ; 獨樹一幟.0040125C
00401005 |. 55 push ebp
00401006 |. 56 push esi ; 獨樹一幟.00406930
00401007 |. 8B7424 10 mov esi,dword ptr ss:[esp+0x10] ; 獨樹一幟.00406930
0040100B |. 8A0B mov cl,byte ptr ds:[ebx]
0040100D |. 33ED xor ebp,ebp
0040100F |. 57 push edi
00401010 |. 8A06 mov al,byte ptr ds:[esi]
00401012 |. 3AC1 cmp al,cl
00401014 |. 0F85 69010000 jnz 獨樹一幟.00401183
壓入 0x1
賦值 ebx 爲 "keycode"
壓入 ebp 不知道是啥
壓入 esi 中 getDialogTextA()函數地址
賦值 esi 爲 賬號
賦值 cl 爲 [ebx]的一個字節 'cl 爲keycode第一個字符'
異或 ebp,ebp
壓入 edi 0x0
賦值 al 爲 [esi]的第一個字節 'al 爲用戶名的第一個字符'
比較 al 和 cl是否相等
如果相等則繼續,否則跳轉到 00401183 '結束函數'
//-----------------------------------------------這裏分析出keycode首字符爲C
0040101A |. 8BFE mov edi,esi ; 獨樹一幟.00406930
0040101C |. 83C9 FF or ecx,-0x1
0040101F |. 33C0 xor eax,eax
00401021 |. F2:AE repne scas byte ptr es:[edi]
00401023 |. F7D1 not ecx ; user32.75860043
00401025 |. 49 dec ecx ; user32.75860043
00401026 |. 83F9 05 cmp ecx,0x5
00401029 |. /0F82 54010000 jb 獨樹一幟.00401183
知識點:
repne scas 在彙編中用於遍歷字符串
ecx 通過or ecx,-0x1 初始化爲 FFFFFFFF
在edi中存放字符串,在AL中存放字符
每次循環 ecx - 1
not ecx 取反
dec ecx 減一 # 因爲字符串遍歷到最後一個截至符後需要再減一纔是真正的字符串長度
賦值 edi 爲 esi '用戶名'
獲取到用戶名長度到 ecx
比較ecx和0x5
//-----------------------------------------------這裏可以分析出用戶名長度至少爲5
0040102F |. 807B 01 2D cmp byte ptr ds:[ebx+0x1],0x2D
00401033 |. 0F85 4A010000 jnz 獨樹一幟.00401183
//-----------------------------------------------判斷KeyCode第二個字符是否爲 0x2D '-'
分析到這裏我們可知道以下幾點:
- CODE 前綴爲 ‘C-’
- 用戶名長度至少爲5
00401039 |. 8BFE mov edi,esi ; 獨樹一幟.00406930
0040103B |. 83C9 FF or ecx,-0x1
0040103E |. 33C0 xor eax,eax
00401040 |. 33D2 xor edx,edx
00401042 |. F2:AE repne scas byte ptr es:[edi]
00401044 |. F7D1 not ecx
00401046 |. 49 dec ecx
這一段代碼很眼熟,前面有,就是取出EDI中字符串長度
00401049 |> /0FBE0C32 /movsx ecx,byte ptr ds:[edx+esi]
0040104D |. |03E9 |add ebp,ecx
0040104F |. |8BFE |mov edi,esi ; 獨樹一幟.00406930
00401051 |. |83C9 FF |or ecx,-0x1
00401054 |. |33C0 |xor eax,eax
00401056 |. |42 |inc edx
00401057 |. |F2:AE |repne scas byte ptr es:[edi]
00401059 |. |F7D1 |not ecx
0040105B |. |49 |dec ecx
0040105C |. |3BD1 |cmp edx,ecx
0040105E |.^\72 E9 \jb short 獨樹一幟.00401049
add ebp,0x6064
這裏可看出是一個循環,彈出條件是 edx == ecx,ecx中存放的是用戶名的長度
循環的操作是遍歷字符串每個字符,字符的ASCII值加到ebp中
第一句是取出edx + esi地址的一個字符,edx初始值爲0,隨着循環 inc edx遞增
跳出循環後 ebp 又加上了一個常數
00401066 |. 55 push ebp
00401067 |. 68 34604000 push 獨樹一幟.00406034 ; ASCII "%lu"
0040106C |. 68 306B4000 push 獨樹一幟.00406B30 ; ASCII "49796"
00401071 |. E8 B6030000 call 獨樹一幟.0040142C
後面覺得越看越不對勁,第一題不會這麼難把??
然後我想着從後往前追溯,看看能不能找到生成好的CODE。
果然,不停的F8就可獲取到
0040118E |. BF 446B4000 mov edi,獨樹一幟.00406B44 ; ASCII "C-B25120-49796"
那麼雖然我有了這個正確的Code,如果題目出的難一點,他要隨機生成用戶的註冊碼,那麼解決方案有2種:
- HOOK 生成Code的函數,獲取到正確的Code
- 分析加密算法
0x5 分析加密算法實現註冊機
這裏其實就已經超綱了,題目解題在0X4就已經結束。有興趣可繼續往下看。
如何去判斷正確的算法流程呢?我們已經有了正確的Code,所以跟着正確的Code應該就可以跑出正確的流程。
00401076 |. 8A16 mov dl,byte ptr ds:[esi]
00401078 |. 8BFE mov edi,esi ; 獨樹一幟.00406930
0040107A |. 83C9 FF or ecx,-0x1
0040107D |. 33C0 xor eax,eax
0040107F |. 8815 446B4000 mov byte ptr ds:[0x406B44],dl
00401085 |. C605 456B4000>mov byte ptr ds:[0x406B45],0x2D
0040108C |. F2:AE repne scas byte ptr es:[edi]
0040108E |. F7D1 not ecx
00401090 |. 49 dec ecx
00401091 |. 0FBE4431 FF movsx eax,byte ptr ds:[ecx+esi-0x1]
00401096 |. 50 push eax
00401097 |. E8 C4020000 call 獨樹一幟.00401360
這一段彙編,先是取出用戶名長度
然後 movsx eax,byte ptr ds:[ecx+esi-0x1],取出最後一個字符
push eax 把這個字符壓棧,執行下面的函數,但是下面的這個函數執行後對程序的堆棧和寄存器沒產生影響,所以這個函數是可忽略的。
0040109C |. A2 466B4000 mov byte ptr ds:[0x406B46],al
004010A1 |. BF 306B4000 mov edi,獨樹一幟.00406B30 ; ASCII "25120"
004010A6 |. 83C9 FF or ecx,-0x1
004010A9 |. 33C0 xor eax,eax
004010AB |. F2:AE repne scas byte ptr es:[edi]
004010AD |. F7D1 not ecx
004010AF |. 2BF9 sub edi,ecx
004010B1 |. 81C5 64600000 add ebp,0x6064
這段代碼又是很眼熟的,然後後面又加上了0x6064
這裏看到了C-B,然後往上看賦值語句
猜想 0x406B44爲字符串數組的首地址
通過後面的調試發現 key格式爲 C-{用戶最後一個字符}{計算而來}-{計算而來}
地址爲 00406B30,這個數據爲重要的數據
所以對他下斷點,定位程序什麼時候對這個內存存在寫入操作
就這樣我定位到了算法內部,然後返回上層函數後,定位到了真正的加密函數中
但是又發現已經在堆棧中存在這個值,說明在執行這個函數之前,就已經進行了加密。
所以追溯之前調用的函數
00401F2A |> /8B45 F0 |/mov eax,[local.4]
00401F2D |. |FF4D F0 ||dec [local.4]
00401F30 |. |85C0 ||test eax,eax
00401F32 |. |7F 06 ||jg short 獨樹一幟.00401F3A
00401F34 |. |8BC6 ||mov eax,esi
00401F36 |. |0BC7 ||or eax,edi
00401F38 |. |74 3B ||je short 獨樹一幟.00401F75
00401F3A |> |8B45 F4 ||mov eax,[local.3]
00401F3D |. |99 ||cdq
00401F3E |. |52 ||push edx
00401F3F |. |50 ||push eax
00401F40 |. |57 ||push edi
00401F41 |. |56 ||push esi
00401F42 |. |8945 C0 ||mov [local.16],eax
00401F45 |. |8955 C4 ||mov [local.15],edx
00401F48 |. |E8 03150000 ||call 獨樹一幟.00403450
00401F4D |. |FF75 C4 ||push [local.15]
00401F50 |. |8BD8 ||mov ebx,eax
00401F52 |. |83C3 30 ||add ebx,0x30
00401F55 |. |FF75 C0 ||push [local.16]
00401F58 |. |57 ||push edi
00401F59 |. |56 ||push esi
00401F5A |. |E8 81140000 ||call 獨樹一幟.004033E0
00401F5F |. |83FB 39 ||cmp ebx,0x39
00401F62 |. |8BF0 ||mov esi,eax
00401F64 |. |8BFA ||mov edi,edx
00401F66 |. |7E 03 ||jle short 獨樹一幟.00401F6B
00401F68 |. |035D D4 ||add ebx,[local.11]
00401F6B |> |8B45 F8 ||mov eax,[local.2]
00401F6E |. |FF4D F8 ||dec [local.2]
00401F71 |. |8818 ||mov byte ptr ds:[eax],bl
00401F73 |.^\EB B5 |\jmp short 獨樹一幟.00401F2A
00401F75 |> 8D45 B7 |lea eax,dword ptr ss:[ebp-0x49]
00401F78 |. 2B45 F8 |sub eax,[local.2]
00401F7B |. FF45 F8 |inc [local.2]
這裏就是我們要的加密函數,其中第一個CALL爲關鍵CALL
反覆分析後發現,前面這段彙編是沒有影響的, 取esp + 0x8位置是重要的,然後通過div ecx,有了進一步發現,發現ECX的值是恆定的,從外層函數可看出,0XA是個常數,div ecx,除數放在EAX中,處理後商放在EAX中,餘數放在EDX中,然後進入下一次循環,這個商又作爲被除的數,然後我也發現餘數就是我們的第一個加密參數的逆序輸出。
那麼問題又回到了追溯這個EAX初始值 0x6261是如何來的?
在之前分析過有個 ebp += 用戶名字符的ASCII 然後加一個0x6064
爲了驗證,我也對eax的來源進行追溯,下了內存訪問斷點,也正好回到了之前分析的位置。
然後就是用c++寫個demo測試以下想法
這裏是 6220 因爲我測試的時候用的用戶名和在OD裏的不一樣,但是如果一樣 這個值是相等的。
所以我的猜想是正確的。
這裏發現25120 02152是反過來的,我仔細一想… % 0xA 不就是一個十六進制轉十進制的過程嘛?所以完全沒必要重新計算。
第一個參數出來了,第二個參數也不遙遠了,通過同樣的內存寫入斷點,追溯關鍵CALL,發現最後還是來到了我們分析過的加密算法,只不過eax初始值不是 0x6220了,而是一個其他的數字,我想到之前不是還有個ebp+0x6064的地方嘛?
果然就是加上一個 6064就可可以了,然後就是轉十進制的事情!
如果按照原算法復現,那應該是這樣
但是實際上並不是那麼複雜的,最後的代碼
#include<iostream>
#include<string>
using namespace std;
int main(){
string name;
cout<<"輸入用戶名:";
cin>>name;
int strLen = name.size();
int ebp,code1,code2;
code1 = code2 = 0;
ebp = 0x0;
for(int i=0;i < strLen;i++){
ebp += int(name[i]);
}
ebp += 0x6064;
code1 = ebp;
code2 =code1 + 0x6064;
cout<<"keys:"<<"C-"<<name[strLen-1]<<code1<<"-"<<code2<<endl;
return 0;
}
0x6
轉發請親們註明出處,有興趣大家一起學習~