天天看點

暴力搜尋記憶體空間獲得 Api 的線性位址

下載下傳本節例子程式和源代碼 (4.43 KB)

暴力?怕怕……呵呵,其實這裡的“暴力”隻是一個形象的比喻。首先說明,本文也是老掉牙的東東了,如果你已經懂得在記憶體中搜尋 Api 的技術,那就不要浪費時間在這篇文章上了。但如果你還是一名初學者,那麼看看本文,應該還是有點幫助的。

首先我們來看看為什麼要在記憶體中搜尋 Api 的線性位址。我們知道,一個 PE 檔案在編譯和連接配接成功後,會有一個 import table ,當需要執行 Api 的時候,會先在 import table 中得到 Api 的位址,然後調用它。這在一般的情況下是足以應付要求的了,可是當有病毒插入宿主的時候,情況就不同了——病毒是在 PE 檔案編譯好之後才插入的,它本身沒有 import table ,那麼要如何得到 Api 的位址呢?我們首先知道,任何一個 Dll ,都可以用 LoadLibraryA 來裝入,然後通過 GetProcAddress 來取得這個 Dll 中的函數的位址。但是,這裡就有個蛋生雞還是雞生蛋的問題了——我們能夠通過 LoadLibraryA 和 GetProcAddress 來獲得别的  Api 的位址,但是我們如何能夠獲得這兩個 Api 的位址呢?現在比較通用的技術是在整個 4GB 的記憶體空間中暴力搜尋  Kernel32.dll 的基位址,然後從 Kernel32.dll 的 export table 中取得 LoadLibraryA 和  GetProcAddress 的位址。有了這兩個 Api ,我們就可以導入别的 Dll ,然後進一步得到那個 Dll 中的函數的線性位址了。

不難吧?不過你或許會有個疑問:為什麼能夠從 4GB 的記憶體中得到 Kernel32.dll 的基位址呢?其實是這樣的,Dll 有一個非常特殊的特性:當有别的程式調用它的時候,它的檔案映象就會動态地映射到調用程序的記憶體位址空間。一般情況下,一個程式在運作的時候, Kernel32.dll  這個 Dll 都會被映射到該程式的記憶體位址空間,成為它的一部分——這樣一來,我們不就可以在宿主的記憶體位址空間中搜尋到 Kernel32.dll  的基位址了嗎?

很好,相信你已經看明白了。Let’s go on!

衆所周知,Kernel32.dll 也是一個  PE 檔案,我們在記憶體中搜尋它的時候,隻需要按照判斷 PE 檔案的方法來執行就 OK 了。值得注意的是,在不同的作業系統下, Kernel32.dll 的基位址是不同的,例如,98 下它是 BFF70000h , 2K 為 77E80000h , XP 為  77E60000h 。由于它們都在 70000000h 以上,是以為了加速搜尋,我們可以就從 70000000h 開始,或者是反過來,從棧頂  [esp] 開始,往下遞減,減少到 70000000h 為止。當然,如果是一個一個位元組地進行搜尋,那麼速度也太慢了吧?!由于 Dll 一般是以  1M 為邊界,是以我們可以用 10000h (64k) 作為跨度,這樣可以大大加快搜尋速度。

值得補充的是, 4GB 的記憶體位址并不是完全可讀的,如果遇到了不能讀的地方,就會産生 GPF (General Protect Fault,一般保護性錯誤)。幸好 Micro$ oft 已經為我們預留了一種辦法——可以用 SEH 來解決。不過在實際的試驗中,我發現 GPE 還沒有真正産生過,是以本文為了盡量簡化操作,并沒有使用 SEH 技術,如果你覺得有需要的話,請自行參考有關資料。

得到了 Kernel32.dll 的基位址後,我們如何得到它的  export table 呢?如上所述, Kernel32.dll 也是一個 PE 檔案,我們完全可以按照處理一般的 PE 檔案的方法來獲得它的 export table 。在 PE 檔案頭有一個叫做 DataDirectory 的資料目錄,它儲存的是 PE 檔案的一些重要部分的起始  RVA 和尺寸,目的是使 PE 檔案的裝入更加快速。 DataDirectory 的第一項就是 export table ,是以我們可以定位到  DataDirectory ,然後讀取它的第一個 VirtualAddress ,這樣就得到了 export table 的 RVA 。

随後,我們就要在 Kernel32.dll 的 export table 中查找我們感興趣的 Api 的位址。這裡涉及到比較繁瑣的 PE 檔案操作,我就不具體進行分析了,請讀者自行參考 Iczelion 的 PE 格式教程的 export table 部分。下面我隻具體談談如何通過函數名字擷取函數位址:

1、定位到 PE Header。

2、從資料目錄讀取引出表的虛拟位址。

3、定位引出表擷取名字數目( NumberOfNames )。

4、并行周遊 AddressOfNames 和 AddressOfNameOrdinals 指向的數組比對名字。如果在  AddressOfNames 指向的數組中找到比對名字,就從 AddressOfNameOrdinals 指向的數組中提取索引值。例如,若發現比對名字的 RVA 存放在 AddressOfNames 數組的第 12 個元素,那就提取 AddressOfNameOrdinals 數組的第  12 個元素作為索引值。如果周遊完 NumberOfNames 個元素,說明目前子產品沒有所要的名字。

5、從 AddressOfNameOrdinals 數組提取的數值作為 AddressOfFunctions 數組的索引。也就是說,如果值是 7 ,就必須讀取 AddressOfFunctions 數組的第 7 個元素,此值就是所要函數的RVA。 

具體到我們的病毒中,有一條公式可以用:

API’s Address = ( API’s Ordinal * 4 ) + AddressOfFunctions’ VA + Kernel32 imagebase 

注意上面的 Kernel32 imagebase ,這個是 Kernel32.dll 的基位址,為什麼要加上它呢?很簡單,因為我們的病毒是附屬在宿主上的,沒有它的話,偏移量就不對啦。(注意了,實際上在很多地方都要加上這個偏移)

不會很難吧?至少我覺得這已經是很初級的了。回想一下我的另外一篇教程《為PE檔案添加新節顯示啟動資訊》,在那裡我使用的技術是用寫死來調用  Api ,這種方法有個很大的缺陷——在不同版本的 Windows 下,Api 位址的寫死不可能相同。看了本文後,相信你就可以輕松地給自己的病毒加上暴力搜尋 Api 的線性位址的功能了——這樣一來,你的病毒就可以在各個版本的 Windows 下運作了。呵呵,是不是很 cooooool  呢?

好了,我相信我已經把基本的概念講清楚了,下面給出示範代碼。如果你在實踐中遇到了什麼困難,歡迎給我來信。[email protected]

Are you ready? Let’s rock!

;*********************************************************

;程式名稱:暴力搜尋記憶體空間獲得 Api 的線性位址

;适用OS:9x/Me/2k/XP

;作者:羅聰

;日期:2002-11-14

;出處:http://www.luocong.com(老羅的缤紛天地)

;本代碼使用了病毒技術,但純粹隻用于技術研究。

;切記:請勿用于非法用途!!!!!!

;注意事項:如欲轉載,請保持本程式的完整,并注明:

;轉載自“老羅的缤紛天地”(http://www.luocong.com)

;*********************************************************

.386

.model flat, stdcall

option casemap:none

;請注意,這裡并沒有引入 kernel32 和 user32:

;引入 comctl32 隻是為了後面調用 InitCommonControls

include /masm32/include/windows.inc

include /masm32/include/comctl32.inc

includelib /masm32/lib/comctl32.lib

GetKernelBase   proto   :DWORD

GetApiAddress   proto   :DWORD, :DWORD

.data

szMyMsg             db  "--=   暴力搜尋記憶體空間獲得 Api 的線性位址  =--", 13, 10, 13, 10,/

                        "請注意:", 13, 10,/

                        "* 本對話框的線性位址是通過暴力搜尋得來 *", 13, 10, 13, 10,/

                        "老羅的缤紛天地",13, 10, "http://www.LuoCong.com", 0

szMyCaption         db  "老羅的病毒基礎教程系列 by LC", 0

aKernel32Base       dd  0

szUser32            db  "user32.dll", 0

szExitProcess       db  "ExitProcess", 0

aExitProcess        dd  0

szLoadLibraryA      db  "LoadLibraryA", 0

aLoadLibraryA       dd  0

szGetProcAddress    db  "GetProcAddress", 0

aGetProcAddress     dd  0

szMessageBoxA       db  "MessageBoxA", 0

aMessageBoxA        dd  0

.code

main:

    ;之是以要調用InitCommonControls(不一定非要它)

    ;是因為在2K下必須随便調用一個函數,否則在2K下不能加載  :(

    invoke InitCommonControls

    ;很眼熟吧?病毒的常用手法……

    call delta

delta:

    pop ebp

    sub ebp, offset delta

    ;獲得 Kernel32.dll 的基位址:

    invoke GetKernelBase, [esp]

    mov aKernel32Base, eax

    ;獲得 Kernel32.dll 中的所需的 Api 的線性位址:

    invoke GetApiAddress, aKernel32Base, addr szExitProcess

    mov aExitProcess, eax

    invoke GetApiAddress, aKernel32Base, addr szLoadLibraryA

    mov aLoadLibraryA, eax

    invoke GetApiAddress, aKernel32Base, addr szGetProcAddress

    mov aGetProcAddress, eax

    ;載入 User32.dll :

    push offset szUser32

    call [ebp + aLoadLibraryA]

    ;獲得 User32.dll 中的 MessageBoxA 的線性位址:

    push offset szMessageBoxA

    push eax

    call [ebp + aGetProcAddress]

    mov aMessageBoxA, eax

    ;呵呵,千呼萬喚始出來,高興了吧??

    push MB_OK or MB_ICONINFORMATION

    push offset szMyCaption

    push offset szMyMsg

    push NULL

    call [ebp + aMessageBoxA]

    ;退出:

    push 0

    call [ebp + aExitProcess]

;**************************************************

;函數功能:查找 Kernel32.dll 的基位址

;**************************************************

GetKernelBase   proc uses esi edi dwKernelRet:DWORD

    LOCAL dwReturn: DWORD

    mov edi, dwKernelRet                ; edi = 堆棧頂

    and edi, 0ffff0000h                 ; 用 AND 獲得初始頁

    .while TRUE

        .if word ptr [edi] == IMAGE_DOS_SIGNATURE       ; 等于“MZ”嗎?

            mov esi, edi                                ; Yes, next...

            add esi, [esi + IMAGE_DOS_HEADER.e_lfanew]  ; 就是 esi + 3ch

            .if word ptr [esi] == IMAGE_NT_SIGNATURE    ; 等于“PE”嗎?

                mov dwReturn, edi                       ; Yes, we got it.

                .break

            .endif

        .endif

        ;以下等同于sub edi, 010000h,即每次減少64k:

        dec edi

        xor di, di

        .break  .if edi < 070000000h    ; 基位址一般不可能小于70000000h

    .endw

    mov eax, dwReturn

    ret

GetKernelBase   endp

;**********************************************************************

;函數功能:從記憶體中 Kernel32.dll 的導出表中擷取某個 API 的入口位址

;**********************************************************************

GetApiAddress   proc uses ecx ebx edx esi edi hModule:DWORD, szApiName:DWORD

    LOCAL dwReturn: DWORD

    LOCAL dwApiLength: DWORD

    mov dwReturn, 0

    ;計算 API 字元串的長度(帶尾部的0)

    mov esi, szApiName

    mov edx, esi

Continue_Searching_Null:

    cmp byte ptr [esi], 0           ; 是否為 Null-terminated char ?

    jz We_Got_The_Length            ; Yeah, we got it.  :)

    inc esi                         ; No, continue searching.

    jmp Continue_Searching_Null     ; searching.......

We_Got_The_Length:

    inc esi                         ; 呵呵, 别忘了還有最後一個“0”的長度。

    sub esi, edx                    ; esi = API Name size

    mov dwApiLength, esi            ; dwApiLength = API Name size

    ;從 PE 檔案頭的資料目錄擷取輸出表的位址

    mov esi, hModule

    add esi, [esi + 3ch]

    assume esi: ptr IMAGE_NT_HEADERS

    mov esi, [esi].OptionalHeader.DataDirectory.VirtualAddress

    add esi, hModule

    assume esi:ptr IMAGE_EXPORT_DIRECTORY   ; esi 指向 Kernel32.dll 的輸出表

    ;周遊 AddressOfNames 指向的數組比對名字:

    mov ebx, [esi].AddressOfNames

    add ebx, hModule                ; 别忘了加上基位址,AddressOfNames 是 RVA

    xor edx, edx                    ; edx = "函數計數",初始化為0

    .repeat

        push esi                    ; 儲存esi,後面會用到。

        mov edi, [ebx]              ; edi = 輸出表中的目前函數名字

        add edi, hModule            ; 别忘了加上基位址

        mov esi, szApiName          ; 函數名字的首位址

        mov ecx, dwApiLength        ; 函數名字的長度

        cld                         ; 設定方向标志

        repz cmpsb                  ; 開始查找,我們先去喝杯咖啡吧  :)

        .if ZERO?                   ; 找到啦?

            pop esi                 ; 恢複 esi

            jmp _Find_Index         ; 查找該函數的位址索引

        .endif

        pop esi                     ; 恢複 esi

        add ebx, 4                  ; 下一個函數(每個函數的位址占用4個位元組)

        inc edx                     ; 增加函數計數

    .until edx >= [esi].NumberOfNames

    jmp _Exit                       ; faint,沒找到,凄然退出……

    ;函數名稱索引 -> 序号索引 -> 位址索引

    ;公式:

    ;API’s Address = ( API’s Ordinal * 4 ) + AddressOfFunctions’ VA + Kernel32 imagebase

_Find_Index:

    sub ebx, [esi].AddressOfNames   ; 上面的 repz cmpsb 那裡,如果比對的話,

                                    ; esi 就指向了下一個函數的首位址,是以要先減掉它。

    sub ebx, hModule                ; 減掉基位址,得到 RVA

    shr ebx, 1                      ; 要除以 2 ,還是因為 repz cmpsb 那行

    add ebx, [esi].AddressOfNameOrdinals    ; AddressOfNameOrdinals

    add ebx, hModule                ; 别忘了基位址

    movzx eax, word ptr [ebx]       ; Now, eax = API’s Ordinal

    shl eax, 2                      ; 要乘以 4 才得到偏移

    add eax,[esi].AddressOfFunctions; + AddressOfFunctions’ VA

    add eax, hModule                ; + Kernel32 imagebase

    ;從位址表得到導出函數位址

    mov eax, [eax]                  ; 得到函數的 RVA

    add eax, hModule                ; 别忘了基位址(說了很多次了,呵呵)

    mov dwReturn, eax               ; 最終得到的函數的線性位址。(呼……好累啊,終于完成了)

_Exit:

    mov eax, dwReturn               ; done!  :)

    ret

GetApiAddress       endp

end main

;********************    over    ********************

;by LC 

繼續閱讀