0-动态获取API函数(又称隐藏IAT)
一、导入表
导入表(Import Directory)存储了有关PE文件在运行的生命周期中所需要的API以及相关的dll模块。

有些AV/EDR会根据导入表中记录的API来判定一个程序是否为高危文件,比如说一个文件在1MB以内,出现了 VirtualAlloc、VirtualProtect、CreateThread 等敏感API时,很大概率会判定为高危文件。
很多工具可以查看文件的导入表,常见的有studyPE+、IDA。
1.1 使用studyPE+查看文件的导入信息
工具下载地址:[原创]【2020年5月1日更新】StudyPE (x86 / x64) 1.11 版-安全工具-看雪-安全社区|安全招聘|kanxue.com

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

二、动态获取API函数的三种方式
在本节中,我将详细介绍动态获取API函数的三种方式,它们分别是
GetModuleHandle+GetProcAddress的组合获取所需的API函数指针使用
PEB获取GetModuleHandle和GetProcAddress的函数指针,再利用GetModuleHandle+GetProcAddress获取所需要的API函数指针使用
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 Learn
GetProcAddress:从指定的动态链接库 (DLL) 检索导出函数 (也称为过程) 或变量的地址。官方文档:GetProcAddress 函数 (libloaderapi.h) - Win32 apps | Microsoft Learn
大致第了解这两个核心API后我们就需要着手代码实现了
(一)示例MessageBoxW
直接使用API,编译后查看导入表信息,发现确实存在MessageBoxW这个API的记录

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

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

(二)实战
使用方法:
使用加密版将shellcode加密,并输出
将shellcode保存到shellcode.txt
用VS的MSVC或Cling-cl编译解密版的代码,注意编译和shellcode位数保存一致,如shellcode是x64,编译的实话要用x64位。
将编译好的exe和shellcode.txt放到目标上,保证这两个文件在同一个目录
加密版
解密版
从本节到后续的所有章节中,如果需要进行免杀测试,则采用的测试标准:
shellcode加密采用
混淆加密中的自定义XOR加密和Base64编码shellcode
分离加载采用 中的本地读取文本文件中的shellcode使用
特征修改中的修改VS的默认编译选项, 不使用特征修改中的修改时间戳、加签名等手段,不使用转换、保护的相关技术。按主题采用相应的技术(如本节
动态获取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模块的相关信息
其中值得我们关注的成员有
InMemoryOrderLinks:按内存映像顺序组织的链表,其主要作用就是利用
CONTAINING_RECORD宏来计算出_LDR_DATA_TABLE_ENTRY结构体的基址DllBase:存放着DLL的基地址,有了基址,我们就可以解析DOS头、NT从而获得导出表的地址。
FullDllName:存放着DLL的完整路径,我们可以利用自实现的
ExtractDllName函数获取DLL的名称,然后与目标DLL的名称进行比对。
2.3.3 遍历导出表的相关数据结构
AddressOfNames:存储所有按名称导出函数的字符串地址(RVA)
AddressOfFunctions:存储所有导出函数的实际入口地址(RVA),每个元素对应一个函数的起始位置。
AddressOfNameOrdinals:建立函数名称索引(来自
AddressOfNames)与函数地址索引(来自AddressOfFunctions)的映射关系
在实践的编程中,我们都是按名称获取API的地址,所以这三个数组间的协调工作如下
2.3.4 代码实现
(一)GetApiAddressByName
先说一下 GetApiAddressByName 函数的大致思路吧,代码其实在 自举的代码幽灵——反射DLL注入(Reflective DLL Injection) 那一节中给出来了。
获取PEB结构体的地址
获取PEB_LDR_DATA(ldr)结构体的地址
获取InMemoryOrderModuleList结构体的地址,其地址也是第一个
LIST_ENTRY元素的首地址使用InMemoryOrderModuleList结构体的Flink指针遍历已加载模块列表,查找目标DLL。
解析目标DLL的PE结构,定位导出表。
遍历导出表,查找目标API名称。
如果找到目标函数,则返回找到的函数地址或NULL。
为了实现 GetApiAddressByName,还需要实现几个辅助函数,它们分别是 my_towlower、MyCompareStringW、MyCompareStringA、ExtractDllName
我简要的说一下它们的作用
my_towlower:将宽字符从大写转换小写,是用于辅助MyCompareStringW函数的MyCompareStringW:不区分大小写的宽字符串比较。这主要查找目标DLL,因为我们的DLL名称是宽字符串表示的MyCompareStringA: ASCII字符串比较函数。这主要用于查找目标API名称,因为微软的API是ASCII字符串表示的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。
主体代码修改
参考资料
Last updated