遍历LDR链表实现shellcode加载
字数 1282 2025-08-06 23:10:31

遍历LDR链表实现Shellcode加载 - 详细教学文档

0x00 前言

Shellcode是不依赖环境,放到任何地方都可以执行的机器码。本文重点讲解如何编写一个独立、可移植的Shellcode,特别是通过遍历LDR链表动态获取API地址的技术。

0x01 Shellcode编写原则

1. 不能有全局变量

  • 原因:Shellcode中的全局变量地址是相对于当前进程的,注入到其他进程后这些地址将无效
  • 解决方案:所有变量必须定义在栈上或寄存器中

2. 不能使用常量字符串

  • 原因:字符串常量也是全局变量,在其他进程中不存在
  • 解决方案:使用字符数组形式定义字符串
char s[] = {'1','2',0};  // 正确方式

3. 不能直接调用系统函数

  • 原因:系统函数调用依赖IAT表,不同进程的IAT表位置不同
  • 解决方案:动态获取API地址:
    1. 通过FS:[0x30]找到PEB
    2. 通过PEB中的LDR链表(PEB+0x0C)找到kernel32.dll
    3. 遍历kernel32.dll的导出表获取LoadLibrary和GetProcAddress

4. 不能嵌套调用其他函数

  • 原因:函数地址在其他进程中无效
  • 解决方案:所有功能必须内联实现

0x02 TEB/PEB结构详解

TEB (Thread Environment Block)

  • 每个线程都有一个TEB结构存储线程属性
  • 获取方式:fs:[0]
  • 关键偏移:
    • 0x30: 指向PEB结构的指针

PEB (Process Environment Block)

  • 记录进程信息的关键结构
  • 关键偏移:
    • 0x00C: 指向_PEB_LDR_DATA结构

PEB_LDR_DATA结构

typedef struct _PEB_LDR_DATA {
    DWORD Length;
    bool Initialized;
    PVOID SsHandle; 
    LIST_ENTRY InLoadOrderModuleList;    // 模块加载顺序链表
    LIST_ENTRY InMemoryOrderModuleList;  // 模块在内存中的顺序链表
    LIST_ENTRY InInitializationOrderModuleList; // 模块初始化顺序链表
} PEB_LDR_DATA, *PPEB_LDR_DATA;

LDR_DATA_TABLE_ENTRY结构

typedef struct _LDR_DATA_TABLE_ENTRY {
    LIST_ENTRY InLoadOrderLinks;
    LIST_ENTRY InMemoryOrderLinks;
    LIST_ENTRY InInitializationOrderLinks;
    PVOID DllBase;          // DLL基地址
    PVOID EntryPoint;
    UINT32 SizeOfImage;
    UNICODE_STRING FullDllName;  // 完整DLL路径
    UNICODE_STRING BaseDllName;  // DLL名称
    // ... 其他成员省略
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

0x03 实现思路

  1. 通过FS寄存器获取TEB,进而获取PEB
  2. 从PEB定位到LDR结构,获取模块链表
  3. 遍历模块链表找到kernel32.dll
  4. 解析kernel32.dll的导出表,获取GetProcAddress地址
  5. 使用GetProcAddress获取LoadLibrary和其他所需API
  6. 实现功能调用

0x04 详细实现过程

1. 定义必要结构体和字符串

// 自定义UNICODE_STRING结构
typedef struct _UNICODE_STRING {
    USHORT Length;
    USHORT MaximumLength;
    PWSTR  Buffer;
} UNICODE_STRING, *PUNICODE_STRING;

// 定义函数指针类型
typedef HMODULE (WINAPI *PLOADLIBRARY)(LPCSTR);
typedef DWORD (WINAPI *PGETPROCADDRESS)(HMODULE, LPCSTR);
typedef DWORD (WINAPI *PMESSAGEBOX)(HWND, LPCSTR, LPCSTR, UINT);

// 定义所需字符串(Unicode和ANSI)
char szKernel32[] = {'k',0,'e',0,'r',0,'n',0,'e',0,'l',0,'3',0,'2',0,'.',0,'d',0,'l',0,'l',0,0,0};
char szUser32[] = {'u','s','e','r','3','2','.','d','l','l',0};
char szGetProcAddress[] = {'G','e','t','P','r','o','c','A','d','d','r','e','s','s',0};
char szLoadLibrary[] = {'L','o','a','d','L','i','b','r','a','r','y','A',0};
char szMessageBox[] = {'M','e','s','s','a','g','e','B','o','x','A',0};

2. 获取LDR链表

__asm {
    mov eax, fs:[0x30]    // 获取PEB
    mov eax, [eax+0x0C]   // PEB->LDR
    add eax, 0x0C         // LDR->InLoadOrderModuleList
    mov pBeg, eax         // 链表头
    mov eax, [eax]        // 第一个模块
    mov pPLD, eax
}

3. 遍历查找kernel32.dll

// 查找kernel32.dll
while (pPLD != pBeg) {
    pLast = (WORD*)pPLD->BaseDllName.Buffer;
    pFirst = (WORD*)szKernel32;

    // 字符串比较
    while (*pFirst && *pLast == *pFirst)
        pFirst++, pLast++;

    if (*pFirst == *pLast) {
        dwKernelBase = (DWORD)pPLD->DllBase;
        break;
    }
    pPLD = (LDR_DATA_TABLE_ENTRY*)pPLD->InLoadOrderLinks.Flink;
}

4. 解析kernel32.dll导出表

// 获取DOS头
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)dwKernelBase;

// 获取NT头
PIMAGE_NT_HEADERS pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pDosHeader + pDosHeader->e_lfanew);

// 获取导出目录
PIMAGE_EXPORT_DIRECTORY pExportDirectory = (PIMAGE_EXPORT_DIRECTORY)
    ((DWORD)dwKernelBase + pNTHeader->OptionalHeader.DataDirectory[0].VirtualAddress);

// 获取三个关键表
DWORD *pAddOfFun_Raw = (DWORD*)((DWORD)dwKernelBase + pExportDirectory->AddressOfFunctions);
WORD *pAddOfOrd_Raw = (WORD*)((DWORD)dwKernelBase + pExportDirectory->AddressOfNameOrdinals);
DWORD *pAddOfNames_Raw = (DWORD*)((DWORD)dwKernelBase + pExportDirectory->AddressOfNames);

5. 查找GetProcAddress函数

DWORD dwCnt = 0;
char* pFinded = NULL, *pSrc = szGetProcAddress;

for (; dwCnt < pExportDirectory->NumberOfNames; dwCnt++) {
    pFinded = (char*)((DWORD)dwKernelBase + pAddOfNames_Raw[dwCnt]);

    // 比较函数名
    while (*pFinded && *pFinded == *pSrc)
        pFinded++, pSrc++;

    if (*pFinded == *pSrc) {
        // 找到函数,计算地址
        pGetProcAddress = (PGETPROCADDRESS)
            (pAddOfFun_Raw[pAddOfOrd_Raw[dwCnt]] + (DWORD)dwKernelBase);
        break;
    }
    pSrc = szGetProcAddress; // 重置比较指针
}

6. 获取其他API并调用

// 获取LoadLibraryA
pLoadLibrary = (PLOADLIBRARY)pGetProcAddress((HMODULE)dwKernelBase, szLoadLibrary);

// 加载user32.dll并获取MessageBoxA
pMessageBox = (PMESSAGEBOX)pGetProcAddress(pLoadLibrary(szUser32), szMessageBox);

// 调用MessageBox
pMessageBox(NULL, szHelloShellCode, 0, MB_OK);

0x05 编译注意事项

  1. 禁用安全检查:在Visual Studio中需要禁用GS安全检查,否则会生成堆栈检查代码

    • 项目属性 → C/C++ → 代码生成 → 安全检查:禁用(/GS-)
  2. 优化设置:建议关闭优化以确保代码顺序不变

    • 项目属性 → C/C++ → 优化:禁用(/Od)

0x06 完整代码示例

#include <windows.h>

typedef struct _UNICODE_STRING {
    USHORT Length;
    USHORT MaximumLength;
    PWSTR  Buffer;
} UNICODE_STRING, *PUNICODE_STRING;

typedef struct _PEB_LDR_DATA {
    DWORD Length;
    bool Initialized;
    PVOID SsHandle; 
    LIST_ENTRY InLoadOrderModuleList;
    LIST_ENTRY InMemoryOrderModuleList;
    LIST_ENTRY InInitializationOrderModuleList;
} PEB_LDR_DATA, *PPEB_LDR_DATA;

typedef struct _LDR_DATA_TABLE_ENTRY {
    LIST_ENTRY InLoadOrderLinks;
    LIST_ENTRY InMemoryOrderLinks;
    LIST_ENTRY InInitializationOrderLinks;
    PVOID DllBase;
    PVOID EntryPoint;
    UINT32 SizeOfImage;
    UNICODE_STRING FullDllName;
    UNICODE_STRING BaseDllName;
    UINT32 Flags;
    USHORT LoadCount;
    USHORT TlsIndex;
    LIST_ENTRY HashLinks;
    PVOID SectionPointer;
    UINT32 CheckSum;
    UINT32 TimeDateStamp;
    PVOID LoadedImports;
    PVOID EntryPointActivationContext;
    PVOID PatchInformation;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

typedef HMODULE (WINAPI *PLOADLIBRARY)(LPCSTR);
typedef DWORD (WINAPI *PGETPROCADDRESS)(HMODULE, LPCSTR);
typedef DWORD (WINAPI *PMESSAGEBOX)(HWND, LPCSTR, LPCSTR, UINT);

DWORD WINAPI ShellCode() {
    PGETPROCADDRESS pGetProcAddress = NULL;
    PLOADLIBRARY pLoadLibrary = NULL;
    PMESSAGEBOX pMessageBox = NULL;
    PLDR_DATA_TABLE_ENTRY pPLD;
    PLDR_DATA_TABLE_ENTRY pBeg;
    WORD *pFirst = NULL, *pLast = NULL;
    DWORD dwKernelBase = 0;

    char szKernel32[] = {'k',0,'e',0,'r',0,'n',0,'e',0,'l',0,'3',0,'2',0,'.',0,'d',0,'l',0,'l',0,0,0};
    char szUser32[] = {'u','s','e','r','3','2','.','d','l','l',0};
    char szGetProcAddress[] = {'G','e','t','P','r','o','c','A','d','d','r','e','s','s',0};
    char szLoadLibrary[] = {'L','o','a','d','L','i','b','r','a','r','y','A',0};
    char szMessageBox[] = {'M','e','s','s','a','g','e','B','o','x','A',0};
    char szHelloShellCode[] = {'H','e','l','l','o','S','h','e','l','l','C','o','d','e',0};

    __asm {
        mov eax, fs:[0x30]
        mov eax, [eax+0x0C]
        add eax, 0x0C
        mov pBeg, eax
        mov eax, [eax]
        mov pPLD, eax
    }

    // 查找kernel32.dll
    while (pPLD != pBeg) {
        pLast = (WORD*)pPLD->BaseDllName.Buffer;
        pFirst = (WORD*)szKernel32;

        while (*pFirst && *pLast == *pFirst)
            pFirst++, pLast++;

        if (*pFirst == *pLast) {
            dwKernelBase = (DWORD)pPLD->DllBase;
            break;
        }
        pPLD = (LDR_DATA_TABLE_ENTRY*)pPLD->InLoadOrderLinks.Flink;
    }

    if (dwKernelBase) {
        // 解析导出表
        PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)dwKernelBase;
        PIMAGE_NT_HEADERS pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pDosHeader + pDosHeader->e_lfanew);
        PIMAGE_EXPORT_DIRECTORY pExportDirectory = (PIMAGE_EXPORT_DIRECTORY)
            ((DWORD)dwKernelBase + pNTHeader->OptionalHeader.DataDirectory[0].VirtualAddress);

        DWORD *pAddOfFun_Raw = (DWORD*)((DWORD)dwKernelBase + pExportDirectory->AddressOfFunctions);
        WORD *pAddOfOrd_Raw = (WORD*)((DWORD)dwKernelBase + pExportDirectory->AddressOfNameOrdinals);
        DWORD *pAddOfNames_Raw = (DWORD*)((DWORD)dwKernelBase + pExportDirectory->AddressOfNames);

        // 查找GetProcAddress
        DWORD dwCnt = 0;
        char* pFinded = NULL, *pSrc = szGetProcAddress;

        for (; dwCnt < pExportDirectory->NumberOfNames; dwCnt++) {
            pFinded = (char*)((DWORD)dwKernelBase + pAddOfNames_Raw[dwCnt]);
            while (*pFinded && *pFinded == *pSrc)
                pFinded++, pSrc++;

            if (*pFinded == *pSrc) {
                pGetProcAddress = (PGETPROCADDRESS)
                    (pAddOfFun_Raw[pAddOfOrd_Raw[dwCnt]] + (DWORD)dwKernelBase);
                break;
            }
            pSrc = szGetProcAddress;
        }
    }

    if (pGetProcAddress) {
        pLoadLibrary = (PLOADLIBRARY)pGetProcAddress((HMODULE)dwKernelBase, szLoadLibrary);
        pMessageBox = (PMESSAGEBOX)pGetProcAddress(pLoadLibrary(szUser32), szMessageBox);
        pMessageBox(NULL, szHelloShellCode, 0, MB_OK);
    }

    return 0;
}

int main() {
    ShellCode();
    return 0;
}

0x07 总结

本技术要点:

  1. 通过FS寄存器访问TEB/PEB结构
  2. 遍历LDR模块链表查找kernel32.dll
  3. 手动解析PE结构获取导出函数地址
  4. 动态获取API实现功能调用
  5. 遵守Shellcode编写四大原则

这种方法可以生成位置无关、可移植的Shellcode,适用于各种注入场景。

遍历LDR链表实现Shellcode加载 - 详细教学文档 0x00 前言 Shellcode是不依赖环境,放到任何地方都可以执行的机器码。本文重点讲解如何编写一个独立、可移植的Shellcode,特别是通过遍历LDR链表动态获取API地址的技术。 0x01 Shellcode编写原则 1. 不能有全局变量 原因:Shellcode中的全局变量地址是相对于当前进程的,注入到其他进程后这些地址将无效 解决方案:所有变量必须定义在栈上或寄存器中 2. 不能使用常量字符串 原因:字符串常量也是全局变量,在其他进程中不存在 解决方案:使用字符数组形式定义字符串 3. 不能直接调用系统函数 原因:系统函数调用依赖IAT表,不同进程的IAT表位置不同 解决方案:动态获取API地址: 通过FS:[ 0x30 ]找到PEB 通过PEB中的LDR链表(PEB+0x0C)找到kernel32.dll 遍历kernel32.dll的导出表获取LoadLibrary和GetProcAddress 4. 不能嵌套调用其他函数 原因:函数地址在其他进程中无效 解决方案:所有功能必须内联实现 0x02 TEB/PEB结构详解 TEB (Thread Environment Block) 每个线程都有一个TEB结构存储线程属性 获取方式: fs:[0] 关键偏移: 0x30: 指向PEB结构的指针 PEB (Process Environment Block) 记录进程信息的关键结构 关键偏移: 0x00C: 指向 _PEB_LDR_DATA 结构 PEB_ LDR_ DATA结构 LDR_ DATA_ TABLE_ ENTRY结构 0x03 实现思路 通过FS寄存器获取TEB,进而获取PEB 从PEB定位到LDR结构,获取模块链表 遍历模块链表找到kernel32.dll 解析kernel32.dll的导出表,获取GetProcAddress地址 使用GetProcAddress获取LoadLibrary和其他所需API 实现功能调用 0x04 详细实现过程 1. 定义必要结构体和字符串 2. 获取LDR链表 3. 遍历查找kernel32.dll 4. 解析kernel32.dll导出表 5. 查找GetProcAddress函数 6. 获取其他API并调用 0x05 编译注意事项 禁用安全检查 :在Visual Studio中需要禁用GS安全检查,否则会生成堆栈检查代码 项目属性 → C/C++ → 代码生成 → 安全检查:禁用(/GS-) 优化设置 :建议关闭优化以确保代码顺序不变 项目属性 → C/C++ → 优化:禁用(/Od) 0x06 完整代码示例 0x07 总结 本技术要点: 通过FS寄存器访问TEB/PEB结构 遍历LDR模块链表查找kernel32.dll 手动解析PE结构获取导出函数地址 动态获取API实现功能调用 遵守Shellcode编写四大原则 这种方法可以生成位置无关、可移植的Shellcode,适用于各种注入场景。