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
HashImport.rar
NoImport.rar
HashString计算器.rar