源代码
https://www.codeproject.com/KB/cpp/607352/FrameworkInjection.zip
一、概述
.NET是一个强大的编程语言,可以用来快速可靠地开发软件,然而.NET也有其不适用的一些场景。本文重点讲解了DLL注入的一个案例,当未加载.NET Runtime时,无法向一个远程进程中注入.NET DLL(托管DLL)。此外,假设.NET Runtime已经在目标进程中加载,那么应该如何调用.NET DLL中的方法?其结构是什么样的?针对64位进程的操作是否与32位进程不同?阅读本文后,你将会了解如何使用官方文档中记录的API实现这些任务。我们的目标是:
1、在32位和64位系统上,在任意进程中启动.NET CLR(公共语言运行库);
2、在任意进程中,加载自定义.NET程序集;
3、在任意进程的上下文中,执行托管代码。
二、拆分步骤,逐步击破
要实现最终的目标,需要进行几项不同的工作。为了使思路更加清晰,我们将其拆分成几个步骤,并在最后进行组合利用。要解决这个难题,需要进行的步骤是:
1、加载CLR(基础):如何在非托管进程中启动.NET框架。
2、加载CLR(高级):如何加载自定义.NET程序,并从非托管代码调用托管方法。
3、DLL注入(基础):如何在远程进程中执行非托管代码。
4、DLL注入(高级):如何在远程进程中执行任意导出的函数。
5、组合利用:将上述方法组合使用,实现我们最终的目标。
注意:我们在引用C++函数和C#函数时,所使用的都是函数的标准方式。我们所说的“远程”进程,是指除当前进程之外的其他任意进程。
三、加载CLR
我们实现目标的第一步,是编写可以加载.NET Runtime和任意程序集的非托管应用程序。
3.1 基础篇
以下示例程序,展示了C++应用程序如何将.NET Runtime加载到其自身:
#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;
}
在上述代码中,有4个调用比较重要,其作用如下:
1、CLRCreateInstance – 指定CLSID_CLRMetaHost,获取指向ICLRMetaHost实例的指针;
2、ICLRMetaHost::GetRuntime – 获取指向特定.NET Runtime的ICLRRuntimeInfo类型指针;
3、ICLRRuntimeInfo::GetInterface – 将CLR加载到当前进程,并获取ICLRRuntimeHost指针;
4、ICLRRuntimeHost::Start – 显示启动CLR,在首次加载托管代码时隐式调用。
在撰写本文时,ICLRMetaHost::GetRuntime的有效版本值为NULL、v1.0.3705、v1.1.4322、v2.0.50727和v4.0.30319,其中NULL加载最新版本的Runtime。需要根据操作系统的实际需要,安装相应版本的Runtime,其版本值保存在%WinDir%Microsoft.NETFramework和%WinDir%Microsoft.NETFramework64中。
编译并运行上述代码后,得到输出结果如下:
一旦按下回车键,就可以通过Process Hacker观察到.NET Runtime已被加载。请注意属性窗口上引用.NET的其他选项卡:
上述示例代码不包含在源代码压缩包中,建议读者自行编写并运行示例。
3.2 进阶篇
在将第一块拼图放好之后,接下来就要将任意.NET程序集加载到进程中,并调用该.NET程序集中的方法。
我们在上述示例的基础之上,继续进行下一步工作。在此之前,CLR被加载到进程中,这一过程是通过获取指向CLR接口的指针来实现的,该指针存储在变量pClrRuntimeHost中。使用pClrRuntimeHost调用ICLRRuntimeHost::Start,以实现将CLR初始化到进程之中。
现在,已经完成CLR的初始化工作,pClrRuntimeHost就可以调用ICLRRuntimeHost::ExecuteInDefaultAppDomain来加载和调用任意.NET程序集中的方法。该函数具有如下签名(Signature):
HRESULT ExecuteInDefaultAppDomain (
[in] LPCWSTR pwzAssemblyPath,
[in] LPCWSTR pwzTypeName,
[in] LPCWSTR pwzMethodName,
[in] LPCWSTR pwzArgument,
[out] DWORD *pReturnValue
);
其中,各参数的简要说明如下:
1、pwzAssemblyPath – .NET程序集的完整路径,可以是exe文件,也可以是dll文件;
2、pwzTypeName – 需调用的方法的完全限定类型名;
3、pwzMethodName – 需调用的方法的名称;
4、pwzArgument – 传递给方法的可选参数;
5、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.nn" +
"I am running inside: [" +
System.Diagnostics.Process.GetCurrentProcess().ProcessName +
"]nn" + (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,以及如何从非托管代码中执行任意方法。但是,如何在任意进程中实现呢?
四、DLL注入
通过DLL注入,可以在远程进程中加载DLL,从而实现远程进程内代码执行。许多DLL注入方法,都在试图实现DllMain内部代码执行。但是,如果尝试从DllMain中启动CLR,将会导致Windows加载程序的死锁。我们可以编写一个尝试从DllMain中启动CLR的示例DLL,来验证这一点,而验证过程就留给感兴趣的读者作为练习。关于.NET初始化、Windows加载程序和加载程序锁定的更多信息,请阅读以下MSDN文章:
http://msdn.microsoft.com/en-us/library/ms172219(v=VS.100).aspx
http://msdn.microsoft.com/en-us/library/ms173266.aspx
http://msdn.microsoft.com/en-us/library/windows/desktop/dd744765(v=vs.85).aspx
当Windows加载程序对另一个模块进行初始化时,将会不可避免地造成CLR无法启动。每个锁都是特定于进程的,由Windows来管理。请注意,任何模块如果已经获得过一个加载程序锁,当其尝试继续获取下一个加载程序锁时,都会导致死锁的发生。
4.1 基础篇
要搞定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:
1、OpenProcess – 获取进程的句柄;
2、GetModuleHandle – 获取指定模块的句柄;
3、LoadLibrary – 在调用进程的地址空间中加载库;
4、GetProcAddress – 从库中获取导出函数的VA(虚拟地址);
5、VirtualAllocEx – 在指定进程中分配空间;
6、WriteProcessMemory – 将字节写入指定地址的指定进程;
7、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:
// 使用PID 3344获取远程进程的句柄
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, 3344);
// 获取LoadLibrary的地址
FARPROC fnLoadLibrary = GetProcAddress(GetModuleHandle(L"Kernel32"), "LoadLibraryW");
// 将test.dll注入到远程进程中
Inject(hProcess, fnLoadLibrary, L"T:\test\test.dll");
一旦CreateRemoteThread被调用,紧接着就会调用WaitForSingleObject以等待线程退出。随后,会调用GetExitCodeThread来获取结果。巧合的是,当LoadLibrary传递到CreateRemoteThread时,成功的调用将导致GetExitCodeThread的lpExitCode在远程进程的上下文中返回已加载库的基址。这一点适用于32位应用程序,但不适用于64位应用程序。原因在于,即使在64位计算机上,GetExitCodeThread的IpExitCode也是DWORD值,因此该地址将会被截断。
这就是我们想要的第三块拼图。总结一下,目前已经解决了以下问题:
1、使用非托管代码加载CLR;
2、从非托管代码执行任意.NET程序集;
3、注入DLL。
4.2 进阶篇
现在,我们已经清楚如何在远程进程中加载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,可以看到Windows加载工具在注入Firefox时将Bootstrap.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模块的截图:
接下来,要在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的位置找到。使用Bootstrap.dll的地址减去ImplantDotNetAssembly的地址,就获得特定函数相对于模块基址的偏移量。经过计算,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。由此,Bootstrap.dll中的ImplantDotNetAssembly函数已经具备了在远程DLL中执行任意函数和初始化CLR的能力。在远程进程加载Bootstrap.dll后,可以计算出远程实例中ImplantDotNetAssembly的地址,然后通过CreateRemoteThread调用。这就是整个过程中的最后一块拼图。接下来,是时候把这些拼图组合到一起了。
五、组合利用
使用非托管DLL(Bootstrap.dll)加载CLR的主要原因是,如果CLR没有在远程进程中运行,那么唯一方法就是从非托管代码中使用。在这里,不能使用Python等脚本语言,因为它们自身就带有一系列依赖项。
另外,如果在每次应用发生变化时都重新进行一次编译,会非常繁琐。因此,我们Inject应用程序能灵活地接受命令行输入,具体可以接受如下选项:
- m 要执行的托管方法名称,例如:IntryPoint。
- 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_buildreleasex64Inject.exe" -m EntryPoint -i "T:FrameworkInjection_buildreleaseanycpuInjectExample.exe" -l InjectExample.Program -a "hello inject" -n "notepad.exe"
在这里,重点关注下针对x86、x64和AnyCPU所构建的DLL之间的差异。理论上,应该使用x86版本的Inject.exe和Bootstrap.dll来注入x86进程,使用x64版本的相应文件注入x64进程。调用者需要确保使用了正确的二进制文件。AnyCPU是.NET中提供的平台,AnyCPU会将正确的体系结构传递给CLR JIT程序集。这样一来,我们就可以将相同的InjectExample.exe程序集注入x86或x64进程。
六、实际测试
接下来,我们进行实际测试。首先,有一些环境上的要求。
编译代码的环境:
Visual Studio 2012 Express +
Visual Studio 2012 Express Update 1 +
运行代码的环境:
.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版本,上述版本支持Windows XP SP3到Windows 8 x64之间的所有Windows操作系统。顺便提一句,Microsoft从VS 2012 U1版本开始,为C++ 11.0 Runtime提供了对XP SP3的支持。
七、总结
一般情况下,总结部分都是回顾整个过程,并且思考其中的技术,提出需要改进的地方。但在本文的总结中,我们打算写一些不同的内容,向大家提供一些彩蛋。
正如文章开头所说的,.NET是一个强大的语言。举例来说,可以利用.NET中的Reflection API来获取有关程序集的类型信息。这意味着,.NET实际上可以用于扫描程序集,并且能够返回可用于注入的有效方法。在源代码压缩包中,有一个名为InjectGUI的.NET项目。该项目是一个针对我们的非托管Inject应用程序的托管包装器(Managed Wrapper)。InjectGUI可以显示正在运行的进程列表,决定是调用Inject的32位还是64位版本,并且扫描.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程序注入到非托管进程的讨论。引用莎士比亚的一句名言:“不要因为结束而哭泣,微笑吧,因为你曾经拥有过”。
发表评论
您还未登录,请先登录。
登录