[翻譯]利用程式碼注入脫殼

看雪資料發表於2015-11-15

原文連結:http://bbs.pediy.com/showthread.php?s=&threadid=4541

來自 聲聲慢 提供的英文原稿

Author: E. Labir
(作者:E. Labir)
翻譯:springkang[DFCG] [Nuke Group][TT]

摘要

本文演示如何透過程式碼注入獲取給定目標的內部資訊。我們的攻擊對當前多數反破解技術來說是完全秘密的,展示了現實生活中的威脅。我們可以重新獲取如下的最相關資訊:
  異常、控制程式碼和有關資訊列表
  呼叫所有DLL檔案(引數、返回程式碼…)的API列表
  輸入表的徹底重建
  入口點
我們的方法具有靈活性,操作起來並不困難。我們給出原始碼並提供一個關於如何分析日誌檔案的真例項子。

關鍵詞:脫殼,程式碼注入,反反破解技術

一、  介紹

除錯的目標從極容易到極難不等。保護良好的軟體常用主要基於SEH(結構化異常處理)的反除錯手段和極混亂的程式碼,這些使得破掉它成為一個無盡的夢魘。在Windows(win9x及後續版),程式生存在自己的地址空間裡。地址空間是平坦的,包含全部(對映的)DLL檔案、資源、和它所需的原料。Windows也提供我們注入自己的程式碼並使它執行在另一程式裡所需的工具。[4]本文使用注入程式碼來嗅探來自目標的大量資訊。我們的方法繞過API重定向和所有的標準反除錯。

我們從注入程式碼獲取的資訊包括對對映到目標地址空間的所有DLL檔案的所有呼叫(還有引數、返回程式碼)―--我們也能夠修改它們。程式碼注入還特別提供了異常及與之有關的資訊(控制程式碼、地址、程式碼…)列表。有了它,我們可以輕易的重新獲取入口點(注入一個追蹤(tracer)),甚至在某些情況下,獲取被偷的位元組。即便這種方法失敗了,所有的這些問題也會大大簡化。

這種方法需要“某種”人為干預去分析日誌檔案,但它節省了大量工作。檢測你正在受到攻擊並非易事,許多殼因此需要重新設計。

我們解決的主題包括:
1)(第一節)殼的描述
2)(第二節)如何注入程式碼並使之執行在目標中
3)(第三節)掛鉤所有的API呼叫
4)(第四節)與API呼叫(記錄日誌、IAT重建)互動
5)(第五節)用注入追蹤找到入口點
6)(第六節)注入程式碼是怎樣隱匿的
7)(第七節)結束,深入研究…

第一節都有程式碼片段(或偽碼),我們也展示了在實驗中出現的例子

二、殼的描述

殼是旨在防止別人檢測程式是如何執行或修改程式的程式。典型地來講,受保護的程式的入口點轉向先執行殼。當殼執行時,它執行以下一些步驟:
  解密目標的區塊(目標=被加殼程式)
  重建輸入表
  跳轉到目標的實際入口點

第一步區塊加密對我們並不重要。通常,在它們在記憶體中解密之前,加密區塊並不是要解決的問題,只要簡單地把它們抓取(dump)到某個檔案,並組合在一起。但第二、三點很重要。

可以用多種複雜方法(several degrees of sophistication,)完成重要的保護。(現實中的一些殼的)一些保護十分脆弱,根本不需我們的方法就可破了它們。我們一貫支援強大的保護應用IAT,即:
1) 刪除輸入表,殼僅(在安全的地方)將API的名字和地址的雜亂資訊儲存到IAT。
2)演算法良好的混亂,並有很多反除錯,反跟蹤…
3)殼並不使用GetProcAddress。相反,它執行自己的演算法,尋找DLL輸出表裡的API。
4)IAT已經重定向(閱讀下面所述)

注意:事實上,第三點對我們沒有影響。然而,一個良好的保護應該這麼做。

看到這麼多的“商業”產品不符合第一點或第三點,真是很驚訝。

第四點(API重定向)如何呢?我們看看“API 重定向”是什麼意思:開啟任意一程式,它肯定輸入kernel32.ExitProcess。現在,看看對它的一個呼叫(call),你會發現類似下面的東西:

call [XXXXXXXXh]      ; call to 
                                 ;kernel32.ExitProcess
                                 ; XXXXXXXXh inside the IAT... 
XXXXXXXXh: YYYYYYYYh             ; address of
                                 ; Kernel32.ExitProcess

另一很平常的可能是:為呼叫call xxxxxxxxh,引數也是一樣的。Windows 載入器(loader)“看見”輸入表,並用重要的APIs地址填充IAT。殼破壞被保護程式的輸入表的資訊,因此當殼把控制權交給目標時,IAT就得到錯誤的值。於是,結果就是殼需要用正確的值填充目標的IAT。

如果你用過一加殼程式,殼符合第四點,例如:Asprotect,Slovak Protector,…IAT看起來像:

XXXXXXXXh: ZZZZZZZZh       ; ZZZZZZZZh is inside a
                                           ; buffer dynamically
                                           ; allocated by
                                           ; the packer, so it will
                                           ; not exist if you
                                           ; remove the packer.
ZZZZZZZZh:  push ebp           ; (*)
            ror eax, 16h
            pushf
            popf
            mov ebp, esp       ; (*)
            call @@1
            db 68h
   @@1:
            add eax, 134h
            ....
            jmp ACTUAL_ENTRY_POINT_OF_API + k

殼將API的開始指令與一些垃圾程式碼(在例子中,除在原始API程式碼裡的以外,有*的指令)混合在一起,它也能包含一些微小的反除錯手段,最終,寫個跳轉(或其他類似的)到位於API+K的入口點處。而K就是已經執行在垃圾程式碼裡的原始指令數。這樣,要找到透過呼叫zzzzzzzzh的實際API或對API掛鉤基本上不可能。

輸入表不是一個直觀明瞭的結構,瞭解它的細節超出了本文的範圍[2]。

一些叫作輸入表重建器的程式能夠模擬一些指令去尋找我們跳轉到的DLL中的地址。然後,你就能夠從輸出表中重新獲得API名字。然而,輸入表重建工具通常十分侷限於它們能嗅到的東西,並不能搞定最新的殼(很明顯,這些殼在釋出前對重建工具進行了測試)。

我們來看看入口點發生了什麼。如我們所說,殼一旦完成工作(對目標解密,重建IAT…),必須把控制權交給被保護的程式。使用Kernel32.CreateThread,或以某種特別的方法,簡單地跳到那裡,從而把它作為一個新執行緒,就可以解決。執行在一個新的執行緒並不是個好主意。執行緒的開始地址太明顯,於是我們(再次)假設最壞的情況:經過很長時間的混亂和保護良好的程式碼後,殼跳到入口點處,跳轉十分隱蔽(自己修改的程式碼等等…)。

另一通常要解決的問題是“被偷程式碼”:殼使用一套預定義的APIs,非常普通的一個是:GetModuleHandleA,並做如下事項:

呼叫GetModuleHandleA,儲存返回
檢視被保護的程式,模式是:


call XXXXXXXXh             ; call to
                            ; kernel32.GetModuleHandleA

現在,刪除這個呼叫,並在每個起始處,用簡單的mov [handle], harcoded_value 替換mov [handle], eax 而harcoded_value之前已經
被殼對GetModuleHandleA的呼叫返回了。

如果破解者不去檢查這個把戲,那麼脫殼後的程式就不能在所有作業系統上執行或有其他的缺陷。殼刪除的這些位元組就是“被偷程式碼”。這種被偷位元組,也就是用一個固定編碼(hardcoded)的值替換一個呼叫,用我們的方法將很輕鬆的定位並重建。

對殼的完整描述大大超出本文的範圍,請參考[1]獲得詳細的解釋。

殼是保護中等價位程式的優先方式,實踐上,所有的共享軟體都依賴於他們的安全。所以,研究他們的利弊成為REC(程式碼逆向工程)的重要研究領域。

三、注入並執行你的程式碼
本節中,我們將大致描述如何注入程式碼並使之執行在目標的地址空間裡。檢視[4]獲取完整的描述。Win9x系統請看[3]。

工程將分為兩部分,參見[4]。我們還需要一個載體程式,負責載入目標程式和注入程式碼。我們把注入程式碼稱為“自動記錄器”(logger),因為它十分精確地描述它所做的一切。

載體需要在目標程式有機會執行前注入並執行我們的記錄器。因此,我們需要建立目標程式為CREATE_SUSPENDED。這樣,目標程式的地址空間將被初始化,但主執行緒要等到我們呼叫ResumeThread後才執行。

結果,載體建立目標程式為CREATE_SUSPENDED,並把記錄器注入到它的地址空間裡。下一步,載體用CreateRemoteThread(Win2k以上系統)執行注入的程式碼。記錄器在目標程式被允許執行前,要做一些基礎工作。當一切準備好後,載體執行目標程式的主執行緒。因此,載體和記錄器需要以某種方式通訊,我們選擇的是透過一個事件,當目標程式(殼)能夠執行時,記錄器設定事件為真。

我們繼續概述如何執行所有的以上步驟(參見win32.hlp),獲得下面的APIs的詳細參考。

; first, we create the event (choose a random name for your event)
;首選,我們建立一個事件(為該事件隨意選個名字)
push offset zsEventName       ; name of event
push FALSE                       ; initial status = FALSE
...
call CreateEventA

; Now, we create the target as CREATE_SUSPENDED.
; The call returns a handle to the created process we need for later.

;現在,我們建立目標程式為CREATE_SUSPENDED
;該呼叫返回一個控制程式碼給建立的程式(我們後面需要)
...
push CREATE_SUSPENDED
...
push offset zsTarget
call CreateProcessA

; The address space of the process has been initialised, but its
; primary thread is suspended. Now, we allocate some memory into
; the target to host our code.

;程式的地址空間已經初始化,但主執行緒仍掛起。現在,我們分配一些記憶體給目標程式,用作給我們的程式碼的宿主。

push PAGE_EXECUTE_READWRITE ; attributes for the allocated 
;memory
...
call VirtualAllocEx


; The return is the image base of the allocated memory. Finally, we can write to it, with
; kernel32.WriteProcessMemory, and run our code with kernel32.CreateRemoteThread.

;返回的是分配記憶體的映像基址。最後,我們可以在裡面寫入kernel32.WriteProcessMemory,並用kernel32.CreateRemoteThread執行我們的程式碼。

...
call WriteProcessMemory
...
call CreateRemoteThread

; At this point, the logger is running while the main thread is suspended.
; The logger will change the status of the event
; we have created at some moment, we need to wait until then before to
; resume the main thread:

這樣,記錄器在主執行緒掛起的時候就執行了。記錄器會改變我們在某個時刻建立的事件的狀態,直到恢復主執行緒執行之前,我們要做的是等待。

push -1                     ; wait infinite time
push dword ptr [hEvent]    ; handle to the event, returned by 
                           ;CreateEventA
call WaitForSingleObject
...
call ResumeThread
push 0
call ExitProcess

如你所見,注入程式碼到另一個程式裡並執行它並不太難。注意目標程式沒有被除錯,因此,它(目標程式)就也沒有什麼好抱怨的了。

四,掛鉤API呼叫
A、問題及相關資訊
如我們上面簡短的評述,殼會模擬API開始的K個指令並跳到(K+1)-th處。前K個指令可以變形,如K=2我們就有:

;KERNEL32.DLL裡的kernel32.ExitProcess的未變入口點

77E55CB5 kernel32.ExitProcess    push ebp
77E55CB6                             mov ebp,esp
77E55CB8                             push -1

你可在緩衝區(buffer)找到的指令的樣本:

     xchg eax, esp
     sub eax, 4
     jmp @@1
     db 68h
@@1:  xchg eax, esp
     mov dword ptr [esp], ebp
     push esp
     pop ebp
     push (77E55CB8+RANDOM_VALUE)
     sub dword ptr [esp], RANDOM_VALUE
     ret

兩者是一樣的,然而第二種不容易運用。緩衝區越長越難模擬,重建輸入表就越困難。

現在,請觀察,如果你在kernel32.ExitProcess的入口點設了斷點,會很輕易的被殼繞過。當然,可以在以後設定該斷點,但那時會知道呼叫引數是很困難的(幾乎不可能)。此外,當別的DLL內部呼叫該斷點地方時,斷點也會突然生效的(be fired up)。
讓在APIs(合適的)地方設定斷點成為可能是一個很重要的問題。控制殼的行為區域就等於立刻控制了它們。

B、步驟
假設kernel32.ExitProcess的API入口點如下:
nop                    ; 1
nop                   ; 2
...                    ; ...
nop                   ; k
...                       ; ...
nop                    ;
jmp kernel32.ExitProcess_ActualStart
殼會模擬前K個nops指令,然後跳到k+1。於是,我們就能在jmp kernel32.ExitProcess_ActualStart. 安全地設定斷點。事實上,初始引數已

經被儲存起來了。
對於給定的DLL(如kernel32),我們要做的如下:
1)獲取kernel32的映像基址
2) 從PE-header得到(take) SizeOfImage
3)將對整個kernel32映像的許可權更改到PAGE_EXECUTE_WRITECOPY
(Change permissions over the whole image of kernel32
to PAGE_EXECUTE_WRITECOPY)
4)儲存DLL中的所有輸出函式的原始入口點,可以在IMAGE_EXPORT_DIRECTORY.ED_AddressOfFunctions.裡找到它們。
5)N= 由kernel32輸出的API的總數,位於IMAGE_EXPORT_DIRECTORY.ED_NumberOfFunctions.
6)將kernel32的每一個API轉移到我們的緩衝區。
7)儲存對DLL的許可權(一些殼用DLL啟用異常)
我們來詳細看看如何將所有的APIs重定向到緩衝區,注意要在殼執行前完成。
我們稱我們使用的緩衝區為DivBuffer。該緩衝區大小為N*M,M是所有緩衝區中的最大值。我們要做的如下:
for (i=0; i<N; ++i){
Change the entry point
of the i-th API to DivBuffer[i*M].
Generate random garbage
and write it to DivBuffer[i*M].
Write some instructions, after the
garbage, to save the value of i.
Write a jump, after the previous
instructions, to our hooker procedure.
將i-th的API的入口點改變到DivBuffer[i*M].
產生隨機垃圾並把它們寫到DivBuffer[i*M]。
在垃圾程式碼後寫一些指令儲存i的值。
在上面的指令後面寫一跳轉,轉到我們的鉤子程式。
}
演算法中,M只是垃圾程式碼大小的上界(對目前的殼而言,M=30是個好的數值)。我們建立的所有的垃圾緩衝區會來到(lead to)鉤子程式(hooker 

procedure),它負責記錄呼叫和原料。
我們概述一下鉤子,它是相當簡單的程式:它儲存暫存器,把一切寫入日誌檔案,然後恢復暫存器,跳轉到API原始入口點(我們已經儲存)。鉤

子看起來像下面的:
hooker PROC
mov eax, esp                      ; save esp and ebp
mov ecx, ebp
pushad
...
;寫日誌檔案
;(殼呼叫的API,以用引數,…)
     ……
;計算殼欲呼叫的API的實際入口點,用它覆蓋下面的dword
          ……
;恢復暫存器
          popad
;跳到API的實際入口點(是 push/ret)
db 68h
; first opcode of push XXXXXXXXh
@ActualEP: db 0,0,0,0
ret
hooker ENDP
可以觀察到,在最開始,我們失去eax和exx的值。沒關係,對Windows而言,eax和ecx都是“垃圾”暫存器,也就是說它們並不被APIs使用。因

此,在內部使用它們是安全的(剩餘的暫存器需要保留),這為我們節省了一些頭痛事,因為我們能儲存那裡的esp和ebp。匆需說,垃圾程式碼需

要保留(preserve)除eax和ecx之外的所有暫存器。

C.當殼呼叫某個API時,發生了什麼?

殼認為它要做如下的事:

1)定位API的映像基址
2)以名字查詢它,通常將該名字的固定編碼(hardcoded)hash值(hash value) 與每個API的hash值相比較,直到匹配為止。
3)模擬API的前K個指令(K每次可以隨機選取)
4)跳轉到第(K+1)-th 個API指令

實際上,它做:

1)定位API的基址
2)以名字查詢,通常比較……
3)模擬第i-th個緩衝區(假設它呼叫第i-th API)的前K個指令
4)跳轉到第(K+1)-th 垃圾指令
最後,我們就能在我們的程式“記錄器”的入口點掛鉤API呼叫。
殼看起來並沒有任何不同之處,但我們要讓呼叫看起來它好像沒有受到保護一樣。(The packer doesn’t see any difference but we have 

kept the call as if it was done without the protection.)

D.操作提示

下列建議可能會幫助你輕鬆上手,編寫自己的記錄器

 Kernel32,user32和advapi32載入於執行在同一作業系統上的所有程式中的相同的映像基址上。因此,記錄器可以從載體程式上繼承(實際上

是全部的)APIs。

 要測試自己的程式,不必從注入程式碼到另一程式開始。在此之前,推薦在載體內分配一個緩衝區,將記錄器複製到那裡,進行測試。這樣, 

你就能做個最小化測試(minimum test)。

 當最終注入程式碼到另一程式,你就能在記錄器的開始處設定斷點。這樣一來,當你用CreateRemoteThread用行它時,它就會蹦潰(crash),

你有機會附載(attach)你的偵錯程式。總的來說,你可以在你想檢查的記錄器的任意一點設一個int3斷點。

 寫到日誌檔案的步驟:

--寫入目標呼叫的APIs的名字
--做一個從十六進位制十進位制到可列印字串的小步驟(記住偏移量是以endian order形式給出的)
(Do a small procedure passing from hexadecimal to a printable string (remember that offsets are given in endian order).
--使用對映你的日誌檔案的共享檔案,以便你不用從記憶體中抓取(dumping)就可以隨時看到並儲存它。從而使你控制可執行程式。
--記住檔案對映不能在記憶體中擴大,用個大點的(1Mb)。
 我們推薦(作為無版權目標程式)Yoda的加密器作實驗。它重定向API呼叫,有少數你必須繞過的異常或小把戲。

五、記錄日誌和IAT重建

看了殼(或目標程式)是如何完成掛鉤所有的呼叫後,我們現在轉到如何對這些呼叫採取行動。我們要解決下面這些主題:
1)記錄API呼叫到日誌
2)改變它們的引數或返回值
3)重新啟用呼叫,以獲取(plaintext)API地址
我們用於此次實驗的殼是市面上最強殼之一。當然,我們不會洩露有助於破掉它的任何資訊(所有的偏移量和原料都已修改)。
讓我們一步一步回顧在現實中如何去做:

A.記錄kernel32日誌

首先,一直要做的是記錄所有指向kernel32的呼叫,僅僅少數從其他DLLs呼叫APIs(不包括後面的ADVAPI32)。
在日誌裡,你要有API和它從何處呼叫(返回值是在dword ptr [esp])。注意你的API可以來自同一(或其他的)DLL的其他APIs裡面呼叫,所有你

就要以某種方式過濾掉這個呼叫。
我們按如下的做:  
mov ebx, dword ptr [esp]            ; take return address
shr ebx, 28                         ; keep only the most 
;significative byte
test ebx, ebx
jnz Dont_Log_Me
這樣做是因為在WinNT裡,kernel32總是載入於77E40000h。有很多更好的方法過濾它們,其中檢視位於PEB(譯者:是什麼?)的已載入模組是否

匹配可能是最好的方法,但這很可靠又十分簡單。我們看看來自被保護的記事本的日誌:
Logger started for DLL KERNEL32.DLL,
target = Notepad.exe
VirtualAlloc From: 00B10024
Param: Buffer size: 00000200 API return: 00A70000
VirtualAlloc From: 00B20101
Param: Buffer size: 00001000 API return: 00A80000
LoadLibraryA From: 00A20BFE
Param: ADVAPI32.DLL <=== interesting!
VirtualFree From: 00A2310B ...
GetLocalTime From: 00A45150 <=== interesting!
...
VirtualFree From: 00D01711 VirtualFree From:00D02224
現在,你必須閱讀了它呼叫的APIs並注意最相關的APIs。我們單獨隔離出kernel32.GetLocalTime是因為它典型的用於30天試用期或之類的。注

意到殼載入了ADVAPI32,這也很有趣,因此,你現在應記錄來自ADVAPI32的所有呼叫。
只要去閱讀一下日誌,你就會精確地明白它是如何工作的。
B、記錄某些選定APIs引數日誌
開始編寫大量彙編程式碼去對成百上千的APIs的引數進行解碼,將是毫無意義的。相反,我們只需選取呼叫的APIs中一小部分進行處理。
可以在esp+4,eps+8,…找到引數,因此,你僅需把它們寫到記錄器裡(小心,因為它們是指標,NULL會毀掉你的記錄器)。
C.修改API呼叫的結果
本例中,我們選擇的API是kernel32.GetLocalTime。我們想鉤住它,並改變它的返回值為一固定日期(於是我們就一直註冊了)。要這樣做,我

們需要在呼叫前知道引數lpSystemTime的值,參見[win32hlp],並在呼叫後立刻修改它指向的資料結構。結果,除寫入日誌外,鉤子還需儲存

lpSystemTime。下面的程式碼演示如何進行完整的過程:
;--------------------------------------------------------; hooking the return address from the call
;--------------------------------------------------------
; first, we save the bytes at the return address because we 
; are going to overwrite them with a jump to our code
cld                              ; clear direction flag
mov esi, dword ptr [esp]         ; take return
mov edi, offset your_buffer      ;
mov ecx, 6                       ; the size of a push/ret
repnz stosb                      
                                 ;

; next we overwrite them with a push/ret leading to our code

mov edi, dword ptr [esp]         ; take return address
mov al, 68h                      ; write the push
stosb                             ;
lea eax, [My_GetLocalTime]       ; write the address of my 
;procedure
stosd                             ;
mov al, 0C3h                      ; write the ret
stosb                             ;

; we also need to keep track of lpSystemTime

mov eax, dword ptr [esp+4]  ; store the parameter lpSystemTime
mov dword ptr [ebp+lpSystemTime], eax
現在,我們讓目標程式完成對kernel32.GetLocalTime的呼叫,因為它已經鉤住我們的程式碼了。注意,目標程式將對它所有的區塊具有讀/寫權

限,所以你對此不必擔心。現在,我們要改變返回值:
;--------------------------------------------------------
; Changing the return to our fake value
;--------------------------------------------------------

; The target jumps here when hooked:

My_GetLocalTime PROC
pushad
pushf               ; not needed in this case but you might have 
                    ;to add it too

; compute the delta handle in ebp

call @@1
@@1:pop ebp
sub ebp, @@1

; modify the returned structure

.mov eax, dword ptr [ebp+lpSystemTime]     ; point to the 
                                           ;SYSTEMTIME structure
mov [eax.wYear], 2004
mov [eax.wMonth], 4
mov [eax.wDayOfWeek], 1
mov [eax.wDay], 8

; write back the bytes at the return instruction

cld
lea esi, [ebp+your_buffer]
lea edi, [ebp+API_ReturnAddress]    ; the value we had at esp 
                                    ;at the hooker
mov ecx, 6
repnz stosb

; restore registers and flags and return

popf
popad
ret
My_GetLocalTime ENDP

這使殼相信我們仍舊是在2004年4月8號。當然,記錄API返回值日誌同儲存eax和API目前使用的所有結構一樣簡單。

D.處理剩餘的DLLs

本例中,我們知道殼載入了ADVAPI32.DLL(對kernel32的一次考查就帶給了我們所有載入的DLLs)。ADVAPI32是個相當重要的DLL,它將我們所需

的APIs包含到登錄檔,那是殼用來儲存它們的註冊資訊的地方。在兩種可能性:

1)總結自己的演算法:掛鉤LoadLibraryA、在每次呼叫時,檢視哪個DLL被載入,同時掛鉤它(譯者:DLL)所有的APIs。
2)簡單地對新DLL運用我們現有的演算法

第一種方法沒有很益處,事實上,在實驗中從未用過它。

E.IAT單一地址嗅探

觀察下列由殼完成的呼叫:

LoadLibraryA From: 00BA08BC Param: COMDLG32.DLL
殼從不使用來自COMDLG32.DLL的APIs,因此,要重建目標的IAT,就必須完成此呼叫。讓我們掛鉤來自COMDLG32所有對APIs的呼叫,然後看看會發生什麼:

Logger started for DLL   COMDLG32
End of log file

一個空的日誌檔案?為什麼?我們只是簡單的執行目標程式,但我們仍不得不強迫它從COMDLG32呼叫一些API。現在,我們執行目標,但還要從選單選擇“選擇字型”(你要用用目標程式,直到你在日誌裡看見一些有趣的東西,這也是我們為什麼推薦建立一個共享檔案作為日誌映像檔案)。

Logger started for DLL COMDLG32
CommDlgExtendedError Return Address: 01002E39
End of log file

好的,有了。我們想想當前連結三個最常見的可能,它們透過下面連結到API:

• call dword ptr [IAT_ENTRY]     : where
IAT_ENTRY is an absolute address inside the IAT.

• call RELATIVE_IAT_ENTRY         : Here, the return
address + RELATIVE_IAT_ENTRY is inside the
IAT (adjust for negative references).

在本例,NotePad.exe中,這些是:

01002E33     call dword ptr [10012AC]
; call we have logged
01002E39     test eax,eax
; return in our log

為區分這兩種情況,只要這樣做:

mov eax, Return_Address
sub eax, 6

cmp byte ptr [eax], 0FFh
je First_IAT_Case

inc eax
cmp byte ptr [eax], 0E8h
je Second_IAT_Case

這讓我們很輕鬆的獲得我們想要的任何API:在跳轉到API原始入口點前,記錄器來到目標程式,試圖嗅出與之相應的IAT地址。該方法可能失敗,例如(NotePad.exe):

01006AEF          mov edi,dword ptr
[KERNEL32.GetModuleHandleA]
01006AF5          call edi

我們可以試試返回的edi值,有可能它已經保留(preserved)了,輸出的提我們“猜”出的IAT地址。另一方面,我們可以限制上面所做的已知情況(在多數情況下足夠了,我們下面就會看到)。
這是從我們實驗中摘取出來的:

lstrcmpW return address    : 01001C8D
IAT address? 010010F0
lstrcpyW return address    : 01001C9F
IAT address? 010010EC
lstrcatW return address    : 01001D4F
IAT address? 010010DC
lstrcpyW return address    : 010040ED
lstrlenW return address    : 010040F6
lstrcpyW return address    : 01004102

你看,lstrcmpW 已經被正確地定位了(lstrcatW也是如此)。它們的值並沒有根據前述的call edi例子對IAT進行猜測。對同一應用程式,user32.dll日誌幾乎產生(yield)了全部的IAT。

我們能在呼叫返回前儲存位元組,並試著以後對它們進行反彙編,注意到這一點很重要(這種情況並不多見)。這樣,我保證我們在多數情況下最起碼可以獲得完整的IAT。然而,我們將看到,還有一種更好的方法。

F.IAT完整重建

在上一節,我們知道要獲取單一API,我們只需記錄正確的DLL日誌,並隨意用用目標程式。因此,我們能一個接一個地增加所有的APIs,直到我們有一個完全工作的應用程式,而這會相當耗時間的。

IAT的完整重建與一個一個增加輸入表相比,相當簡單。我們來看看如何做:

1)首先,我們需要從目標程式使用的每個DLL中重新獲取(retrieve)一個API(必須用用目標程式完成,但這次我們只需每個DLL的一個API,幾分鐘內就可完成)。
2)每個DLL的IAT部分是以0結束的陣列,如下面:

offset 0: ?????????
; dd immediately before the IAT
offset 1: DLL1_API1
; first API imported from this DLL
offset 2: DLL1_API2
; second API imported from this DLL
offset 3: ...
;
offset N: 0
; null terminating dd

位於offset 0的dword是未知內容。它可能是NULL,結束的IAT前面部分(對另一個DLL而言),也可能是垃圾。
問題是我們知道offset1,…,offsetN中的一個,但我們需要知道全部的。事實上,我們甚至不能假設該陣列將有一個NULL結束的dword,因為殼會在那裡設其他的值來迷惑我們。

3)下面的演算法解決此問題:

; input: eax = guessed IAT address
; ouput: reconstruction of the
; imports table for the DLL to
; which eax belongs to

xor ecx, ecx                      ; counter

while ([eax+4*ecx] != 0)
{
push ecx                          ; save ecx
push eax                          ; save eax
call dword ptr [eax+4*ecx]     ; Compel the target
; to do the call.
; This sends us to
; the buffer created
; by the packer to
; emulate the first
; instructions of
; the call.
get the API at our logger     ;
restore the stack              ;
pop eax                          ; restore eax, ecx
pop ecx                           ;
inc ecx                          ; next
}

這就從eax開始的重新獲取了所有的地址,最終我們只需往回移動,直到前一個0。

當然,當我們確實呼叫eax,找到有疑問號(interrogation)標誌的offset時,我們會使程式蹦潰(十分可能)。結果是,我們需要設一個she控制程式碼,保護該演算法的執行。

G.API 呼叫斷點及附加到偵錯程式

通常,你想從某個呼叫開始檢查你自己。用我們的方法,在一個API呼叫上設定斷點顯得太直接了。讓我們看看兩種方法:

當一個API呼叫返回地址是給定的時:

記錄器並不是簡單時記錄位於[esp]的返回地址,並與一儲存的值進行比較:

mov eax, dword ptr [esp]
cmp eax, RETURN_FROM_GETLOCALTIME

為什麼不呢?因為殼會執行在動態分配的緩衝區上,所以映像基址是變化的,導致了不同的返回地址。相反,我們可以取得返回地址的最不significative的位元組看看是否匹配:

mov eax, dword ptr [esp]
mov ebx, RETURN_ADDRESS_FOR_GETLOCALTIME
shl eax, 16
shl ebx, 16
cmp eax, ebx
je my_breakpoint

在第n-th次時我們呼叫給定的API:

我們有個在鉤子程式呼叫的API,把它與一個固定編碼值(hardcoded value)進行比較,用個計數器記住該API被呼叫的次數。完工。
對斷點本身在很多選項,我們所用的用於顯示一個關於資訊,然後進入一個無限迴圈,如:here: jmp here。現在,你可以附加到你的偵錯程式了(attach your debugger),暫停程式,NOP掉jmp,開始除錯。
The savings are spectacular。

六、記錄所有的異常日誌

獲得由殼啟用的異常列表同樣有幫助。如果我們想附加偵錯程式並使用追蹤,它特別有幫助,因為我們知道,它不會被任何異常殺掉。理解異常的最好教程是由Jeremy Gordon寫的“Exception for assembler
Programmers“,必讀。我們假定本文的最小背景。眾所周知,鉤住NTDLL.ZwContinue會給我們帶來很多異常,但不是所有的資訊我們都要。當我們有所鬆懈或(可能譯得不對,原文是:we have unwindings)當控制程式碼拒絕修復異常並把它傳給下一個時, 問題來了。這種情況下,我們需要對工程做點逆向以找到正確的斷點。結果如下:

容易部分:NTDLL.ZwContinue首先將一個指標作為引數傳給上下文(context),第三個引數為異常程式碼,第六個為異常發生的地址。因此,掛鉤ntdll.ZwContinue對非疏忽(non-unwinding)和非拒絕(non-refused)異常已經足夠了。
較困難部分(在WinXP上完成,對其他作業系統有細微差別):我們選擇except32.exe,用偵錯程式開啟它,選擇“在控制程式碼1處理異常”。這使得前兩個控制程式碼拒絕修復異常。現在,按“引起異常”按鈕,你來到這裡:

00410608 div cl
0041060A retn

我們透過異常到控制程式碼(pass exception to the handler),但單步進入(在olly中shift+F7)。現在我們看到:

77F4109C mov ebx,dword ptr [esp]
77F4109F push ecx
77F410A0 push ebx
77F410A1 call ntdll.77F51763
77F410A6 or al,al
77F410A8 je short ntdll.77F410B6
77F410AA pop ebx
77F410AB pop ecx
77F410AC push 0
77F410AE push ecx
77F410AF call ntdll.ZwContinue

改變77F410AG的返回值顯示當異常沒有修復時,ntdll.77F51763返回0。然而,如果我們來到ntdll.ZwContinue,我們知道我們忽略了unwindings或之類的。我們除錯到ntdll.77F51763:

77F51763 push ebp
77F51764 mov ebp,esp
77F51766 sub esp,60
...
77F51771 call ntdll.77F51820
77F51776 test al,al
77F51778 jnz ntdll.77F806B9

這次,改變返回值為1直接導致ntdll.ZwContinue。另一方面,對拒絕的或正常的異常而言,返回值是一樣的。因此,我們不用單步跟入(step into)。下一個在趣的呼叫是這樣的,我們必須除錯到那裡,因為否則的話我們就會在ntdll.ZwContinue:

77F517E8 push esi
77F517E9 call ntdll.77F7333F
...

加上一點耐心,我們到這裡了,它呼叫異常控制程式碼(檢查這對不同例子的正確性):

77F7339B mov ecx,dword ptr [ebp+18]
77F7339E call ecx ; Except.0041080A

總結一下,要記錄所有的異常(WinXP)日誌,我們需要掛鉤call ecx和ntdll.ZwContinue。

注意:第一個異常發生在目標程式準備執行之前,它事實上是由載入器引起的,不用記錄它。

這樣,我們能輕鬆的記錄我們需要的關於異常的所有資訊。
七.入口點定位

我們來看看由殼完成的呼叫。上面摘要的程式碼片段的最後兩個呼叫如下:

...
VirtualFree From: 00D01711
VirtualFree From: 00D02224

這些呼叫很明顯是由殼完成的(即使我們不知道這一點,我們也能從返回地址開始拷回一些指令,當它們變得混亂時就更明顯了)。當然,入口點還在後面。

事實上,我們能檢查日誌檔案(DLLs,異常或其他的),然後輕鬆地、儘可能準確地給出入口點的下限(lower bound)。尋找入口點的努力將顯著的減少。

殼會知道這種攻擊,並在最開始時將所有異常和呼叫編組。這不會節省我們很多工作(無論如何,這將是一個好的改善,所有基於seh控制程式碼的反除錯都能自由的搞定)。處理這些難點需要注入追蹤。

A.  追蹤

有幾種方法編寫追蹤程式碼:
1) 作為偵錯程式的一部分    As part of a debugger.
2)自我追蹤程式碼           Self-tracing code.
3) 程式碼模擬              Code Emulator.

第一個對我們沒有什麼興趣,我們假設目標程式有大量的反除錯手段。第三個超出本文的範圍。讓我們把注意力放在第二點上:

早在DOS時代早期就已經使用的自我追蹤程式碼(self-tracing code)是一種相當強的反破解保護。基本上,我們所有的程式碼執行的同時,陷阱標誌設為ON,意思是我們的she控制程式碼在每個指令都會被呼叫。

陷阱標誌設定如下:

pushf
or dword ptr [esp], 100h
popf
nop                   ; needed for some processors

這會產生一個EXCEPTION-SINGLE-STEP並調出seh控制程式碼。該控制程式碼(某種)對上下文(context)有ring-0存取許可權,能為下一指令再次修改陷阱標誌。在每個seh控制程式碼裡,你能設定陷阱標誌為:

push dword ptr [eax.cx_EFlags]
; eax points to the context
or dword ptr [esp], 100h
pop dword ptr [eax.cx_EFlags]
然而,我們的seh控制程式碼更詭秘點,我們需要:

1)記錄所有long “jmps”日誌
2)避開反跟蹤手段

B.記錄所有的long “jmps”

當陷阱標誌設為真時,我們在cx_Eip處收到下一指令將執行的地址,在EXCEPTION_RECORD.ExceptionAddress收到異常發生的地址。我們僅需要評估他們的不同之處:
mov ebx, dword ptr
[EXCEPTION_RECORD.ExceptionAddress]
mov ecx, dword ptr
[CONTEXT.cx_Eip]
cmp ebx, ecx
ja DontXchg
xchg ebx, ecx
  
DontXchg:
sub ebx, ecx
cmp ebx, 0FFFFh
jb DontLog
; don’t log jumps shorter than 0FFFFh
call LogJmp
; log this long jump

它會記錄所有的跳轉,下一步,你只要看看日誌檔案,自己做個決定。例如,下面是個真實的日誌:

VirtualFree API return: 00C01B74
API return: 00000001
<== last call we had available
Entry Point? 00090392
Entry Point? 00C01B74
Entry Point? 01006AE0

正確的是01006AE0,如果你 看見00090392和00C01B74在由殼先前分配的緩衝區裡,你就會輕鬆知道。

C.避開反跟蹤手段

殼完成所有的異常和呼叫後,跟蹤開始了,這去掉了可能用來殺死跟蹤的99%的手段。在實踐中,我們要檢查日誌,決定跟蹤被殺時發生了什麼,甚至如果需要的話,可以附載到偵錯程式(attach the debugger)。

作為例子,我們看看如何去掉rdtsc手段。Rdtsc(read timestamp counter)是個半文件記載(semi-documented)的機器碼,它讀取位於edx:eax的CPU的當前時間印戳,edx是最significative(譯者:這個詞不太好譯 :-( )部分。例如,可以按如下方法操作該手段:

; read timestamp counter
rdtsc
push edx     ; save most significative part

; loop to loose time
mov ecx, 0FFFFh
next:
xor eax, eax
loopd next

; read again timestamp and compare
rdtsc
pop eax
cmp eax, edx
jne IAmTraced

程式被追蹤時,執行得更慢了(因為我們執行的同樣設定了陷阱標誌)。搞定這個手段十分容易,只要在控制程式碼中這樣做:

mov ebx, dword ptr
[EXCEPTION_RECORD.ExceptionAddress]

cmp byte ptr [ebx], 0Fh
jne NotRdtscOpcode

cmp byte ptr [ebx+1], 31h
jne NotRdtscOpcode

; if we are here is cos the current
; instruction has been an rdtsc.
; Mark this so we can change cx_Edx
; the next time the handler
; is called.

mov dword ptr [IAmAtRDTSC], TRUE

下一次重複時,必須:

mov dword ptr [CONTEXT.cx_Edx],
MY_CONSTANT_TIMESTAMP
mov dword ptr [IAmAtRDTSC], FALSE     ; initialize

不要使用MY_CONSTANT_TIMESTAMP = 0,那太明顯了。
在更復雜的版本中,我們必須計算自上一個rdtsc以來的指令數並決定cx_Edx的增加與否。這實際上去掉了所有類似rdtsc的手段。其他的手段需要其他的處理方法。

D.如何安裝追蹤(器)(tracer)

還有一個細節我們需要處理,那就是如何安裝seh控制程式碼。在殼所有的異常出現後,seh控制程式碼實際上不再需要,也就意味著我們能覆蓋(overwrite)最後一個異常,以便我們能捕獲所有的異常(由於陷阱標誌設定為ON)。要安裝控制程式碼:

lea eax, [Tracer32_handler]
mov ebx, dword ptr fs:[0]
mov dword ptr [ebx+4], eax

總結,我們的追蹤(器)將(緩慢地)執行殼,記錄它所有的跳轉(jmp’s),直到到達入口點。去找到記錄日誌檔案,使用它。

八、這些方法是如何隱藏的?

本節我們討論一些檢測這些指令以及他們弱點的可能方法。

A.執行緒模擬

目標程式能模擬執行在系統中的所有的執行緒,檢查有多少與它相應(correspondd to)。這起不了什麼作用。注意,在主要的工作已經完成後,,注入的程式碼實際上由目標程式呼叫,也就是說我們能夠:

1)用VirtualAllocEx分配記憶體
2)取得入口點
3)從入口點開始,用ReadProcessMemory儲存一個或多個記憶體頁面
4)用GetThreadContext儲存主執行緒的上下文(context)
5)用我們的程式碼覆蓋入口點,在此情況下,記錄器做下列任務:
  a)開啟日誌檔案,作為共享檔案對映(shared file mapping)
   b)為垃圾緩衝區分配記憶體(hooks)
   c)掛鉤kernel32,把它重定向到分配的記憶體
   d)設定一個事件為真,以便載體知道它已經完成
   e)等待
6)執行我們的程式碼
7)用SuspendThread停止我們的程式碼
8)用WriteProcessMemory寫回到原始程式碼裡
9)用SetThreadContext恢復原始的上下文(context)
10)用ResumeThread恢復殼的主執行緒

唯一一點是要確保重定向APIs的緩衝區的存在,即使如果殼的主執行緒終止了。這可以透過建立該緩衝區為命名共享檔案對映(named shared file mapping),從載體開啟它來完成。這樣,緩衝區直到載體同意才會釋放。

B.定位到我們複製程式碼的緩衝區

同樣,這不是個很好的主意。我們的程式碼能隱藏在kernel32的.reloc區塊裡…。事實上,我們監視類似CreateFileA的所有APIs的呼叫,試著開啟檔案,改變他們的返回值到INVALID_HANDLE_VALUE。

C.編寫一個模擬器,跳過API開始處的大量指令

有作用,但它也用於破解目的。如果我們能編寫一個模擬器,它能跨過許多指令,甚至一些反除錯手段,我們就能把它用作輸入表重建器。

D.將DLLs與位於系統目錄下面的DLLs進行檢查

殼需要進行一些呼叫,以重新獲取這些DLLs,然後開啟它們。這在我們的日誌裡很明顯(記錄NtCreateFile日誌)

E.將已載入的DLLs與一些固定編碼(hardcoded)校驗進行檢查

有作用,但不是對所有的Windows版本都適合。殼應該會知道任何DLL的新版本。注意,Windows每一個月左右就有新的安全補丁,其中一些會更新DLLs。

F.分析API的起始指令,看看是否是“API-like”之類的指令

這些怎麼樣?

push ebp
mov ebp,esp
push -1
push dword ptr [ebp+4]
push dword ptr [ebp+8]

我們也可產生API-like的指令,注意,以前的所有指令可以轉化,於是我們仍然可以輕鬆地記錄輸入引數日誌(這樣,add esp,16取消所有的計算)。

G.向量AddressOfFunctions具有在DLL映像檔案外的數值

這也很容易搞定。用儲存在DLLs的.reloc區塊的一些指令連結我們的記錄器。一旦DLL已經被載入到記憶體裡,.reloc就不再需要了。(還有更復雜的方法,幾乎絕對難以檢測undectable)。

總結:我們的方法相當隱匿

九.結束,進一步的研究

本文介紹了一種從嚴密保護的軟體裡獲取內部資訊的新方法。該方法十分隱匿,產生足夠的資訊,顯著減少破解許多堅實目標的時間。

我們的方法主要缺點是:
1)需要人工干預進行日誌分析
2)需要知道我們在上一節使用的所有手段

注意,由於目前反破解的軟體並不知道這些方法,(2)僅在以後不方便。

反破解的軟體應該防止入侵(instrusions),因此,進一步的研究應轉移到如何防止它們。然而,如我們在上一節所見,這可不是件輕鬆的事。

最終評價:本文所解釋的一切可以在兩到三個星期內編碼完成(努力程度“中“)

References
[1] Havok, “Asprotected notepad” Codebreakers-Journal, First Issue
2004.
[2] Labir, E., “Adding imports by hand” Codebreakers-Journal, First
Issue 2004.
[3] Natzgul, “How to access the memory of a process” available at
Fravia.
[4] Kruse, T. “Processless Applications - Remote threads on Microsoft
Windows 2000, XP and 2003” Codebreakers-Journal,
First Issue 2004.


【翻譯後記】
呼呼,第一次翻譯如此長篇大作,花了我一個星期的時間:-)。很累的說,主要是眼睛吃不消,:-( 
感謝原文作者的美文,文字流暢,表達優美,讓我再一次的學習英語。
還要thx聲聲慢,他提供了此文的資料,希望他能再多多提供好的英文資料:-)
還要感謝我的母校:江西遂川中學。以及中學英語老師:郭世澤老師和王立新老師。你們的辛勤教導,才有了我的今天!

由於時間和水平有限,翻譯錯誤之處難免。請大家多多指正。

QQ:14695672
Email:springkang2003@yahoo.com.cn


相關文章