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

阅读量333088

|评论3

|

发布时间 : 2018-10-29 14:30:51

x
译文声明

本文是翻译文章,文章来源:codeproject.com

原文地址:https://www.codeproject.com/Articles/607352/Injecting-Net-Assemblies-Into-Unmanaged-Processes

译文仅供参考,具体内容表达以及含义原文为准。

源代码

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程序注入到非托管进程的讨论。引用莎士比亚的一句名言:“不要因为结束而哭泣,微笑吧,因为你曾经拥有过”。

本文翻译自codeproject.com 原文链接。如若转载请注明出处。
分享到:微信
+11赞
收藏
P!chu
分享到:微信

发表评论

Copyright © 北京奇虎科技有限公司 三六零数字安全科技集团有限公司 安全KER All Rights Reserved 京ICP备08010314号-66