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






4.98/5 (239投票s)
在运行时修改方法的 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 中的地址!
现在只剩下最后一个强制参数——MethodDesc
的 this
指针。获取此指针并不难。实际上 MethodBase.MethodHandle.Value
== CORINFO_METHOD_HANDLE
== MethodDesc
地址 == MethodDesc
的 this
指针。
因此,我在非托管代码中定义了下面的 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 创建该定义方法的不同实例。(实例可能会共享,您可以在下方看到泛型方法实例的类型)。
- 共享的泛型方法实例
- 非共享的泛型方法实例
- 共享泛型类中的实例方法
- 非共享泛型类中的实例方法
- 共享泛型类中的静态方法
- 非共享泛型类中的静态方法
以下行是演示程序中定义的简单泛型方法:
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 在给定的 AppDomain 和 Module 中搜索实例化的方法。
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 来标识二进制文件。