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

.NET CLR 注入:运行时修改 IL 代码

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (239投票s)

2012 年 9 月 22 日

LGPL3

10分钟阅读

viewsIcon

633035

downloadIcon

18738

在运行时修改方法的 IL 代码,即使方法已被 JIT 编译。支持发布模式 / x64 & x86,以及 .NET 2.0 至 4.5 的各种版本。

 

引言   

您可以下载演示程序进行尝试,它演示了在运行时修改 .Net 方法代码。

  • 支持 .NET 2.0 至 4.5 的各种版本
  • 支持要修改的方法的各种类型,包括动态方法和泛型方法。
  • 支持发布模式的 .NET 进程。
  • 支持 x86 & x64。

在运行时修改 .NET 方法的 MSIL 代码非常酷,它可以帮助实现挂钩软件保护以及其他令人惊叹的功能。这就是我想要的,但在这条路上有一个巨大的挑战——在我们有机会修改之前,MSIL 代码可能已经被 JIT 编译器编译成了原生代码;而且 .NET CLR 的实现并未文档化,并且在每个版本中都会发生变化,我们需要一种可靠且稳定的方法,而无需依赖精确的内存布局。

总之,经过一周多的研究,我终于成功了!这是一个简单的示例方法:

protected string CompareOneAndTwo()
{
    int a = 1;
    int b = 2;
    if (a < b)
    {
        return "Number 1 is less than 2";
    }
    else
    {
        return "Number 1 is greater than 2 (O_o)";
    }
}

当然,它会返回“数字 1 小于 2”;让我们尝试让它返回不正确的结果“数字 1 大于 2 (O_o)”。

查看此方法的 MSIL 代码,我们可以通过将操作码从 Bge_S 更改为 Blt_S 来实现。然后,跳转将以不同的逻辑工作,从而返回错误的结果,这就是我所需要的。

如果在演示应用程序中尝试,它会显示一个错误的结果,如下所示。

这是替换 IL 的代码,我假设行之间有足够的注释。

挂钩 .NET 方法

根据 ECMA-335 中的解释,操作码 jmp 用于将控制转移到目标方法。与操作码 call 不同,它会将当前参数传递给目标方法——这要简单得多。

例如,示例应用程序中有一个方法声明。

string TargetMethod(string a, string b)
{
      return a + "," + b;
}

要挂钩上述方法,首先使用相同的参数和返回类型声明目标方法。

string ReplaceMethod(string a, string b)
{
    return string.Format( "This method is hooked, a={0};b={1};", a, b);
}

然后,准备要插入到 TargetMethod 中的 IL 代码。

MethodInfo replaceMethod = type.GetMethod("ReplaceMethod", BindingFlags.NonPublic | BindingFlags.Instance);

byte[] ilCodes = new byte[5];
ilCodes[0] = (byte)OpCodes.Jmp.Value;
ilCodes[1] = (byte)(replaceMethod.MetadataToken & 0xFF);
ilCodes[2] = (byte)(replaceMethod.MetadataToken >> 8 & 0xFF);
ilCodes[3] = (byte)(replaceMethod.MetadataToken >> 16 & 0xFF);
ilCodes[4] = (byte)(replaceMethod.MetadataToken >> 24 & 0xFF);

第一个字节是操作码 jmp,然后是目标方法的令牌。IL 代码已更新到 TargetMethod

MethodInfo targetMethod = type.GetMethod("TargetMethod", BindingFlags.NonPublic | BindingFlags.Instance);
InjectionHelper.UpdateILCodes(targetMethod, ilCodes);

最后 TargetMethod 被挂钩。

使用代码 

InjectionHelper.cs 文件复制到您的项目中,它包含几个方法。

public static class InjectionHelper
{
    // Load the unmanaged injection.dll, the initlaization happens in a background thread
    public static void Initialize()
 
    // Unload the unmanaged injection.dll
    public static void Uninitialize()
 
    // Update the IL Code of a Method.
    public static void UpdateILCodes(MethodInfo method, byte[] ilCodes, int nMaxStack = -1) 
 
    // The method returns until the initialization is completed
    public static Status WaitForIntializationCompletion()
}

InjectionHelper.Initialize 方法从当前程序集目录加载非托管的 injection.dll,因此所有相关文件都需要在那里,或者您可以修改代码来更改位置。

这是文件列表。

文件名 描述
Injection32.dll 本文使用的非托管 DLL(x86 版本)
Injection64.dll 本文使用的非托管 DLL(x64 版本)

 

背景  

替换 IL 代码

首先,看一下 CLR 和 JIT 的工作原理。

JIT 实现 DLL(.Net4.0+ 的 clrjit.dll / .NET 2.0+ 的 mscorjit.dll)导出一个 _stdcall 方法 getJit,该方法返回 ICorJitCompiler 接口。

CLR 实现 DLL(.NET 4.0+ 的 clr.dll / .NET 2.0+ 的 mscorwks.dll)调用 getJit 方法以获取 ICorJitCompiler 接口,然后调用其 compileMethod 方法将 MSIL 代码编译为原生代码。

CorJitResult compileMethod(ICorJitInfo * pJitInfo, CORINFO_METHOD_INFO * pMethodInfo, 
   UINT nFlags, LPBYTE * pEntryAddress, ULONG * pSizeOfCode);

这部分很简单,只需找到 compileMethod 方法的位置,然后使用 EasyHook 替换入口点。

// ICorJitCompiler interface from JIT dll
class ICorJitCompiler 
{
public:
	typedef CorJitResult (__stdcall ICorJitCompiler::*PFN_compileMethod)(ICorJitInfo * pJitInfo, CORINFO_METHOD_INFO * pMethodInfo, UINT nFlags, LPBYTE * pEntryAddress, ULONG * pSizeOfCode);

	CorJitResult compileMethod(ICorJitInfo * pJitInfo, CORINFO_METHOD_INFO * pMethodInfo, UINT nFlags, LPBYTE * pEntryAddress, ULONG * pSizeOfCode)
	{
		return (this->*s_pfnComplieMethod)( pJitInfo, pMethodInfo, nFlags, pEntryAddress, pSizeOfCode);
	}
private:
	static PFN_compileMethod s_pfnComplieMethod;
};

// save the real address
LPVOID pAddr = tPdbHelper.GetJitCompileMethodAddress();
LPVOID* pDest = (LPVOID*)&ICorJitCompiler::s_pfnComplieMethod;
*pDest = pAddr;

// and this is my compileMethod
CorJitResult __stdcall CInjection::compileMethod(ICorJitInfo * pJitInfo , CORINFO_METHOD_INFO * pCorMethodInfo , UINT nFlags , LPBYTE * pEntryAddress , ULONG * pSizeOfCode )
{
	ICorJitCompiler * pCorJitCompiler = (ICorJitCompiler *)this;
	
	// TO DO: Replace IL code before invoking the real compileMethod
	CorJitResult result = pCorJitCompiler->compileMethod( pJitInfo, pCorMethodInfo, nFlags, pEntryAddress, pSizeOfCode);
	return result;
}

// hook and replace JIT's compileMethod with my own
NTSTATUS ntStatus = LhInstallHook( (PVOID&)ICorJitCompiler::s_pfnComplieMethod 
	, &(PVOID&)CInjection::compileMethod
	, NULL
	, &s_hHookCompileMethod
	);
 

修改 JIT 编译方法的 IL 代码

现在我们到了这里,上面的 compileMethod 方法不会被 CLR 调用来编译 JIT 编译的方法。为了解决这个问题,我的想法是恢复 CLR 中的数据结构到 JIT 编译之前的状态。在这种情况下,compileMethod 将再次被调用,我们可以替换 IL。

因此,我们必须稍微深入研究 CLR 的实现,SSCLI(共享源公共语言基础结构)是微软的一个很好的参考,尽管它已经过时,我们不能在代码中使用它。

上图有点过时,但基本结构是相同的。对于 .NET 中的每个“类”,内存中至少有一个 MethodTable 结构。每个 MethodTable 都与一个 EEClass 相关联,该结构存储用于反射和其他用途的运行时类型信息。

对于每个“方法”,内存中至少有一个对应的 MethodDesc 数据结构,其中包含该方法的标志/槽地址/入口地址/等信息。

在方法被 JIT 编译之前,槽指向一个 JMI 桩(prestub),它会触发 JIT 编译;当 IL 代码被编译时,槽会被重写以指向 JMI 桩,JMI 桩会直接跳转到编译后的原生代码。

要恢复数据结构,首先清除标志,然后将入口地址修改回临时入口地址,等等。我通过直接修改内存成功地在调试器中做到了这一点。但这很混乱,它依赖于数据结构的布局,并且代码对于不同版本的 .NET 来说是不可靠的。

我一直在寻找一种可靠的方法,幸运的是,我在 SSCLI 源代码(vm/method.cpp)中找到了 MethodDesc::Reset 方法。

void MethodDesc::Reset()
{
    CONTRACTL
    {
        THROWS;
        GC_NOTRIGGER;
    }
    CONTRACTL_END
 
    // This method is not thread-safe since we are updating
    // different pieces of data non-atomically.
    // Use this only if you can guarantee thread-safety somehow.

    _ASSERTE(IsEnCMethod() || // The process is frozen by the debugger
             IsDynamicMethod() || // These are used in a very restricted way
             GetLoaderModule()->IsReflection()); // Rental methods                                                                 

    // Reset any flags relevant to the old code
    ClearFlagsOnUpdate();
 
    if (HasPrecode())
    {
        GetPrecode()->Reset();
    }
    else
    {
        // We should go here only for the rental methods
        _ASSERTE(GetLoaderModule()->IsReflection());
 
        InterlockedUpdateFlags2(enum_flag2_HasStableEntryPoint | enum_flag2_HasPrecode, FALSE);
 
        *GetAddrOfSlotUnchecked() = GetTemporaryEntryPoint();
    }
 
    _ASSERTE(!HasNativeCode());
}

正如您上面所见,它为我做了同样的事情。因此,我只需要调用此方法即可将 MethodDesc 状态重置为 JIT 编译之前。

当然,我不能使用 SSCLI 中的 MethodDesc,而且 MethodDesc 是 MS 内部使用的,其确切实现和布局除了微软之外没有人知道。

千山万水疑无路,柳暗花明又一村。

幸运的是,此内部方法的地址存在于微软符号服务器的 PDB 符号中,它解决了我的问题。通过解析 PDB 文件可以知道 Reset() 方法在 CLR DLL 中的地址!

现在只剩下最后一个强制参数——MethodDescthis 指针。获取此指针并不难。实际上 MethodBase.MethodHandle.Value == CORINFO_METHOD_HANDLE == MethodDesc 地址 == MethodDescthis 指针。

因此,我在非托管代码中定义了下面的 MethodDesc 类。

class MethodDesc
{
	typedef void (MethodDesc::*PFN_Reset)(void);
	typedef BOOL (MethodDesc::*PFN_IsGenericMethodDefinition)(void);
	typedef ULONG (MethodDesc::*PFN_GetNumGenericMethodArgs)(void);
	typedef MethodDesc * (MethodDesc::*PFN_StripMethodInstantiation)(void);
	typedef BOOL (MethodDesc::*PFN_HasClassOrMethodInstantiation)(void);
	typedef BOOL (MethodDesc::*PFN_ContainsGenericVariables)(void);	
	typedef MethodDesc * (MethodDesc::*PFN_GetWrappedMethodDesc)(void);
	typedef AppDomain * (MethodDesc::*PFN_GetDomain)(void);
	typedef Module * (MethodDesc::*PFN_GetLoaderModule)(void);

public:
	void Reset(void) { (this->*s_pfnReset)(); }
	BOOL IsGenericMethodDefinition(void) { return (this->*s_pfnIsGenericMethodDefinition)(); }
	ULONG GetNumGenericMethodArgs(void) { return (this->*s_pfnGetNumGenericMethodArgs)(); }
	MethodDesc * StripMethodInstantiation(void) { return (this->*s_pfnStripMethodInstantiation)(); }
	BOOL HasClassOrMethodInstantiation(void)  { return (this->*s_pfnHasClassOrMethodInstantiation)(); }
	BOOL ContainsGenericVariables(void) { return (this->*s_pfnContainsGenericVariables)(); }
	MethodDesc * GetWrappedMethodDesc(void) { return (this->*s_pfnGetWrappedMethodDesc)(); }
	AppDomain * GetDomain(void) { return (this->*s_pfnGetDomain)(); }
	Module * GetLoaderModule(void) { return (this->*s_pfnGetLoaderModule)(); }
	
private:
	static PFN_Reset s_pfnReset;
	static PFN_IsGenericMethodDefinition s_pfnIsGenericMethodDefinition;
	static PFN_GetNumGenericMethodArgs s_pfnGetNumGenericMethodArgs;
	static PFN_StripMethodInstantiation s_pfnStripMethodInstantiation;
	static PFN_HasClassOrMethodInstantiation s_pfnHasClassOrMethodInstantiation;
	static PFN_ContainsGenericVariables s_pfnContainsGenericVariables;
	static PFN_GetWrappedMethodDesc s_pfnGetWrappedMethodDesc;
	static PFN_GetDomain s_pfnGetDomain;
	static PFN_GetLoaderModule s_pfnGetLoaderModule;
}; 

上面的静态变量存储了来自 CLR DLL 的 MethodDesc 实现的内部方法的地址。它们在我的非托管 DLL 加载时被初始化。公共成员只是使用 this 指针调用内部方法。

现在调用微软的内部方法变得非常容易。例如:

MethodDesc * pMethodDesc = (MethodDesc*)pMethodHandle;
pMethodDesc->Reset();

从 PDB 符号文件中查找内部方法的地址

内部方法的虚拟地址可以从 PDB 符号文件中得知。通过虚拟地址,我们可以通过加上 DLL 的基地址来知道方法的实际地址。

Method Address = Method Virtual Address + base address of dll.  

在以前的版本中,PDB 文件是本地下载并使用 Microsoft symcheck.exe 解析的。

在当前版本中,我创建了一个 Web 服务来在服务器上解析地址,并将虚拟地址返回给客户端。这将减少初始化时间。

此外,在收集了大部分虚拟地址后,不同二进制文件的虚拟地址被存储在 DLL 资源中。在初始化期间,injection.dll 将首先在本地查找虚拟地址,并且只有在当前二进制文件的虚拟地址未找到时才请求 Web 服务。在这种情况下,当找不到虚拟地址时,Web 服务将只是一个备用方案。

将 MethodDesc 重置为 JIT 编译之前的状态

现在一切都准备好了。非托管 DLL 导出一个供托管代码调用的方法,该方法接受来自托管代码的 IL 代码和 MethodBase.MethodHandle.Value

// structure to store the IL code for replacement
typedef struct _ILCodeBuffer
{
	LPBYTE						pBuffer;
	DWORD						dwSize;
} ILCodeBuffer, *LPILCodeBuffer;


// method to be called by managed code
BOOL CInjection::StartUpdateILCodes( MethodTable * pMethodTable
	, CORINFO_METHOD_HANDLE pMethodHandle
	, mdMethodDef md
	, LPBYTE pBuffer
	, DWORD dwSize
	)
{
	MethodDesc * pMethodDesc = (MethodDesc*)pMethodHandle;

	// reset this MethodDesc
	pMethodDesc->Reset();

	ILCodeBuffer tILCodeBuffer;
	tILCodeBuffer.pBuffer = pBuffer;
	tILCodeBuffer.dwSize = dwSize;
	tILCodeBuffer.bIsGeneric = FALSE;

	// save the IL code for the method
	s_mpILBuffers.insert( std::pair< CORINFO_METHOD_HANDLE, ILCodeBuffer>( pMethodHandle, tILCodeBuffer) );

	return TRUE;
}

上面的代码只是调用 Reset() 方法,并将 IL 代码存储在一个映射中,当方法被编译时,compileMethod 将使用该映射。

compileMethod 中,只需替换 ILCode,代码如下:

CorJitResult __stdcall CInjection::compileMethod(ICorJitInfo * pJitInfo
	, CORINFO_METHOD_INFO * pCorMethodInfo
	, UINT nFlags
	, LPBYTE * pEntryAddress
	, ULONG * pSizeOfCode
	)
{
	ICorJitCompiler * pCorJitCompiler = (ICorJitCompiler *)this;
	LPBYTE pOriginalILCode = pCorMethodInfo->ILCode;
	unsigned int nOriginalSize = pCorMethodInfo->ILCodeSize;

	
	ILCodeBuffer tILCodeBuffer = {0};

	MethodDesc * pMethodDesc = (MethodDesc*)pCorMethodInfo->ftn;

	// find the method to be replaced
	std::map< CORINFO_METHOD_HANDLE, ILCodeBuffer>::iterator iter = s_mpILBuffers.find((CORINFO_METHOD_HANDLE)pMethodDesc);

	if( iter != s_mpILBuffers.end() )
	{
		tILCodeBuffer = iter->second;
		pCorMethodInfo->ILCode = tILCodeBuffer.pBuffer;
		pCorMethodInfo->ILCodeSize = tILCodeBuffer.dwSize;
	}

	CorJitResult result = pCorJitCompiler->compileMethod( pJitInfo, pCorMethodInfo, nFlags, pEntryAddress, pSizeOfCode);

	return result;
}  

泛型方法

 

泛型方法在内存中映射到一个 MethodDesc。但是,使用不同的类型参数调用泛型方法可能会导致 CLR 创建该定义方法的不同实例。(实例可能会共享,您可以在下方看到泛型方法实例的类型)。

  1. 共享的泛型方法实例
  2. 非共享的泛型方法实例
  3. 共享泛型类中的实例方法
  4. 非共享泛型类中的实例方法
  5. 共享泛型类中的静态方法
  6. 非共享泛型类中的静态方法

 

以下行是演示程序中定义的简单泛型方法:

string GenericMethodToBeReplaced<T, K>(T t, K k)  

第一次调用 GenericMethodToBeReplaced<string, int>("11", 2) 时,CLR 会创建一个 InstantiatedMethodDesc 实例(MethodDesc 的子类,其标志被标记为 mcInstantiated),该实例存储在对应方法的模块的 InstMethodHashTable 数据结构中。

调用 GenericMethodToBeReplaced<long, int>(1, 2) 会导致创建另一个 InstantiatedMethodDesc 实例。

因此,我们需要查找并重置泛型方法的所有 InstantiatedMethodDesc,以便我们可以替换 IL 代码而不遗漏。

根据 SSCLI 源代码(vm/proftoeeinterfaceimpl.cpp),有一个名为 LoadedMethodDescIterator 的类可以使用。它接受 3 个参数,并通过 MethodToken 在给定的 AppDomainModule 中搜索实例化的方法。

 

LoadedMethodDescIterator MDIter(ADIter.GetDomain(), pModule, methodId);
while(MDIter.Next())
{
    MethodDesc * pMD = MDIter.Current();
    if (pMD)
    {
        _ASSERTE(pMD->IsIL());
        pMD->SetRVA(rva);
    }
} 

查看 CLR 的 PDB 符号文件中的方法,可以检索到 LoadedMethodDescIterator 类的构造函数 / Next / Current 方法的地址。因此,我们可以利用 CLR 中的这个类。

 

即使我们不知道 LoadedMethodDescIterator 实例的确切大小也没关系,只需定义一个足够大的内存块来容纳实际的实例数据即可。

class LoadedMethodDescIterator
{
private:
	BYTE dummy[10240]; 
}; 

 

实际上,从 .Net2.0 到 .Net4.5,Next() 方法和构造函数都发生了一些变化。

// .Net 2.0 & 4.0
LoadedMethodDescIterator(AppDomain * pAppDomain, Module *pModule,	mdMethodDef md)

// .Net 4.5
LoadedMethodDescIterator(AppDomain * pAppDomain, Module *pModule,	mdMethodDef md,enum AssemblyIterationMode mode)  
// .Net 2.0
BOOL LoadedMethodDescIterator::Next(void)
// .Net 4.0 / 4.5
BOOL LoadedMethodDescIterator::Next(CollectibleAssemblyHolder<DomainAssembly *> *)  

因此,我们需要检测当前的 .Net Framework 版本并调用正确的方法。主要问题来自 .Net 4.5,它是 .Net4.0 的就地升级。因此,在演示代码中,这是通过检测 CLR 二进制版本号来完成的。

// detect the version of CLR
BOOL DetermineDotNetVersion(void)
{
	WCHAR wszPath[MAX_PATH] = {0};
	::GetModuleFileNameW( g_hClrModule, wszPath, MAX_PATH);
	CStringW strPath(wszPath);
	int nIndex = strPath.ReverseFind('\\');
	if( nIndex <= 0 )
		return FALSE;
	nIndex++;
	CStringW strFilename = strPath.Mid( nIndex, strPath.GetLength() - nIndex);
	if( strFilename.CompareNoCase(L"mscorwks.dll") == 0 )
	{
		g_tDotNetVersion = DotNetVersion_20;
		return TRUE;
	}

	if( strFilename.CompareNoCase(L"clr.dll") == 0 )
	{
		VS_FIXEDFILEINFO tVerInfo = {0};
		if ( CUtility::GetFileVersion( wszPath, &tVerInfo) &&
			 tVerInfo.dwSignature == 0xfeef04bd)
		{
			int nMajor = HIWORD(tVerInfo.dwFileVersionMS);
			int nMinor = LOWORD(tVerInfo.dwFileVersionMS);
			int nBuildMajor = HIWORD(tVerInfo.dwFileVersionLS);
			int nBuildMinor = LOWORD(tVerInfo.dwFileVersionLS);

			if( nMajor == 4 && nMinor == 0 && nBuildMajor == 30319 )
			{
				if( nBuildMinor < 10000 )
					g_tDotNetVersion = DotNetVersion_40;
				else
					g_tDotNetVersion = DotNetVersion_45;
				return TRUE;
			}
		}
		return FALSE;
	}

	return FALSE;
} 

现在,我们可以定义我们的 LoadedMethodDescIterator 类,它将附加到 CLR 实现。

enum AssemblyIterationMode { AssemblyIterationMode_Default = 0 };

class LoadedMethodDescIterator
{
	typedef void (LoadedMethodDescIterator::*PFN_LoadedMethodDescIteratorConstructor)(AppDomain * pAppDomain, Module *pModule,	mdMethodDef md);
	typedef void (LoadedMethodDescIterator::*PFN_LoadedMethodDescIteratorConstructor_v45)(AppDomain * pAppDomain, Module *pModule,	mdMethodDef md, AssemblyIterationMode mode);
	typedef void (LoadedMethodDescIterator::*PFN_Start)(AppDomain * pAppDomain, Module *pModule, mdMethodDef md);
	typedef BOOL (LoadedMethodDescIterator::*PFN_Next_v4)(LPVOID pParam);
	typedef BOOL (LoadedMethodDescIterator::*PFN_Next_v2)(void);
	typedef MethodDesc* (LoadedMethodDescIterator::*PFN_Current)(void);
public:
	LoadedMethodDescIterator(AppDomain * pAppDomain, Module *pModule, mdMethodDef md)
	{
		memset( dummy, 0, sizeof(dummy));
		memset( dummy2, 0, sizeof(dummy2));
		if( s_pfnConstructor )
			(this->*s_pfnConstructor)( pAppDomain, pModule, md);
		if( s_pfnConstructor_v45 )
			(this->*s_pfnConstructor_v45)( pAppDomain, pModule, md, AssemblyIterationMode_Default);
	}

	void Start(AppDomain * pAppDomain, Module *pModule, mdMethodDef md) 
	{ 
		(this->*s_pfnStart)( pAppDomain, pModule, md); 
	}
	BOOL Next() 
	{
		if( s_pfnNext_v4 )
			return (this->*s_pfnNext_v4)(dummy2); 

		if( s_pfnNext_v2 )
			return (this->*s_pfnNext_v2)(); 

		return FALSE;
	}
	MethodDesc* Current() { return (this->*s_pfnCurrent)(); }
private:
	// we don't know the exact size of LoadedMethodDescIterator, so add enough memory here
	BYTE dummy[10240]; 

	// class CollectibleAssemblyHolder<class DomainAssembly *> parameter for Next() in .Net4.0 and above	
	BYTE dummy2[10240]; 

	// constructor for .Net2.0 & .Net 4.0
	static PFN_LoadedMethodDescIteratorConstructor s_pfnConstructor;

	// constructor for .Net4.5
	static PFN_LoadedMethodDescIteratorConstructor_v45 s_pfnConstructor_v45;

	static PFN_Start s_pfnStart;
	static PFN_Next_v4 s_pfnNext_v4;
	static PFN_Next_v2 s_pfnNext_v2; 
	static PFN_Current s_pfnCurrent;

public:
	static void MatchAddress(PSYMBOL_INFOW pSymbolInfo)
	{
		LPVOID* pDest = NULL;
		if( wcscmp( L"LoadedMethodDescIterator::LoadedMethodDescIterator", pSymbolInfo->Name) == 0 )
		{
			switch(g_tDotNetVersion)
			{
			case DotNetVersion_20:
			case DotNetVersion_40:
				pDest = (LPVOID*)&(LoadedMethodDescIterator::s_pfnConstructor);
				break;

			
			case DotNetVersion_45:
				pDest = (LPVOID*)&(LoadedMethodDescIterator::s_pfnConstructor_v45);
				break;

			default:
				ATLASSERT(FALSE);
				return;
			}
		}
		else if( wcscmp( L"LoadedMethodDescIterator::Next", pSymbolInfo->Name) == 0 )
		{
			switch(g_tDotNetVersion)
			{
			case DotNetVersion_20:
				pDest = (LPVOID*)&(LoadedMethodDescIterator::s_pfnNext_v2);
				break;

			case DotNetVersion_40:
			case DotNetVersion_45:
				pDest = (LPVOID*)&(LoadedMethodDescIterator::s_pfnNext_v4);
				break;

			default:
				ATLASSERT(FALSE);
				return;
			}
		}
		else if( wcscmp( L"LoadedMethodDescIterator::Start", pSymbolInfo->Name) == 0 )
			pDest = (LPVOID*)&(LoadedMethodDescIterator::s_pfnStart);
		else if( wcscmp( L"LoadedMethodDescIterator::Current", pSymbolInfo->Name) == 0 )
			pDest = (LPVOID*)&(LoadedMethodDescIterator::s_pfnCurrent);
		
		if( pDest )
			*pDest = (LPVOID)pSymbolInfo->Address;
	}

};

最后,使用 LoadedMethodDescIterator 来重置泛型方法的所有实例化 MethodDesc,如下所示。

// find out all the instantiations of this generic method
Module * pModule = pMethodDesc->GetLoaderModule();
AppDomain * pAppDomain = pMethodDesc->GetDomain();
if( pModule )
{
	LoadedMethodDescIterator * pLoadedMethodDescIter = new LoadedMethodDescIterator( pAppDomain, pModule, md);
	while(pLoadedMethodDescIter->Next())
	{
		MethodDesc * pMD = pLoadedMethodDescIter->Current();
		if( pMD )
			pMD->Reset();
	}
	delete pLoadedMethodDescIter;
}
 

值得关注的点

编译优化

我发现,如果方法过于简单,并且 IL 代码只有几个字节,该方法可能会被编译为内联模式。在这种情况下,重置 MethodDesc 没有任何帮助,因为执行甚至没有到达那里。有关更多详细信息,请参阅 CEEInfo::canInline(SSCLI 中的 vm/jitinterface.cpp)。

动态方法

要更新动态方法的 IL 代码,我们需要非常小心。为其他类型的方法填充不正确的 IL 代码只会导致 InvalidProgramException;但动态方法中的不正确 IL 代码可能会导致 CLR 和整个进程崩溃!而且动态方法的 IL 代码与其他方法不同。最好从另一个动态方法生成 IL 代码,然后复制并更新。

注入正在运行的 .Net 进程

要修改运行中的 .Net 进程而没有源代码,您可以通过使用 EasyHook 的 RhInjectLibrary 将自己的 .Net 程序集注入到目标进程中。然后,在目标进程中加载 .Net 程序集后,调用 InjectionHelper.UpdateILCodes 来更新目标方法。有关 EasyHook 的更多信息可以在其 文档中找到。

历史   

 

 

  • 2012 年 9 月 22 日 - 第一个版本
  • 2012 年 10 月 5 日 - 为泛型方法添加了 LoadedMethodDescIterator
  • 2012 年 10 月 8 日 - 添加了 x64 支持
  • 2012 年 10 月 10 日 - 添加了对 .Net4.5 的支持
  • 2012 年 10 月 11 日 - 添加了缓存功能以加速初始化过程
  • 2012 年 10 月 13 日 - 将地址偏移量缓存嵌入到资源中。
  • 2012 年 10 月 14 日 - 修正了代码以确保可以在 VS2012 中编译。
  • 2014 年 8 月 7 日 - 发布了第二版:使用 Web 服务作为备用方案来查找 PDB 中的虚拟地址。从 PE 文件的哈希值更改为符号 ID 来标识二进制文件。
© . All rights reserved.