/DotNetDetour

DotNetDetour is a software package for monitoring and instrumenting API calls

Primary LanguageC#

Build Status 996.icu LICENSE HitCount

📖DotNetDetour

DotNetDetour是一个用于.net方法hook的类库

特点

  • 支持32bit和64bit的.net程序
  • 支持静态方法,实例方法、属性方法、泛型类型的方法、泛型方法的hook
  • 支持.net基础类库方法的hook
  • 无任何性能影响,无需知道和改动被hook的方法源码

实现原理

https://bbs.csdn.net/topics/391958344

基础示例

  1. git clone本项目最新源码使用;或者NuGet安装(可能未及时更新):Install-Package DotNetDetour, 或者:Install-Package kissstudio.DotNetDetour

  2. 参考以下例子实现IMethodHook接口,使用特性标记要Hook的方法

namespace Test.Solid {
    //假设有一个已存在的类(并且无法修改源码,如.Net框架的方法)
    public class SolidClass{
        public string Run(string msg){
            return msg+"(run)";
        }
    }
}

namespace Test{
    //我们自行实现一个类来修改Run方法的行为,此类用IMethodHook接口修饰
    public class MyClass:IMethodHook{
        //我们实现一个新Run方法,并标记为HookMethod,覆盖SolidClass中的Run方法
        [HookMethod("Test.Solid.SolidClass")]
        public string Run(string msg){
            return "Hook " + Run_Original(msg);
        }
        
        //实现一个占位方法,此方法代表被Hook覆盖的原始方法
        [OriginalMethod]
        public string Run_Original(string msg){
            return null; //这里写什么无所谓,能编译过即可
        }
    }
}
  1. 在程序中执行安装操作(只需运行一次即可),最佳运行时机:必须在被Hook方法被调用前执行,最好程序启动时运行一次即可。
MethodHook.Install();
  1. 当执行到被Hook的方法时,该调用将被转到我们的Hook方法执行:
var msg=new SolidClass().Run("Hello World!");

//Hook Hello World!(run)

📖Hook场景

普通方法Hook

静态和非静态的普通方法Hook操作都是一模一样的,两步到位:新建一个类实现IMethodHook接口,编写普通Hook方法,用HookMethod特性标记此方法,有无static修饰、返回值类型(仅针对引用性质的类型,非int等值类型)不同都不影响,但参数签名要和被Hook的原始方法一致,值类型和引用类型尽量不要混用。

第一步:新建一个类实现IMethodHook接口

我们编写的Hook方法所在的类需要实现IMethodHook接口,此接口是一个空接口,用于快速的查找Hook方法。

或者使用IMethodHookWithSet接口(算Plus版吧),此接口带一个HookMethod(MethodBase method)方法,这个类每成功进行一个Hook的初始化,就会传入被Hook的原始方法(可判断方法名称来确定是初始化的哪个方法),这个方法可用于获取方法所在的类(如:私有类型),可用于简化后续的反射操作;注意:此方法应当当做静态方法来进行编码。

第二步:编写Hook方法,用HookMethod特性标记

HookMethod(type,targetMethodName,originalMethodName) ,type参数支持:Type类型对象、类型完全限定名。如果能直接获取到类型对象,就使用Type类型对象;否则必须使用此类型的完全限定名(如:私有类型),如:System.Int32System.Collections.Generic.List`1[[System.String]]

[HookMethod("Namespace.xxx.MyClass", "TargetMethodName", "OriginalMethodName")]
public string MyMethod(string param){...}

[HookMethod(typeof(MyClass))]
public string MyMethod(string param){...}

如果我们的方法名称和被Hook的目标方法名称一致,无需提供targetMethodName参数。

如果我们提供目标原始方法的占位方法OriginalMethod,并且名称为目标原始方法名称 + _Original,或者当前类内只有一个Hook方法,无需提供originalMethodName参数。

注意:方法参数

参数签名要和被Hook的原始方法一致,如果不一致将导致无法找到原始方法(原因:存在重载方法无法确认是哪个的问题)。

如果存在我们无法使用的参数类型的时候(如:私有类型),我们可以用object等其他引用类型代替此类型(注意不要用值类型,否则可能出现内存访问错误),并把此参数用RememberType进行标记:

//目标方法:
public string SolidMethod(MyClass data, int code){...}

//我们的Hook方法:
public string MyMethod([RememberType("Namespace.xxx.MyClass")]object data, int code){...}

可选:提供OriginalMethod特性标记的原始方法

如果我们还想调用被Hook的原始方法,我们可以提供一个占位方法,此方法用OriginalMethod进行标记即可。此方法只起到代表原始方法的作用,不需要可以不提供,要求:参数签名必须和我们写的Hook方法一致(原因:存在重载方法无法确认是哪个的问题)。

此方法默认名称格式为目标原始方法名称 + _Original,不使用这个名称也可以,但如果使用其他名称并且当前类中有多个Hook方法,必须在Hook方法HookMethod特性中进行设置originalMethodName进行关联。

[OriginalMethod]
public string SolidMethod_Original(object data, int code){

可选:给我们的Hook方法传递参数

我们编写Hook方法是在被Hook的原始方法被调用时才会执行的,我们可能无法修改调用过程的参数(如果是能修改方法的话就跳过此节),虽然我们编写的Hook方法可以是非静态方法,但我们应当把它当静态方法来看待,虽然可以用属性字段(非静态的也当做静态)之类的给我们的Hook方法传递数据,但如果遇到并发,是不可靠的。

我们可以通过当前线程相关的上下文来传递数据,比如:HttpContextCallContextAsyncLocalThreadLoacl。推荐使用CallContext.LogicalSetData来传递数据,如果可以用HttpContext就更好了(底层也是用CallContext.HostContext来实现的)。ThreadLoacl只能当前线程用,遇到异步、多线程就不行了。AsyncLocal当然是最好的,但稍微低些版本的.Net Framework还没有这个。

[HookMethod("Namespace.xxx.MyClass", "TargetMethodName", "OriginalMethodName")]
public string MyMethod(string param){
    if (CallContext.LogicalGetData("key") == (object)"value") {
        //执行特定Hook代码
        return;
    }
    //执行其他Hook代码
    ...
}

//调用
CallContext.LogicalSetData("key", "value");
new MyClass().MyMethod("");
CallContext.LogicalSetData("key", null);

注:虽然大部分多线程、异步环境下调用上下文是会被正确复制传递的,但如果哪里使用了ConfigeAwait(false)或者其他影响上下文的操作(定时回调、部分异步IO回调好像也没有传递),当我们的Hook方法执行时,可能上下文数据并没有传递进来。

异步方法Hook

异步方法的Hook方法需要用async来修饰、返回Task类型,其他和普通方法Hook没有区别。

小提醒:不要在存在SynchronizationContext(如:HttpContext、UI线程)的线程环境中直接在同步方法中调用异步方法,真发生异步行为时100%死锁,可以强制关闭SynchronizationContext来规避此种问题,但会引发一系列问题。如果使用过程中发生死锁,跟我们进行的Hook操作没有关系

[HookMethod(typeof(MyClass))]
public async Task<int> MyMethodAsync() {...}

//异步环境调用
val=await new MyClass().MyMethodAsync();

//同步环境调用
var bak = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(null);
try {
    val=new MyClass().MyMethodAsync().Result;
} finally {
    SynchronizationContext.SetSynchronizationContext(bak);
}

属性Hook

属性其实是get_xxx()名称的普通方法,比如MyProperty属性Hook get_MyProperty()这个普通方法即可。

[HookMethod("Namespace.xxx.MyClass")]
public string get_MyProperty(){...}

[OriginalMethod]
public string get_MyProperty_Original(){...}

或者在get块上方进行标记,规则和普通方法一致:

public string MyProperty{
    [HookMethod("Namespace.xxx.MyClass")]
    get{ ... }
}

public string MyProperty_Original{
    [OriginalMethod]
    get{ ... }
}

注:Hook属性时有可能能成功设置此Hook,但不一定会执行我们的代码,可能是编译过程中优化了整个调用过程,跳过了部分属性方法,直接返回了最深层次的调用值,如下面这种类似的属性获取方式:

int A{get{return B;}}
int B{get{return C;}}
int C{get{return 123;}}

我们Hook A属性,能成功设置Hook方法,但我们调用A属性时,并不会执行我们的Hook方法。换B也不行,只有Hook C才行。也许是编译的时候把A、B的调用直接优化成了对C的调用,我们只需要对最深层次的属性调用进行Hook就能避免此问题。(这个只是演示可能会出现的问题,我们自己特意写代码去测试并不能复现)。

字段Hook

不支持,应该直接用反射来操作。

构造方法Hook

我们编写个返回值为void、方法名称为类名称的普通方法即可实现。如果方法名称无法使用类名称时,需在HookMethod中设置targetMethodName.ctor。其他规则和普通方法一致。

[HookMethod("Namespace.xxx.MyClass")]
public void MyClass(string param) {
    ...
    MyClass_Original(param);//可选调用自身实例化方法
    ...
}

[OriginalMethod]
public void MyClass_Original(string param) {}

泛型类的方法Hook

形如class MyClass<T>{ T MyMethod(T param, object param2){...} }这种泛型,对里面的方法进行Hook。泛型类中方法的Hook和普通方法Hook没有多大区别,只是在提供HookMethod特性的type参数时需要对类型具体化,比如调用的地方使用的是int类型,那么我们就Hook int类型的此类:typeof(MyClass<int>)Namespace.xxx.MyClass&#96;1[[System.Int32]],其他和普通方法规则相同。

由于存在引用类型值类型两种类型,并且表现不一致,我们在具体化时要分开对待。

值类型泛型参数

每种使用到的值类型泛型参数的具体类型都需要单独实现Hook,intbool等为值类型都要单独实现,如int类型写法:

[HookMethod("Namespace.xxx.MyClass`1[[System.Int32]]")]
public int MyMethod(int param, object param2) {

引用类型泛型参数

每种使用到引用类型参数的具体类型都共用一个Hook,注意是:同一个泛型类中的同一个方法只能用一个相同方法进行Hookstring普通object等都是引用类型都共用一个Hook,如string类型写法:

[HookMethod("Namespace.xxx.MyClass`1[[System.Object]]")]
public object MyMethod(object param, object param2) {
    if(param is string){
        ... //string 类型实现代码
    } else if(param is xxxx){
        ... //其他引用类型实现代码
    }

泛型方法Hook

形如T MyMethod<T>(T param, object param2)这种泛型方法,我们对这种方法进行Hook时需要把类型具体化,并用RememberType(isGeneric: true)特性标记涉及到的泛型参数,比如调用的地方是int类型,那么我们就Hook int类型的此方法int MyMethod([RememberType(isGeneric: true)]int param, object param2),其实最终还是一个普通方法,按普通方法规则来写代码。

由于存在引用类型值类型两种类型,并且表现不一致,我们在具体化时要分开对待。

值类型泛型参数

每种使用到值类型泛型参数的都单独实现Hook,intbool等为值类型都要单独实现,如int类型写法:

[HookMethod("Namespace.xxx.MyClass")]
public int MyMethod([RememberType(isGeneric: true)]int param) {

引用类型泛型参数

不支持,引用类型泛型参数的方法Hook目前是不支持的,如:MyMethod<object>(object_xxx)MyMethod<string>(string_xxx)都是不支持的。表现为泛型方法被正确Hook后,并不会走我们的逻辑,具体原因不明。

📖其他

关于测试项目内存访问异常

vs的测试功能会启动一个执行引擎,其默认选项是复用执行引擎。 反复运行测试时对修改汇编指令会造成影响。 从菜单关闭该选项Test -> Test Settings -> Keep Test Execution Engine Running,即可解除此影响。

另外调试测试是不能得出正确的结果的,可能是汇编代码不能在调试模式下工作。

老版本兼容

bigbaldy1128 2016-5开源此项目后,到2018-12 kissstudioxiangyuecn 升级了此项目代码,把相关代码方式升级和合理化了一番(参考 #4 #5 )。

已对3个主要的方法都进行了变更:

  1. MonitorClrMethodHook -> MethodHook
  2. MonitorAttributeRelocatedMethodAttribute -> HookMethodAttribute
  3. OriginalAttributeShadowMethodAttribute(不兼容) -> OriginalMethodAttribute

ShadowMethodAttribute外(升级需要改动被标记的方法名称,因而无法兼容),这3个变更都是兼容的,但不推荐继续使用老方法,并且将来可能会从类库里面移除。