160个Crackme038之P-Code初窥门径
文章目錄
- VB的變量類型
- 實戰分析
- 第一部分 基礎校驗
- 第二部分 根據用戶名計算結果
- 第三部分 除以圓周率
- 第四部分 干擾代碼
- 第五部分 關鍵比較
- 寫出注冊機
- 總結
- 附上分析過程
【軟件名稱】:CyberBlade.2.exe
【軟件大小】:61.0 KB
【下載地址】:https://github.com/TonyChen56/160-Crackme
【加殼方式】:未加殼
【保護方式】:Name/Serial
【編譯語言】:VB P-Code
【調試環境】:W10 x64
【使用工具】 OD,VBExplorer,VB Decompiler
【破解日期】:2019-5-1
【破解目的】:學習分析P-Code類型的程序,理解P-Code虛擬機的解釋過程
【目標程序】:
這一次的目標程序的這個Crackme,是160個Crackme里面的第38個。運行時需要 Visual Basic 5.0 運行庫支持。這個Crackme的分析,用到了三個工具,每個工具都有各自的用途:
- OD:用于跟蹤P-Code偽指令的具體細節及在靜態分析過程中無法查看的數據
- VBExplorer 用于查看P-Code偽指令及注釋
- VB Decompiler 用于靜態查看反匯編的偽代碼,減少OD跟蹤偽指令的工作量
VB的變量類型
想要分析這個Crackme,首先需要了解VB變量類型在內存中的存儲方式。
VB中的variant類型屬于一種結構體,該結構體的前兩個字節表示變量的類型,后面有3個WORD是保留的,接下來才是其真正的值,如下圖:
也就是說VB變量中真正的數據是存儲在首地址+8的位置處,下圖顯示了VB的所有的變量類型及含義
實戰分析
首先用 VB Decompiler反編譯目標程序,找到Check按鈕的點擊事件,分析整個點擊事件的校驗過程
這個程序的校驗過程分為五個部分,下面講解每一個部分的校驗過程
第一部分 基礎校驗
首先根據靜態分析的結果可以看到,該程序首先會校驗用戶名和序列號是否為空,然后判斷序列號長度是否小于5個字節,否則提示錯誤。即使看不懂反匯編后的VB代碼,也可以通過字符串知道整個過程。
第二部分 根據用戶名計算結果
第二部分的校驗過程看的就不那么清晰了,需要利用OD動態跟蹤每一個偽指令的具體操作流程。
用VBExplorer對目標程序進行反編譯,一直往下拉,根據字符串直接忽略第一部分的基礎校驗,來到0040E380的位置
我們可以看到第一個被執行的偽指令是0040E380處的0D,接下來將程序載入OD,數據窗口跟隨->0040E380
然后給第一個字節0D下內存訪問斷點,F9運行
然后隨便輸入一個用戶名和序列號,點擊Check
程序首先會讀取一個字節的操作碼到AL,
來看下VB Explorer中顯示的操作碼,后面的注釋提示這是在調用一個函數,0D后面的是操作碼的參數,接著esi自增1,指向操作碼的參數
接著通過一個jmp跳轉去執行操作碼,0x741BED94是地址跳轉表的首地址,eax保存下一條指令的操作碼,由于每一個跳轉地址是一個DWORD,所以用eax乘以4的值加上跳轉表的基地址來索引下一條偽指令的解釋單元,我們跟隨這個jmp
首先把[ebp-0x4C]賦值給eax,然后將eax壓棧。我們需要知道eax的含義。數據窗口跟隨之后,發現是一個指針,再次選中前四個字節,數據窗口跟隨DWORD,然后將數據顯示方式切換為長型->地址
這其實是一個函數的跳轉表,再接著把esi的內容A0賦值給edi,而esi始終執行的是操作碼
可以看到這一步實際上是在取操作碼的參數A0了
接著取出eax的內容,然后將eax加上edi,eax實際是跳轉表的首地址,那么參數A0,就是跳轉表的偏移
接著call eax,我們不需要跟進這個函數,只需要關注棧中的第二個參數0x19F134即可。直接步過這個函數
可以看到棧中的參數顯示出了我們剛才輸入的用戶名,再接著單步到xor eax,eax的地址
這里把eax的值清零了。也就是說第一條偽指令已經執行完成了。
這個就是P-Code虛擬機的解釋過程,其中esi始終指向要解釋的偽指令,eax保存的是將要解釋的偽指令。
接著看下一條偽指令,6C是操作碼,64FF是參數。后面的注釋告訴我們這條偽指令是在將某個DWORD值入棧。繼續跟蹤
首先取出操作碼6C,然后esi+5執向偽指令參數,接著跳轉執行這條偽指令,直接跟進jmp
首先取出參數FF64放到eax中,FF64是一個負數,
也就是十進制的9C,這也是為什么VBExplorer里會顯示LOCAL_009C的原因,這個9C代表[ebp-9C],是個局部變量
接著將[eax+ebp]入棧,壓棧的是剛剛輸入的用戶名[eax]的值是-98,這里其實是將[ebp-98]局部變量壓入棧,然后eax清零,表示這條偽指令結束。
繼續看下一條,偽代碼解釋的求長度
首先取出偽指令,然后直接跟進jmp
這里調用vbaLenBster,參數是之前輸入的用戶名,接著將用戶名長度入棧后,eax清零,接著看下一條FD6934EF
這條偽指令后面并沒有給出解釋,但是沒有關系,我們可以根據偽指令的執行過程猜測指令含義,這里其實是一個雙操作碼的指令,
首先取出操作碼FD,直接跟進jmp,什么都沒有做,直接將eax清零。之后再次取出操作碼69,繼續跟進jmp
這里將bx賦值為0x3,然后跳轉,繼續跟進
接著取出參數FF34,FF34也是個負數,然后再將FF34加上ebp,代表這是一個局部變量
接下來將ecx賦值給[eax+0x8]的地址處,我們數據窗口跟隨eax,然后將bx賦值給eax,賦值完成后eax值如下:
還記得VB的變量類型嗎?前兩個字節是變量類型,03代表是Long,中間是6個字節的保留位,首地址+8的位置才是真正的數值。
這個07是之前通過vbaLenBstr獲取到的用戶名長度,在這里轉成了變量,并將變量首地址壓棧。
現在我們就能通過實際的跟蹤結果來得出這條指令的含義了。就是將int值轉成變量類型。由于整個跟蹤過程實在是復雜,我這里只貼出算法的關鍵部分
在40E3C1處截取用戶名的第一個字符串
接著在40E3CD處將截取的用戶名每一位轉成ASCII值
接著將用戶名每一位的ASCII值轉為十進制后進行字符串拼接
整個過程循環,循環次數為用戶名的長度,可以直接在這個地方下斷點看到最后的結果
即拼接用戶名的ASCII十進制字符串,第二部分的算法就完成
在VB偽代碼中,var_94就是最后拼接的結果
第三部分 除以圓周率
接下來是第三部分,直接來看VB Decompiler中的偽代碼
這一部分的邏輯也很清晰,如果用戶名拼接的字符串長度大于9的話,就將這個結果轉為浮點數除以圓周率,一直除到結果的長度小于9。這個部分我也用OD詳細跟過每一條偽指令,確實和靜態反匯編的邏輯是一樣的
但是在loc_40E449的位置,將var_94和一個值進行了異或,并且還減去了另外一個值。這兩個數值我們無從得知,只能跟蹤OD
根據偽指令的助記符XorVar和SubVar快速定位到這兩個地址,下內存訪問斷點,很快就能找到這兩個值
可以看到這里實際上是將0x30F85678和用戶名的結果進行異或
然后減去0xD8B3,找到了這兩個數,第三部分也就結束了
第四部分 干擾代碼
這個是最有意思的,你會發現代碼初始化了10次循環,循環將一個變量和Key值進行比較,但問題在于Then分支沒有任何代碼。這也就是說不管這個循環中的比較成立與否 都對我們沒有任何影響
第五部分 關鍵比較
最后一部分,比較序列號減去var_94是否等于用戶名長度,也就是說用戶名計算的結果再加上用戶名的長度就是真正的序列號。最后對這個程序的校驗過程做一個總結
總結:
寫出注冊機
接著我們根據已經分析的算法寫出這個程序的注冊機,這個注冊機用C++寫還是太費勁了 直接用python快
Name = "GuiShou" s = int(''.join([str(ord(i)) for i in Name]))while len(str(s))>9:s = int(s // 3.141592654)Serial = (s ^ 0x30F85678) - 55475 + len(Name) print(Serial)總結
P-Code的程序并非如傳言一樣不可戰勝,只要你有足夠的耐心,配合VB Decompiler+VBExplorer+OD的黃金組合,剩下的就是純體力活了。
P-Code類的程序用OD跟蹤偽指令雖然能看到每一處實現細節,但是畢竟還是太費力了。這個時候如果能總結出一套相對比較完整的P-Code的偽指令及每個參數的具體含義再配合WKTVBDebugger,調試P-Code就顯得游刃有余了。如果有大佬總結出來了還請發我一份 哈哈。
附上分析過程
最后附上分析過程和相關文件
:0040E380 0DA0000300 VCallHresult ;Call ptr_0040342C 獲取輸入的用戶名 :0040E385 6C64FF ILdRf ;Push DWORD [LOCAL_009C] 將用戶名壓入堆棧 :0040E388 4A FnLenStr ;vbaLenBstr 求用戶名長度 :0040E389 FD6934FF CVarI4 ; 將用戶名長度轉為變量 :0040E38D 2F64FF FFree1Str ;SysFreeString [LOCAL_009C]; [LOCAL_009C]=0 釋放用戶名內存 :0040E390 1A68FF FFree1Ad ;Push [LOCAL_0098]; Call [[[LOCAL_0098]]+8]; [[LOCAL_0098]]=0 釋放局部變量 :0040E393 FE68B4FE8901 ForVar ; 初始化循環次數 開始循環 :0040E399 0464FF FLdRfVar ;Push LOCAL_009C 將局部變量0壓入堆棧 :0040E39C 21 FLdPrThis ;[SR]=[stack2] :0040E39D 0F0003 VCallAd ;Return the control index 02 :0040E3A0 1968FF FStAdFunc ; :0040E3A3 0868FF FLdPr ;[SR]=[LOCAL_0098] ***********Reference To:[propget]TextBox.Text| :0040E3A6 0DA0000300 VCallHresult ;Call ptr_0040342C 獲取輸入的用戶名 :0040E3AB 046CFF FLdRfVar ;Push LOCAL_0094 將局部變量0壓入堆棧 :0040E3AE 2824FF0100 LitVarI2 ;PushVarInteger 0001 將局部變量1壓入堆棧 :0040E3B3 04D4FE FLdRfVar ;Push LOCAL_012C 將局部變量0壓入堆棧 :0040E3B6 FC22 CI4Var ;vbaI4Var 將變量1轉為數字 :0040E3B8 3E64FF FLdZeroAd ;Push DWORD [LOCAL_009C]; [LOCAL_009C]=0 將用戶名壓入堆棧 :0040E3BB 4644FF CVarStr ; 將用戶名字符串轉為變量 :0040E3BE 0404FF FLdRfVar ;Push LOCAL_00FC 將局部變量0壓入堆棧 **********Reference To->msvbvm50.rtcMidCharVar | :0040E3C1 0A0A001000 ImpAdCallFPR4 ;Call ptr_00401006; check stack 0010; Push EAX 截取用戶名的第一個字符 :0040E3C6 0404FF FLdRfVar ;Push LOCAL_00FC 將用戶名的第一個字符壓入堆棧 :0040E3C9 FDFEB0FE CStrVarVal ; 將用戶名的第一個字符從變量轉為字符串 **********Reference To->msvbvm50.rtcAnsiValueBstr| :0040E3CD 0B0B000400 ImpAdCallI2 ;Call ptr_0040100C; check stack 0004; Push EAX 將用戶名的第一個字符轉為ASCII值 :0040E3D2 4434FF CVarI2 ; 將用戶名的第一個字符的ASCII值轉為變量 :0040E3D5 FBEFE4FE ConcatVar ; 將ASCII值的十進制進行字符串拼接 :0040E3D9 FCF66CFF FStVar ; 將拼接的字符串轉為變量 :0040E3DD 2FB0FE FFree1Str ;SysFreeString [LOCAL_0150]; [LOCAL_0150]=0 釋放字符串 :0040E3E0 1A68FF FFree1Ad ;Push [LOCAL_0098]; Call [[[LOCAL_0098]]+8]; [[LOCAL_0098]]=0 :0040E3E3 36060044FF24FF04 FFreeVar ;Free 0006/2 variants 釋放變量 :0040E3EC 04D4FE FLdRfVar ;Push LOCAL_012C :0040E3EF FE7EB4FE2D01 NextStepVar ; 開始下一輪循環 :0040E3F5 046CFF FLdRfVar ;Push LOCAL_0094 將用戶名拼接的字符串壓入堆棧——7111710583104111117(0x13) :0040E3F8 FBEB44FF FnLenVar ;vbaLenVar 求用戶名拼接的字符串長度 :0040E3FC 2854FF0900 LitVarI2 ;PushVarInteger 0009 將整形變量9壓入堆棧 :0040E401 5D HardType ; 修改變量9的類型 :0040E402 FB74 GtVarBool ;Push (Pop1 >= Pop2) 比較長度是否大于9 :0040E404 1CB901 BranchF ;If Pop=0 then ESI=0040E425 如果不大于9則跳轉到0040E425 :0040E407 046CFF FLdRfVar ;Push LOCAL_0094 將用戶名拼接的字符串壓入堆棧 :0040E40A FEC454FF50455254 LitVarR8 ; 將參數一(圓周率)的類型修改為浮點數 :0040E416 FBBC44FF DivVar ; 將用戶名拼接的字符串除以圓周率 :0040E41A FBE124FF FnFixVar ; 相當于字符串拷貝 :0040E41E FCF66CFF FStVar ; 將用戶名拼接的字符串轉為浮點數 :0040E422 1E8901 Branch ;ESI=0040E3F5 如果長度大于9則跳轉至0040E3F5 :0040E425 046CFF FLdRfVar ;Push LOCAL_0094 將浮點數結果壓入堆棧 :0040E428 FEC154FF7856F830 LitVarI4 ; 將浮點數轉為整形 :0040E430 FB1744FF XorVar ; 將結果和30F85678進行異或 :0040E434 FCF66CFF FStVar ; 保存結果 :0040E438 046CFF FLdRfVar ;Push LOCAL_0094 將異或后的結果壓棧 :0040E43B 080800 FLdPr ;[SR]=[STACK_0008] :0040E43E 8A4C00 MemLdStr ;Push DWORD [[SR]+004C] 將0xDBD3壓棧 :0040E441 FD6954FF CVarI4 ; 將0xDBD3轉為變量 :0040E445 FB9C44FF SubVar ; 用異或后的結果減去0xDBD3 :0040E449 FCF66CFF FStVar ; 保存結果需要分析記錄和相關文件可以到我的Github下載:https://github.com/TonyChen56/160-Crackme
總結
以上是生活随笔為你收集整理的160个Crackme038之P-Code初窥门径的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 脚本类恶意程序分析技巧汇总
- 下一篇: 160个Crackme039