【技术分享】shellcode编程:在内存中解析API地址

admin 2023-12-08 02:10:02 AnQuanKeInfo 来源:ZONE.CI 全球网 0 阅读模式

http://p2.qhimg.com/t01003acdbfff8efbe4.png

翻译:myswsun

预估稿费:200RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿

0x00 前言

针对Windows的所有的与位置无关代码(PIC)的核心功能的基础就是实时解析API函数的地址。它是一个非常重要的任务。在这里我介绍两种流行的方法,使用导入地址表(IAT)和导出地址表(EAT)是目前为止最稳定的方法。

自从2007年Windows Vista发布以来,地址空间布局随机化(ASLR)在可执行文件和动态链接库中启用,这些开启ASLR的库用来缓解漏洞利用。

但是在ASLR出现之前,20年前的病毒开发者同样遇到一个相似的问题,kernel32.dll基址的无意的“随机化”。

第一个Windows 95的病毒叫做Bizatch,由Quantum/VLAD在一个Windows 95的beta版本上编写。

Mr. Sandman, Jacky Qwerty 和 GriYo讨论过kernel32的问题、Win32下面PE感染的GetModuleHandle解决方案,和当时不清楚的进程环境块(PEB)在后来由Ratter在“在NT下从PEB获取重要数据”中讨论。

Jacky Qwerty公布了一种类GetProcAddress的功能,成为病毒中解析API的标准方法。

在这之后,作者开始通过CRC32的校验和来解析API,可以隐藏代码中的API字符串,同时减少空间。

在1999年LethalMind展示了一种他自己的校验和解析API地址的方法。在2002年LSD组织提出了在Win32汇编(shellcode)中获取API的算法,之后被很多Win32 shellcode效仿。

上述是关于API获取的方案的一个简短的历史。到了今天,在漏洞利用时已经出现了很多高级技术,但是他们和保护机制强相关,在这不做讨论。

下面展示的左右结构能在微软SDK中WinNT.h头文件中找到。

你还能在pecoff.docx中找到PE/PE+文件格式的详细描述。

0x01 Image DOS Header

在每个PE文件开始都能找到一个MS-DOS可执行文件或者一个“存根”(即MZ)使得可验证为有效的MS-DOS可执行文件。

在这里我们需要e_lfnew字段,加上当前模块基址能得到NT_IMAGE_HEADERS的指针。

http://p8.qhimg.com/t01acb576429c70ac79.png


0x02 Image NT Headers

因为在内存中映射的PE映像的基址是随机的,只有重要结构的相对虚拟地址(RVA)保存在PE文件中。

为了将RVA转化为虚拟地址(VA),可以使用以下宏。

http://p8.qhimg.com/t01571868ba04473254.png

通过基址加上e_lfanew,然后获得指向IMAGE_NT_HEADERS的指针。

下面两个结构在头文件WinNT.h中定义了,但是编译时根据架构只是用一个。

http://p9.qhimg.com/t01b30efc4d29fc7acd.png

0x03 Image Optional Header

在可选头的末尾是一个IMAGE_DATA_DIRECTORY结构的数组。

// Directory Entries
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0   // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1   // Import Directory
//
// Optional header format.
//
typedef struct _IMAGE_OPTIONAL_HEADER {
  //
  // Standard fields.
  //
  WORD    Magic;
  BYTE    MajorLinkerVersion;
  BYTE    MinorLinkerVersion;
  DWORD   SizeOfCode;
  DWORD   SizeOfInitializedData;
  DWORD   SizeOfUninitializedData;
  DWORD   AddressOfEntryPoint;
  DWORD   BaseOfCode;
  DWORD   BaseOfData;
  //
  // NT additional fields.
  //
  DWORD   ImageBase;
  DWORD   SectionAlignment;
  DWORD   FileAlignment;
  WORD    MajorOperatingSystemVersion;
  WORD    MinorOperatingSystemVersion;
  WORD    MajorImageVersion;
  WORD    MinorImageVersion;
  WORD    MajorSubsystemVersion;
  WORD    MinorSubsystemVersion;
  DWORD   Win32VersionValue;
  DWORD   SizeOfImage;
  DWORD   SizeOfHeaders;
  DWORD   CheckSum;
  WORD    Subsystem;
  WORD    DllCharacteristics;
  DWORD   SizeOfStackReserve;
  DWORD   SizeOfStackCommit;
  DWORD   SizeOfHeapReserve;
  DWORD   SizeOfHeapCommit;
  DWORD   LoaderFlags;
  DWORD   NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

0x04 Image Data Directory

每个目录拥有一个相对虚拟地址和大小。为了访问导出和导入目录,可简单的通过RVA2VA的宏得到虚拟地址。

http://p5.qhimg.com/t01303ff17f01a54c36.png

VirtualAddress:

数据结构的相对虚拟地址。例如,如果这个结构是导入目录,这个字段填充IMAGE_IMPORT_DESCRIPTOR数组的相对虚拟地址。

Size:

包含指向的数据结构的大小。


0x05 Image Export Directory

因为导出目录是目录表的第一项,我们将解释这种获取API的方法。

http://p6.qhimg.com/t01953751a77dfa3f0d.png

我们只对5个字段有兴趣:

Name

DLL名字字符串的相对虚拟地址

NumberOfNames

通过名字导出的API的个数

AddressOfFunctions

指向所有函数的VA数组的相对虚拟机地址。每个VA加上模块基址,能得到一个导出函数的地址。

AddressOfNames

指向所有函数名的VA数组的相对虚拟机地址。每个VA加上模块基址,能得到表示API的非0结尾的字符串的地址。

AddressOfNameOrdinals

序号数组的相对虚拟地址。每个序号表示一个AddressOfFunctions数组的索引。

下面的函数使用DLL和API名字的CRC-32C哈希值,从导出表中获取API的地址。

参数base明显是DLL的基址,参数hash是2个CRC-32C的哈希值。crc32c(DLL字符串)+crc32c(API字符串)。

LPVOID search_exp(LPVOID base, DWORD hash)
{
  PIMAGE_DOS_HEADER       dos;
  PIMAGE_NT_HEADERS       nt;
  DWORD                   cnt, rva, dll_h;
  PIMAGE_DATA_DIRECTORY   dir;
  PIMAGE_EXPORT_DIRECTORY exp;
  PDWORD                  adr;
  PDWORD                  sym;
  PWORD                   ord;
  PCHAR                   api, dll;
  LPVOID                  api_adr=NULL;
  dos = (PIMAGE_DOS_HEADER)base;
  nt  = RVA2VA(PIMAGE_NT_HEADERS, base, dos->e_lfanew);
  dir = (PIMAGE_DATA_DIRECTORY)nt->OptionalHeader.DataDirectory;
  rva = dir[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
  // if no export table, return NULL
  if (rva==0) return NULL;
  
  exp = (PIMAGE_EXPORT_DIRECTORY) RVA2VA(ULONG_PTR, base, rva);
  cnt = exp->NumberOfNames;
  // if no api, return NULL
  if (cnt==0) return NULL;
  
  adr = RVA2VA(PDWORD,base, exp->AddressOfFunctions);
  sym = RVA2VA(PDWORD,base, exp->AddressOfNames);
  ord = RVA2VA(PWORD, base, exp->AddressOfNameOrdinals);
  dll = RVA2VA(PCHAR, base, exp->Name);
  
  // calculate hash of DLL string
  dll_h = crc32c(dll);
  
  do {
    // calculate hash of api string
    api = RVA2VA(PCHAR, base, sym[cnt-1]);
    // add to DLL hash and compare
    if (crc32c(api) + dll_h == hash) {
      // return address of function
      api_adr = RVA2VA(LPVOID, base, adr[ord[cnt-1]]);
      return api_adr;
    }
  } while (--cnt && api_adr==0);
  return api_adr;
}

一个重要的事情是这个函数不能解析通过序号导出的API,前向引用有时也是个问题。

下面是实现相同功能的汇编代码。

; in:  ebx = base of module to search
;      ecx = hash to find
;
; out: eax = api address resolved in EAT
;
search_expx:
    pushad
    ; eax = IMAGE_DOS_HEADER.e_lfanew
    mov    eax, [ebx+3ch]
    ; first directory is export
    ; ecx = IMAGE_DATA_DIRECTORY.VirtualAddress
    mov    ecx, [ebx+eax+78h]
    jecxz  exp_l2
    ; eax = crc32c(IMAGE_EXPORT_DIRECTORY.Name)
    mov    eax, [ebx+ecx+0ch]
    add    eax, ebx
    call   crc32c
    mov    [esp+_edx], eax
    ; esi = IMAGE_EXPORT_DIRECTORY.NumberOfNames
    lea    esi, [ebx+ecx+18h]
    push   4
    pop    ecx         ; load 4 RVA
exp_l0:
    lodsd              ; load RVA
    add    eax, ebx    ; eax = RVA2VA(ebx, eax)
    push   eax         ; save VA
    loop   exp_l0
    pop    edi          ; edi = AddressOfNameOrdinals
    pop    edx          ; edx = AddressOfNames
    pop    esi          ; esi = AddressOfFunctions
    pop    ecx          ; ecx = NumberOfNames
   sub    ecx, ebx     ; ecx = VA2RVA(NumberOfNames, base)
    jz     exp_l2       ; exit if no api
exp_l3:
    mov    eax, [edx+4*ecx-4] ; get VA of API string
    add    eax, ebx           ; eax = RVA2VA(eax, ebx)
    call   crc32c             ; generate crc32 of api string
    add    eax, [esp+_edx]    ; add crc32 of DLL string
    cmp    eax, [esp+_ecx]    ; found match?
    loopne exp_l3             ; --ecx && eax != hash
    jne    exp_l2             ; exit if not found
    xchg   eax, ebx
    xchg   eax, ecx
    movzx  eax, word [edi+2*eax] ; eax = AddressOfOrdinals[eax]
    add    ecx, [esi+4*eax] ; ecx = base + AddressOfFunctions[eax]
exp_l2:
    mov    [esp+_eax], ecx
    popad
    ret

这就是从导出目录获取API的方法。通过导入表更加巧妙。


0x06 Image Import Descriptor

2009年微软发布的EMET阻止了一些从导出目录获取API的shellcode。

EMET从5.2版本开始,包含了导出表访问过滤(EAF)和EAF+功能,都会阻止尝试从模块读取导出和导入目录。

通常,一个shellcode使用IAT解析其他函数前会先获取GetModuleHandleGeProcAddress的地址。

如果PE文件从其他模块导入API,这个导入目录将包含导入描述符的数组,每个代表一个模块。

http://p2.qhimg.com/t01c8a492bc92dda58d.png

来看下面3个字段:

OriginalFirstThunk

包含导入函数名的偏移。

Name

非0结尾字符串表示的导入API的源模块名。

FirstThunk

包含真实函数地址的偏移。


0x07 Image Thunk Data

每个描述符包含了指向Image Thunk Data结构数组的指针。每个入口表示了导入的API的信息。

http://p3.qhimg.com/t010563c6908394e184.png

在代码中,我跳过了那些使用序号导入的入口。

来自OriginalFirstThunk的AddressOfData字段是指向IMPORT_BY_NAME结构的RVA。

FirstThunk的Function字段指向我们要搜索的API的真实地址。


0x08 Image By Name

因为我们不处理从序号导入的情况,所以我们不关心hint字段,只需要非0结尾字符串表示的API名。

http://p6.qhimg.com/t0128293975be8cb44d.png

Hint

包含索引到DLL函数导出表的位置。这个字段被PE加载器使用,因此它能够在DLL导出表中快速的查找函数。这个值不是必须的,有些链接器将这个字段设置为0。

Name

包含导入函数的名字。是一个ASCIIZ字符串。注意Name字段的大小是可变的。提供的结构方便使您可以使用描述性名称引用数据结构。

下面的代码使用DLL和API名字的CRC-32C哈希值,从导出表中获取API的地址。

LPVOID search_imp(LPVOID base, DWORD hash)
{
  DWORD                    dll_h, i, rva;
  PIMAGE_IMPORT_DESCRIPTOR imp;
  PIMAGE_THUNK_DATA        oft, ft;
  PIMAGE_IMPORT_BY_NAME    ibn;
  PIMAGE_DOS_HEADER        dos;
  PIMAGE_NT_HEADERS        nt;
  PIMAGE_DATA_DIRECTORY    dir;
  PCHAR                    dll;
  LPVOID                   api_adr=NULL;
  
  dos = (PIMAGE_DOS_HEADER)base;
  nt  = RVA2VA(PIMAGE_NT_HEADERS, base, dos->e_lfanew);
  dir = (PIMAGE_DATA_DIRECTORY)nt->OptionalHeader.DataDirectory;
  rva = dir[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
  
  // if no import table, return
  if (rva==0) return NULL;
  imp  = (PIMAGE_IMPORT_DESCRIPTOR) RVA2VA(ULONG_PTR, base, rva);
  
  for (i=0; api_adr==NULL; i++) 
  {
    if (imp[i].Name == 0) return NULL;
    
    dll   = RVA2VA(PCHAR, base, imp[i].Name);
    dll_h = crc32c(dll); 
    
    rva   = imp[i].OriginalFirstThunk;
    oft   = (PIMAGE_THUNK_DATA)RVA2VA(ULONG_PTR, base, rva);
    
    rva   = imp[i].FirstThunk;
    ft    = (PIMAGE_THUNK_DATA)RVA2VA(ULONG_PTR, base, rva);
        
    for (;; oft++, ft++) 
    {
      if (oft->u1.Ordinal == 0) break;
      // skip import by ordinal
      if (IMAGE_SNAP_BY_ORDINAL(oft->u1.Ordinal)) continue;
      
      rva = oft->u1.AddressOfData;
      ibn = (PIMAGE_IMPORT_BY_NAME)RVA2VA(ULONG_PTR, base, rva);
      
      if ((crc32c(ibn->Name) + dll_h) == hash) {
        api_adr = (LPVOID)ft->u1.Function;
        break;
      }
    }
  }
  return api_adr;
}

相同功能的汇编代码如下,但是有了些优化。

in: ebx = base of module to search
;     ecx = hash to find
;
; out: eax = api address resolved in IAT
;
search_impx:
    xor    eax, eax    ; api_adr = NULL
    pushad
    ; eax = IMAGE_DOS_HEADER.e_lfanew
    mov    eax, [ebx+3ch]
    add    eax, 8     ; add 8 for import directory
    ; eax = IMAGE_DATA_DIRECTORY.VirtualAddress
    mov    eax, [ebx+eax+78h]
    test   eax, eax
    jz     imp_l2
    lea    ebp, [eax+ebx]
imp_l0:
    mov    esi, ebp      ; esi = current descriptor
    lodsd                ; OriginalFirstThunk +00h
    xchg   eax, edx      ; temporarily store in edx
    lodsd                ; TimeDateStamp      +04h
    lodsd                ; ForwarderChain     +08h
    lodsd                ; Name_              +0Ch
    test   eax, eax
    jz     imp_l2        ; if (Name_ == 0) goto imp_l2;
    add    eax, ebx
    call   crc32c
    mov    [esp+_edx], eax
    lodsd                 ; FirstThunk
    mov    ebp, esi       ; ebp = next descriptor
    lea    esi, [edx+ebx] ; esi = OriginalFirstThunk + base
    lea    edi, [eax+ebx] ; edi = FirstThunk + base
imp_l1:
    lodsd                 ; eax = oft->u1.Function, oft++;
    scasd                 ; ft++;
    test   eax, eax       ; if (oft->u1.Function == 0)
    jz     imp_l0         ; goto imp_l0
    cdq
    inc    edx         ; will be zero if eax >= 0x80000000
    jz     imp_l1      ; oft->u1.Ordinal & IMAGE_ORDINAL_FLAG
    lea    eax, [eax+ebx+2] ; oft->Name_
    call   crc32c           ; get crc of API string
    add    eax, [esp+_edx] ; eax = api_h + dll_h
    cmp    [esp+_ecx], eax ; found match?
    jne    imp_l1
    mov    eax, [edi-4]    ; ft->u1.Function
imp_l2:
    mov    [esp+_eax], eax
    popad
    ret

0x09 Process Environment Block

也许这个部分应该放在所有的内容之前?

另一个“进步”是在2002年由Ratter / 29A公布的在NT下从PEB获得重要数据的方法。有一个更简单的方法从PEB中获取kernel32.dll的模块基址。

在这里我使用来自Matt Graeber’s PIC_Bindshell的结构。

LPVOID getapi (DWORD dwHash)
{
  PPEB                     peb;
  PMY_PEB_LDR_DATA         ldr;
  PMY_LDR_DATA_TABLE_ENTRY dte;
  LPVOID                   api_adr=NULL;
  
#if defined(_WIN64)
  peb = (PPEB) __readgsqword(0x60);
#else
  peb = (PPEB) __readfsdword(0x30);
#endif
  ldr = (PMY_PEB_LDR_DATA)peb->Ldr;
  
  // for each DLL loaded
  for (dte=(PMY_LDR_DATA_TABLE_ENTRY)ldr->InLoadOrderModuleList.Flink;
       dte->DllBase != NULL && api_adr == NULL; 
       dte=(PMY_LDR_DATA_TABLE_ENTRY)dte->InLoadOrderLinks.Flink)
  {
    api_adr=search_imp(dte->DllBase, dwHash);
  }
  return api_adr;
}

下面是相同算法的汇编,做了一些优化。

; LPVOID getapix(DWORD hash);
getapix:
_getapix:
    pushad
    mov    ecx, [esp+32+4] ; ecx = hash
    push   30h
    pop    eax
    mov    eax, [fs:eax] ; eax = (PPEB) __readfsdword(0x30);
    mov    eax, [eax+12] ; eax = (PMY_PEB_LDR_DATA)peb->Ldr
    mov    edi, [eax+12] ; edi = ldr->InLoadOrderModuleList.Flink
    jmp    gapi_l1
gapi_l0:
    call   search_expx
    test   eax, eax
    jnz    gapi_l2
    mov    edi, [edi]    ; edi = dte->InMemoryOrderLinks.Flink
gapi_l1:
    mov    ebx, [edi+24] ; ebx = dte->DllBase
    test   ebx, ebx
    jnz    gapi_l0
gapi_l2:
    mov    [esp+_eax], eax
    popad
    ret


0xA hash算法

上述所有的例子,我都是用CRC-32C。C代表使用的Castagnoli多项式。用这个的原因是测试的80000个API都不会有冲突。一些其他的hash算法也提供了足够好的结果,但是使用CRC-32C的优势是INTEL处理器发布的SSE4.2的支持。

然而与0x20做或操作不是CRC-32C特有的。在这里仅仅是在哈希前将字符串转为小写。有时kernel32.dll也会出现大写的情况。

uint32_t crc32c(const char *s)
{
  int i;
  uint32_t crc=0;
  
  do {
    crc ^= (uint8_t)(*s++ | 0x20);
    
    for (i=0; i<8; i++) {
      crc = (crc >> 1) ^ (0x82F63B78 * (crc & 1));
    }
  } while (*(s - 1) != 0);
  return crc;
}

这是使用内置指令的代码。

;
    xor    eax, eax
    cdq
crc_l0:
    lodsb
    or     al, 0x20
    crc32  edx, al
    cmp    al, 0x20
    jns    crc_l0

下面是CPU不支持SSE4.2的代码。

; in: eax = s
; out: crc-32c(s)
;
crc32c:    
    pushad
    xchg   eax, esi          ; esi = s
    xor    eax, eax          ; eax = 0
    cdq                      ; edx = 0
crc_l0:
    lodsb                    ; al = *s++ | 0x20
    or     al, 0x20
    xor    dl, al            ; crc ^= c
    push   8
    pop    ecx    
crc_l1:
    shr    edx, 1            ; crc >>= 1
    jnc    crc_l2
    xor    edx, 0x82F63B78
crc_l2:
    loop   crc_l1
    sub    al, 0x20          ; until al==0
    jnz    crc_l0    
    mov    [esp+_eax], edx
    popad
    ret

当然,CRC-32C不是绝对没冲突的。有时需要考虑使用加密哈希算法。最简单的是有Daniel Bernstein的CubeHash

也可以使用一个小块或流密码加密字符串和截断密文为32或64位。解决冲突是值得探索的。


0xB 总结

解析导入和导出表不是困难的任务。所有的文档和代码将被提供,就没了不使用PIC技术的解析API的方法。使用硬编码API地址或者通过序号查找是个灾难。

Getapi.c包含了通过CRC-32C定位API的C代码。X86.asmx64.asm包含了通过CRC-32C定位API的汇编代码。

weinxin
版权声明
本站原创文章转载请注明文章出处及链接,谢谢合作!
评论:0   参与:  0