遍历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地址:
- 通过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结构
- 0x00C: 指向
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 实现思路
- 通过FS寄存器获取TEB,进而获取PEB
- 从PEB定位到LDR结构,获取模块链表
- 遍历模块链表找到kernel32.dll
- 解析kernel32.dll的导出表,获取GetProcAddress地址
- 使用GetProcAddress获取LoadLibrary和其他所需API
- 实现功能调用
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 编译注意事项
-
禁用安全检查:在Visual Studio中需要禁用GS安全检查,否则会生成堆栈检查代码
- 项目属性 → C/C++ → 代码生成 → 安全检查:禁用(/GS-)
-
优化设置:建议关闭优化以确保代码顺序不变
- 项目属性 → 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 总结
本技术要点:
- 通过FS寄存器访问TEB/PEB结构
- 遍历LDR模块链表查找kernel32.dll
- 手动解析PE结构获取导出函数地址
- 动态获取API实现功能调用
- 遵守Shellcode编写四大原则
这种方法可以生成位置无关、可移植的Shellcode,适用于各种注入场景。