[說明]第一篇是胡亂的譯,胡亂的貼上去的,竟......啥也不說了,
感謝kanxue老大的lift和linhanshi大哥的encourage.
爲此又貼一篇,
同樣簡單,但因爲我是個新手,譯得肯定有很多漏洞和爭議,因此嚴重希望博得大夥竊笑的同時,
別忘了批我,幫助我提高:).
[譯者]aalloverred
[譯文]
擊敗調試器
這次我們看看如何使別人跟蹤你的代碼變得困難,將要學習的是調試器相關知識,它們是如何工作
的,及你的程序如何檢測到它們.
-----------------------
躲避調試器-保護你的程序
-----------------------
作者 Giovanni Tropeano 於11/2004
爲 OSIX 而作
<<文章目錄<<
...前言
...調試器簡史
...調試器是如何工作的
...跟蹤,及如何戰勝它
...躲過斷點
:::前言:::
以下面這句話作爲開始--
引用:
/你不可能也不會編出無法被破解的程序./
但這並不是說不能將此變得非常非常困難.
閒言少敘,書歸正傳.本文中,我們將要學習一些反調試技巧,你可以將它們用於你自己的程序中.
我爲OSIX寫的第一篇文章中討論了自修改代碼,並試圖擺脫調試器.現在要討論的是擊敗(或至少是
其變得困難)調試器.
:::調試器簡史:::
以前有一個Debug.com,它是第一個Windows調試器.它屬於標準MS-DOS包中的一部分,而今天除了用
於學習彙編很少有其他用途了.勉強適合破解用的調試器最早是隨80286出現的,但它們不能造成什
麼實際的傷害.
但是,80386出現後情況有了變化,主要的複雜情況是軟件(Windows?)需要更好的調試器.就是這個時
候,隨着一些調試器的不斷強大,它們開始成爲程序員的威脅.80年代末期,Softice出現在舞臺上,給受
保護程序及它們的開發者帶來不少的麻煩.從那時候起,Softice就作爲黑客調試工具傳了下來.但最近
Olly在年輕一代破解者中越來越常用.
現在由於分析軟件的可能性,與黑客對抗就成了無用的掙扎.而與此同時還有威脅來自於那些原本是
新手,但讀過了許多"如何破解程序"問題集後(幸運的是每個人都可以得到這些東西),現在正在尋找
一些東西要來練練手的人.我們想要擺脫的就是這些在尋找新的挑戰的新手.
:::調試器如何工作:::
不知道調試器如何工作就要對抗它會是空談.因此理解調試器究竟是如何工作的很重要.
所有的調試器都無非屬於下面兩類:
.使用處理器的調試能力
.獨立的模仿處理器,監視被測試程序的運行
迄今還沒有高水平的可以不被代碼檢測到或者避過代碼的檢測的模仿型的調試器,而且現在看來在
未來的一段時間內也不會出現.
關鍵是,開發這樣一個仿真器值得麼?因爲我們已經能夠步入代碼,控制某個地址的指令的執行,監視
某個內存地址(或輸入輸出端口)的指向,改變信號任務,等等.我認爲不值得.不管怎樣,繼續下去...
好,下面將要進行深入點的研究,試着跟上.
調試器會檢查標誌寄存器的陷阱位是否爲1.如果是,就在每條指令後自動生成一個INT 1 調試異常,
並且將控制權交給調試器.由此可知,代碼可以通過分析標誌寄存器來檢測跟蹤(調試).所以調試器爲
了不被發現,它必須識別出讀取標誌寄存器的指令,模仿其運行,並且爲陷阱標誌返回零.說起來容易
做起來難,是吧?
有四個調試寄存器:
1. DR0
2. DR1
3. DR2
4. DR3
它們存儲了四個檢測點的線性地址.當然還有一個寄存器保存了每個點的條件,它就是DR7.當任何
一個條件爲真時,處理器就會拋出INT1異常,控制權也就交給了調試器,有四個條件:
1.一條指令被執行
2.某個內存地址的內容被改變了
3.某個內存地址被讀取或改變,但沒有被執行
4.一個輸入輸出端口被引用
現在要討論的是軟件斷點.軟件斷點是唯一隻有用處理器的完全仿真器才能被隱藏的東西.如果將一
個字節的代碼--0xCC插入到一條指令前,再試圖執行它就會引發INT 0x3異常.爲了發現是否至少有一
個點被設置了斷點,程序僅需計算其校驗和就可以了.爲了做到這一點,可以使用MOV, MOVS, LODS,
POP, CMP, CMPS或其他任何指令;沒有任何調試器能夠跟蹤模仿其中的任何一個(據我所知!).
:::跟蹤,以及如何戰勝它:::
因爲完全不可見的調試器還僅僅是一種"可能",大部分還都是可以被檢測到的.
大部分調試器使用一字節的0xCC代碼.
讓我們看一個簡單的保護方法:
列表1.C++中一種簡單的保護方案
int main(int argc, char* argv[])
{
// 加密後的字符串 "Hello, Free World!"
char s0[]="/x0C/x21/x28/x28/x2B/x68/x64/x02/x36/
/x21/x21/x64/x13/x2B/x36/x28/x20/x65/x49/x4E";
__asm
{
BeginCode: ; 正在被調試的
; 代碼的開始
pusha ; 所有的通用
; 寄存器都被保護起來.
lea ebx, s0 ; ebx=&s0[0]
GetNextChar: ; 開始
xor eax, eax ; eax = 0;
lea esi, BeginCode ; esi = &BeginCode
lea ecx, EndCode ; 代碼的長度
sub ecx, esi ; "正在被調試"也被計算在內
HarvestCRC: ; 計算
lodsb ; 下一字節載入到al.
Add eax, eax ; 計算校驗和.
loop HarvestCRC ; 直到(--cx>0)
xor [ebx], ah ; 下一個字符被解密.
Inc ebx ; 指向下個字符的指針
cmp [ebx], 0 ; 直到字符串結束
jnz GetNextChar ; 繼續解密
popa ; 恢復所有的寄存器.
EndCode: ; 被調試代碼的結尾
nop ; 這裏的斷點是安全的.
}
printf(I s0); ; 顯示字符串.
return 0;
}
仔細看看這段代碼(注意註釋).程序啓動後,句子"Hello,Free World!"將會出現在屏幕上.但是當它運行
在調試器下時,哪怕只有一個斷點設置在了BeginCode和EndCode之間,一些像"Jgnnm."Dpgg"Umpnf#0"
這樣毫無意義的垃圾信息就會出現在屏幕上.不賴,是麼?現在纔有些到了點子上了.你甚至可以把計
算校驗和的過程放在另一個線程中來加強這種保護.
說到線程,它需要對事物使用專門的方法.對我們這些凡人來說,要實現同時運行在幾個地方的程序
有點難.而常用的調試器都有一個弱點,即它們是分別調試每個進程的,從來不同時調試.下面的例
子演示瞭如何將此用於保護.
表2 . 分別地調試線程的弱點
// 這個函數將要在另外一個線程內執行.
// 它的作用是神不知鬼不覺地改變用戶名字符串中字符的大小寫 .
void My(void *arg)
{
int p=1;
// 這個指針指向正在加密的字節.
// 注意加密並沒有從第一個字節開始執行,
//因爲那樣的話,斷點就能設在緩存的開始
//而躲過檢測.
//如果碰到的不是換行符'/n'就執行.
while ( ((char *) arg) [p] !='/n')
{
// 下一個字符是否未被初始化? 同時也是在等待.
while( ((char *) arg) [p]<0x20 );
// 第五位發生翻轉.
// 這也就轉換了拉丁字符的大小寫.
((char *) arg) [p] ^=0x20;
// 指向將被處理的下一個字節.
p++;
}
}
int main(int argc, char* argv[])
{
char name[100];
//存儲用戶名的緩存
char buff[100];
//存儲密碼的緩存
//用戶名緩存中填充0.
// (有的編譯器會這樣做,但並不是所有的.)
memset (&name[0], 0, 100);
//子程序My在另外一個線程裏執行.
_beginthread(&My, NULL, (void *) &name[0]);
//需要輸入用戶名.
printf("Enter name:"); fgets(&name[0], 66, stdin);
//需要輸入密碼.
//注意: 用戶輸入密碼的過程中, 第二線程有充分的時間
// 將用戶名中所有字符轉換大小寫.這很隱蔽,對程序的分析也不會
// 跟蹤至此.在一個不善於反應程序各部分間的影響的調試器下
// 調試時尤其如此.
printf("Enter password:"); fgets(&buff[0], 66, stdin);
// 用戶名和密碼通參考值作比較.
//
if (! (strcmp(&buff[0], "password/n")
//注意: 因爲輸入的用戶名被改變了,所以
//用strcmp(&name[0], "Osix/n"),
//而不是strcmp(&name[0], "OSIX/n")來比較它.
// (這乍一看時很不明顯.)
|| strcmp(&name[0], "OSIX/n")))
// 正確的用戶名和密碼
printf("USER OK/n");
else
// 錯誤:錯誤的用戶名或密碼
printf("Wrong user or password!/n");
return 0;
}
看一下列表中的程序,我們關心是程序看來要接受OSIX:password的, 但實際的答案卻
是Osix:password.我們來研究的稍微仔細些.用戶輸入用戶名後,第二個線程就處理存有用
戶名的緩存,並且轉換了大小寫(除了第一個字符).所以就知道了,當一個線程被調試的
時候,所有其他線程都在各自獨立的工作着.而這些其他線程也可以隨意的干涉正在被調
試的線程的工作(比如說更改它的代碼).啊...到現在這纔開始有些可能了!
這裏又有一些東西得考慮了.因爲已經知道線程可以被控制,但是如果保護開發者放置了
多於四個的斷點,調試寄存器就不再可信了,我們就不得不使用0xCC字節,而這樣做的話,由上文
可知是很容易被檢測到的.
若是被調試的程序裏使用了結構化異常處理(structural exception handling ,SEH ),調試器就會把
情況搞得更糟,著名的Softice也不例外.那樣正在被調試的指令要麼會"擊敗"調試器,將自己
由調試器的控制中釋放出來,要麼將控制權交給庫異常,而它只不過是在調用幾個代碼足以淹沒
破解者的服務程序之後將控制權交給程序處理.
然而,與以前的Softice版本相比,這種情況有了進步.以前,Softice嚴格控制某些中斷,比如它
不允許程序獨立執行除零的操作.
讓我們再看看另外一些代碼吧?!當下面的例子在Softice 包括4.05及以下的任何版本下運行,調試
器到達行int c=c/ (a-b)時都將會突然中止執行,失去對程序的控制權.但是這種情況可以得到
改正,只要提前在__except塊的第一條指令處設置斷點即可.那麼問題是如何如不用深入源代
碼就找到這個塊的位置呢?而黑客是不可能有源代碼的.
列表3. 使用結構化異常處理(structural exception handling)的一個例子
int main(int argc, char* argv[])
{
// 受保護的塊
__try{
int a=1;
int b=1;
int c=c/ (a-b);
// 這裏是做執行除零的操作.
// 使用了多條指令是因爲
// 大部分編譯器遇到形如
// int a=a/0的表達時都會返回錯誤;
// 當SoftIce執行下面的指令時, 它就失去了
// 對被調試程序的控制權. 它落到了一些
// 從不會獲得控制權卻可能產生誤導的代碼上.
// 如果變量a和b被賦予的是某些函數的返回值,
// 而不是立即數, 二者的相等的關係
// 在程序被反彙編了之後
// 就不那麼明顯了. 結果就是, 黑客可能會浪費時間
// 去分析那些無用的代碼 ,嘿嘿呵!
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
// 發生除數爲零的異常時,這裏的代碼將會獲得
// 控制權 , 但是SoftIce意識不到這種情況。
// 取而代之,要求手動在__except塊的第一條指令
// 處設置斷點 .
// 要確定塊__except的地址, 黑客必須準確地指出
// SEH支持是如何在某個特定的
// 編譯器中實現的.
}
}
對於破解者來說,他們處理這樣的保護時必須深入的研究結構化異常在系統級別上和在調試器級別
上是如何進行的.對於一個業餘的破解者來說工作量太大了,不是麼?
因爲SEH在不同的編譯器中實現方法是不同的,這也就難怪SoftIce不支持它了.這對程序員來說是
好事,對破解者來說就太糟糕了!
所以前面的例子都是強烈對抗中斷的,同時也容易實現.它在從Windows95開始的Windows家族的所
有操作系統下都能很好的起作用.
:::躲過斷點:::
在系統函數上下斷點是破解者擁有的強大武器.假設某個保護試圖打開key文件.在Windows下,唯一
一個在文獻中記錄着的做法就是調用CreateFile函數(實際上,CreateFileA和CreateFileW是分別對應文件
名的ASCII和UNICODE形式而已). 所有其他由早期的Windows版本繼承而來的函數都只不過是封裝
了CreateFile而已.
知道了這些,黑客就會在函數開始的起始地址設置斷點(這個地址黑客是知道的),從而迅速的定位調
用了這個函數的保護代碼.
儘管如此,並不是所有的黑客都知道打開文件可以有另外的方法:通過調用由NTDLL.DLL輸出的
ZwCreateFile (或者NtCreateFile)函數,或者通過調用INT 0x2Eh中斷直接定位kernel.這不僅僅對CreateFile
成立,kernel中的所有函數都是這樣的.還有有用一點是做到這些不需要什麼權限.這樣的調用甚至可
以是源於程序代碼的.
這些小把戲不會阻止破解者太長時間.這很糟糕.但是將這個小定時炸彈放在那裏是值得的(在塊
__try中調用INT 0x2E中斷).
現在,怎麼處理USER和GDI模塊中那些用來讀取用戶輸入的註冊信息(按慣例,是一個序列號或者密
碼)的函數(比如,GetWindowsText)呢?因爲我們知道這些函數都是以指令PUSH EBP/MOV EBP, ESP開
始的,指令代碼可以另外的執行它:即不將控制權直接的傳到函數開始,而是開始處三個字節後(因爲
PUSH EBP會改變堆棧,傳遞控制權時必須使用JMP而不是CALL.)這樣設置在函數開始出的斷點就不
會產生任何作用.這樣的技巧可能會使一個熟練的黑客都暫時的誤入歧途.
斷點可被分爲兩類:開發者設置在程序內的斷點和調試器本身設置的動態斷點.第一類很清楚:要將
控制權在某個必要的地方轉移給調試器就必須使用__asm{int 0x3}.
在程序的任意位置設置的斷點就有些複雜了.調試器會保存指定位置處內存地址的當前值,然後再
在那裏寫入代碼0xCC.退出調試中斷之前調試器會將全部位置都恢復原樣,並修改堆棧中存儲的IP
的值使其指向已恢復的指令的開始處.(否則,它就會指向它的中間.)
圖1:進入中斷子程序時堆棧的內容
--------
8086處理器的斷點機制的缺點是什麼呢?最讓人感覺不好的就是調試器設置斷點時必須直接修改代
碼.
SoftIce中使用步過(F10鍵)方式跟蹤程序時只是隱式的將斷點設在下一條指令的前面,這樣就會破
壞保護代碼中用到的校驗和.
最簡單的解決方法是一條指令一條指令的跟蹤--當然,這是在說笑;這種情況必須設置硬件斷點.碰到
類似的情況時,我們的前輩們(1980年代的黑客)通常都是手動將程序解密,再結合NOP指令將解密過
程替換掉.這樣,調試程序時就不會再出現問題了(如果保護中沒有使用其它陷阱的話).在IDA出現以
前,解密程序都是作爲一個獨立的程序在C(Pascal,BASIC)中編制的.現在這項工作已經變得簡單了,因
爲解密在反彙編器內部實現已經成爲了可能.
解密實際上就是用IDA-C語言對解密程序重新編制.這種情況下,從BeginCode到EndCode的校驗和必
須計算出來,算上每個字節的和再用校驗和的低字節載入下一個字符.獲得的值用來使用異或操作
處理s0字符串.所有這些都可以使用下面的代碼實現(假設反彙編代碼中已經存在了適當的標籤):
列表 239. 用IDA-C重新實現解密功能
auto a; auto p; auto crc; auto ch;
for (p=LocByName("s0"); Byte(p) !=0; p++)
{
crc = 0;
for(a=LocByName("BeginCode"); a<(LocByName("EndCode")); a++)
{
ch = Byte(a);
// 因爲IDA不支持byte和word類型
// (是個遺憾), 必須讓它參與位運算.
// CRC低字節被清除,
//然後CH的內容拷貝到那裏.
crc = crc & 0xFFFFFF00;
crc = crc | ch;
crc=crc+crc;
}
//從CRC中取出高字節.
crc = crc & 0xFFFF;
crc = crc / 0x100;
// 字符串的下一個字節被解密.
PatchByte(p, Byte(p) ^ crc);
}
如果沒有IDA,在HIEW中也可以實現這個過程,如下:
NoTrace.exe ?W PE 00001040 a32 <Editor> 28672 ? Hiew 6.04 (c)SEN
00401003: 83EC18 sub esp, 018 ;"$"
00401006: 53 push ebx
00401007: 56 push esi
00401008: 57 push edi
00401009: B905000000 000005
0040100E: BE30604000 [Byte/Forward ] 406030 ;" @'0"
00401013: 8D7DE8 1>mov bl, al | AX=0061 p][-0018]
00401016: F3A5 2 add ebx, ebx | BX=44C2
00401018: A4 3 | CX=0000
run from here: 4 | DX=0000
00401019: 6660 5 | SI=0000 [0FFFFFFE8]
0040101B: 8D9DE8FFFF 6 | DI=0000
00401021: 33C0
.0040101B: 8D9DE8FFFFFF
.00401021: 33C0 xor eax, eax
.00401023: 8D3519104000 lea esi, [000401019]; < BeginCode
.00401029: 8D0D40104000 lea ecx, [000401040]; < EndCode
.0040102F: 2BCE sub ecx, esi
.00401031: AC lodsb
00401032: 03C0 add eax, eax
00401034: E2FB loop 000001031
00401036: 3023 xor [ebx], ah
00401038: 43 inc ebx
00401039: 803B00 cmp b, [ebx], 000 ;" "
0040103C: 75E3 jne 000001021
0040103E: 6661 popa
to here:
00401040: 90 nop
00401041: 8D45E8 lea eax, [ebp][-0018]
00401044: 50 push eax
00401045: E80C000000 call 000001056
0040104A: 83C404 add esp, 004
1Help 2Size 3Direct 4Clear 5ClrReg 6 7Exit 8 9Store 10Load
第一步,計算校驗和.文件載入HIEW後,要找到所需的代碼片斷.然後按兩次<Enter>鍵將其轉換到彙編
模式,在按下組合鍵<F8>+<F5>跳到入口點,這就找到了開始代碼主過程.下一步,按<F3>鍵進入文
件編輯狀態,用組合鍵<Ctrl>+<F7>調出解密編輯窗口(這個組合鍵隨着版本的不同而不同).然後輸入
下面的代碼:
mov bl, al
add ebx, ebx
可以用其他的寄存器代替EBX,但是不能用EAX,因爲HIEW每次讀取下一個字節前都會將EAX清除.
現在鼠標到了0x401019這一行,按<F7>鍵運行解密過程到0x401040這一行(但不包括這行).如果這些都
作的沒錯的話,高字節BX中的值0x44正是校驗和.
第二步裏,找到的加密行(它的偏移,載入了ESI),然後與0x44異或.(按<F3> 鍵轉到編輯模式,按
<Ctrl>+<F8> 鍵指定加密用的key, 0x44, 再按 <F8>鍵按行執行解密程序.)
NoTrace.exe ?W PE 00006040 <Editor> 28672 ? Hiew 6.04 (c)SEN
00006030: 48 65 6C 6C-6F 2C 20 46-72 65 65 20-57 6F 72 6C Hello, Free World
00006040: 20 65 49 4E-00 00 00 00-7A 1B 40 00-01 00 00 00 eIN z$@ $
現在剩下的就是將行0x401036處的XOR用NOP指令換掉,否則程序運行時,XOR會破壞解密代碼(會再
加密一次),程序也就不再工作了.
去除了這個保護後,再調試這個程序時,在需要的範圍內就不會產生嚴重的後果了.
好了,到此爲止了.又一篇關於彙編和調試的文章,很長,但希望還算有用.
再見!
Trope