65.9K
CodeProject 正在变化。 阅读更多。
Home

将 .NET 程序集注入到非托管进程

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (69投票s)

2013年6月23日

MIT

16分钟阅读

viewsIcon

161647

downloadIcon

5141

深入分析如何在非托管进程和托管进程中注入 .NET 运行时和任意 .NET 程序集;以及如何在这些进程中执行托管代码。

Screenshot

目录

引言

*** 注意 *** 该项目已迁移至 GitHub:https://github.com/perspectivism/injecting-net-assemblies

.NET 是一个强大的语言,可以快速可靠地开发软件。然而,在某些任务中 .NET 并不适用。本文重点介绍了一种特定情况:DLL 注入。在未加载 .NET 运行时的远程进程中,无法注入 .NET DLL(也称为托管 DLL)。此外,即使 .NET 运行时已加载到某个进程中,如何调用 .NET DLL 中的方法?架构如何?64 位进程是否需要与 32 位进程不同的关注点?本文旨在展示如何使用文档化的 API 来完成所有这些任务。我们将一起

  • 在任意进程中启动 .NET CLR(公共语言运行时),无论其位宽如何。
  • 在任意进程中加载自定义 .NET 程序集。
  • 在任意进程的上下文中执行托管代码。

回归基础

为了实现我们的目标,需要发生几件事情。为了使问题更易于管理,我们将将其分解成几个部分,最后再重新组合。解决这个难题的步骤是

  1. 加载 CLR(基础) - 介绍如何在非托管进程中启动 .NET Framework
  2. 加载 CLR(高级) - 介绍如何加载自定义 .NET 程序集并在非托管代码中调用托管方法
  3. DLL 注入(基础) - 介绍如何在远程进程中执行非托管代码
  4. DLL 注入(高级) - 介绍如何在远程进程中执行任意导出的函数
  5. 整合起来 - 解决方案出现了;将所有内容整合在一起

注意:作者遵循约定,使用“函数”来指代 C++ 函数,使用“方法”来指代 C# 函数。“远程”进程指任何非当前进程。

加载 CLR

编写一个可以加载 .NET 运行时和任意程序集的非托管应用程序是我们实现目标的第一步。

基本原理

下面的示例程序说明了 C++ 应用程序如何将 .NET 运行时加载到自身中

#include <metahost.h>

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

#import "mscorlib.tlb" raw_interfaces_only \
    high_property_prefixes("_get","_put","_putref") \
    rename("ReportEvent", "InteropServices_ReportEvent")

int wmain(int argc, wchar_t* argv[])
{
    char c;
    wprintf(L"Press enter to load the .net runtime...");
    while (getchar() != '\n');	
    
    HRESULT hr;
    ICLRMetaHost *pMetaHost = NULL;
    ICLRRuntimeInfo *pRuntimeInfo = NULL;
    ICLRRuntimeHost *pClrRuntimeHost = NULL;
    
    // build runtime
    hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_PPV_ARGS(&pMetaHost));
    hr = pMetaHost->GetRuntime(L"v4.0.30319", IID_PPV_ARGS(&pRuntimeInfo));
    hr = pRuntimeInfo->GetInterface(CLSID_CLRRuntimeHost, 
        IID_PPV_ARGS(&pClrRuntimeHost));
    
    // start runtime
    hr = pClrRuntimeHost->Start();
    
    wprintf(L".Net runtime is loaded. Press any key to exit...");
    while (getchar() != '\n');
    
    return 0;
}

在上述代码中,值得关注的调用是

CLRCreateInstance 给定 CLSID_CLRMetaHost,获取 ICLRMetaHost 实例的指针
ICLRMetaHost::GetRuntime 获取指向特定 .NET 运行时的 ICLRRuntimeInfo 类型指针
ICLRRuntimeInfo::GetInterface 将 CLR 加载到当前进程中并获取 ICLRRuntimeHost 指针
ICLRRuntimeHost::Start 显式启动 CLR,在首次加载托管代码时隐式调用

在撰写本文时,ICLRMetaHost::GetRuntime 的有效版本值包括 NULL、“v1.0.3705”、“v1.1.4322”、“v2.0.50727”和“v4.0.30319”,其中 NULL 加载最新版本的运行时。目标运行时应安装在系统上,并且上述版本值应存在于 _%WinDir%\Microsoft.NET\Framework_ 或 _%WinDir%\Microsoft.NET\Framework64_ 中。

编译并运行上述代码,从控制台和 Process Hacker 中可以看到以下输出

console screenshot

process hacker screenshot

按下回车键后,可以通过 Process Hacker 观察到 .NET 运行时已被加载。注意属性窗格中引用 .NET 的附加选项卡

console screenshot

process hacker screenshot

上述示例代码不包含在源代码下载中。然而,建议读者将其作为一项练习来构建和运行该示例。

高级

随着谜题的第一块到位,下一步是在进程中加载任意 .NET 程序集并调用该 .NET 程序集中的方法。

继续基于上述示例,CLR 已在进程中加载。这是通过获取 CLR 接口的指针实现的;该指针存储在变量 pClrRuntimeHost 中。使用 pClrRuntimeHost,调用了 ICLRRuntimeHost::Start 来将 CLR 初始化到进程中。

现在 CLR 已初始化,pClrRuntimeHost 可以调用 ICLRRuntimeHost::ExecuteInDefaultAppDomain 来加载和调用任意 .NET 程序集中的方法。该函数具有以下签名

HRESULT ExecuteInDefaultAppDomain (
    [in] LPCWSTR pwzAssemblyPath,
    [in] LPCWSTR pwzTypeName, 
    [in] LPCWSTR pwzMethodName,
    [in] LPCWSTR pwzArgument,
    [out] DWORD *pReturnValue
);

对每个参数的简要说明

pwzAssemblyPath .NET 程序集的完整路径;可以是 EXE 或 DLL 文件
pwzTypeName 要调用的方法的完全限定类型名称
pwzMethodName 要调用的方法的名称
pwzArgument 传递给方法的可选参数
pReturnValue 方法的返回值

并非 .NET 程序集中的所有方法都可以通过 ICLRRuntimeHost::ExecuteInDefaultAppDomain 调用。有效的 .NET 方法必须具有以下签名

static int pwzMethodName (String pwzArgument);

顺便提一下,访问修饰符,如 publicprotectedprivateinternal,不会影响方法的可见性;因此,它们已被排除在签名之外。

以下 .NET 应用程序将在后续所有示例中用作注入到进程中的托管 .NET 程序集

using System;
using System.Windows.Forms;

namespace InjectExample
{
    public class Program
    {
        static int EntryPoint(String pwzArgument)
        {
            System.Media.SystemSounds.Beep.Play();

            MessageBox.Show(
                "I am a managed app.\n\n" + 
                "I am running inside: [" + 
                System.Diagnostics.Process.GetCurrentProcess().ProcessName + 
                "]\n\n" + (String.IsNullOrEmpty(pwzArgument) ? 
                "I was not given an argument" : 
                "I was given this argument: [" + pwzArgument + "]"));

            return 0;
        }

        static void Main(string[] args)
        {
            EntryPoint("hello world");
        }
    }
}

上述示例应用程序的编写方式使其可以通过 ICLRRuntimeHost::ExecuteInDefaultAppDomain 调用,或独立运行;两种方法都会产生类似的行为。最终目标是,当注入到非托管远程进程时,该应用程序将在该进程的上下文中执行,并显示一个显示远程进程名称的消息框。

基于 基础 部分的示例代码,以下 C++ 程序将加载上述 .NET 程序集并执行 EntryPoint 方法

#include <metahost.h>

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

#import "mscorlib.tlb" raw_interfaces_only \
    high_property_prefixes("_get","_put","_putref") \
    rename("ReportEvent", "InteropServices_ReportEvent")

int wmain(int argc, wchar_t* argv[])
{
    HRESULT hr;
    ICLRMetaHost *pMetaHost = NULL;
    ICLRRuntimeInfo *pRuntimeInfo = NULL;
    ICLRRuntimeHost *pClrRuntimeHost = NULL;
    
    // build runtime
    hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_PPV_ARGS(&pMetaHost));
    hr = pMetaHost->GetRuntime(L"v4.0.30319", IID_PPV_ARGS(&pRuntimeInfo));
    hr = pRuntimeInfo->GetInterface(CLSID_CLRRuntimeHost, 
        IID_PPV_ARGS(&pClrRuntimeHost));
    
    // start runtime
    hr = pClrRuntimeHost->Start();
    
    // execute managed assembly
    DWORD pReturnValue;
    hr = pClrRuntimeHost->ExecuteInDefaultAppDomain(
    	L"T:\\FrameworkInjection\\_build\\debug\\anycpu\\InjectExample.exe", 
    	L"InjectExample.Program", 
    	L"EntryPoint", 
    	L"hello .net runtime", 
    	&pReturnValue);
    
    // free resources
    pMetaHost->Release();
    pRuntimeInfo->Release();
    pClrRuntimeHost->Release();
    
    return 0;
}

以下屏幕截图显示了应用程序的输出

test .net assembly output

到目前为止,谜题已经解决了两部分。现在已经理解了如何使用非托管代码加载 CLR,以及如何从非托管代码执行任意 .NET 程序集。但如何在任意进程中做到这一点?

DLL 注入

DLL 注入是一种通过在远程进程中加载 DLL 来在该进程中执行代码的策略。许多 DLL 注入策略都侧重于在 DllMain 中执行代码。不幸的是,尝试从 DllMain 启动 CLR 会导致 Windows 加载器死锁。可以通过编写一个尝试从 DllMain 启动 CLR 的示例 DLL 来独立验证这一点。验证留给读者作为练习。有关 .NET 初始化、Windows 加载器和加载器锁的更多信息,请参阅以下 MSDN 文章

不可避免的结果是,当 Windows 加载器正在初始化另一个模块时,无法启动 CLR。每个锁都是进程特定的,由 Windows 管理。请记住,任何尝试在已获得锁的情况下再次获取加载器锁的模块都将导致死锁。

基本原理

Windows 加载器的问题似乎很大;每当问题看起来很大时,将其分解成更易于管理的部分会很有帮助。开发一种将最简单的 DLL 注入到远程进程中的策略是一个好的开始。请看以下示例代码

#define WIN32_LEAN_AND_MEAN
#include <windows.h>

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;
}

上述代码实现了一个简单的 DLL。要在远程进程中注入此 DLL,需要以下 Windows API

OpenProcess 获取进程的句柄
GetModuleHandle 获取给定模块的句柄
LoadLibrary 在调用进程的地址空间中加载库
GetProcAddress 获取库中导出函数的 VA(虚拟地址)
VirtualAllocEx 在给定进程中分配空间
WriteProcessMemory 在给定地址的给定进程中写入字节
CreateRemoteThread 在远程进程中创建线程

继续,下面函数的作用是执行已加载到远程进程中的 DLL 的导出函数

DWORD_PTR Inject(const HANDLE hProcess, const LPVOID function, 
    const wstring& argument)
{
    // allocate some memory in remote process
    LPVOID baseAddress = VirtualAllocEx(hProcess, NULL, GetStringAllocSize(argument), 
        MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    
    // write argument into remote process	
    BOOL isSucceeded = WriteProcessMemory(hProcess, baseAddress, argument.c_str(), 
        GetStringAllocSize(argument), NULL);
    
    // make the remote process invoke the function
    HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, 
        (LPTHREAD_START_ROUTINE)function, baseAddress, NULL, 0);
    
    // wait for thread to exit
    WaitForSingleObject(hThread, INFINITE);
    
    // free memory in remote process
    VirtualFreeEx(hProcess, baseAddress, 0, MEM_RELEASE);
    
    // get the thread exit code
    DWORD exitCode = 0;
    GetExitCodeThread(hThread, &exitCode);
    
    // close thread handle
    CloseHandle(hThread);
    
    // return the exit code
    return exitCode;
}

请记住,本节的目标是在远程进程中加载库。下一个问题是如何使用上述函数在远程进程中注入 DLL?答案在于假设 _kernel32.dll_ 已映射到每个进程的地址空间中。LoadLibrary 是 _kernel32.dll_ 的一个导出函数,其函数签名与 LPTHREAD_START_ROUTINE 匹配,因此可以将其作为启动例程传递给 CreateRemoteThread。回想一下 LoadLibrary 的作用是在调用进程的地址空间中加载库,而 CreateRemoteThread 的作用是在远程进程中创建线程。以下代码片段说明了如何在 ID 为 3344 的进程中加载我们的测试 DLL

// get handle to remote process with PID 3344
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, 3344);

// get address of LoadLibrary
FARPROC fnLoadLibrary = GetProcAddress(GetModuleHandle(L"Kernel32"), "LoadLibraryW");

// inject test.dll into the remote process
Inject(hProcess, fnLoadLibrary, L"T:\\test\\test.dll");

继续基于上述示例,调用 CreateRemoteThread 后,会调用 WaitForSingleObject 来等待线程退出。接下来,调用 GetExitCodeThread 获取结果。碰巧的是,当 LoadLibrary 被传递给 CreateRemoteThread 时,成功调用将导致 GetExitCodeThreadlpExitCode 返回远程进程上下文中已加载库的基地址。这对于 32 位应用程序很有效,但对于 64 位应用程序则不行。原因是 GetExitCodeThreadlpExitCode 是一个 DWORD 值,即使在 64 位机器上也是如此,因此地址会被截断。

此时,谜题已解决了三部分。回顾一下,已解决以下问题

  1. 使用非托管代码加载 CLR
  2. 从非托管代码执行任意 .NET 程序集
  3. 注入 DLL

高级

现在已经了解了如何在远程进程中加载 DLL;可以继续讨论如何在远程进程中启动 CLR。

LoadLibrary 返回时,加载器上的锁将解除。DLL 已进入远程进程的地址空间,可以通过后续对 CreateRemoteThread 的调用来调用导出的函数;前提是函数签名与 LPTHREAD_START_ROUTINE 匹配。然而,这不可避免地会引发更多问题。如何在远程进程中调用导出的函数,以及如何获取这些函数的指针?由于 GetProcAddress 没有匹配的 LPTHREAD_START_ROUTINE 签名,如何获取 DLL 中函数的地址?此外,即使可以调用 GetProcAddress,它仍然需要一个远程 DLL 的句柄。对于 64 位机器,如何获取此句柄?

是时候再次分而治之了。下面的函数在 x86 和 x64 系统上都能可靠地返回给定进程中给定模块的句柄(巧合地也是基地址)

DWORD_PTR GetRemoteModuleHandle(const int processId, const wchar_t* moduleName)
{
    MODULEENTRY32 me32; 
    HANDLE hSnapshot = INVALID_HANDLE_VALUE;
    
    // get snapshot of all modules in the remote process 
    me32.dwSize = sizeof(MODULEENTRY32); 
    hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, processId);
    
    // can we start looking?
    if (!Module32First(hSnapshot, &me32)) 
    {
    	CloseHandle(hSnapshot);
    	return 0;
    }
    
    // enumerate all modules till we find the one we are looking for 
    // or until every one of them is checked
    while (wcscmp(me32.szModule, moduleName) != 0 && Module32Next(hSnapshot, &me32));
    
    // close the handle
    CloseHandle(hSnapshot);
    
    // check if module handle was found and return it
    if (wcscmp(me32.szModule, moduleName) == 0)
    	return (DWORD_PTR)me32.modBaseAddr;
    
    return 0;
}

知道远程进程中 DLL 的基地址是朝着正确的方向迈出的一步。是时候制定一个获取任意导出函数地址的策略了。回顾一下,我们知道如何调用 LoadLibrary 并在远程进程中获取已加载模块的句柄。有了这个,在本地(调用进程内)调用 LoadLibrary 并获取已加载模块的句柄就非常简单了。这个句柄(也是模块的基地址)可能与远程进程中的句柄相同,也可能不同,即使是同一个库。尽管如此,通过一些基本数学运算,我们可以获取任意所需导出函数的地址。想法是,尽管模块的基地址可能因进程而异,但任何给定函数相对于模块基地址的偏移量都是恒定的。例如,取源代码下载中 Bootstrap DLL 项目找到的以下导出函数

__declspec(dllexport) HRESULT ImplantDotNetAssembly(_In_ LPCTSTR lpCommand)

在远程调用此函数之前,必须先在远程进程中加载 _Bootstrap.dll_ 模块。使用 Process Hacker,以下是当 _Bootstrap.dll_ 注入到 Firefox 时 Windows 加载器将其放置在内存中的位置的屏幕截图

Firefox Bootstap.dll

继续我们的策略,这里有一个示例程序,它在本地(调用进程内)加载 _Bootstrap.dll_ 模块

#include <windows.h>

int wmain(int argc, wchar_t* argv[])
{
    HMODULE hLoaded = LoadLibrary(
        L"T:\\FrameworkInjection\\_build\\release\\x86\\Bootstrap.dll");
    system("pause");
    return 0;
}

当运行上述程序时,以下是 Windows 决定加载 _Bootstrap.dll_ 模块的位置的屏幕截图

Test Bootstrap.dll

下一步是在 wmain 中调用 GetProcAddress 来获取 ImplantDotNetAssembly 函数的地址

#include <windows.h>

int wmain(int argc, wchar_t* argv[])
{
    HMODULE hLoaded = LoadLibrary(
        L"T:\\FrameworkInjection\\_build\\debug\\x86\\Bootstrap.dll");
    
    // get the address of ImplantDotNetAssembly
    void* lpInject = GetProcAddress(hLoaded, "ImplantDotNetAssembly");
    
    system("pause");
    return 0;
}

模块内函数的地址将始终高于模块的基地址。这里就是基本数学发挥作用的地方。下表有助于说明

Firefox.exe | Bootstrap.dll @ 0x50d0000 | ImplantDotNetAssembly @ ?
test.exe | Bootstrap.dll @ 0xf270000 | ImplantDotNetAssembly @ 0xf271490 (lpInject)

_Test.exe_ 显示 _Bootstrap.dll_ 加载地址为 0xf270000,并且 ImplantDotNetAssembly 在内存中的地址为 0xf271490。从 ImplantDotNetAssembly 的地址减去 _Bootstrap.dll_ 的地址将得到函数相对于模块基地址的偏移量。数学计算表明 ImplantDotNetAssembly 在模块中的偏移量为 (0xf271490 - 0xf270000) = 0x1490 字节。然后可以将此偏移量加到远程进程中 _Bootstrap.dll_ 模块的基地址上,以可靠地获得 ImplantDotNetAssembly 相对于远程进程的地址。计算 _Firefox.exe_ 中 ImplantDotNetAssembly 地址的数学计算表明,该函数位于地址 (0x50d0000 + 0x1490) = 0x50d1490。以下函数计算给定模块中给定函数的偏移量

DWORD_PTR GetFunctionOffset(const wstring& library, const char* functionName)
{
    // load library into this process
    HMODULE hLoaded = LoadLibrary(library.c_str());
    
    // get address of function to invoke
    void* lpInject = GetProcAddress(hLoaded, functionName);
    
    // compute the distance between the base address and the function to invoke
    DWORD_PTR offset = (DWORD_PTR)lpInject - (DWORD_PTR)hLoaded;
    
    // unload library from this process
    FreeLibrary(hLoaded);
    
    // return the offset to the function
    return offset;
}

值得注意的是,ImplantDotNetAssembly 故意匹配 LPTHREAD_START_ROUTINE 的签名;传递给 CreateRemoteThread 的所有方法也应该如此。通过能够执行远程 DLL 中的任意函数的能力,CLR 的初始化逻辑已置于 _Bootstrap.dll_ 中的 ImplantDotNetAssembly 函数内。一旦 _Bootstrap.dll_ 在远程进程中加载,就可以计算远程实例的 ImplantDotNetAssembly 地址,然后通过 CreateRemoteThread 调用。这就是谜题的最后一块;现在是时候将所有内容整合在一起了。

整合

使用非托管 DLL (_Bootstrap.dll_) 加载 CLR 的主要原因是,如果 CLR 在远程进程中未运行,启动它的唯一方法就是从非托管代码(除非使用 Python 等脚本语言,它有自己的依赖项)。

此外,最好让 Inject 应用程序足够灵活,能够接受命令行输入;因为谁愿意每次应用更改时都重新编译?Inject 应用程序接受以下选项

- m 要执行的托管方法的名称。例如:EntryPoint
- i 要注入到远程进程中的托管程序集的完全限定路径。例如:C:\InjectExample.exe
- l 托管程序集的完全限定类型名称。例如:InjectExample.Program
- a 传递给托管函数的可选参数。
- n 要注入的目标进程的进程 ID 或名称。例如:1500 或 notepad.exe

Injectwmain 方法如下所示

int wmain(int argc, wchar_t* argv[])
{	
    // parse args (-m -i -l -a -n)
    if (!ParseArgs(argc, argv))
    {
        PrintUsage();
        return -1;
    }
    
    // enable debug privileges
    EnablePrivilege(SE_DEBUG_NAME, TRUE);
    
    // get handle to remote process
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, g_processId);
    
    // inject bootstrap.dll into the remote process
    FARPROC fnLoadLibrary = GetProcAddress(GetModuleHandle(L"Kernel32"), 
        "LoadLibraryW");
    Inject(hProcess, fnLoadLibrary, GetBootstrapPath()); 
    
    // add the function offset to the base of the module in the remote process
    DWORD_PTR hBootstrap = GetRemoteModuleHandle(g_processId, BOOTSTRAP_DLL);
    DWORD_PTR offset = GetFunctionOffset(GetBootstrapPath(), "ImplantDotNetAssembly");
    DWORD_PTR fnImplant = hBootstrap + offset;
    
    // build argument; use DELIM as tokenizer
    wstring argument = g_moduleName + DELIM + g_typeName + DELIM + 
        g_methodName + DELIM + g_Argument;
    
    // inject the managed assembly into the remote process
    Inject(hProcess, (LPVOID)fnImplant, argument);
    
    // unload bootstrap.dll out of the remote process
    FARPROC fnFreeLibrary = GetProcAddress(GetModuleHandle(L"Kernel32"), 
        "FreeLibrary");
    CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)fnFreeLibrary, 
        (LPVOID)hBootstrap, NULL, 0);
    
    // close process handle
    CloseHandle(hProcess);
    
    return 0;
}

以下是 _Inject.exe_ 应用程序被调用以将 .NET 程序集 _InjectExample.exe_ 注入 _notepad.exe_ 的屏幕截图,以及使用的命令行

"T:\FrameworkInjection\_build\release\x64\Inject.exe" -m EntryPoint -i "T:\FrameworkInjection\_build\release\anycpu\InjectExample.exe" -l InjectExample.Program -a "hello inject" -n "notepad.exe"

inject notepad.exe

值得一提并区分针对 x86、x64 或 AnyCPU 构建的 DLL 注入的差异。在正常情况下,_Inject.exe_ 和 _Bootstrap.dll_ 的 x86 版本应用于注入 x86 进程,x64 版本应用于注入 x64 进程。调用者有责任确保使用正确的二进制文件。AnyCPU 是 .NET 中提供的一种平台。目标为 AnyCPU 意味着 CLR 会为适当的体系结构 JIT 编译程序集。这就是为什么相同的 _InjectExample.exe_ 程序集可以注入到 x86 或 x64 进程中的原因。

运行代码

接下来是更有趣的部分!要使用默认设置运行代码,需要满足一些先决条件。

构建代码

  1. Visual Studio 2012 Express +
  2. Visual Studio 2012 Express Update 1 +

运行代码

  1. .Net Framework 4.0 +
  2. Visual C++ Redistributable for Visual Studio 2012 Update 1 +
  3. Windows XP SP3+

为了简化代码构建任务,源代码下载中有一个名为 _build.bat_ 的文件,该文件将负责繁重的工作,前提是已安装了先决条件。它将编译调试和发布版本以及相应的 x86、x64 和 AnyCPU 版本。每个项目也可以独立构建,并可以在 Visual Studio 中编译。脚本 _build.bat_ 将把二进制文件放在一个名为 _build_ 的文件夹中。

代码也注释得很清楚。此外,选择 C++ 11.0 和 .NET 4.0 是因为这两个运行时都可以在从 XP SP3 x86 到 Windows 8 x64 的所有 Windows 操作系统上执行。顺便提一下,Microsoft 在 VS 2012 U1 中为 C++ 11.0 运行时添加了 XP SP3 支持。

结语

传统上,此时论文倾向于结束并反思所学的技术和其他改进领域。虽然这是一个进行此类练习的好地方,但作者决定做些不同的事情,并为读者提供一篮子

easter eggs

引言 中所述,.NET 是一个强大的语言。例如,.NET 中的 Reflection API 可用于获取程序集的类型信息。这意味着 .NET 可用于扫描程序集并返回可用于注入的有效方法!源代码下载包含一个名为 InjectGUI 的 .NET 项目。该项目包含一个非托管 Inject 应用程序的托管包装器。InjectGUI 显示正在运行的进程列表,决定是调用 32 位还是 64 位版本的 Inject,以及扫描 .NET 程序集以查找可注入的有效方法。在 InjectGUI 中有一个名为 _InjectWrapper.cs_ 的文件,其中包含包装器逻辑。

还有一个名为 MethodItem 的辅助类,其定义如下

public class MethodItem
{
    public string TypeName { get; set; }
    public string Name { get; set; }
    public string ParameterName { get; set; }
}

以下来自 ExtractInjectableMethods 方法的代码片段将获取类型为 List<MethodItem>Collection,该集合与所需的方法签名匹配

// find all methods that match: 
//    static int pwzMethodName (String pwzArgument)

private void ExtractInjectableMethods()
{
    // ...

    // open assembly
    Assembly asm = Assembly.LoadFile(ManagedFilename);

    // get valid methods
    InjectableMethods = 
        (from c in asm.GetTypes()
        from m in c.GetMethods(BindingFlags.Static | 
            BindingFlags.Public | BindingFlags.NonPublic)
        where m.ReturnType == typeof(int) &&
            m.GetParameters().Length == 1 &&
            m.GetParameters().First().ParameterType == typeof(string)
        select new MethodItem
        {
            Name = m.Name,
            ParameterName = m.GetParameters().First().Name,
            TypeName = m.ReflectedType.FullName
        }).ToList();

    // ...
}

现在已提取了有效的(可注入的)方法,UI 还应该知道要注入的进程是 32 位还是 64 位。为此,需要 Windows API 的一点帮助

[DllImport("kernel32.dll", SetLastError = true, CallingConvention = 
    CallingConvention.Winapi)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool IsWow64Process([In] IntPtr process, 
    [Out] out bool wow64Process);

IsWow64Process 仅在 64 位操作系统上定义,并且在应用程序是 32 位时返回 true。在 .NET 4.0 中,已引入以下属性:Environment.Is64BitOperatingSystem。这可以帮助确定 IsWow64Process 函数是否已定义,如下面的包装器函数所示

private static bool IsWow64Process(int id)
{
    if (!Environment.Is64BitOperatingSystem)
        return true;

    IntPtr processHandle;
    bool retVal;

    try
    {
        processHandle = Process.GetProcessById(id).Handle;
    }
    catch
    {
        return false; // access is denied to the process
    }

    return IsWow64Process(processHandle, out retVal) && retVal;
}

InjectGUI 项目中的逻辑相当简单。要理解 UI,需要对 WPF 和依赖项属性有一定的了解,然而,所有与注入相关的相关逻辑都在 InjectWrapper 类中。UI 使用 Modern UI for WPF 构建,图标来自 Modern UI Icons。这两个项目都是开源的,作者与两者均无关联。以下是 InjectGUI 运行时的屏幕截图

inject gui

结语 第二部分

至此,关于将 .NET 程序集注入到非托管进程的讨论结束了。不要因为结束而哭泣。因为它的发生而微笑。~ 威廉·莎士比亚

历史

  • 2023/06/21:文章和代码已迁移至 GitHub
  • 2013/06/22:初始发布
© . All rights reserved.