【pker / CVC.GB】 
8、API函数地址的获得 
-------------------- 
回忆一下刚才我们是如何调用API的:首先,引入表是由一系列的IMAGE_IMPORT_DESCRIPTOR 
结构组成的,这个结构中有一个FirstThunk字段,它指向一个数组,这个数组中的值在文件 
被pe ldr加载到内存后被改写成函数的真正入口。一些编译器在调用API时把后面的地址指向 
一个跳转表,这个跳转表中的jmp后面的地址就是FirstThunk中函数的真正入口。对于FASM 
编译器,由于PE文件的引入表是由我们自己建立的,所以我们可以直接使用FirstThunk数组 
中的值。 
无论是哪种情况,总之,call的地址在编译时就被确定了。而我们的病毒代码是要插入到宿 
主的代码中去的,所以我们的call指令后面的地址必须是在运行时计算的。那么怎么找到API 
函数的地址呢?我们可以到宿主的引入表中去搜索那个对应函数的FirstThunk,但是这样做 
有一个问题,我们需要函数并不一定是宿主程序需要的。换句话说,就是可能我们需要的函 
数在宿主的引入表中不存在。这使我们不得不考虑别的实现。我们可以直接从模块的导出表 
中搜索API的地址。 

8.1、暴力搜索kernel32.dll 
------------------------- 
在kernel32.dll中有两个API -- LoadLibraryA和GetProcAddress。前者用来加载一个动态 
链接库,后者用来从一个已加载的动态链接库中找到API的地址。我们只要得到这两个函数 
就可以调用任何库中的任意函数了。 
在上一节中我们说过,程序被加载后[esp]的值是kernel32.dll中的ExitThread的地址,所以 
我们可以肯定kernel32.dll是一定被加载的模块。所以我们第一步就是要找到kernel32.dll 
在内存中的基地址。 
那么我们从哪里入手呢?我们可以使用硬编码,比如Win2k下一般是77e60000h,WinXP SP1 
是77e40000h,SP2是7c800000h等。但是这么做不具有通用性,所以这里我们介绍一个通用 
也是现在最流行的方法:暴力搜索kernel32.dll。 
大概的思想是这样的:我们只要找到得到任意一个位于kernel32.dll地址空间的地址,从这 
个地址向下搜索就一定能得到kernel32.dll的基址。还记得刚才说的那个[esp]么,那个 
ExitThread的地址就是位于kernel32.dll中的,我们可以从这里入手。考虑如下代码: 
            mov     edi,[esp]               ; get address of kernel32!ExitThread 
            and     edi,0ffff0000h          ; base address must be aligned by 1000h 
        krnl_search: 
            cmp     word [edi],'MZ'         ; 'MZ' signature? 
            jnz     not_pe                  ; it's not a PE, continue searching 
            lea     esi,[edi+3ch]           ; point to e_lfanew 
            lodsd                           ; get e_lfanew 
            test    eax,0fffff000h          ; DOS header+DOS stub mustn't > 4k 
            jnz     not_pe                  ; it's not a PE, continue searching 
            add     eax,edi                 ; point to IMAGE_NT_HEADER 
            cmp     word [eax],'PE'         ; 'PE' signature? 
            jnz     not_pe                  ; it's not a PE, continue searching 
            jmp     krnl_found 
        not_pe: 
            dec     edi 
            xor     di,di                   ; decrease 4k bytes 
            cmp     edi,70000000h           ; the base cannot below 70000000h 
            jnb     krnl_search 
            xor     edi,edi                 ; base not found 
        krnl_found: 
            ...                             ; now EDI contains the kernel base 
                                            ; zero if not found 
程序首先把ExitThread的地址和0ffff0000h相与,因为kernel32.dll在内存中一定是1000h字 
节对齐的(什么?为什么?还记得IMAGE_OPTIONAL_HEADER中的SectionAlignment么 :P)。 
然后我们比较EDI指向的字单元是不是MZ标识,如果不是那么一定不是一个PE文件的起始位 
置;如果是,那么我们就得到e_lfanew。我们先检查这个偏移是不是小于4k,因为这个值一 
般是不会大于4k的。如果仍然符合条件,我们把这个值与EDI相加,如果EDI就是kernel32的 
基址那么这时相加的结果应该指向IMAGE_NT_HEADER,所以我们检查这个字单元,如果是PE 
标识,那么我们可以肯定这就是我们要找的kernel32了;如果不是把EDI的值减少4k并继续 
查找。一般kernel32.dll的基址不会低于70000000h的,所以我们可以把这个地址作为下界, 
如果低于这个地址我们还没有找到kernel32那么我们可以认为我们找不到kernel32了 :P 
但是上面的作为有一些缺陷,因为我们的代码是要插入到宿主体内的,所以我们不能保证在 
我们的代码执行前堆栈没有被破坏。假如宿主在我们的代码执行前进行了堆栈操作那么我们 
很可能就得不到kernel32.dll了。 
还有一个方法,就是遍历SEH链。在SEH链中prev字段为0ffffffffh的ER结构的异常处理例程 
是在kernel32.dll中的。所以我们可以找到这个ER结构,然后... 
下面我给出一个完整的程序,演示了如何搜索kernel32.dll并显示: 
format  PE GUI 4.0 
entry   __start 


; code section... 

section '.text' code    readable writeable executable 
    szText: times 20h   db  0 

    ; 
    ; _get_krnl_base: get kernel32.dll's base address 
    ; 
    ; input: 
    ;       nothing 
    ; 
    ; output: 
    ;       edi:    base address of kernel32.dll, 0 if not found 
    ; 
    _get_krnl_base: 
            mov     esi,[fs:0] 
        visit_seh: 
            lodsd 
            inc     eax 
            jz      in_krnl 
            dec     eax 
            xchg    esi,eax 
            jmp     visit_seh 
        in_krnl: 
            lodsd 
            xchg    eax,edi 
            and     edi,0ffff0000h          ; base address must be aligned by 1000h 
        krnl_search: 
            cmp     word [edi],'MZ'         ; 'MZ' signature? 
            jnz     not_pe                  ; it's not a PE, continue searching 
            lea     esi,[edi+3ch]           ; point to e_lfanew 
            lodsd                           ; get e_lfanew 
            test    eax,0fffff000h          ; DOS header+DOS stub mustn't > 4k 
            jnz     not_pe                  ; it's not a PE, continue searching 
            add     eax,edi                 ; point to IMAGE_NT_HEADER 
            cmp     word [eax],'PE'         ; 'PE' signature? 
            jnz     not_pe                  ; it's not a PE, continue searching 
            jmp     krnl_found 
        not_pe: 
            dec     edi 
            xor     di,di                   ; decrease 4k bytes 
            cmp     edi,70000000h           ; the base cannot below 70000000h 
            jnb     krnl_search 
            xor     edi,edi                 ; base not found 
        krnl_found: 
            ret 

    ; 
    ; main entrance... 
    ; 
    __start: 
            call    _get_krnl_base 
            push    edi                     ; now EDI contains the kernel base 
            call    push_format             ; zero if not found 
            db      'kernel32 base = 0x%X',0 
        push_format: 
            push    szText 
            call    [wsprintf] 
            add     esp,0ch 
            xor     eax,eax 
            push    eax 
            call    push_caption 
            db      'kernel',0 
        push_caption: 
            push    szText 
            push    eax 
            call    [MessageBox] 
            ret 


; import section... 

section '.idata' import data    readable 
    ; image import descriptor 
    dd      0,0,0,RVA usr_dll,RVA usr_thunk 
    dd      0,0,0,0,0 
    ; dll name 
    usr_dll     db      'user32.dll',0 
    ; image thunk data 
    usr_thunk: 
        MessageBox      dd      RVA __imp_MessageBox 
        wsprintf        dd      RVA __imp_wsprintf 
                        dd      0 
    ; image import by name 
    __imp_MessageBox    dw      0 
                        db      'MessageBoxA',0 
    __imp_wsprintf      dw      0 
                        db      'wsprintfA',0 

8.2、搜索导出表,获取API地址 
---------------------------- 
在开始之前,如果大家对前面导出表的知识还不熟悉,那么请务必再复习一遍,否则后边的 
内容会显得很晦涩... 
好了,我们继续吧 :P 
整个搜索的过程说起来很简单,但做起来很麻烦,让我们一点一点来。首先我们要先导出函 
数名称表中找到我们要得到的函数,并记下它在这个数组中的索引值。然后通过这个索引值 
在序号数组中找到它对应的序号。最后通过这个序号在导出函数入口表中找到其入口。 
下面我们慢慢来。先要匹配函数名。假设edx中存放着kernel32.dll的基址,esi中存放着API 
的名称。考虑如下代码: 
            mov     ebx,edx                 ; save module image base for 
                                            ; later use 
            push    esi                     ; save API name 
            xchg    esi,edi 
            xor     ecx,ecx 
            xor     al,al 
            dec     ecx 
            repnz   scasb 
            neg     ecx 
            dec     ecx 
            push    ecx                     ; save length of the API name 
            lea     edi,[edx+3ch] 
            add     edx,dword [edi]         ; edx points to IMAGE_NT_HEADER 
            push    edx                     ; save IMAGE_NT_HEADER 
            mov     edi,dword [edx+78h]     ; edi has the RVA of export table 
            add     edi,ebx                 ; edi points to export table 
            lea     esi,[edi+18h] 
            lodsd                           ; eax get NumberOfNames 
            push    eax                     ; save NumberOfNames 
            mov     esi,[edi+20h] 
            add     esi,ebx                 ; now points to name RVA table 
            xor     edx,edx 
        match_api_name: 
            lodsd 
            add     eax,ebx 
            xchg    eax,edi                 ; get a API name 
            xchg    esi,ebp 
            mov     ecx,dword [esp+08h]     ; length of API name 
            mov     esi,dword [esp+0ch]     ; API name buffer 
            repz    cmpsb 
            jz      api_name_found 
            xchg    esi,ebp 
            inc     edx 
            cmp     edx,dword [esp] 
            jz      api_not_found 
            jmp     match_api_name 
上面的代码首先把kernel32.dll的基址复制到ebx中保存,然后计算了API名称的长度(包括 
零)并进行匹配,如果匹配成功则edx包含了这个函数在函数名数组中的索引值。下面在序号 
数组中通过这个索引值得到这个函数的序号。考虑如下代码: 
            shl     edx,1 
            mov     esi,[esp+04h]           ; export table address 
            mov     eax,[esi+24h] 
            add     eax,ebx                 ; ordinal table 
            movzx   edx,word [eax+edx] 
            shl     edx,2 
            mov     eax,[esi+1ch] 
            add     eax,ebx                 ; function address table 
            mov     eax,[eax+edx] 
            add     eax,ebx                 ; found!!! 
首先我们可以得到序号数组的RVA,然后把这个值与模块(这里是kernel32.dll)的基地址 
相加,这样就得到了数组的内存地址。由于序号数组是WORD型的,所以我们的索引值必须要 
乘以2。然后通过这个值在数组中索引到函数在导出函数入口表中的索引。由于这个数组是 
DWORD型的,所以我们这个索引要乘以4。我们很容易得到导出函数入口表的内存地址。最后 
我们通过刚才的索引得到函数的入口地址。 
下面我们看一个完整的代码: 
format  PE GUI 4.0 
entry   __start 


; code section... 

section '.text' code    readable writeable executable 
    ; 
    ; _get_krnl_base: get kernel32.dll's base address 
    ; 
    ; input: 
    ;       nothing 
    ; 
    ; output: 
    ;       edi:    base address of kernel32.dll, zero if not found 
    ; 
    _get_krnl_base: 
            mov     esi,[fs:0] 
        visit_seh: 
            lodsd 
            inc     eax 
            jz      in_krnl 
            dec     eax 
            xchg    esi,eax 
            jmp     visit_seh 
        in_krnl: 
            lodsd 
            xchg    eax,edi 
            and     edi,0ffff0000h          ; base address must be aligned by 1000h 
        krnl_search: 
            cmp     word [edi],'MZ'         ; 'MZ' signature? 
            jnz     not_pe                  ; it's not a PE, continue searching 
            lea     esi,[edi+3ch]           ; point to e_lfanew 
            lodsd                           ; get e_lfanew 
            test    eax,0fffff000h          ; DOS header+DOS stub mustn't > 4k 
            jnz     not_pe                  ; it's not a PE, continue searching 
            add     eax,edi                 ; point to IMAGE_NT_HEADER 
            cmp     word [eax],'PE'         ; 'PE' signature? 
            jnz     not_pe                  ; it's not a PE, continue searching 
            jmp     krnl_found 
        not_pe: 
            dec     edi 
            xor     di,di                   ; decrease 4k bytes 
            cmp     edi,70000000h           ; the base cannot below 70000000h 
            jnb     krnl_search 
            xor     edi,edi                 ; base not found 
        krnl_found: 
            ret 

    ; 
    ; _get_apiz: get apiz from a loaded module, something like GetProcAddress 
    ; 
    ; input: 
    ;       edx:    module handle (module base address) 
    ;       esi:    API name 
    ; 
    ; output: 
    ;       eax:    API address, zero if fail 
    ; 
    _get_apiz: 
            push    ebp 
            mov     ebp,esp 
            push    ebx 
            push    ecx 
            push    edx 
            push    esi 
            push    edi 
            or      edx,edx                 ; module image base valid? 
            jz      return 
            mov     ebx,edx                 ; save module image base for 
                                            ; later use 
            push    esi                     ; save API name 
            xchg    esi,edi 
            xor     ecx,ecx 
            xor     al,al 
            dec     ecx 
            repnz   scasb 
            neg     ecx 
            dec     ecx 
            push    ecx                     ; save length of the API name 
            lea     edi,[edx+3ch] 
            add     edx,dword [edi]         ; edx points to IMAGE_NT_HEADER 
            push    edx                     ; save IMAGE_NT_HEADER 
            mov     edi,dword [edx+78h]     ; edi has the RVA of export table 
            add     edi,ebx                 ; edi points to export table 
            push    edi                     ; save address of export table 
            lea     esi,[edi+18h] 
            lodsd                           ; eax get NumberOfNames 
            push    eax                     ; save NumberOfNames 
            mov     esi,[edi+20h] 
            add     esi,ebx                 ; now points to name RVA table 
            xor     edx,edx 
        match_api_name: 
            lodsd 
            add     eax,ebx 
            xchg    eax,edi                 ; get a API name 
            xchg    esi,eax 
            mov     ecx,dword [esp+0ch]     ; length of API name 
            mov     esi,dword [esp+10h]     ; API name buffer 
            repz    cmpsb 
            jz      api_name_found 
            xchg    esi,eax 
            inc     edx 
            cmp     edx,dword [esp] 
            jz      api_not_found 
            jmp     match_api_name 
        api_not_found: 
            xor     eax,eax 
            xor     edi,edi 
            jmp     return 
        api_name_found: 
            shl     edx,1 
            mov     esi,[esp+04h]           ; export table address 
            mov     eax,[esi+24h] 
            add     eax,ebx                 ; ordinal table 
            movzx   edx,word [eax+edx] 
            shl     edx,2 
            mov     eax,[esi+1ch] 
            add     eax,ebx                 ; function address table 
            mov     eax,[eax+edx] 
            add     eax,ebx                 ; found!!! 
        return: 
            add     esp,14h 
            pop     edi 
            pop     esi 
            pop     edx 
            pop     ecx 
            pop     ebx 
            mov     esp,ebp 
            pop     ebp 
            ret 

    ; 
    ; main entrance... 
    ; 
    __start: 
            call    _get_krnl_base          ; get kernel32.dll base address 
            or      edi,edi 
            jz      exit 
            xchg    edi,edx                 ; edx <-- kernel32.dll's image base 
            call    @f 
            db      'LoadLibraryA',0 
        @@: 
            pop     esi                     ; esi <-- api name 
            call    _get_apiz 
            or      eax,eax 
            jz      exit 
            mov     [__addr_LoadLibrary],eax 
            call    @f 
            db      'GetProcAddress',0 
        @@: 
            pop     esi 
            call    _get_apiz 
            or      eax,eax 
            jz      exit 
            mov     [__addr_GetProcAddress],eax 
            call    @f 
            db      'user32.dll',0 
        @@: 
            mov     eax,12345678h 
        __addr_LoadLibrary  =   $-4 
            call    eax 
            call    @f 
            db      'MessageBoxA',0 
        @@: 
            push    eax 
            mov     eax,12345678h 
        __addr_GetProcAddress   =   $-4 
            call    eax 
            xor     ecx,ecx 
            push    ecx 
            call    @f 
            db      'get_apiz',0 
        @@: 
            call    @f 
            db      'Can you find the import section from this app ^_^',0 
        @@: 
            push    ecx 
            call    eax 
        exit: 
            ret 
posted on 2008-10-13 22:49  一个人的天空@  阅读(447)  评论(0编辑  收藏  举报