启动流程
BasicEntry
- 作为
dll
导出接口 PrepareEnv
- 先构建运行时
ntdll.dll
kernel32.dll
- 获取当前
dll
句柄 EXE
被当做DLL
加载时,首先修复重定位数据,此时的全局变量等还无法访问
因为Windows
操作系统不会修复当做DLL
加载的EXE
程序
FixRelocationTable
:PE
文件重定位表修复
详见下文重定位项- 作为
DLL
的入口点重设
详见下文 - 修复导入表
详见下文
- 先构建运行时
1 2 3 4 5 6 7 8 |
// 这个结构定义了重定位表中的一个“块”(Block) // 每个块负责一页(通常为4KB)内存范围内的重定位项 typedef struct _IMAGE_BASE_RELOCATION { DWORD VirtualAddress; // 该块起始地址的RVA(相对虚拟地址) DWORD SizeOfBlock; // 整个块(包含后面所有项)的总大小 // 紧接着这个结构之后,是一个WORD类型的数组,每个WORD是一个重定位项 } IMAGE_BASE_RELOCATION; |
RunCommonEntry
- 启动一个线程,入口点为
WinMainCrtEntry
,阻塞等待该线程处理完成 FixLdrInfo
详见下文_DllMainCRTStartup
如果是EXE
作为DLL
运行,需要调用_DllMainCRTStartup
函数执行初始化,否则程序不能正常运行wWinMain
- 启动一个线程,入口点为
1 2 3 4 5 6 7 8 9 10 11 12 13 |
BOOL RunCommonEntry() { HANDLE dll_worker_thread = CreateThread(NULL, 0, WinMainCrtEntry, NULL, 0, NULL); if (dll_worker_thread == NULL) { return FALSE; } WaitForSingleObject(dll_worker_thread, INFINITE); CloseHandle(dll_worker_thread); return g_is_exit_normal.load(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
DWORD WINAPI WinMainCrtEntry(LPVOID unused) { UNREFERENCED_PARAMETER(unused); try { // 如果是EXE作为DLL运行,需要调用_DllMainCRTStartup函数执行初始化,否则程序不能正常运行; if (!g_is_crt_initilized && exe_loader::IsRunAsExeDll()) { g_is_crt_initilized.store(true); exe_loader::FixLdrInfo(); if (!_DllMainCRTStartup(exe_loader::GetCurrentModule(), DLL_PROCESS_ATTACH, nullptr)) { g_is_exit_normal.store(false); return 0; } } wWinMain(exe_loader::GetCurrentModule(), nullptr, GetCommandLineW(), SW_SHOWNORMAL); } catch (...) { // 异常处理 g_is_exit_normal.store(false); } return 0; } |
wWinMain
_DllMainCRTStartup
被调用后,最后会调到wWinMain
这里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#ifndef _DLLMODEL int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nCmdShow) { ::MessageBox(NULL, L"1", L"2", MB_OK); LPTSTR command = NULL; if (exe_loader::IsRunAsDll()) { BasicEntryEx(g_InputBuffer, g_OutputBuffer, _countof(g_OutputBuffer)); ExitThread(0); return 0; } return test::InstallMain(hInstance, hPrevInstance, lpCmdLine, nCmdShow); } #endif |
- 如果是
exe
模式,走test::InstallMain
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
namespace test { int InstallMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpstrCmdLine, int nCmdShow) { UNREFERENCED_PARAMETER(hPrevInstance); UNREFERENCED_PARAMETER(lpstrCmdLine); // TODO: 在此处放置代码。 // 初始化全局字符串 LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING); LoadStringW(hInstance, IDC_TESTDEMO, szWindowClass, MAX_LOADSTRING); MyRegisterClass(hInstance); // 执行应用程序初始化: if (!InitInstance(hInstance, nCmdShow)) { return FALSE; } HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_TESTDEMO)); MSG msg; // 主消息循环: while (GetMessage(&msg, nullptr, 0, 0)) { if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) { TranslateMessage(&msg); DispatchMessage(&msg); } } return (int)msg.wParam; // return TRUE; } } // namespace test |
- 如果是
dll
模式,走BasicEntryEx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#ifndef _DLLMODEL extern "C" __declspec(dllexport) int __stdcall BasicEntryEx(LPCWSTR lpstrCmdLine,LPWSTR pOutBuffer,int nBufferLen) #else extern "C" __declspec(dllexport) int __stdcall BasicEntry(LPCWSTR lpstrCmdLine, LPWSTR pOutBuffer, int nBufferLen) #endif { int nRet = 0; // 安装流程只走一次 if (g_nInstallCount == 0) { g_nInstallCount++; nRet = test::InstallMain(g_hModule, NULL, (LPTSTR)lpstrCmdLine, SW_NORMAL); if (pOutBuffer != NULL && nBufferLen > 0) { _sntprintf_s(pOutBuffer, nBufferLen, nBufferLen - 1, _T("%d"), nRet); } } return nRet; } |
其他
获取表目录
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
PIMAGE_NT_HEADERS32 GetNtHeaders(HMODULE module_handle) { BYTE* image_base = (BYTE*)module_handle; WORD* PeOffs; IMAGE_NT_HEADERS* pe_header; if (!image_base) return NULL; // Verifica che sia un PE if (image_base[0] != 'M' || image_base[1] != 'Z') return NULL; PeOffs = (WORD*)&(image_base[0x3C]); pe_header = (IMAGE_NT_HEADERS*)(image_base + (*PeOffs)); return pe_header; } PIMAGE_DATA_DIRECTORY GetDataDirectory(HMODULE module_handle, DATA_TABLE_ENTRY_INDEX entry_index) { auto pe_header = GetNtHeaders(module_handle); if (!pe_header) { return NULL; } if (pe_header->Signature != 0x4550 || pe_header->OptionalHeader.NumberOfRvaAndSizes < 1 || pe_header->OptionalHeader.DataDirectory[0].VirtualAddress == 0) { return NULL; } PIMAGE_DATA_DIRECTORY import_table_directory = &pe_header->OptionalHeader.DataDirectory[(int)entry_index]; if (!import_table_directory) { return NULL; } return import_table_directory; } |
关于重定位表项
- 每个重定位项是一个16位(2字节)的值,其结构通过位域解析
- 高
4
位(Bits 12-15
):类型- 常见值
3
(IMAGE_REL_BASED_HIGHLOW
) 表示这是一个需要完整修正的32
位地址(在64
位系统上也可能是64
位)
- 常见值
- 低
12
位(Bits 0-11
):偏移- 表示需要修正的地址距离所在块
VirtualAddress
的偏移量
- 表示需要修正的地址距离所在块
- 判断
- 代码首先从
PE
文件头的数据目录表中获取重定位表的位置和大小 - 如果表不存在或为空,说明这个模块不需要重定位(比如它被加载到了预期的基地址),函数直接返回
FALSE
- 代码首先从
1 2 3 4 5 |
auto relocation_table_directory = GetDataDirectory(module_handle, DATA_TABLE_ENTRY_INDEX::DTE_RELOCATION_TABLE); if (!relocation_table_directory || relocation_table_directory->VirtualAddress == 0 || relocation_table_directory->Size == 0) { return FALSE; } |
- 计算基址差值
module_handle
是模块当前在内存中的实际基地址odule_default_base
是模块期望被加载到的基地址(存储在PE
头部的ImageBase
字段中)- 这两个地址的差值(
Delta
)就是所有绝对地址需要调整的量
1 2 |
LPVOID module_mapping_base = GetImageMappingOfModule(Runtime, module_handle); ULONG_PTR module_default_base = GetModuleDefaultVABase((HMODULE)module_mapping_base); |
- 定位重定位表项
- 通过
VirtualAddress
(一个RVA
)加上基地址,得到内存中重定位表的起始指针 for
循环的条件是判断当前块是否有效(地址和大小均不为0
)
一个全零的IMAGE_BASE_RELOCATION
结构标志着重定位表的结束
- 通过
1 2 |
auto relocs = (PIMAGE_BASE_RELOCATION)((LPBYTE)module_handle + relocation_table_directory->VirtualAddress); for (; relocs && relocs->VirtualAddress && relocs->SizeOfBlock; ) |
- 处理每个重定位块内的项
- 一个块的大小减去
IMAGE_BASE_RELOCATION
结构本身的大小(8
字节),剩下的空间全部用于存储WORD
(双字节)项 - 所以项的数量是
(SizeOfBlock - 8) / 2
- 一个块的大小减去
1 2 |
auto items = (relocs->SizeOfBlock - 8) / 2; // 计算本块内有多少个重定位项 auto reloc_dir_head = (PWORD)((PBYTE)relocs + sizeof(*relocs)); // 指向块后面的第一个WORD项 |
- 解析每个重定位项并进行修正
- 这是最核心的循环。对块内的每一个项,提取其类型和偏移
- 只有当类型是
IMAGE_REL_BASED_HIGHLOW
(值为3
) 时,才进行地址修正,这对应着32
位或64
位的绝对地址寻址
1 2 3 4 5 6 7 8 9 |
for (int j = 0; j < items; j++) { auto type = reloc_dir_head[j] >> 12; // 取高4位,得到类型 auto offset = reloc_dir_head[j] & 0x0FFF; // 取低12位,得到偏移 if (IMAGE_REL_BASED_HIGHLOW == type) // 判断是否为需要修正的类型 { // ... 修正逻辑 } } |
- 核心修正算法
- 公式就是:新地址 = 旧地址 - 期望基址 + 实际基址
1 2 3 4 5 6 |
auto repair_address_from_mapping = (PULONG_PTR)((LPBYTE)module_mapping_base + relocs->VirtualAddress + offset); ULONG_PTR original_value = *repair_address_from_mapping; ULONG_PTR reloc_offset = original_value - module_default_base; ULONG_PTR new_address = reloc_offset + (ULONG_PTR)module_handle; ULONG_PTR target_repair_address = (ULONG_PTR)((LPBYTE)module_handle + relocs->VirtualAddress + offset); WriteMemory(Runtime, (LPVOID)target_repair_address, &new_address, sizeof(new_address)); |
- 移动到下一个块并清理
- 根据当前块的
SizeOfBlock
跳转到下一个重定位块
- 根据当前块的
1 |
relocs = (PIMAGE_BASE_RELOCATION)((PBYTE)relocs + relocs->SizeOfBlock); |
判断是不是DLL
1 2 3 4 5 6 7 8 9 |
BOOL IsDll(HMODULE module_handle) { PIMAGE_NT_HEADERS32 nt_headers = GetNtHeaders(module_handle); if (nt_headers) { return (nt_headers->FileHeader.Characteristics & IMAGE_FILE_DLL) != 0; } return FALSE; } |
将当前PE
结构中的EXE
标志改为DLL
标志
1 2 3 4 5 6 7 8 9 |
PIMAGE_NT_HEADERS32 nt_headers = GetNtHeaders(module_handle); if (nt_headers) { DWORD old_protect = 0; runtime._VirtualProtect(nt_headers, sizeof(IMAGE_NT_HEADERS32), PAGE_READWRITE, &old_protect); nt_headers->FileHeader.Characteristics &= ~IMAGE_FILE_EXECUTABLE_IMAGE; nt_headers->FileHeader.Characteristics |= IMAGE_FILE_DLL; runtime._VirtualProtect(nt_headers, sizeof(IMAGE_NT_HEADERS32), old_protect, &old_protect); } |
入口点重设
- 实现了一项非常底层的技术:在内存中将一个
PE
(Portable Executable
)模块从可执行程序(EXE
)的动态特性改造为动态链接库(DLL
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
VOID ResetEntryPointAsDll(IMPORT_RUNTIME &runtime, HMODULE module_handle) { // 1. 将当前PE结构中的EXE标志改为DLL标志 PIMAGE_NT_HEADERS32 nt_headers = GetNtHeaders(module_handle); if (nt_headers) { DWORD old_protect = 0; runtime._VirtualProtect(nt_headers, sizeof(IMAGE_NT_HEADERS32), PAGE_READWRITE, &old_protect); nt_headers->FileHeader.Characteristics &= ~IMAGE_FILE_EXECUTABLE_IMAGE; nt_headers->FileHeader.Characteristics |= IMAGE_FILE_DLL; runtime._VirtualProtect(nt_headers, sizeof(IMAGE_NT_HEADERS32), old_protect, &old_protect); } // 2. 将入口点函数指向_DllMainCRTStartup { PIMAGE_OPTIONAL_HEADER32 optional_header = &nt_headers->OptionalHeader; DWORD old_protect = 0; runtime._VirtualProtect(optional_header, sizeof(PIMAGE_OPTIONAL_HEADER32), PAGE_READWRITE, &old_protect); optional_header->AddressOfEntryPoint = (ULONG_PTR)((LPBYTE)_DllMainCRTStartup - (LPBYTE)module_handle); runtime._VirtualProtect(optional_header, sizeof(PIMAGE_OPTIONAL_HEADER32), old_protect, &old_protect); } } |
- 流程:
- 修改
PE
文件头Characteristics
标志 - 取消
exe
标志IMAGE_FILE_EXECUTABLE_IMAGE
- 设置
dll
标识IMAGE_FILE_DLL
- 重定向入口点至
_DllMainCRTStartup
- 计算相对虚拟地址
RVA
- 更新
AddressOfEntryPoint
字段 - 详细解析如下
- 修改
- 获取PE头结构并修改文件特征标志
GetNtHeaders
是一个辅助函数,目的是从模块基地址(module_handle
)定位到PE
文件的NT
头结构(IMAGE_NT_HEADERS32
)
NT
头包含了PE
文件的签名、文件头和可选头- 由于
PE
头在内存中通常被设置为只读(PAGE_READONLY
),直接写入会导致访问违规
因此,代码使用runtime._VirtualProtect
(一个通过函数指针抽象的系统API
调用)临时将内存页的属性改为可读可写(PAGE_READWRITE
)
这是进行底层修改的关键步骤 FileHeader.Characteristics
字段包含了描述文件属性的一系列标志- 修改完成后,立即将内存保护属性恢复原状
1 2 3 4 5 6 7 8 9 10 11 12 13 |
PIMAGE_NT_HEADERS32 nt_headers = GetNtHeaders(module_handle); if (nt_headers) { DWORD old_protect = 0; runtime._VirtualProtect(nt_headers, sizeof(IMAGE_NT_HEADERS32), PAGE_READWRITE, &old_protect); // 此行代码使用 按位与和取反 操作,清除了 IMAGE_FILE_EXECUTABLE_IMAGE标志 // 该标志表示图像可以被加载后直接运行,是EXE的典型特征 nt_headers->FileHeader.Characteristics &= ~IMAGE_FILE_EXECUTABLE_IMAGE; // 此行代码使用 按位或 操作,设置了 IMAGE_FILE_DLL标志 // 该标志表明该文件是一个DLL,不能直接运行,而是一个供其他代码调用的模块 nt_headers->FileHeader.Characteristics |= IMAGE_FILE_DLL; runtime._VirtualProtect(nt_headers, sizeof(IMAGE_NT_HEADERS32), old_protect, &old_protect); } |
- 重定向入口点至
DLL
的CRT
启动函数OptionalHeader
是NT头的一部分,它包含了对于加载器至关重要的信息,其中就包括AddressOfEntryPoint
这个字段指定了系统加载模块后,执行代码的起始地址(相对于模块基地址的偏移量,即RVA
)- 计算新的入口点
RVA
,代码中将入口点设置为_DllMainCRTStartup
_DllMainCRTStartup
是C/C++运行时库(CRT
)为DLL
提供的标准入口函数
详见下文 (ULONG_PTR)((LPBYTE)_DllMainCRTStartup - (LPBYTE)module_handle)
这行代码计算的是_DllMainCRTStartup
函数在当前内存中相对于模块基地址的偏移量(RVA
)
因为AddressOfEntryPoint
需要的是一个RVA
,而不是绝对地址
1 2 3 |
PIMAGE_OPTIONAL_HEADER32 optional_header = &nt_headers->OptionalHeader; // ... (VirtualProtect调用) optional_header->AddressOfEntryPoint = (ULONG_PTR)((LPBYTE)_DllMainCRTStartup - (LPBYTE)module_handle); |
修复导入表
PE
文件导入表(Import Address Table, IAT
)的动态修复功能- 模拟了
Windows
操作系统加载器在运行程序时的一个核心步骤:将程序代码中调用外部DLL
函数的"桩"地址,替换成DLL
函数在内存中的实际地址
- 模拟了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
static BOOL FixIatTable(PIMPORT_RUNTIME Runtime, HMODULE module_handle) { auto import_table_directory = GetDataDirectory(module_handle, DATA_TABLE_ENTRY_INDEX::DTE_IMPORT_TABLE); if (!import_table_directory) { return FALSE; } auto import_table_descriptor = (PIMAGE_IMPORT_DESCRIPTOR)((LPBYTE) module_handle + import_table_directory->VirtualAddress); // Loop import table. while (import_table_descriptor->Characteristics != 0 && import_table_descriptor->OriginalFirstThunk != 0 && import_table_descriptor->FirstThunk != 0) { DWORD name_offset = import_table_descriptor->Name; DWORD original_first_thunk = import_table_descriptor->OriginalFirstThunk; DWORD first_thunk_offset = import_table_descriptor->FirstThunk; LPSTR import_module_name = (LPSTR)((LPBYTE)module_handle + name_offset); HMODULE imported_module = Runtime->_LoadLibraryA(import_module_name); if (!imported_module) { return FALSE; } IMAGE_THUNK_DATA32* thunk = (IMAGE_THUNK_DATA32*)((LPBYTE)module_handle + first_thunk_offset); while (thunk) { ULONG_PTR thunkValue = thunk->u1.AddressOfData; if (thunkValue == 0) { break; } auto thunkFoa = thunkValue & 0X7FFFFFFF; if (!(thunkValue >> 31)) { IMAGE_IMPORT_BY_NAME* byName = (IMAGE_IMPORT_BY_NAME*)( (LPBYTE)module_handle + thunkFoa); LPVOID imported_function_address = Runtime->_GetProcAddress(imported_module, (LPSTR)byName->Name); if (!imported_function_address) { return FALSE; } WriteMemory(Runtime, (LPVOID)&thunk->u1.Function, &imported_function_address, sizeof(LPVOID)); } else { LPVOID imported_function_address = Runtime->_GetProcAddress(imported_module, (LPSTR)thunkFoa); if (!imported_function_address) { return FALSE; } WriteMemory(Runtime, (LPVOID)&thunk->u1.Function, &imported_function_address, sizeof(LPVOID)); } thunk++; } import_table_descriptor++; } return TRUE; } |
- 获取导入表并初始化
GetDataDirectory
函数从PE
文件头的数据目录表中获取导入表的位置和大小- 如果获取失败(例如,该模块没有导入表),函数直接返回
FALSE
- 计算导入表在内存中的实际起始地址
module_handle
是模块的基地址,加上VirtualAddress
(一个相对虚拟地址RVA
),得到第一个IMAGE_IMPORT_DESCRIPTOR
结构的指针
1 2 3 4 5 6 |
auto import_table_directory = GetDataDirectory(module_handle, DATA_TABLE_ENTRY_INDEX::DTE_IMPORT_TABLE); if (!import_table_directory) { return FALSE; } auto import_table_descriptor = (PIMAGE_IMPORT_DESCRIPTOR)((LPBYTE)module_handle + import_table_directory->VirtualAddress); |
- 遍历导入表(外层循环:处理每个
DLL
)while
循环遍历导入表,直到遇到一个全零的IMAGE_IMPORT_DESCRIPTOR
结构,这表示导入表的结束- 对于每个描述符,它指向该模块所依赖的一个
DLL
- 根据描述符中的
Name
字段(RVA
),计算出该DLL
文件名字符串在内存中的地址 - 调用
Runtime->_LoadLibraryA
动态加载这个DLL
,并获取其模块句柄(HMODULE
)
1 2 3 4 5 |
while (import_table_descriptor->Characteristics != 0 && ...) { DWORD name_offset = import_table_descriptor->Name; LPSTR import_module_name = (LPSTR)((LPBYTE)module_handle + name_offset); HMODULE imported_module = Runtime->_LoadLibraryA(import_module_name); |
- 遍历导入函数(内层循环:处理当前
DLL
中的每个函数)- 根据描述符的
FirstThunk
字段(指向IAT
)定位到函数地址列表 - 内层
while
循环遍历这个列表,直到遇到一个NULL
的IMAGE_THUNK_DATA32
项,这表示该DLL
的导入函数列表结束 - 每个
thunk
结构体最初并不直接包含函数地址,而是包含一个提示如何找到函数的信息
- 根据描述符的
1 2 3 4 5 6 7 8 |
IMAGE_THUNK_DATA32* thunk = (IMAGE_THUNK_DATA32*)((LPBYTE)module_handle + first_thunk_offset); while (thunk) { ULONG_PTR thunkValue = thunk->u1.AddressOfData; if (thunkValue == 0) break; // 以NULL指针结束遍历 // ... 处理每个thunk thunk++; } |
- 核心修复逻辑:名称导入
- 当最高位为0时,
thunkValue
是一个RVA
,指向一个IMAGE_IMPORT_BY_NAME
结构 - 这个结构包含一个提示(
Hint
)和真正的函数名称字符串 - 通过
Runtime->_GetProcAddress
使用函数名来获取该函数在已加载DLL
中的实际地址 WriteMemory
函数将这个实际地址写回到thunk->u1.Function
指向的内存位置,即 修复IAT
中该函数的项
- 当最高位为0时,
1 2 3 4 5 6 |
if (!(thunkValue >> 31)) // 检查最高位是否为0 { IMAGE_IMPORT_BY_NAME* byName = (IMAGE_IMPORT_BY_NAME*)((LPBYTE)module_handle + (thunkValue & 0X7FFFFFFF)); LPVOID imported_function_address = Runtime->_GetProcAddress(imported_module, (LPSTR)byName->Name); WriteMemory(Runtime, (LPVOID)&thunk->u1.Function, &imported_function_address, sizeof(LPVOID)); } |
- 核心修复逻辑:序号导入
- 当最高位为1时,
thunkValue
的低31
位直接就是函数的导出序号 Runtime->_GetProcAddress
通过这个序号(强制转换为LPCSTR
)来获取函数地址- 同样,将获取到的地址写回
IAT
- 当最高位为1时,
1 2 3 4 5 |
else { LPVOID imported_function_address = Runtime->_GetProcAddress(imported_module, (LPSTR)(thunkValue & 0X7FFFFFFF)); WriteMemory(Runtime, (LPVOID)&thunk->u1.Function, &imported_function_address, sizeof(LPVOID)); } |
_DllMainCRTStartup
- 它的主要职责是初始化
CRT
环境(如全局变量、静态对象等) - 然后调用开发者编写的
DllMain
函数 - 并正确处理
fdwReason
(如DLL_PROCESS_ATTACH
,DLL_THREAD_ATTACH
等事件)
Ldr
信息修复
- 用于在 运行时手动修复当前模块在
PEB
(进程环境块)的加载器数据结构中的信息- 相关概念
peb
见下文
- 相关概念
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
VOID FixLdrInfo() { if (!g_CurrentModuleHandle) { return; } MemoryBarrier(); PPEB pPEB = GetPeb(); PPEB_LDR_DATA pLdr = pPEB->Ldr; PLIST_ENTRY pFlink = (PLIST_ENTRY)(pLdr->InLoadOrderModuleList.Flink); PLIST_ENTRY pBlink = (PLIST_ENTRY)(pLdr->InLoadOrderModuleList.Blink); PLDR_DATA_TABLE_ENTRY pModuleTab = NULL; for (PLIST_ENTRY pLink = pFlink; pLink != pBlink; pLink = pLink->Flink) { PLDR_DATA_TABLE_ENTRY module_link = (PLDR_DATA_TABLE_ENTRY)pLink; if (module_link->DllBase == g_CurrentModuleHandle) { pModuleTab = (PLDR_DATA_TABLE_ENTRY)pLink; break; } } if (!pModuleTab) { return; } pModuleTab->EntryPoint = (PVOID)((LPBYTE)_DllMainCRTStartup); //pModuleTab->Flags pModuleTab->ImageDll = 1; pModuleTab->ProcessAttachCalled = 1; //PIMAGE_TLS_DIRECTORY tls_image = (PIMAGE_TLS_DIRECTORY)GetDataDirectory( // CurrentModuleHandle, DATA_TABLE_ENTRY_INDEX::DTE_TLS_TABLE); //if (tls_image) //{ // PLDRP_TLS_ENTRY TlsEntry = (PLDRP_TLS_ENTRY)RtlAllocateHeap(RtlProcessHeap(), 0, sizeof(*TlsEntry)); // if ( !TlsEntry ) { // return; // } // pModuleTab->TlsIndex = 0xffff; // TlsEntry->Tls = *tls_image; // //InsertTailList(&LdrpTlsList, &TlsEntry->Links); //} } |
- 安全检查与初始化
MemoryBarrier()
是一个内存屏障调用,用于确保在获取PEB
指针之前,之前的所有内存操作都已完成,防止CPU
或编译器的乱序执行导致问题GetPeb
获取当前进程PEB
指针
在x86
架构上,这通常通过读取fs
段寄存器的特定偏移量来实现(例如mov eax, fs:[0x30]
)pLdr
指向了PEB
中的加载器数据结构,这是操作的目标
1 2 3 4 |
if (!g_CurrentModuleHandle) { return; } MemoryBarrier(); PPEB pPEB = GetPeb(); PPEB_LDR_DATA pLdr = pPEB->Ldr; |
1 2 3 4 |
PPEB GetPeb() { return (PPEB)__readfsdword(0x30); } |
- 遍历模块列表
- 获取了
InLoadOrderModuleList
链表的头节点(Flink
指向第一个节点)和尾节点(Blink
指向最后一个节点) for
循环遍历链表
- 获取了
1 2 3 4 |
PLIST_ENTRY pFlink = (PLIST_ENTRY)(pLdr->InLoadOrderModuleList.Flink); PLIST_ENTRY pBlink = (PLIST_ENTRY)(pLdr->InLoadOrderModuleList.Blink); ... for (PLIST_ENTRY pLink = pFlink; pLink != pBlink; pLink = pLink->Flink) |
- 定位目标模块
- 循环体内,将链表节点指针(
PLIST_ENTRY
)强制转换为PLDR_DATA_TABLE_ENTRY
这是因为LDR_DATA_TABLE_ENTRY
结构的第一个成员就是LIST_ENTRY
这种技巧在Windows
内核编程中非常常见,称为"侵入式链表" - 然后比较当前节点的
DllBase
(模块基地址)是否与我们要找的g_CurrentModuleHandle
相等
如果相等,就找到了目标模块的加载器数据表项,记录下指针并跳出循环
- 循环体内,将链表节点指针(
1 2 3 4 5 |
PLDR_DATA_TABLE_ENTRY module_link = (PLDR_DATA_TABLE_ENTRY)pLink; if (module_link->DllBase == g_CurrentModuleHandle) { pModuleTab = (PLDR_DATA_TABLE_ENTRY)pLink; break; } |
- 找到目标模块的
LDR_DATA_TABLE_ENTRY
后,函数开始执行关键的修复步骤:- 将
EntryPoint
设置为_DllMainCRTStartup
。这是C/C++
运行时库(CRT
)提供的标准DLL
入口函数,它负责初始化全局对象,然后调用用户编写的DllMain
函数
这个操作强制将模块的入口行为标记为DLL
ImageDll = 1
:明确告知系统此模块是一个DLL
镜像,而不是EXE
ProcessAttachCalled = 1
:这是一个非常关键且具有"欺骗性"的操作
它告诉系统,该模块的DllMain
(在传递DLL_PROCESS_ATTACH
通知时)已经被调用过了
这样做的目的可能是为了阻止系统加载器再次尝试初始化该模块,避免重复初始化或冲突,常用于绕过正常的模块加载序列
- 将
1 2 3 |
pModuleTab->EntryPoint = (PVOID)((LPBYTE)_DllMainCRTStartup); pModuleTab->ImageDll = 1; pModuleTab->ProcessAttachCalled = 1; |
- 被注释的
TLS
(线程本地存储,TLS
允许每个线程拥有全局变量的独立副本)处理- 从
PE
头部获取TLS
目录 - 为当前模块分配一个
TLS
入口 - 将其加入到系统的
TLS
管理列表中,确保每个新线程创建时能正确初始化该模块的TLS
数据
- 从
PEB(Process Environment Block)
- 每个进程都有一个
PEB
,它包含了进程的全局信息,如命令行参数、环境变量、已加载模块列表等 PEB_LDR_DATA
- 这是
PEB
中一个非常重要的成员,它维护着三个双向链表,记录了进程加载的所有模块(EXE
和DLL
)的信息 - 函数中使用的
InLoadOrderModuleList
就是其中之一,它按模块的加载顺序排列
- 这是
LDR_DATA_TABLE_ENTRY
- 链表中的每个节点都是这个结构。它详细描述了一个已加载模块的信息,包括:
DllBase
:模块加载到内存中的基地址(HMODULE
)EntryPoint
:模块的入口点地址(例如DllMain
)Flags
:模块的属性标志ImageDll
:标记该模块是否是DLL
镜像ProcessAttachCalled
:标记该模块的DllMain
是否已因进程附加而被调用过
声明:本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ Windows 核心编程 _ 用户模式:线程同步三07/23
- ♥ Windows 核心编程 _ 线程内幕07/06
- ♥ Soui八06/20
- ♥ Windbg关于死锁的简单调试分析总结09/13
- ♥ Windows 核心编程 _ 内核对象:线程同步二07/30
- ♥ Windows核心编程_必备知识04/27