在笔者上一篇文章《内核取应用层模块基地址》
中简单为大家介绍了如何通过遍历PLIST_ENTRY32
链表的方式获取到32位
应用程序中特定模块的基地址,由于是入门系列所以并没有封装实现太过于通用的获取函数,本章将继续延申这个话题,并依次实现通用版GetUserModuleBaseAddress()
取远程进程中指定模块的基址和GetModuleExportAddress()
取远程进程中特定模块中的函数地址,此类功能也是各类安全工具中常用的代码片段。
首先封装一个lyshark.h
头文件,此类头文件中的定义都是微软官方定义好的规范,如果您想获取该结构的详细说明文档请参阅微软官方,此处不做过多的介绍。
#include <ntifs.h> #include <ntimage.h> #include <ntstrsafe.h>
NTKERNELAPI PPEB NTAPI PsGetProcessPeb(IN PEPROCESS Process); NTKERNELAPI PVOID NTAPI PsGetProcessWow64Process(IN PEPROCESS Process);
typedef struct _PEB32 { UCHAR InheritedAddressSpace; UCHAR ReadImageFileExecOptions; UCHAR BeingDebugged; UCHAR BitField; ULONG Mutant; ULONG ImageBaseAddress; ULONG Ldr; ULONG ProcessParameters; ULONG SubSystemData; ULONG ProcessHeap; ULONG FastPebLock; ULONG AtlThunkSListPtr; ULONG IFEOKey; ULONG CrossProcessFlags; ULONG UserSharedInfoPtr; ULONG SystemReserved; ULONG AtlThunkSListPtr32; ULONG ApiSetMap; } PEB32, *PPEB32;
typedef struct _PEB_LDR_DATA { ULONG Length; UCHAR Initialized; PVOID SsHandle; LIST_ENTRY InLoadOrderModuleList; LIST_ENTRY InMemoryOrderModuleList; LIST_ENTRY InInitializationOrderModuleList; } PEB_LDR_DATA, *PPEB_LDR_DATA;
typedef struct _PEB { UCHAR InheritedAddressSpace; UCHAR ReadImageFileExecOptions; UCHAR BeingDebugged; UCHAR BitField; PVOID Mutant; PVOID ImageBaseAddress; PPEB_LDR_DATA Ldr; PVOID ProcessParameters; PVOID SubSystemData; PVOID ProcessHeap; PVOID FastPebLock; PVOID AtlThunkSListPtr; PVOID IFEOKey; PVOID CrossProcessFlags; PVOID KernelCallbackTable; ULONG SystemReserved; ULONG AtlThunkSListPtr32; PVOID ApiSetMap; } PEB, *PPEB;
typedef struct _PEB_LDR_DATA32 { ULONG Length; UCHAR Initialized; ULONG SsHandle; LIST_ENTRY32 InLoadOrderModuleList; LIST_ENTRY32 InMemoryOrderModuleList; LIST_ENTRY32 InInitializationOrderModuleList; } PEB_LDR_DATA32, *PPEB_LDR_DATA32;
typedef struct _LDR_DATA_TABLE_ENTRY32 { LIST_ENTRY32 InLoadOrderLinks; LIST_ENTRY32 InMemoryOrderLinks; LIST_ENTRY32 InInitializationOrderLinks; ULONG DllBase; ULONG EntryPoint; ULONG SizeOfImage; UNICODE_STRING32 FullDllName; UNICODE_STRING32 BaseDllName; ULONG Flags; USHORT LoadCount; USHORT TlsIndex; LIST_ENTRY32 HashLinks; ULONG TimeDateStamp; } LDR_DATA_TABLE_ENTRY32, *PLDR_DATA_TABLE_ENTRY32;
typedef struct _LDR_DATA_TABLE_ENTRY { LIST_ENTRY InLoadOrderLinks; LIST_ENTRY InMemoryOrderLinks; LIST_ENTRY InInitializationOrderLinks; PVOID DllBase; PVOID EntryPoint; ULONG SizeOfImage; UNICODE_STRING FullDllName; UNICODE_STRING BaseDllName; ULONG Flags; USHORT LoadCount; USHORT TlsIndex; LIST_ENTRY HashLinks; ULONG TimeDateStamp; } LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
|
取进程中模块基址
实现取进程中模块基址,该功能在《内核取应用层模块基地址》
中详细介绍过原理,这段代码核心原理如下所示,此处最需要注意的是如果是32位进程
则我们需要得到PPEB32 Peb32
结构体,该结构体通常可以直接使用PsGetProcessWow64Process()
这个内核函数获取到,而如果是64位进程
则需要将寻找PEB的函数替换为PsGetProcessPeb()
,其他的枚举细节与上一篇文章中的方法一致。
#include <ntifs.h> #include <windef.h> #include "lyshark.h"
PVOID GetUserModuleBaseAddress(IN PEPROCESS EProcess, IN PUNICODE_STRING ModuleName, IN BOOLEAN IsWow64) { if (EProcess == NULL) return NULL; __try { LARGE_INTEGER Time = { 0 }; Time.QuadPart = -250ll * 10 * 1000;
if (IsWow64) { PPEB32 Peb32 = (PPEB32)PsGetProcessWow64Process(EProcess); if (Peb32 == NULL) { return NULL; }
for (INT i = 0; !Peb32->Ldr && i < 10; i++) { KeDelayExecutionThread(KernelMode, TRUE, &Time); }
if (!Peb32->Ldr) { return NULL; }
for (PLIST_ENTRY32 ListEntry = (PLIST_ENTRY32)((PPEB_LDR_DATA32)Peb32->Ldr)->InLoadOrderModuleList.Flink; ListEntry != &((PPEB_LDR_DATA32)Peb32->Ldr)->InLoadOrderModuleList; ListEntry = (PLIST_ENTRY32)ListEntry->Flink) { UNICODE_STRING UnicodeString; PLDR_DATA_TABLE_ENTRY32 LdrDataTableEntry32 = CONTAINING_RECORD(ListEntry, LDR_DATA_TABLE_ENTRY32, InLoadOrderLinks); RtlUnicodeStringInit(&UnicodeString, (PWCH)LdrDataTableEntry32->BaseDllName.Buffer);
if (RtlCompareUnicodeString(&UnicodeString, ModuleName, TRUE) == 0) { return (PVOID)LdrDataTableEntry32->DllBase; } } } else { PPEB Peb = PsGetProcessPeb(EProcess); if (!Peb) { return NULL; }
for (INT i = 0; !Peb->Ldr && i < 10; i++) { KeDelayExecutionThread(KernelMode, TRUE, &Time); }
if (!Peb->Ldr) { return NULL; }
for (PLIST_ENTRY ListEntry = Peb->Ldr->InLoadOrderModuleList.Flink; ListEntry != &Peb->Ldr->InLoadOrderModuleList; ListEntry = ListEntry->Flink) { PLDR_DATA_TABLE_ENTRY LdrDataTableEntry = CONTAINING_RECORD(ListEntry, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);
if (RtlCompareUnicodeString(&LdrDataTableEntry->BaseDllName, ModuleName, TRUE) == 0) { return LdrDataTableEntry->DllBase; } } } } __except (EXCEPTION_EXECUTE_HANDLER) { return NULL; } return NULL; }
|
那么该函数该如何调用传递参数呢,如下代码是DriverEntry
入口处的调用方法,首先要想得到特定进程的特定模块地址则第一步就是需要PsLookupProcessByProcessId
找到模块的EProcess
结构,接着通过PsGetProcessWow64Process
得到当前被操作进程是32位还是64位,通过调用KeStackAttachProcess
附加到进程内存中,然后调用GetUserModuleBaseAddress
并传入需要获取模块的名字得到数据后返回给NtdllAddress
变量,最后调用KeUnstackDetachProcess
取消附加即可。
NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath) { HANDLE ProcessID = (HANDLE)7924;
PEPROCESS EProcess = NULL; NTSTATUS Status = STATUS_SUCCESS; KAPC_STATE ApcState;
DbgPrint("Hello LyShark.com \n");
Status = PsLookupProcessByProcessId(ProcessID, &EProcess); if (Status != STATUS_SUCCESS) { DbgPrint("获取EProcessID失败 \n"); return Status; }
BOOLEAN IsWow64 = (PsGetProcessWow64Process(EProcess) != NULL) ? TRUE : FALSE;
if (!MmIsAddressValid(EProcess)) { DbgPrint("地址不可读 \n"); Driver->DriverUnload = UnDriver; return STATUS_SUCCESS; }
KeStackAttachProcess((PRKPROCESS)EProcess, &ApcState);
__try { UNICODE_STRING NtdllUnicodeString = { 0 }; PVOID NtdllAddress = NULL;
RtlInitUnicodeString(&NtdllUnicodeString, L"Ntdll.dll"); NtdllAddress = GetUserModuleBaseAddress(EProcess, &NtdllUnicodeString, IsWow64); if (!NtdllAddress) { DbgPrint("没有找到基址 \n"); Driver->DriverUnload = UnDriver; return STATUS_SUCCESS; }
DbgPrint("[*] 模块ntdll.dll基址: %p \n", NtdllAddress); } __except (EXCEPTION_EXECUTE_HANDLER) { }
KeUnstackDetachProcess(&ApcState);
Driver->DriverUnload = UnDriver; return STATUS_SUCCESS; }
|
替换DriverEntry
入口函数处的ProcessID
并替换为当前需要获取的应用层进程PID,运行驱动程序即可得到该进程内Ntdll.dll
的模块基址,输出效果如下;
取模块中函数地址
实现获取特定模块中特定函数的基地址,通常我们通过GetUserModuleBaseAddress()
可得到进程内特定模块的基址,然后则可继续通过GetModuleExportAddress()
获取到该模块内特定导出函数的内存地址,至于获取导出表中特定函数的地址则可通过如下方式循环遍历导出表函数获取。
PVOID GetModuleExportAddress(IN PVOID ModuleBase, IN PCCHAR FunctionName, IN PEPROCESS EProcess) { PIMAGE_DOS_HEADER ImageDosHeader = (PIMAGE_DOS_HEADER)ModuleBase; PIMAGE_NT_HEADERS32 ImageNtHeaders32 = NULL; PIMAGE_NT_HEADERS64 ImageNtHeaders64 = NULL; PIMAGE_EXPORT_DIRECTORY ImageExportDirectory = NULL; ULONG ExportDirectorySize = 0; ULONG_PTR FunctionAddress = 0;
if (ModuleBase == NULL) { return NULL; }
if (ImageDosHeader->e_magic != IMAGE_DOS_SIGNATURE) { return NULL; }
ImageNtHeaders32 = (PIMAGE_NT_HEADERS32)((PUCHAR)ModuleBase + ImageDosHeader->e_lfanew); ImageNtHeaders64 = (PIMAGE_NT_HEADERS64)((PUCHAR)ModuleBase + ImageDosHeader->e_lfanew);
if (ImageNtHeaders64->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) { ImageExportDirectory = (PIMAGE_EXPORT_DIRECTORY)(ImageNtHeaders64->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress + (ULONG_PTR)ModuleBase); ExportDirectorySize = ImageNtHeaders64->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size; } else { ImageExportDirectory = (PIMAGE_EXPORT_DIRECTORY)(ImageNtHeaders32->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress + (ULONG_PTR)ModuleBase); ExportDirectorySize = ImageNtHeaders32->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size; }
PUSHORT pAddressOfOrds = (PUSHORT)(ImageExportDirectory->AddressOfNameOrdinals + (ULONG_PTR)ModuleBase); PULONG pAddressOfNames = (PULONG)(ImageExportDirectory->AddressOfNames + (ULONG_PTR)ModuleBase); PULONG pAddressOfFuncs = (PULONG)(ImageExportDirectory->AddressOfFunctions + (ULONG_PTR)ModuleBase);
for (ULONG i = 0; i < ImageExportDirectory->NumberOfFunctions; ++i) { USHORT OrdIndex = 0xFFFF; PCHAR pName = NULL;
if ((ULONG_PTR)FunctionName <= 0xFFFF) { OrdIndex = (USHORT)i; } else if ((ULONG_PTR)FunctionName > 0xFFFF && i < ImageExportDirectory->NumberOfNames) { pName = (PCHAR)(pAddressOfNames[i] + (ULONG_PTR)ModuleBase); OrdIndex = pAddressOfOrds[i]; } else { return NULL; }
if (((ULONG_PTR)FunctionName <= 0xFFFF && (USHORT)((ULONG_PTR)FunctionName) == OrdIndex + ImageExportDirectory->Base) || ((ULONG_PTR)FunctionName > 0xFFFF && strcmp(pName, FunctionName) == 0)) { FunctionAddress = pAddressOfFuncs[OrdIndex] + (ULONG_PTR)ModuleBase; break; } } return (PVOID)FunctionAddress; }
|
如何调用此方法,首先将ProcessID
设置为需要读取的进程PID,然后将上图中所输出的0x00007FF9553C0000
赋值给BaseAddress
接着调用GetModuleExportAddress()
并传入BaseAddress
模块基址,需要读取的LdrLoadDll
函数名,以及当前进程的EProcess
结构。
NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath) { HANDLE ProcessID = (HANDLE)4144; PEPROCESS EProcess = NULL; NTSTATUS Status = STATUS_SUCCESS;
Status = PsLookupProcessByProcessId(ProcessID, &EProcess); if (Status != STATUS_SUCCESS) { DbgPrint("获取EProcessID失败 \n"); return Status; }
PVOID BaseAddress = (PVOID)0x00007FF9553C0000; PVOID RefAddress = 0;
RefAddress = GetModuleExportAddress(BaseAddress, "LdrLoadDll", EProcess); DbgPrint("[*] 函数地址: %p \n", RefAddress);
Driver->DriverUnload = UnDriver; return STATUS_SUCCESS; }
|
运行这段程序,即可输出如下信息,此时也就得到了x64.exe
进程内ntdll.dll
模块里面的LdrLoadDll
函数的内存地址,如下所示;