0-动态获取API函数(又称隐藏IAT)

一、导入表

导入表(Import Directory)存储了有关PE文件在运行的生命周期中所需要的API以及相关的dll模块。

有些AV/EDR会根据导入表中记录的API来判定一个程序是否为高危文件,比如说一个文件在1MB以内,出现了 VirtualAllocVirtualProtectCreateThread 等敏感API时,很大概率会判定为高危文件。

很多工具可以查看文件的导入表,常见的有studyPE+、IDA。

1.1 使用studyPE+查看文件的导入信息

工具下载地址:[原创]【2020年5月1日更新】StudyPE (x86 / x64) 1.11 版-安全工具-看雪-安全社区|安全招聘|kanxue.comarrow-up-right

1.2 在IDA的Import界面查看文件文件的导入信息。

二、动态获取API函数的三种方式

在本节中,我将详细介绍动态获取API函数的三种方式,它们分别是

  1. GetModuleHandle+GetProcAddress 的组合获取所需的API函数指针

  2. 使用 PEB 获取 GetModuleHandleGetProcAddress 的函数指针,再利用 GetModuleHandle+GetProcAddress 获取所需要的API函数指针

  3. 使用 PEB 获取所需要的API函数指针

网上也有人用 LoadLibrary 作为 GetModuleHandle 的平替,其效果相差无几,只是它们的机制有些区别,比如说在 自举的代码幽灵——反射DLL注入(Reflective DLL Injection) 中我们就是利用 LoadLibrary+GetProcAddress 给我的恶意DLL加载相应的DLL获取需要的API函数指针,值得关注的一点是恶意DLL是由其自身的导出函数 ReflectiveLoader 完成自身的加载,这意味着相应的DLL并没有加载到自身的地址空间中,所以需要利用 LoadLibrary 而不应该使用 GetModuleHandle

所以还是需要看场合使用GetModuleHandle还是LoadLibrary

特性

GetModuleHandle

LoadLibrary

主要作用

获取已加载模块的句柄(不增加引用计数)

加载模块到进程地址空间(增加引用计数)

适用场景

模块已加载,需重复获取句柄(如动态调用函数)

模块未加载,需显式加载并获取句柄

引用计数管理

不增加模块引用计数

增加模块引用计数(需配合FreeLibrary

返回值有效性

若模块未加载返回 NULL

若模块已加载返回现有句柄(引用计数+1)

2.1 函数指针声明

动态获取API函数需要在C/C++代码中声明相应API的函数声明,所以我们先看看API函数指针的声明方式

其声明的格式如下

例如

  • typedef:定义一个新类型。

  • FARPROC:返回类型,表示一个通用的函数指针(常用于动态获取的API函数)。

  • WINAPI:调用约定,即 __stdcall(Windows API标准调用约定)。

  • GETPROCADDR:新类型的名称(自定义的函数指针类型名)。

  • (HMODULE hModule, LPCSTR lpProcName):参数列表,与 GetProcAddress 的参数一致。

补充:调用约定定义了函数调用时 参数传递顺序堆栈清理责任(调用者或被调用者)以及 函数名修饰规则WINAPI 是 Windows 开发中的一个宏定义,用于指定函数使用 __stdcall 调用约定

调用约定
参数顺序
堆栈清理者
典型应用场景
变参支持

__stdcall

右→左

被调用者

Windows API、COM接口

__cdecl

右→左

调用者

C/C++默认、可变参数函数

__fastcall

右→左

被调用者

部分寄存器传参优化场景

如何知道API的返回类型和参数列表呢?这就需要到微软的官方文档查看相应API的函数原型,然后把返回类型和参数列表复制过来即可。

2.2 GetModuleHandle+GetProcAddress

GetModuleHandle:检索指定模块的模块句柄。 该模块必须由调用进程加载。官方文档:GetModuleHandleA 函数 (libloaderapi.h) - Win32 apps | Microsoft Learnarrow-up-right

GetProcAddress:从指定的动态链接库 (DLL) 检索导出函数 (也称为过程) 或变量的地址。官方文档:GetProcAddress 函数 (libloaderapi.h) - Win32 apps | Microsoft Learnarrow-up-right

大致第了解这两个核心API后我们就需要着手代码实现了

(一)示例MessageBoxW

直接使用API,编译后查看导入表信息,发现确实存在MessageBoxW这个API的记录

使用动态获取API后,程序能正常弹窗

查看其导入表,发现没有 MessageBoxW 的相关信息。

(二)实战

使用方法

  1. 使用加密版将shellcode加密,并输出

  2. 将shellcode保存到shellcode.txt

  3. 用VS的MSVC或Cling-cl编译解密版的代码,注意编译和shellcode位数保存一致,如shellcode是x64,编译的实话要用x64位。

  4. 将编译好的exe和shellcode.txt放到目标上,保证这两个文件在同一个目录

加密版

解密版

从本节到后续的所有章节中,如果需要进行免杀测试,则采用的测试标准:

  1. shellcode加密采用 混淆加密 中的自定义 XOR 加密和Base64编码

  2. shellcode分离加载采用 中的本地读取文本文件中的shellcode

  3. 使用 特征修改 中的修改VS的默认编译选项, 不使用 特征修改 中的修改时间戳、加签名等手段,不使用 转换保护 的相关技术。

  4. 按主题采用相应的技术(如本节 动态获取API函数)进行免杀测试,如果一种方法不行会采用多种防御规避技术的组合。

值得关注的是,在未二开cs的beacon植入物时,火绒6是可以检测到内存中的beacon,这是因为cs的beacon的内存特征实在太过明显,火绒肯定是增加了某种匹配规则了。还有一点是火绒只记录安全日志不会主动查杀,可能是由于查杀的话会导致被注入进程的崩溃或异常。

2.3 PEB + GetModuleHandle+GetProcAddress

老读者应该发现了—— PEB 这玩意儿已经高频刷屏五次!今天咱们不再赘述这位老熟人,直接放大招:看它如何化身内存隐身衣——绕过AV/EDR的关键跳板就在这里!记住,玩转 PEB 结构,才是免杀中的顶级操作。

如果认真读过 自举的代码幽灵——反射DLL注入(Reflective DLL Injection) 这一章节的朋友就会明白,我们可以遍历peb结构体中的ldr成员中的InMemoryOrderModuleList链表获取dll名称,遍历函数所在的dll导出表获得必要的函数的名称,如果匹配成功就返回目标函数的地址。

这里使用PEB是自实现 GetModuleHandle+GetProcAddress 将其整合成一个函数,我命名为:GetApiAddressByName

2.3.1 获取PEB结构体的地址

不同位数系统的下的PEB结构体在TEB或者寄存器中的偏移量是不同的,且同一位数系统下都存在着两种获取PEB的方法,下面我将详细介绍。

首先我们要明白,GS/FS寄存器存储着当前TEB指针,而TEB结构体偏移0x30/0x18的位置上也存储着当前TEB指针,所以就出现了两种方法获取PEB指针的方法,它们之间的适用场景不同。

方法一:避免直接依赖寄存器,兼容性更强。可以规避对 0x60 偏移的系统调用监控(如CrowdStrike的PEB扫描规则) 方法二:代码简洁、效率高,常用于shellcode开发。

(1)在64位系统下获得PEB

方法一:从TEB出发

TEB的指针存放在 GS 寄存器偏移0x30的位置,而Visual Studio提供 __readgsqword(0x30) 这个宏定义,可以方便我们读取TEB指针。

在线程环境块 (TEB) 结构中是存放着指向 PEB 结构的指针,PEB指针在TEB结构体中偏移0x60的位置上。

方法二:直接从GS寄存器出发

我们可以利用 __readgsqword(0x60) 读取到PEB指针,从而跳过TEB 结构直接检索 PEB指针。

(2)在32位系统下获得PEB

方法一:从TEB出发

在32位系统上,FS寄存器承担了GS寄存器的角色,TEB的指针存放在FS寄存器偏移0x18的位置上可以利用 __readfsdword(0x18) 这个宏定义读取TEB的指针。

在TEB结构体0x30的位置上存放着PEB的指针

方法二:直接从GS寄存器出发

同理,我们也可以利用 __readfsdword(0x30) 直接读取PEB指针。

2.3.2 枚举DLL所需要的结构体

(1)_PEB_LDR_DATA

在PEB结构体0x18的位置上可以看的 _PEB_LDR_DATA 类型的指针

_PEB_LDR_DATA 的数据结构定义如下

(2)_LIST_ENTRY

可以从 _PEB_LDR_DATA 结构体中看到三个 _LIST_ENTRY 结构体成员,其中最重要的是InMemoryOrderModuleList,它位于 _PEB_LDR_DATA 结构体0x20的位置,不用太过关注它们的偏移量,因为32和64系统中它们的偏移量是不同的且我们是可以访问到这些成员的。我们看一下 _LIST_ENTRY 结构体的定义

_LIST_ENTRY 是一个双向链表,它分别使用 Flink 和 Blink 元素作为头指针和尾指针,这意味着 Flink 指向列表中的下一个节点,而元素 Blink 指向列表中的上一个节点,它们都是指向 LDR_DATA_TABLE_ENTRY 结构的指针。在本节中,只使用 Flink 就可以完成所有 LDR_DATA_TABLE_ENTRY 结构体的遍历。

(3)LDR_DATA_TABLE_ENTRY

_LDR_DATA_TABLE_ENTRY 是Windows内核中用于描述已加载模块(如DLL或驱动)信息的关键数据结构。其数据结构如下

一个 _LDR_DATA_TABLE_ENTRY 描述一个已加载DLL模块的相关信息

其中值得我们关注的成员有

  1. InMemoryOrderLinks:按内存映像顺序组织的链表,其主要作用就是利用 CONTAINING_RECORD 宏来计算出 _LDR_DATA_TABLE_ENTRY 结构体的基址

  2. DllBase:存放着DLL的基地址,有了基址,我们就可以解析DOS头、NT从而获得导出表的地址。

  3. FullDllName:存放着DLL的完整路径,我们可以利用自实现的 ExtractDllName 函数获取DLL的名称,然后与目标DLL的名称进行比对。

2.3.3 遍历导出表的相关数据结构

  1. AddressOfNames:存储所有按名称导出函数的字符串地址(RVA)

  2. AddressOfFunctions:存储所有导出函数的实际入口地址(RVA),每个元素对应一个函数的起始位置。

  3. AddressOfNameOrdinals:建立函数名称索引(来自AddressOfNames)与函数地址索引(来自AddressOfFunctions)的映射关系

在实践的编程中,我们都是按名称获取API的地址,所以这三个数组间的协调工作如下

2.3.4 代码实现

(一)GetApiAddressByName

先说一下 GetApiAddressByName 函数的大致思路吧,代码其实在 自举的代码幽灵——反射DLL注入(Reflective DLL Injection) 那一节中给出来了。

  1. 获取PEB结构体的地址

  2. 获取PEB_LDR_DATA(ldr)结构体的地址

  3. 获取InMemoryOrderModuleList结构体的地址,其地址也是第一个 LIST_ENTRY 元素的首地址

  4. 使用InMemoryOrderModuleList结构体的Flink指针遍历已加载模块列表,查找目标DLL。

  5. 解析目标DLL的PE结构,定位导出表。

  6. 遍历导出表,查找目标API名称。

  7. 如果找到目标函数,则返回找到的函数地址或NULL。

为了实现 GetApiAddressByName,还需要实现几个辅助函数,它们分别是 my_towlowerMyCompareStringWMyCompareStringAExtractDllName

我简要的说一下它们的作用

  1. my_towlower:将宽字符从大写转换小写,是用于辅助 MyCompareStringW 函数的

  2. MyCompareStringW:不区分大小写的宽字符串比较。这主要查找目标DLL,因为我们的DLL名称是宽字符串表示的

  3. MyCompareStringA: ASCII字符串比较函数。这主要用于查找目标API名称,因为微软的API是ASCII字符串表示的

  4. ExtractDllName:因为 LDR_DATA_TABLE_ENTRY 这个结构体的 FullDllName.Buffer 字段表示的是完整的DLL路径,我们需要从DLL路径中提取出DLL名称 自定义宽字符转小写my_towlower` 函数

不区分大小写的宽字符串比较函数MyCompareStringW

ASCII字符串比较函数 MyCompareStringA

提取 DLL 名称的函数ExtractDllName

GetApiAddressByName的实现如下

(二)实战

2.4 完全PEB

与2.3的实现大致相同,细微的差异就是我们不再利用 GetModuleHandle+GetProcAddress 的组合获取所需的API,而是利用自定义实现的 GetApiAddressByName 函数来获取。其效果我感觉是与2.3相差无几,但有一点需要提及的就是如果进程没有加载API所在的DLL的话,这个方法就失效了,但是这种情况还是非常少的,我们使用的敏感API大部分都在的Kernel32.dll中,而大部分的进程都会加载Kernel32.dll。

主体代码修改

参考资料

1、 什么?你连这都不会还学免杀?之「API动态解析」arrow-up-right

2、 【免杀】隐藏导入表(IAT)的六种方式arrow-up-right

3、免杀基础-IAT隐藏arrow-up-right

4、隐藏IAT(导入表)敏感API笔记 - root@Ev1LAsH ~ (killer.wtf)arrow-up-right

5、文章 - 通过隐藏导入表的方式规避杀软 - 先知社区arrow-up-right

6、文章 - PEB及其武器化 - 先知社区arrow-up-right

Last updated