【反調試】去除各種反調試

前不久破解一個軟件的時候遇到了各種反調試,折騰的自己各種難受,最終爆破了之後感覺心情大快就順手寫下了這篇文章

使用工具

十六進制分析工具:winhex
查殼工具:PEID
脫殼工具:ollydump插件或者LordPE
脫殼修復工具:ImportREC
逆向工具:OllyDbg

分析過程

PE修復

打開源程序所在文件夾,發現有一個crackme,雙機運行程序發現有這個提示:
這裏寫圖片描述
應該是文件的PE結構被修改了,winhex載入分析發現:

這裏寫圖片描述
果然是PE結構的問題,在DOS頭後面的PE頭的16進制應該爲50 45,將上述52修改爲45,保存文件之後發現仍然運行不了,證明PE結構仍然存在問題。
分析PE頭後面的IMAGE_FILE_HEADER(映像文件頭,NT頭),對比結構:

typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;                //運行平臺
    WORD    NumberOfSections;       //文件的區塊數目
    DWORD   TimeDateStamp;          //文件創建的日期和時間
    DWORD   PointerToSymbolTable;   //指向符號表
    DWORD   NumberOfSymbols;        //符號表中符號個數
    WORD    SizeOfOptionalHeader;       //IMAGE_OPTIONAL_HEADER32結構大小
    WORD    Characteristics;            //文件屬性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

上述地址分別相對於50450000之後偏移04h(4C01),06h(0300),08h,0Ch,10h,14h,16h對比之後發現在運行平臺上爲1F0h。
而我們熟知運行平臺如下圖:
這裏寫圖片描述
故將其修改爲14Ch,保存文件。修復PE結構之後可以正常運行了
雙機運行之後如下圖
這裏寫圖片描述
輸入用戶名和註冊碼之後提示不正確,而且在過了一段時間之後程序自動退出,應該加了時間控制(此時主窗口已經退出,只有錯誤提示框)
這裏寫圖片描述

脫殼

用PEID分析載入分析之後發現:
這裏寫圖片描述
程序加了WinUpack的殼好在只是一個普通的壓縮弱殼,用PEID自帶的插件Krypto ANAlzer掃了一遍程序
這裏寫圖片描述
瞭解到該程序並沒有使用什麼知名的加密算法。
因爲這裏加了殼,不便於靜態分析,故筆者在這裏並未使用IDA。用OD載入程序,OEP被殼修改,要先脫殼。因爲WinUpack爲弱殼,所以根據OEP定律,單步運行至OEP改變時右鍵數據窗口中跟隨,然後下硬件訪問斷點,運行之後程序停在OEP,因爲加了殼使OD並沒有完全正常解析指令,:
這裏寫圖片描述
開始脫殼,用ollydump記錄下程序OEP,lordpe轉存,脫殼之後因爲IAT被破壞所以無法正常運行軟件。
使用ImportREC修復脫殼後程序,將 OEP改爲上述14EC,獲取輸入表,發現全部有效,然後修復上面脫殼的轉存文件:
這裏寫圖片描述
能正確運行。OD再次載入,停在正確的OEP,正式開始破解:(這裏因爲重建了輸入表,所以程序的大小會比之前的源程序要大一些,屬於正常情況)
這裏寫圖片描述

初步分析

看到程序的入口點應該想到程序使用了較爲高級的花指令,僞裝了一些API調用,然後通過call eax致使無法查到這些API的調用。
根據最開始的提示Error(標題欄)和“註冊碼錯誤”,使用字符串查找如下:
這裏寫圖片描述
Ctrl+G(轉到上述地址),到這些地址處發現:
這裏寫圖片描述

去除單步異常

退回到OEP一步步分析:運行到這一步時程序會自動終止:
這裏寫圖片描述
因爲前面有一個捕獲異常函數SetUnhandledExceptionFilter,在程序被調試時,ptr ds[eax]此處地址爲0是不可讀寫的,而這裏向一塊不可寫的內存中寫入0x1,自然觸發異常,終止程序。
Nop掉這個函數和異常觸發的mov。

去除父進程校驗

重新載入程序至:
這裏寫圖片描述
這個是窗口主函數了,這個API的第4個參數爲00401340,就是窗口主程序所在地址了,轉到在00401340下int 3斷點,運行至後得:
先不着急單步,瀏覽一遍代碼之後發現程序在此段中多次調用了0040101E處的函數,enter進去之後發現第二層反調試:

00401027  |.  68 28010000   push 0x128                               ; /Length = 128 (296.)
0040102C  |.  8D85 D8FEFFFF lea eax,[local.74]                       ; |
00401032  |.  50            push eax                                 ; |Destination
00401033  |.  E8 FA050000   call <jmp.&kernel32.RtlZeroMemory>       ; \RtlZeroMemory
00401038  |.  C785 D8FEFFFF>mov [local.74],0x128
00401042  |.  6A 00         push 0x0                                 ; /ProcessID = 0
00401044  |.  6A 02         push 0x2                                 ; |Flags = TH32CS_SNAPPROCESS
00401046  |.  E8 AB050000   call <jmp.&kernel32.CreateToolhelp32Snap>; \CreateToolhelp32Snapshot
0040104B  |.  8985 D4FEFFFF mov [local.75],eax
00401051  |.  8D85 D8FEFFFF lea eax,[local.74]
00401057  |.  50            push eax                                 ; /lppe
00401058  |.  FFB5 D4FEFFFF push [local.75]                          ; |hSnapshot
0040105E  |.  E8 C3050000   call <jmp.&kernel32.Process32First>      ; \Process32First
00401063  |.  EB 1F         jmp Xdump1.00401084
00401065  |>  E8 98050000   /call <jmp.&kernel32.GetCurrentProcessId>; [GetCurrentProcessId
0040106A  |.  3B85 E0FEFFFF |cmp eax,[local.72]
00401070  |.  74 26         |je Xdump1.00401098
00401072  |.  8D85 D8FEFFFF |lea eax,[local.74]
00401078  |.  50            |push eax                                ; /lppe
00401079  |.  FFB5 D4FEFFFF |push [local.75]                         ; |hSnapshot
0040107F  |.  E8 A8050000   |call <jmp.&kernel32.Process32Next>      ; \Process32Next
00401084  |>  0BC0           or eax,eax
00401086  |.^ 75 DD         \jnz Xdump1.00401065

上面主要是通過調用系統快照函數(紅色字體標註部分),然後遍歷這個系統當前的進程ID,直到找到當前dump的進程ID後跳走,數據窗口中跟蹤[local.72]地址,發現確實在遍歷進程名和ID。證實了上面我的想法。
而在以下代碼中發現了第二層反調試的真面目:
這裏寫圖片描述
將當前進程的父進程與系統下Explorer.exe進行對比。
繼續單步運行:第二次判斷父進程是否爲CMD.exe
這裏寫圖片描述
這裏因爲一般運行在windows系統下進程調度時,大部分進程都是有父進程Explorer.exe或者cmd.exe創建的,而當程序處於調試狀態時父進程肯定是調試進程,所以這一層反調試能針對很多調試軟件起到很好的反調試作用。
在程序中多次調用了這一層反調試,所以單純的nop需要靠IDC腳本實現多次,這裏我們讓

00401114  |. /74 68         je Xdump1.0040117E

改爲jmp 00401117E,讓它恆跳走,讓程序誤以爲父進程校驗正確。
保存文件之後想到剛開始註冊時會有成功或者失敗提示,那麼是調用了MessageBox這個函數。根據這個信息,我們想到了查找函數。於是查找api調用如下
這裏寫圖片描述
發現這裏並沒有messagebox,這裏應該是到了宏,調用api之前將api名字做了隱藏,之後直接call eax,程序剛開始時代碼說明了這一點:
這裏寫圖片描述

去除時間差校驗

在調用的API中發現了GetDlgItem,這是一個破綻。直接下int 3斷點,運行之後發現自己還沒來得及輸入用戶名和註冊碼程序自動退出,這讓我想到剛剛主窗口中的兩個可疑的函數SetTimer

0040138E  |.  6A 00         push 0x0                                 ; /Timerproc = NULL
00401390  |.  68 E8030000   push 0x3E8                               ; |Timeout = 1000. ms
00401395  |.  6A 06         push 0x6                                 ; |TimerID = 6
00401397  |.  FF75 08       push [arg.1]                             ; |hWnd
0040139A  |.  E8 45020000   call <jmp.&user32.SetTimer>              ; \SetTimer004013C4  |.  6A 00         push 0x0                                 ; /Timerproc = NULL
004013C6  |.  68 10270000   push 0x2710                              ; |Timeout = 10000. ms
004013CB  |.  6A 05         push 0x5                                 ; |TimerID = 5
004013CD  |.  FF75 08       push [arg.1]                             ; |hWnd
004013D0  |.  E8 0F020000   call <jmp.&user32.SetTimer>              ; \SetTimer

兩個都是SetTimer,這個就能解釋之前程序會自動退出的原因了,SetTmier函數即爲每隔固定的一個時間向所在窗口發送消息。上面這段應該發送的是WM_CLOSE而銷燬了窗口。,分析代碼知道一個是1000ms一個是10000ms,而我們在反調試分析代碼過程中所需要的時間遠遠大於這些時間,所以自然會退出。這也是利用調試時間差起到反調試的思路。
將timeout參數值改成FFFF,時間應該我們足夠逆向分析用了。保存文件之後載入文件,進一步分析

逆向分析

在getdlgitem上下int 3斷點,成功斷下:

0040143B  |.  6A 03         push 0x3                                 ; /ControlID = 3
0040143D  |.  FF75 08       push [arg.1]                             ; |hWnd
00401440  |.  E8 8D010000   call <jmp.&user32.GetDlgItem>            ; \GetDlgItem
00401445  |.  A3 60304000   mov dword ptr ds:[0x403060],eax
0040144A  |.  6A 04         push 0x4                                 ; /ControlID = 4
0040144C  |.  FF75 08       push [arg.1]                             ; |hWnd
0040144F  |.  E8 7E010000   call <jmp.&user32.GetDlgItem>            ; \GetDlgItem
00401454  |.  A3 64304000   mov dword ptr ds:[0x403064],eax
00401459  |.  68 74304000   push dump3.00403074                      ; /lParam = 403074
0040145E  |.  6A 32         push 0x32                                ; |wParam = 32
00401460  |.  6A 0D         push 0xD                                 ; |Message = WM_GETTEXT
00401462  |.  FF35 60304000 push dword ptr ds:[0x403060]             ; |hWnd = C0B2C
00401468  |.  E8 71010000   call <jmp.&user32.SendMessageA>          ; \SendMessageA
0040146D  |.  68 F4304000   push dump3.004030F4                      ; /lParam = 4030F4
00401472  |.  6A 32         push 0x32                                ; |wParam = 32
00401474  |.  6A 0D         push 0xD                                 ; |Message = WM_GETTEXT
00401476  |.  FF35 64304000 push dword ptr ds:[0x403064]             ; |hWnd = A07EC
0040147C  |.  E8 5D010000   call <jmp.&user32.SendMessageA>          ; \SendMessageA

程序使用SendMessageA,將字符串的內容送至00403074和004030F4兩處,避免使用GetDlgItemTextA函數直接能獲取明文。
單步跟蹤
這裏寫圖片描述
發現算法在call eax之後來到如下代碼。跟進之後算法分析見代碼中註釋:

004011D9   > \A1 56304000   mov eax,dword ptr ds:[0x403056]          ;  核心算法,此地址處存放用戶名的長度
004011DE   .  83F8 06       cmp eax,0x6                              ;  用戶名長度>=6
004011E1   .  0F8C 97000000 jl dump3.0040127E
004011E7   .  50            push eax
004011E8   .  59            pop ecx
004011E9   .  8D35 00304000 lea esi,dword ptr ds:[0x403000]          ;  預定字符串S1
004011EF   .  8D3D 74304000 lea edi,dword ptr ds:[0x403074]          ;  用戶名
004011F5   >  33C0          xor eax,eax
004011F7   .  33DB          xor ebx,ebx
004011F9   .  8B07          mov eax,dword ptr ds:[edi]               ;  將4位用戶名給eax
004011FB   .  8B1E          mov ebx,dword ptr ds:[esi]               ;  將4位s1給ebx
004011FD   .  25 FF000000   and eax,0xFF                             ;  去掉高位保留第一位,即取一位用戶名
00401202   .  81E3 FF000000 and ebx,0xFF                             ;  去掉高位保留第一位,取S一位
00401208   .  33C3          xor eax,ebx                              ;  二者異或
0040120A   .  0305 4E304000 add eax,dword ptr ds:[0x40304E]          ;  然後累加
00401210   .  A3 4E304000   mov dword ptr ds:[0x40304E],eax
00401215   .  46            inc esi
00401216   .  47            inc edi                                  ;  循環向後讀取
00401217   .^ E2 DC         loopd Xdump3.004011F5
00401219   .  33C9          xor ecx,ecx
0040121B   .  8B0D 5A304000 mov ecx,dword ptr ds:[0x40305A]          ;  註冊碼長度
00401221   .  8D35 25304000 lea esi,dword ptr ds:[0x403025]          ;  預定字符串S2
00401227   .  8D3D F4304000 lea edi,dword ptr ds:[0x4030F4]          ;  註冊碼
0040122D   >  33C0          xor eax,eax
0040122F   .  33DB          xor ebx,ebx
00401231   .  8B07          mov eax,dword ptr ds:[edi]               ;  算法同上
00401233   .  8B1E          mov ebx,dword ptr ds:[esi]
00401235   .  25 FF000000   and eax,0xFF
0040123A   .  81E3 FF000000 and ebx,0xFF
00401240   .  33C3          xor eax,ebx
00401242   .  0305 52304000 add eax,dword ptr ds:[0x403052]
00401248   .  A3 52304000   mov dword ptr ds:[0x403052],eax
0040124D   .  46            inc esi
0040124E   .  47            inc edi
0040124F   .^ E2 DC         loopd Xdump3.0040122D                    ;  循環讀取
00401251   .  A1 52304000   mov eax,dword ptr ds:[0x403052]
00401256   .  8B1D 4A304000 mov ebx,dword ptr ds:[0x40304A]
0040125C   .  85DB          test ebx,ebx
0040125E   .  75 3A         jnz Xdump3.0040129A                      ;  不等跳至失敗
00401260   .  8505 4E304000 test dword ptr ds:[0x40304E],eax         ;  比較用戶名與S1異或和是否等於註冊碼與S2的異或和
00401266   .  75 32         jnz Xdump3.0040129A                      ;  不等則跳向失敗
00401268   .  6A 00         push 0x0
0040126A   .  68 98114000   push dump3.00401198                      ;  ASCII "Success"

在jnz時將標誌位Z改爲1之後看到程序註冊成功了,
這裏寫圖片描述
然後直接將jnz修改爲nop
保存之後按照理論上來講,應該爆破成功,此時無論輸入任何用戶名和註冊碼都應該會提示成功,但是當我們再次運行程序的時候發現

去除SMC

我們在到反彙編窗口發現代碼剛剛爆破的jnz被還原了
這裏寫圖片描述
看來是有代碼SMC防爆自校驗,防止爆破技術,回溯之前代碼,發現在程序載入時又一個這樣的call
這裏寫圖片描述
跟進之後發現:

004012F3  /$  B8 66124000   mov eax,dump4.00401266
004012F8  |.  A3 90384000   mov dword ptr ds:[0x403890],eax
004012FD  |.  8B18          mov ebx,dword ptr ds:[eax]
004012FF  |.  66:81FB 753A  cmp bx,0x3A75
00401304  |.  74 41         je Xdump4.00401347
00401306  |.  68 94384000   push dump4.00403894                      ; /pOldProtect = dump4.00403894
0040130B  |.  6A 40         push 0x40                                ; |NewProtect = PAGE_EXECUTE_READWRITE
0040130D  |.  6A 10         push 0x10                                ; |Size = 10 (16.)
0040130F  |.  FF35 90384000 push dword ptr ds:[0x403890]             ; |Address = NULL
00401315  |.  E8 2C030000   call <jmp.&kernel32.VirtualProtect>      ; \VirtualProtect
0040131A  |.  A1 90384000   mov eax,dword ptr ds:[0x403890]
0040131F  |.  BB 753A0000   mov ebx,0x3A75
00401324  |.  66:8918       mov word ptr ds:[eax],bx
00401327  |.  B8 6E124000   mov eax,dump4.0040126E
0040132C  |.  A3 90384000   mov dword ptr ds:[0x403890],eax
00401331  |.  8B18          mov ebx,dword ptr ds:[eax]
00401333  |.  66:81FB 7532  cmp bx,0x3275
00401338  |.  74 0D         je Xdump4.00401347
0040133A  |.  A1 90384000   mov eax,dword ptr ds:[0x403890]
0040133F  |.  BB 75320000   mov ebx,0x3275
00401344  |.  66:8918       mov word ptr ds:[eax],bx
00401347  \>  C3            retn

原來在程序載入的時候會對上面兩個關鍵的jnz進行檢驗,因爲jnz機器碼爲7532如果發現被修改了就再次修改回去,這樣就無法簡單的nop成功了。
於是我們將這個函數也nop掉保存文件,再次註冊時成功了

總算分析的差不多了,貼上一張圖
這裏寫圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章