前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >二进制技巧-利用非传统方法显示调用 api 函数

二进制技巧-利用非传统方法显示调用 api 函数

作者头像
渗透攻击红队
发布2021-10-14 11:02:01
1K0
发布2021-10-14 11:02:01
举报
文章被收录于专栏:漏洞知识库

域安TEAM隶属于 渗透攻击红队 ,是一支集各个厂商攻击手的专业红队,专注于安全研究、红队培训以及渗透案例分析。文章将会不定时更新,请勿利用文中相关技术从事非法测试,如因此产生的一切不良后果与文章作者和本公众号无关!

利用非传统方法显示调用 api 函数

前言

本文将介绍一种在内存中查找函数地址的方法,从而隐藏导入表存在调用函数的痕迹。

下面将对该方法进行详细的介绍。

GetProcAddress()

我们的思路就是自己来实现GetProcAddress的方式来寻找API地址

同时我们不直接使用API名称,我们采用对API名称计算一个hash,通过这个hash去寻找比对需要的API ,我们这种方法在本文后续中简称为hash API

我们先了解一下 GetProcAddress的工作原理:

获取EAT结构的函数名称地址数组并跳转到该地址,即 IMAGE_EXPORT_DIRECTORY.AddressOfNames

此处存储着当前找到的模块中的所有的导出函数的名称,通过与这些字符串逐个比较,可以找到指定的函数名称。将此时数组的索引记作 Index

查找并跳转到 ordinal地址数组所在的位置,即 IMAGE_EXPORT_DIRECTORY.AddressOfNameOrdinals

在 ordinal 地址数组中利用之前找到的索引 Index作为偏移量,查找对应的 ordinal 值,类似 AddressOfNameOrdinals[Index]

查找导出函数地址,流程转移到导出函数地址数组的位置,即 IMAGE_EXPORT_DIRECTORY.AddressOfFunctions

以得到的 ordinal的值为数组索引,得到函数起始地址,很像AddressOfFunctions[ordinal]

备注:有的模块是序号导出,因此直接 Index== ordinal 会导致找不到,只有按照上面的方法去查找才不会出错。

了解导出表的结构

导出表的结构如下:

IMAGE_EXPORT_DIRECTORYIMAGE_OPTIONAL_HEADER32.DataDirectory[0].VirtualAddress 的值就是 IMAGE_EXPORT_DIRECTORY 结构体数组的RVA起始地址。

其具体结构如下:

代码语言:javascript
复制
typedef struct _IMAGE_EXPORT_DIRECTORY {
 // 标志, 未使用
DWORD   Characteristics; 
// 时间戳
DWORD   TimeDateStamp;    
// 未使用
WORD    MajorVersion;     
// 未使用
WORD    MinorVersion;     
// 指向该导出表的文件名字符串 
    DWORD   Name;             
// 导出函数的起始值    
DWORD   Base;             
// 实际导出的函数个数
DWORD   NumberOfFunctions;   
// 导出的函数中具名的函数个数
DWORD   NumberOfNames;               
// 导出函数地址数组
DWORD   AddressOfFunctions;        
// 函数名称地址数组
DWORD   AddressOfNames;            
// Ordinal 地址数组  (数组元素个数=NumberOfNames)
    DWORD   AddressOfNameOrdinals;   
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

Hash API 代码详解

0x01 与其他方法相比的优缺点

1.对API名称hash可以极大的节省空间,这点在Shellcode中体现尤为明显。

2.版本兼容性好一些,因为其是遍历到指定的模块,然后检查指定模块的IAT表,从中获得函数名,转换成hash与要寻找的函数hash进行比较。有些shellcode作者的编写则会按照一些习惯或者编写shellcode时所处系统的特点去寻找模块,更换版本之后shellcode可能就出问题,比如XP和win7之间引入了kernelbase.dll就会导致某些找kernel32.dll的shellcode失效。

3.这相当于我们没有直接使用系统的GetProcAddress去寻找API,也没有使用loadlibrary去加载模块,正常开发没什么用,但对于某些场景,比如保护代码时,这种方法是有挺大作用的。

4.缺点也有,不如特定顺序寻址的方式灵活小巧。

我们对其中MSF的代码进行解读来了解这种技术,代码见 metasploit(https://github.com/rapid7/metasploit-framework/blob/master/external/source/shellcode/windows/x86/src/block/block_api.asm)。

0x02 寻找 InMemoryOrderModuleList

windows环境中, FS 段寄存器指向线程环境块(TEB)的地址,当在内存区运行shellcode时,我们需要从 TEB 所在的地址处移动48字节来得到 PEB 的地址,InMemoryOrderModuleList则需要从PEB中寻找

如下:

代码语言:javascript
复制
api_call:
    pushad                 ; 保存所有寄存器状态
    mov ebp, esp           ; 创建新的栈帧
    xor eax, eax           ; 将EAX清零
    mov edx, [fs:eax+48]   ; 将指针指向PEB

TEB的结构如图所示:

找到PEB之后呢,再从PEB 再偏移12字节就能得到Ldr数据结构的地址,PEB的结构如下图所示:

Ldr结构体包含了进程加载模块的信息,将其再偏移20字节,就是InMemoryOrderModuleList,我们就能从 InMemoryOrderModuleList 结构体中获得第一个已加载的模块。

LDR结构体PEB_LDR_DATA的结构如图:

下面是我们拿到PEB之后,从PEB找到LDR,然后再从LDR找到InMemoryOrderModuleList结构体的代码

代码语言:javascript
复制
; Get PEB->Ldr
    mov edx, [edx+12]      
;  得到 InMemoryOrderModuleList 的地址,也就是加载的第一个模块的地址
    mov edx, [edx+20]      

现在指针指向 InMemoryOrderModuleList

它是一个 LIST_ENTRY 的结构。

结构如下:

代码语言:javascript
复制
typedef struct _LIST_ENTRY 
{
   struct _LIST_ENTRY *Flink;
   struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;

微软官方文档中对该结构体的解释如下:

Thehead of a doubly-linked list that contains the loaded modules for the process.Each item in the list is a pointer to an LDR_DATA_TABLE_ENTRY structure

也就是这是个双向链表,指向了进程装载的模块,FLink和Blink都指向一个LDR_DATA_TABLE_ENTRY结构,只不过Flink指向后一个,Blink指向前一个

LDR_DATA_TABLE_ENTRY 的结构如下:

代码语言:javascript
复制
typedef struct _LDR_DATA_TABLE_ENTRY
{
     LIST_ENTRY InLoadOrderLinks;
     LIST_ENTRY InMemoryOrderLinks;
     LIST_ENTRY InInitializationOrderLinks;
     PVOID DllBase;
     PVOID EntryPoint;
     ULONG SizeOfImage;
     UNICODE_STRING FullDllName;
     UNICODE_STRING BaseDllName;
     ULONG Flags;
     WORD LoadCount;
     WORD TlsIndex;
     union
     {
          LIST_ENTRY HashLinks;
          struct
          {
               PVOID SectionPointer;
               ULONG CheckSum;
          };
     };
     union
     {
          ULONG TimeDateStamp;
          PVOID LoadedImports;
     };
     _ACTIVATION_CONTEXT * EntryPointActivationContext;
     PVOID PatchInformation;
     LIST_ENTRY ForwarderLinks;
     LIST_ENTRY ServiceTagLinks;
     LIST_ENTRY StaticLinks;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

现在我们已经完成了 PEB -> TEB -> Ldr ->InMemoryOrderModuleList 得到了第一个被进程加载模块。

0x03 计算模块Hash

代码语言:javascript
复制
继续下面的代码         
; 获得指向模块名称BaseDllName的地址  
    1.mov esi, [edx+40]      
; 指向BaseDllName->MaximumLength,表示该内存缓冲区的总大小
    2.movzx ecx, word [edx+38]     
    3.xor edi, edi           

1处的指令中的edx+40表示 LDR_DATA_TABLE_ENTRY->BaseDllName ,也就是模块的名称,这是个UNICODE_STRING结构体。

继续看后面的代码

代码语言:javascript
复制
loop_modname:            
; 逐字节读取模块名
    lodsb                  
;这里是为了统一切换成大写
    cmp al, 'a'            
    jl not_lowercase      
    sub al, 0x20           
not_lowercase:           
; 将hash值循环右移13位
    ror edi, 13            
; hash值与下一个字符相加,等到新的hash值
add edi, eax          
; 循环相加,循环次数为ecx,即BaseDllName的缓冲区大小 
    loop loop_modname

我们这里采用了一个字符右移13次的方法,没什么特别的意义,出自一位师傅随手的hash算法,有以后再与新的字符相加,重复右移,最后得到一个摘要值,这种算法偏简单,碰撞机会可能也比较大。算法可以自己进行修改。

这里的循环右移13次没有什么特别的含义,只是为了确保得到一个唯一的值,与Hash算法的出发点一样。如果你愿意你可以修改一下改成自己的算法,如 左移,多重复移动几次,甚至与其他的运算混合使用,只要唯一就可以。 很多恶意软件的作者都对这个 Hash 算法进行了自己的修改以达到免杀的目的。

0x04 寻找模块加载基址DllBase

代码语言:javascript
复制
; 保存edx,edx的值指向InMemoryOrderModuleList   
push edx               
; 保存edi,edi为之前计算的模块hash
push edi         
; [edx+16]为 `LDR_DATA_TABLE_ENTRY->DllBase` ,镜像加载内存中的基址         
mov edx, [edx+16]   
; [edx+60] 指向 IMAGE_DOS_HEADER结构体中的e_lfanew,表示PE头的RVA地址    
mov ecx, [edx+60]      

拿到了PE头和DllBase,我们就能去获得导出函数表EAT

0x05 定位导出表EAT

代码语言:javascript
复制
; DllBase+PE头RVA地址+120等于导出表EAT的RVA地址    
    mov ecx, [ecx+edx+120] 
 ; ECX为0则跳转,即没有导出函数则跳转到下一个模块
    jecxz get_next_mod1   
; 导出地址表EAT的RVA加上模块基地址DllBase为VA
    add ecx, edx           
; 保存当前模块的EAT的VA
    push ecx               
; 获取函数名称地址表  IMAGE_EXPORT_DIRECTORY->AddressOfNames 的RVA
    mov ebx, [ecx+32]      
; 函数名称地址数组VA 等于其 RVA + 模块的基地址DllBase
    add ebx, edx           
; 获取导出函数中具名的函数个数 IMAGE_EXPORT_DIRECTORY->NumberOfNames
    mov ecx, [ecx+24]    
  (与讲解此处关系不大的代码略过)  
get_next_mod:            
; 弹出当前模块的eat到edi中
    pop edi                
get_next_mod1:           
; 弹出之前的保存的hash
    pop edi                
; 还原之前保存的 InMemoryOrderModuleList 地址
    pop edx               
; InMemoryOrderModuleList是链表结构,[edx]指向指向下一个加载的模块
    mov edx, [edx]        
; 跳转去处理下一个模块
    jmp short next_mod    

PE头偏移120字节就是导出表EAT的地址即 IMAGE_OPTIONAL_HEADER32.DataDirectory[0].VirtualAddress 的RVA地址。

0x06 通过Hash查找目标函数

这段代码主要是在模块内遍历所有函数,计算hash,与目标函数的hash值进行比对,相等则表示成功找到。

代码语言:javascript
复制
get_next_func:           ;
    jecxz get_next_mod     ; ECX为0,说明搜索结束,跳转到下一个模块,这里是从后往前进行搜索。
    dec ecx                ; ecx减一,即导出的函数中具名的函数个数NumberOfNames作为循环计数器 
    mov esi, [ebx+ecx*4]   ; 获取导出函数的函数名字符串
    add esi, edx           ; 获取该 FunctionName 的 VA (DllBase + RVA)
    xor edi, edi           ; 清除保存函数hash的edi
loop_funcname:           ;
    lodsb                  ; 逐字节读取esi中保存的函数名称,结果放入eax中
    ror edi, 13            ; 循环右移13次计算hash
    add edi, eax           ; 与下一次函数名字符相加
    cmp al, ah             ; 比较名字的下一个字节
    jne loop_funcname      ; 不为空则继续循环
    add edi, [ebp-8]       ; [ebp-8]为之前计算过的模块hash,与这里的函数hash相加得到总的hash
    cmp edi, [ebp+36]      ; 与目标函数的hash值比较
    jnz get_next_func      ; 如果不相等,则重复查找

值得注意的是代码 movesi, [ebx+ecx*4] ,其中 ebx 表示函数名称地址表 AddressOfNames 的VA地址,其中 ecx 作为索引,标准的数组通过索引找元素的汇编写法

如果成功找到,则修复堆栈,调用函数

代码语言:javascript
复制
    pop eax                ; 还原当前模块的EAT的VA
    mov ebx, [eax+36]      ; 获取 AddressOfNameOrdinals 的RVA
    add ebx, edx           ; 与DllBase相加得到VA
    mov cx, [ebx+2*ecx]    ; 在AddressOfNameOrdinals中获得目标函数的index
    mov ebx, [eax+28]      ; 获得函数地址表 AddressfFunctions 的RVA
    add ebx, edx           ; 与DllBase相加得到VA
    mov eax, [ebx+4*ecx]   ; 获得目标函数的RVA
    add eax, edx           ; 与DllBase相加得到最终函数的VA,

moveax, [ebx+4*ecx] 是获得目标函数的RVA,即 DllBase +IMAGE_EXPORT_DIRECTORY->AddressOfFunctions) + (4 * OrdinalIndex)

至此就基本完成了通过Hash 寻址API基本完成,剩下的就是处理细节以及调用函数的问题了。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-09-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 渗透攻击红队 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 0x01 与其他方法相比的优缺点
  • 0x02 寻找 InMemoryOrderModuleList
  • 0x03 计算模块Hash
  • 0x04 寻找模块加载基址DllBase
  • 0x05 定位导出表EAT
  • 0x06 通过Hash查找目标函数
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档