0xd00's blog

Back

详解 Windows DLL 远程注入的实现原理Blur image

简单介绍DLL#

DLL (Dynamic-Link Library),即“动态链接库”,是一个编译后的代码和资源库,它允许多个可执行文件.exe在运行时动态加载并共享其功能,类似于Linux的共享对象.so。它的核心优势是运行时链接(Runtime linking),而非编译时将所有代码都静态链接到主程序中。

DLL文件和EXE文件一样,都使用PE文件格式。他们的主要区别在于PE头中的一个标志位,以及DLL文件一般不能独立运行,需要通过其他程序加载并运行。

PE头中的标志位

导出表#

正常情况下DLL需要通过导出表来展示一个可供调用的函数列表,当一个程序想要调用DLL中的函数时,它就会通过这个导出表来找到函数的实际内存地址。

本例中方便起见不导出函数,直接通过DLL_PROCESS_ATTACH来展示DLL被载入后的运行效果。

DLL本地注入#

这个 DLL 在被加载时,会利用其 DllMain 入口点弹出一个消息框。

dll.c
#include <Windows.h>
#include <stdio.h>

BOOL APIENTRY DllMain(HMODULE hModule, DWORD dwReason, LPVOID lpReserved) {

    switch (dwReason) {
    case DLL_PROCESS_ATTACH: {
        MessageBoxA(NULL, "DLL Injected Successfully!", "Success", MB_OK);
        break;
    };
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }

    return TRUE;
}
c

加载器程序调用 LoadLibraryA 来加载 Dll.dll。这个 API 会触发 Windows 加载器执行完整的 DLL 加载流程:解析 PE 头、映射段、处理导入、最后调用 DllMain。

loader.c
#include <Windows.h>
#include <stdio.h>
#include <process.h>

int main() {
    const char* dllPath = "mydll.dll";
    printf("Attempting to load %s...\n", dllPath);

    HMODULE hDll = LoadLibraryA(dllPath);

    if (hDll == NULL) {
        printf("Failed to load DLL. Error code: %d\n", GetLastError());
        return 1;
    }

    printf("DLL loaded successfully. Handle: %p\n", hDll);
    printf("Check for the message box!\n");

    system("pause");

    FreeLibrary(hDll);
    printf("DLL unloaded.\n");

    return 0;
}
c

成功运行loader并弹出messagebox

DLL远程注入#

远程注入的核心思想是:在目标进程的上下文中,强制其调用 LoadLibraryA 来加载我们指定的 DLL。由于进程地址空间的隔离性,这需要一系列精确的跨进程内存操作。

定位目标进程#

通过 CreateToolhelp32Snapshot API 遍历系统进程,根据进程名找到其 PID。

// 因为现代Windows默认使用Unicode编码,所以这里直接使用宽字符比较函数 _wcsicmp
DWORD FindProcessIdByName(const wchar_t* processName) {
    PROCESSENTRY32W entry; // 明确使用宽字符版本
    entry.dwSize = sizeof(PROCESSENTRY32W);

    HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

    if (Process32FirstW(snapshot, &entry)) { // 使用 Process32FirstW
        do {
            // 使用 _wcsicmp 进行不区分大小写的宽字符比较
            if (_wcsicmp(entry.szExeFile, processName) == 0) {
                CloseHandle(snapshot);
                return entry.th32ProcessID;
            }
        } while (Process32NextW(snapshot, &entry)); // 使用 Process32NextW
    }

    CloseHandle(snapshot);
    return 0;
}
c

获取目标句柄#

调用 OpenProcess获取一个拥有足够权限(PROCESS_ALL_ACCESS)的句柄。

    HANDLE hProcess = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_CREATE_THREAD, FALSE, pid);
    if (hProcess == NULL) {
        wprintf(L"Failed to open target process (Error: %d). Try running as Administrator.\n", GetLastError());
        return 1;
    }
c

在目标进程中分配内存#

使用VirtualAllocEx在目标进程的地址空间中申请一块足以保存DLL的内存空间。

LPVOID pRemoteMem = VirtualAllocEx(hProcess, NULL, dllPathSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (pRemoteMem == NULL) {
    wprintf(L"VirtualAllocEx failed. Error: %d\n", GetLastError());
    CloseHandle(hProcess);
    return 1;
}
plaintext

写入DLL路径#

使用 WriteProcessMemory 将我们的 DLL 路径字符串写入到刚刚在远程进程中分配的内存里。

    // 写入内存时,也要提供正确的字节大小
    if (!WriteProcessMemory(hProcess, pRemoteMem, dllPath, dllPathSize, NULL)) {
        wprintf(L"WriteProcessMemory failed. Error: %d\n", GetLastError());
        VirtualFreeEx(hProcess, pRemoteMem, 0, MEM_RELEASE);
        CloseHandle(hProcess);
        return 1;
    }
c

查找 LoadLibraryW 地址#

LoadLibraryW 用于加载一个调用它的进程内部的 DLL。 由于目标是加载远程进程内部而非本地进程内部的 DLL,所以不能直接调用它。 相反,必须检索 LoadLibraryW 的地址并将其作为参数传递给进程中远程创建的线程,同时将 DLL 名称作为其参数。 这起作用是因为 LoadLibraryW WinAPI 的地址在远程进程中和在本地进程中相同。 为了确定 WinAPI 的地址,需要使用 GetProcAddressGetModuleHandle

    // 必须使用 LoadLibraryW,而不是 LoadLibraryA
    LPVOID pLoadLibraryW = (LPVOID)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW");
    if (pLoadLibraryW == NULL) {
        wprintf(L"GetProcAddress for LoadLibraryW failed. Error: %d\n", GetLastError());
        CloseHandle(hProcess);
        return 1;
    }
c

创建远程线程#

调用 CreateRemoteThread,让它在目标进程中创建一个新线程。这个新线程的起始执行地址被设置为 LoadLibraryA 的地址,其接收的参数则被设置为我们刚刚写入的 DLL 路径的地址。

HANDLE hRemoteThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pLoadLibraryW, pRemoteMem, 0, NULL);
if (hRemoteThread == NULL) {
    wprintf(L"CreateRemoteThread failed. Error: %d\n", GetLastError());
    VirtualFreeEx(hProcess, pRemoteMem, 0, MEM_RELEASE);
    CloseHandle(hProcess);
    return 1;
}
plaintext

完整代码#

injector.cpp
#include <Windows.h>
#include <stdio.h>
#include <TlHelp32.h>

// 因为现代Windows默认使用Unicode编码,所以这里直接使用宽字符比较函数 _wcsicmp
DWORD FindProcessIdByName(const wchar_t* processName) {
    PROCESSENTRY32W entry; // 明确使用宽字符版本
    entry.dwSize = sizeof(PROCESSENTRY32W);

    HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

    if (Process32FirstW(snapshot, &entry)) { // 使用 Process32FirstW
        do {
            // 使用 _wcsicmp 进行不区分大小写的宽字符比较
            if (_wcsicmp(entry.szExeFile, processName) == 0) {
                CloseHandle(snapshot);
                return entry.th32ProcessID;
            }
        } while (Process32NextW(snapshot, &entry)); // 使用 Process32NextW
    }

    CloseHandle(snapshot);
    return 0;
}

int wmain() { // 使用 wmain 作为 Unicode程序的入口点
    const wchar_t* targetProcessName = L"notepad.exe"; // 使用 L 前缀创建宽字符字符串
    wchar_t dllPath[MAX_PATH];

    // 使用 GetFullPathNameW
    if (GetFullPathNameW(L"Dll.dll", MAX_PATH, dllPath, NULL) == 0) {
        wprintf(L"Failed to get full path of DLL. Error: %d\n", GetLastError());
        return 1;
    }

    wprintf(L"Targeting process: %s\n", targetProcessName);
    wprintf(L"DLL to inject: %s\n", dllPath);

    DWORD pid = FindProcessIdByName(targetProcessName);
    if (pid == 0) {
        wprintf(L"Could not find process. Is %s running?\n", targetProcessName);
        return 1;
    }
    wprintf(L"Found PID: %d\n", pid);

    HANDLE hProcess = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_CREATE_THREAD, FALSE, pid);
    if (hProcess == NULL) {
        wprintf(L"Failed to open target process (Error: %d). Try running as Administrator.\n", GetLastError());
        return 1;
    }

    // 计算内存大小时,要乘以 sizeof(wchar_t)
    size_t dllPathSize = (wcslen(dllPath) + 1) * sizeof(wchar_t);

    LPVOID pRemoteMem = VirtualAllocEx(hProcess, NULL, dllPathSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    if (pRemoteMem == NULL) {
        wprintf(L"VirtualAllocEx failed. Error: %d\n", GetLastError());
        CloseHandle(hProcess);
        return 1;
    }

    // 写入内存时,也要提供正确的字节大小
    if (!WriteProcessMemory(hProcess, pRemoteMem, dllPath, dllPathSize, NULL)) {
        wprintf(L"WriteProcessMemory failed. Error: %d\n", GetLastError());
        VirtualFreeEx(hProcess, pRemoteMem, 0, MEM_RELEASE);
        CloseHandle(hProcess);
        return 1;
    }

    // 必须使用 LoadLibraryW,而不是 LoadLibraryA
    LPVOID pLoadLibraryW = (LPVOID)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW");
    if (pLoadLibraryW == NULL) {
        wprintf(L"GetProcAddress for LoadLibraryW failed. Error: %d\n", GetLastError());
        CloseHandle(hProcess);
        return 1;
    }

    HANDLE hRemoteThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pLoadLibraryW, pRemoteMem, 0, NULL);
    if (hRemoteThread == NULL) {
        wprintf(L"CreateRemoteThread failed. Error: %d\n", GetLastError());
        VirtualFreeEx(hProcess, pRemoteMem, 0, MEM_RELEASE);
        CloseHandle(hProcess);
        return 1;
    }

    wprintf(L"Injection successful. Waiting for remote thread to finish...\n");
    WaitForSingleObject(hRemoteThread, INFINITE);

    CloseHandle(hRemoteThread);
    VirtualFreeEx(hProcess, pRemoteMem, 0, MEM_RELEASE);
    CloseHandle(hProcess);

    wprintf(L"Cleanup complete.\n");
    return 0;
}
c

运行效果#

image-20250723154543840

详解 Windows DLL 远程注入的实现原理
https://blog.0xd00.com/blog/createremotethread-dll-injection
Author 0xd00
Published at 2025年7月23日
Comment seems to stuck. Try to refresh?✨