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






4.99/5 (69投票s)
深入分析如何在非托管进程和托管进程中注入 .NET 运行时和任意 .NET 程序集;以及如何在这些进程中执行托管代码。
目录
引言
*** 注意 *** 该项目已迁移至 GitHub:https://github.com/perspectivism/injecting-net-assemblies
.NET 是一个强大的语言,可以快速可靠地开发软件。然而,在某些任务中 .NET 并不适用。本文重点介绍了一种特定情况:DLL 注入。在未加载 .NET 运行时的远程进程中,无法注入 .NET DLL(也称为托管 DLL)。此外,即使 .NET 运行时已加载到某个进程中,如何调用 .NET DLL 中的方法?架构如何?64 位进程是否需要与 32 位进程不同的关注点?本文旨在展示如何使用文档化的 API 来完成所有这些任务。我们将一起
- 在任意进程中启动 .NET CLR(公共语言运行时),无论其位宽如何。
- 在任意进程中加载自定义 .NET 程序集。
- 在任意进程的上下文中执行托管代码。
回归基础
为了实现我们的目标,需要发生几件事情。为了使问题更易于管理,我们将将其分解成几个部分,最后再重新组合。解决这个难题的步骤是
- 加载 CLR(基础) - 介绍如何在非托管进程中启动 .NET Framework
- 加载 CLR(高级) - 介绍如何加载自定义 .NET 程序集并在非托管代码中调用托管方法
- DLL 注入(基础) - 介绍如何在远程进程中执行非托管代码
- DLL 注入(高级) - 介绍如何在远程进程中执行任意导出的函数
- 整合起来 - 解决方案出现了;将所有内容整合在一起
注意:作者遵循约定,使用“函数”来指代 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 中可以看到以下输出
按下回车键后,可以通过 Process Hacker 观察到 .NET 运行时已被加载。注意属性窗格中引用 .NET 的附加选项卡
上述示例代码不包含在源代码下载中。然而,建议读者将其作为一项练习来构建和运行该示例。
高级
随着谜题的第一块到位,下一步是在进程中加载任意 .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);
顺便提一下,访问修饰符,如 public
、protected
、private
和 internal
,不会影响方法的可见性;因此,它们已被排除在签名之外。
以下 .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;
}
以下屏幕截图显示了应用程序的输出
到目前为止,谜题已经解决了两部分。现在已经理解了如何使用非托管代码加载 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
时,成功调用将导致 GetExitCodeThread
的 lpExitCode
返回远程进程上下文中已加载库的基地址。这对于 32 位应用程序很有效,但对于 64 位应用程序则不行。原因是 GetExitCodeThread
的 lpExitCode
是一个 DWORD
值,即使在 64 位机器上也是如此,因此地址会被截断。
此时,谜题已解决了三部分。回顾一下,已解决以下问题
- 使用非托管代码加载 CLR
- 从非托管代码执行任意 .NET 程序集
- 注入 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 加载器将其放置在内存中的位置的屏幕截图
继续我们的策略,这里有一个示例程序,它在本地(调用进程内)加载 _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_ 模块的位置的屏幕截图
下一步是在 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 |
Inject
的 wmain
方法如下所示
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"
值得一提并区分针对 x86、x64 或 AnyCPU 构建的 DLL 注入的差异。在正常情况下,_Inject.exe_ 和 _Bootstrap.dll_ 的 x86 版本应用于注入 x86 进程,x64 版本应用于注入 x64 进程。调用者有责任确保使用正确的二进制文件。AnyCPU 是 .NET 中提供的一种平台。目标为 AnyCPU 意味着 CLR 会为适当的体系结构 JIT 编译程序集。这就是为什么相同的 _InjectExample.exe_ 程序集可以注入到 x86 或 x64 进程中的原因。
运行代码
接下来是更有趣的部分!要使用默认设置运行代码,需要满足一些先决条件。
构建代码
运行代码
- .Net Framework 4.0 +
- Visual C++ Redistributable for Visual Studio 2012 Update 1 +
- 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 支持。
结语
传统上,此时论文倾向于结束并反思所学的技术和其他改进领域。虽然这是一个进行此类练习的好地方,但作者决定做些不同的事情,并为读者提供一篮子
如 引言 中所述,.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
运行时的屏幕截图
结语 第二部分
至此,关于将 .NET 程序集注入到非托管进程的讨论结束了。不要因为结束而哭泣。因为它的发生而微笑。~ 威廉·莎士比亚
历史
- 2023/06/21:文章和代码已迁移至 GitHub
- 2013/06/22:初始发布