暴力搜索内存空间获得 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、并行遍历 AddressOfNamesAddressOfNameOrdinals 指向的数组匹配名字。如果在 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 呢?

好了,我相信我已经把基本的概念讲清楚了,下面给出示范代码。如果你在实践中遇到了什么困难,欢迎给我来信。lcother@163.net

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

老罗
2002-11-17