《Windows PE》9.2 动态加载技术-获取kernel32.dll基址
上一节中我们介绍了如何动态调用DLL的方法。首先调用LoadLibrary函数动态加载DLL,然后调用GetProcAddress通过函数名获取DLL内的函数地址。但是LoadLibrary和GetProcAddress函数的调用仍然依赖于PE文件内的导入表和函数地址表的存在。在操作系统加载PE文件到内存时获取这两个函数的地址。本节我们将介绍一种完全不依赖于导入表和函数地址表的方法,实现对DLL动态链接库中函数的调用。简单来说,需要经过以下三步:
步骤1获取kernel32.dll的基地址。
步骤2获取GetProcAddress函数的地址(进一步获取LoadLibrary函数的地址)。
步骤3在代码中使用获取的函数地址编程。
本节必须掌握的知识点:
获取kernel32.dll基址
获取GetProcAddress 地址
获取API函数地址
自定义资源
9.2.1 获取kernel32.dll基址
动态加载技术中必须获取的LoadLibrary和GetProcAddress函数都是kernel32.dll的导出函数。因此,我们要获取这两个函数地址的前提是必须获取kernel32.dll基址。接下来我们分别介绍4种不同的获取kernel32.dll基址方法。
■方法一:暴力搜索
几乎所有的win32可执行文件(pe格式文件)运行的时候都加载kernel32.dll,可执行文件进入入口点执行后esp中存放的一般是Kernel32. dll中的某个地址,沿着这个地址向上查找就可以找到kernel32的基地址。当系统服务进程调用CreateProcess函数在完成创建应用程序后,会先将一个返回地址压入到堆栈顶端,而这个返回地址恰好在Kernel32.dll中,利用这个原理我们可以顺着这个返回地址按64KB大小往地址搜索,那么我们一定可以找到Kernel32模块的基地址。WinMain可以看做是Windows调用CreateProcess创建的一个子程序,其压入的返回地址就是Ret返回地址。从堆栈中的 Ret 地址转换 Kernel32.dll 的基址,并在 Kernel32.dll的导出表中查找 GetProcAddress 函数的入口地址。
【注】这种方法在win 64位系统下获取地址错误。
我们通过实验来检测这种方法的有效性。
实验六十五:获取kernel32.dll基址方法一
利用CreateProcess函数的返回地址获取Kernel32.dll基址。
●C语言实现
KernelBase1.c
/*------------------------------------------------------------------------FileName:KernelBase1.c实验65:方法一(暴力搜索)仅支持Windows 32位系统(c) bcdaren, 2024
-----------------------------------------------------------------------*/ #include <windows.h>BOOL CALLBACK ProcDlgMain(HWND, UINT, WPARAM, LPARAM);
DWORD _GetKernelBase(DWORD _dwKernelRet);int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,LPSTR lpCmdLine, int nShowCmd)//默认填写
{HANDLE hDllKernel32;DWORD address;const TCHAR szCaption[] = TEXT("kernal32.dll");TCHAR szText[256];//方法1:CreateProcess函数在完成创建应用程序后,会先将一个返回地址压入到堆栈顶端,_asm {mov eax,dword ptr ss:[esp]mov address,eax}//调用_GetKernelBase获取Kernel32.dll 的基址hDllKernel32 = (HANDLE)_GetKernelBase(address); //测试wsprintf(szText, TEXT("kernal32.dll的基址为:%p"), hDllKernel32);MessageBox(NULL, szText, szCaption, MB_OK);return 0;
}//在内存中扫描 Kernel32.dll 的基址
DWORD _GetKernelBase(DWORD _dwKernelRet)
{DWORD dwReturn = 0;//查找Kernel32.dll的基地址//对于windows loader来说,装载exe, dll等文件的粒度是64KB对齐DWORD address = _dwKernelRet & 0xffff0000;while (TRUE){if (*(PWORD)address == IMAGE_DOS_SIGNATURE){if (*(PWORD)(address + *(PDWORD)(address + 0x003c)) ==
IMAGE_NT_SIGNATURE){dwReturn = address;break;}}address -= 0x010000; //每次一页间隔搜寻//特殊的系统DLL:采用Copy - On - Write策略,相对固定的基址,//这些基址值在各个不同系统中都满足的条件是 > 0x70000000if (address < 0x070000000)break;}return dwReturn;
}
运行:
图9-2 获取kernel32.dll基址方法一
总结
这个结果显然是错误的。我们来分析一下错误的原因:
笔者使用VS2017编译,版本为X86 Debug版,使用内联汇编,获取栈顶返回地址address的值为0x0122123a,这个地址似乎不是很合理,通常kernel32.dll的地址处于较高处。调用_GetKernelBase函数获取PE文件头部的起始地址,返回值为0,这肯定是一个错误的结果。
如果我们编译X86 release版本,获取栈顶返回地址address的值为,肯定是错误的地址。
因为我们使用了VS编译工具,编译后的程序会添加CRT运行时库的一些代码,而且内联汇编中的ESP寄存器的值也并非我们想要的结果——存储CreateProcess函数返回地址。
X64版本不支持内联汇编,而且并不是固定的基址,因此使用这种方法一定是错误的。有兴趣的读者可以自己测试一下。
为了排除VS编译工具的干扰,我们测试一下纯汇编代码实现。
●汇编语言实现
;------------------------
;FileName:KernelBase1.asm
;实验65:方法一(暴力搜索)
;仅支持Windows 32位系统
;(c) bcdaren, 2024
;------------------------.386.model flat,stdcalloption casemap:noneinclude windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib;数据段.data
szText db 256 dup(0)
szCaption db 'kernal32.dll',0
szBuffer db "kernal32.dll的基址为:%p",0
kernel32Base dd ?;代码段.code
;------------------------------------
; 根据kernel32.dll中的一个地址获取它的基地址
;------------------------------------
_getKernelBase proc _dwKernelRetAddresslocal @dwRetpushadmov @dwRet,0mov edi,_dwKernelRetAddressand edi,0ffff0000h ;查找指令所在页的边界,以1000h对齐.repeat.if word ptr [edi]==IMAGE_DOS_SIGNATURE ;找到kernel32.dll的dos头mov esi,ediadd esi,[esi+003ch].if word ptr [esi]==IMAGE_NT_SIGNATURE ;找到kernel32.dll的PE头标识mov @dwRet,edi.break.endif.endifsub edi,010000h.break .if edi<070000000h.until FALSEpopadmov eax,@dwRetret
_getKernelBase endp ;===================================
start:; 从堆栈中的 Ret 地址转换 Kernel32.dll 的基址;Ret地址是ExitThread函数,;然后ExitThread函数又会自动去调用ExitProcess终止程序执行;因此Ret地址肯定位于Kernel32.dll模块中mov eax,dword ptr [esp]invoke _getKernelBase,eaxmov kernel32Base,eaxinvoke wsprintf,addr szText,addr szBuffer,eaxinvoke MessageBox,NULL,addr szText,addr szCaption,MB_OKret ;注意使用ret返回end start# makefile文件# 宏定义
NAME = KernelBase1
EXE = $(NAME).exe #输出文件
OBJS = $(NAME).obj #目标文件
ML_FLAG = /c /coff #编译选项
LINK_FLAG = /subsystem:windows #链接选项# 定义依赖关系和执行命令
$(EXE):$(OBJS)link $(LINK_FLAG) $(OBJS)# 定义汇编编译和资源编译默认规则
.asm.obj:ml $(ML_FLAG) $<
.rc.res:rc $<# 清除临时文件
clean:del *.objdel *.res
运行:
图9-3 方法一的汇编语言实现
总结
上述汇编版本为X86汇编的实现,当系统进程调用CreateProcess函数创建子进程(本示例程序)时,加载并初始化示例进程后执行,当执行完毕后,可以调用ExitProcess函数返回操作系统。
【注意】示例程序并没有这样做,而是直接使用ret指令返回,ret指令返回的地址就是当初CreateProcess函数调用时压入堆栈的返回地址。这个ret地址是ExitThread函数地址,然后ExitThread函数又会自动去调用ExitProcess终止程序执行。因此Ret地址肯定位于Kernel32.dll模块中。
由于虚拟内存空间以4KB为单位对齐,因此我们将ret返回地址取整,然后以4KB(1000h)为单位遍历内存空间,当遍历到DOS头特征和PE头特征时,就可以确定该地址为kernel32.dll基址。
请读者编写程序,并在OD调试器的内存映射窗口核对64位和32位Windows系统环境中获取的kernel32.dll基址是否正确。
此外,请读者尝试编译X64版本的汇编程序,并测试其正确性(获取ret返回地址失败)。
结论:利用ret返回地址的方法获取kernel32.dll的方法仅针X86 32位汇编程序有效。
■方法二:使用SEH的链表来查找
实验六十六:方法二(使用SEH的链表来查找)
●内联汇编
/*------------------------------------------------------------------------FileName:KernelBase2.c实验66:方法二(使用SEH的链表来查找)(c) bcdaren, 2024
-----------------------------------------------------------------------*/
#include <windows.h>BOOL CALLBACK ProcDlgMain(HWND, UINT, WPARAM, LPARAM);int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,LPSTR lpCmdLine, int nShowCmd)//默认填写
{HANDLE hDllKernel32;DWORD address;const TCHAR szCaption[] = TEXT("kernal32.dll");TCHAR szText[256];/*方法2:使用SEH的链表来查找原理:在SEH中默认的unhandled exception hander 是利用kernel32.DLL中的一个函数设置的。因此可以历遍所有的exception hander,找到最后一个成员,该成员的前4个字节是0ffffffffh, 后4个字节是kernel32.dll中的函数。 注:这种方法仅支持Windows 32位系统,Windows 64位系统获取的是ntdll.dll的基址*/_asm{mov esi, fs:[0]lodsdL1 :inc eaxjz L2dec eaxxchg esi, eaxlodsdjmp L1L2 :lodsdL3 :dec eaxxor ax, axcmp WORD ptr[eax], 0x05A4Djnz L3mov address, eaxmov hDllKernel32, eax}wsprintf(szText, TEXT("kernal32.dll的基址为:%p"), hDllKernel32);MessageBox(NULL, szText, szCaption, MB_OK);return 0;
}
运行:
图9-4 使用SEH的链表来查找
总结
异常处理链表就是Windows系统提供的处理异常的机制,当系统遇到一个不知道如何处理的异常时就会查找异常处理链表,找到对应的异常处理程序,把保存的处理程序地址赋给eip,并执行处理程序,避免系统崩溃,异常处理链表的最后一项是默认异常处理函数UnhandledExceptionFilter,因为UnhandledExceptionFilter在kernel32.dll中,所以从UNhandledExceptionFilter地址向上搜索即可找到kernel32的基地址。
在32位系统中,kernel32.dll是一个核心系统库,包含了大量的系统函数和API接口,包括异常处理等。而在64位系统中,ntdll.dll库更加底层,包含了操作系统内部的核心函数和服务,包括异常处理、线程管理、内存管理等。
随着Windows操作系统从32位逐渐迁移到64位架构,一些核心功能和服务被移到了更底层的ntdll.dll库中,以更好地支持64位体系结构的特性和需求。这也包括异常处理函数UnhandledExceptionFilter的迁移。
【注】 fs:[0]为SEH异常处理链的首地址。SEH异常处理链表搜索的方法仅适用于Windows 32位系统。
■方法三:利用PEB结构来查找
实验六十七:方法三(利用PEB结构来查找)
●32位程序
/*------------------------------------------------------------------------FileName:KernelBase3.c实验67:方法三(利用PEB结构来查找)(c) bcdaren, 2024
-----------------------------------------------------------------------*/
#include <windows.h>BOOL CALLBACK ProcDlgMain(HWND, UINT, WPARAM, LPARAM);int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,LPSTR lpCmdLine, int nShowCmd)//默认填写
{HANDLE hDllKernel32;DWORD address;const TCHAR szCaption[] = TEXT("kernal32.dll");TCHAR szText[256];/*方法2:利用PEB结构来查找原理:FS段寄存器作为选择子指向当前的TEB结构,在TEB偏移0x30处是PEB指针。而在PEB偏移的0x0c处是指向PEB_LDR_DATA结构的指针,位于 PEB_LDR_DATA结构偏移0x1c处,是一个叫InInitialzationOrderModuleList的成员,它是指向LDR_MODULE链表结构中相应的双向链表头部的指针,该链表加载的DLL的顺序是ntdll.dll,kernel32.dll.因此该成员所指的链表偏移0x08处为kernel32.dll地址。注:支持win32和64位系统下的X86程序。*//*1.加载顺序的方式:*/_asm{mov eax, fs:[0x30] ; 指向PEB的指针mov eax, [eax + 0x0c] ; 指向PEB_LDR_DATA的指针
; 根据PEB_LDR_DATA得出InLoadOrderModuleList的Flink字段mov esi, [eax + 0x0c] lodsdmov eax, [eax]; 指向下一个节点mov eax, [eax + 18h] ; Kernel32.dll的基地址mov address, eaxmov hDllKernel32, eax}/*2.内存顺序的方式:_asm{mov eax, fs:[0x30] ; 指向PEB的指针mov eax, [eax + 0x0c] ; 指向PEB_LDR_DATA的指针
; 根据PEB_LDR_DATA得出InMemoryOrderModuleList的Flink字段mov esi, [eax + 0x14] lodsdmov eax, [eax] ; 指向下一个节点mov eax, [eax + 10h] ; Kernel32.dll的基地址mov address, eaxmov hDllKernel32, eax}*//*3.初始化顺序的方式:KernelBase.dll的基地址_asm{mov eax, fs:[0x30] ; 指向PEB的指针mov eax, [eax + 0x0c] ; 指向PEB_LDR_DATA的指针
; 根据PEB_LDR_DATA得出InInitializationOrderModuleList的Flink字段mov esi, [eax + 0x1c] lodsdmov eax, [eax + 0x08] ; Kernel.dll的基地址mov address,eaxmov hDllKernel32,eax}*/wsprintf(szText, TEXT("kernal32.dll的基址为:%p"), hDllKernel32);MessageBox(NULL, szText, szCaption, MB_OK);return 0;
}
运行:
图9-5 利用PEB结构来查找
总结
在32位程序中,分别使用了3种不同的方法在PEB进程环境块中获取kernel32.dll的基址。3种方式分别为:
1.加载顺序的方式。
2.内存顺序的方式。
3.初始化顺序的方式。
在NT内核系统中fs寄存器指向TEB结构,TEB+0x30处指向PEB结构,PEB+0x0c处指向PEB_LDR_DATA结构。
此方法是通过TEB获得PEB结构地址,然后再获得PEB_LDR_DATA结构地址,然后遍历模块列表,查找kernel32.dll模块的基地址。
TEB是线程环境块(Thread Environment Block)结构, 我们的fs段选择子所对应的段指向TEB,也就是fs:[0]指向TEB.那么TEB的ProcessEnvironmentBlock结构成员指向我们的PEB进程环境块结构(Process Environment Block),然后通过PEB结构来获得PEB_LDR_DATA。 接下来我们通过windbg来查看下相关结构。
●查看TEB结构,通过windbg的dt命令。
lkd> dt _TEBnt!_TEB+0x000 NtTib : _NT_TIB+0x01c EnvironmentPointer : Ptr32 Void+0x020 ClientId : _CLIENT_ID+0x028 ActiveRpcHandle : Ptr32 Void+0x02c ThreadLocalStoragePointer : Ptr32 Void+0x030 ProcessEnvironmentBlock : Ptr32 _PEB
可以看到TEB结构的0x30偏移处存储的我们的PEB结构的地址
●PEB和TEB结构
1.TEB结构
//// Thread Environment Block (TEB)//typedef struct _TEB{NT_TIB Tib; /* 00h */PVOID EnvironmentPointer; /* 1Ch */CLIENT_ID Cid; /* 20h */PVOID ActiveRpcHandle; /* 28h */PVOID ThreadLocalStoragePointer; /* 2Ch */struct _PEB *ProcessEnvironmentBlock; /* 30h */ULONG LastErrorValue; /* 34h */ULONG CountOfOwnedCriticalSections; /* 38h */PVOID CsrClientThread; /* 3Ch */struct _W32THREAD* Win32ThreadInfo; /* 40h */ULONG User32Reserved[0x1A]; /* 44h */ULONG UserReserved[5]; /* ACh */PVOID WOW32Reserved; /* C0h */LCID CurrentLocale; /* C4h */ULONG FpSoftwareStatusRegister; /* C8h */PVOID SystemReserved1[0x36]; /* CCh */LONG ExceptionCode; /* 1A4h */struct _ACTIVATION_CONTEXT_STACK *ActivationContextStackPointer; /* 1A8h */UCHAR SpareBytes1[0x28]; /* 1ACh */GDI_TEB_BATCH GdiTebBatch; /* 1D4h */CLIENT_ID RealClientId; /* 6B4h */PVOID GdiCachedProcessHandle; /* 6BCh */ULONG GdiClientPID; /* 6C0h */ULONG GdiClientTID; /* 6C4h */PVOID GdiThreadLocalInfo; /* 6C8h */ULONG Win32ClientInfo[62]; /* 6CCh */PVOID glDispatchTable[0xE9]; /* 7C4h */ULONG glReserved1[0x1D]; /* B68h */PVOID glReserved2; /* BDCh */PVOID glSectionInfo; /* BE0h */PVOID glSection; /* BE4h */PVOID glTable; /* BE8h */PVOID glCurrentRC; /* BECh */PVOID glContext; /* BF0h */NTSTATUS LastStatusValue; /* BF4h */UNICODE_STRING StaticUnicodeString; /* BF8h */WCHAR StaticUnicodeBuffer[0x105]; /* C00h */PVOID DeallocationStack; /* E0Ch */PVOID TlsSlots[0x40]; /* E10h */LIST_ENTRY TlsLinks; /* F10h */PVOID Vdm; /* F18h */PVOID ReservedForNtRpc; /* F1Ch */PVOID DbgSsReserved[0x2]; /* F20h */ULONG HardErrorDisabled; /* F28h */PVOID Instrumentation[14]; /* F2Ch */PVOID SubProcessTag; /* F64h */PVOID EtwTraceData; /* F68h */PVOID WinSockData; /* F6Ch */ULONG GdiBatchCount; /* F70h */BOOLEAN InDbgPrint; /* F74h */BOOLEAN FreeStackOnTermination; /* F75h */BOOLEAN HasFiberData; /* F76h */UCHAR IdealProcessor; /* F77h */ULONG GuaranteedStackBytes; /* F78h */PVOID ReservedForPerf; /* F7Ch */PVOID ReservedForOle; /* F80h */ULONG WaitingOnLoaderLock; /* F84h */ULONG SparePointer1; /* F88h */ULONG SoftPatchPtr1; /* F8Ch */ULONG SoftPatchPtr2; /* F90h */PVOID *TlsExpansionSlots; /* F94h */ULONG ImpersionationLocale; /* F98h */ULONG IsImpersonating; /* F9Ch */PVOID NlsCache; /* FA0h */PVOID pShimData; /* FA4h */ULONG HeapVirualAffinity; /* FA8h */PVOID CurrentTransactionHandle; /* FACh */PTEB_ACTIVE_FRAME ActiveFrame; /* FB0h */PVOID FlsData; /* FB4h */UCHAR SafeThunkCall; /* FB8h */UCHAR BooleanSpare[3]; /* FB9h */} TEB, *PTEB;
2.PEB结构
typedef struct _PEB{UCHAR InheritedAddressSpace; // 00hUCHAR ReadImageFileExecOptions; // 01hUCHAR BeingDebugged; // 02hUCHAR Spare; // 03hPVOID Mutant; // 04hPVOID ImageBaseAddress; // 08hPPEB_LDR_DATA Ldr; // 0ChPRTL_USER_PROCESS_PARAMETERS ProcessParameters; // 10hPVOID SubSystemData; // 14hPVOID ProcessHeap; // 18hPVOID FastPebLock; // 1ChPPEBLOCKROUTINE FastPebLockRoutine; // 20hPPEBLOCKROUTINE FastPebUnlockRoutine; // 24hULONG EnvironmentUpdateCount; // 28hPVOID* KernelCallbackTable; // 2ChPVOID EventLogSection; // 30hPVOID EventLog; // 34hPPEB_FREE_BLOCK FreeList; // 38hULONG TlsExpansionCounter; // 3ChPVOID TlsBitmap; // 40hULONG TlsBitmapBits[0x2]; // 44hPVOID ReadOnlySharedMemoryBase; // 4ChPVOID ReadOnlySharedMemoryHeap; // 50hPVOID* ReadOnlyStaticServerData; // 54hPVOID AnsiCodePageData; // 58hPVOID OemCodePageData; // 5ChPVOID UnicodeCaseTableData; // 60hULONG NumberOfProcessors; // 64hULONG NtGlobalFlag; // 68hUCHAR Spare2[0x4]; // 6ChLARGE_INTEGER CriticalSectionTimeout; // 70hULONG HeapSegmentReserve; // 78hULONG HeapSegmentCommit; // 7ChULONG HeapDeCommitTotalFreeThreshold; // 80hULONG HeapDeCommitFreeBlockThreshold; // 84hULONG NumberOfHeaps; // 88hULONG MaximumNumberOfHeaps; // 8ChPVOID** ProcessHeaps; // 90hPVOID GdiSharedHandleTable; // 94hPVOID ProcessStarterHelper; // 98hPVOID GdiDCAttributeList; // 9ChPVOID LoaderLock; // A0hULONG OSMajorVersion; // A4hULONG OSMinorVersion; // A8hULONG OSBuildNumber; // AChULONG OSPlatformId; // B0hULONG ImageSubSystem; // B4hULONG ImageSubSystemMajorVersion; // B8hULONG ImageSubSystemMinorVersion; // C0hULONG GdiHandleBuffer[0x22]; // C4hPVOID ProcessWindowStation; // ???} PEB, *PPEB;
原理:在NT内核系统中fs寄存器指向TEB结构,TEB+0x30处指向PEB结构,PEB+0x0c处指向PEB_LDR_DATA结构,PEB_LDR_DATA+0x1c处存放一些动态链接库地址,第一个指向ntdl.dll,第二个就是kernel32.dll的基地址了。
【注意】fs:[0x30]仅限Windows 32位系统。
●64位程序
1.内联汇编函数kernelBase.asm
public KernelHandleCPUX64 = 1.code
KernelHandle proc
IFDEF CPUX64mov rax, gs:[60h]mov rax, [rax + 18h]mov rax, [rax + 30h]mov rax, [rax]mov rax, [rax]mov rax, [rax + 10h]
ENDIFret
KernelHandle endp
end
UniversalkernelBase.c
/*------------------------------------------------------------------------FileName:UniversalkernelBase.c实验67:方法三(利用PEB结构来查找)获取X64 kernel32.dll的基址(c) bcdaren, 2024
-----------------------------------------------------------------------*/
#include <windows.h>
#define CPUX64
EXTERN_C PVOID64 _cdecl KernelHandle();int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,LPSTR lpCmdLine, int nShowCmd)
{const TCHAR szText[] = TEXT("X64 kernel32.dll的基地址为%p");static TCHAR szBuffer[256];HMODULE hKernel64 = NULL;#ifdef CPUX64hKernel64 = (HMODULE)(KernelHandle());//输出模块基地址wsprintf(szBuffer, szText, hKernel64);MessageBox(NULL, szBuffer, NULL, MB_OK);
#endifExitProcess(0);return 0;
}
运行:
图9-6 利用PEB结构来查找X64 kernel32.dll基址
总结
在Windows 64位系统下,gs:[60h]指向PEB进程环境块。
由于Winodws 64位系统不再支持内联汇编,因此需要改成内联函数的形式。将汇编代码改为独立的kernelBase.asm模块。
●asm文件编译VS配置
1.asm文件右键--属性,配置如下,点击应用。
图9-7 X64内联函数VS配置一
2.自定义生成工具,设置命令行和输出项
图9-8 X64内联函数VS配置二
命令行:ml64 /Fo $(IntDir)%(fileName).obj /c %(fileName).asm
输出:$(IntDir)%(fileName).obj
3、内联汇编函数调用
EXTERN_C PVOID64 _cdecl KernelHandle();
hKernel64 = (HMODULE)(KernelHandle());