bigbaldy1128/DotNetDetour

不能hook非公开的方法,无法创建带有非公开类型参数的hook,魔改了一下Monitor.cs

xiangyuecn opened this issue · 11 comments

举个例子

我要hook A类里面的两个私有方法aa、bb:

  • aa算是比较正常的一个私有方法;
  • bb是一个带外部不能访问类型参数的方法。

例子代码

namespace ConsoleApplication1 {
	class Program {
		static void Main(string[] args) {
			Monitor.Install();
			new A().CallB();
			Console.ReadLine();
		}
	}
	class A {
		public A() { aa(); }
		//需要hook的方法1
		private void aa() {
			Console.WriteLine("A.aa()");
		}
		//需要hook的方法2
		private void bb(B b) {
			if (b == null) {
				Console.WriteLine("A.bb(null)");
			} else {
				Console.WriteLine("A.bb(B)");
				b.Run();
			}
		}

/********到此为止*****************/

		public void CallB() {
			bb(new B());
		}
		private class B{
			public void Run() {
				Console.WriteLine("B.Run()");
			}
		}
	}
	public class AHook : IMethodMonitor {
		[Monitor("ConsoleApplication1", "A")]
		private void aa() {
			Console.WriteLine("hook aa()");
			org();
		}
		[MethodImpl(MethodImplOptions.NoInlining)]
		[Original]
		private void org() {
			return;
		}
	}
	public class BHook : IMethodMonitor {
		[Monitor("ConsoleApplication1", "A")]
		private void bb(object b) {
			Console.WriteLine("hook bb()");
			org(b);
			org(null);
		}
		[MethodImpl(MethodImplOptions.NoInlining)]
		[Original]
		private void org(object b) {
			return;
		}
	}
}

未修改前执行结果

两个方法都无法hook

A.aa()
A.bb(B)
B.Run()

魔改后的结果

两个方法成功hook,并且调用未发现异常

hook aa()
A.aa()
hook bb()
A.bb(B)
B.Run()
A.bb(null)

Monitor.cs改动

Install()

两处GetMethods()调用,改成加上flags参数,flags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static

InstallInternal()

GetExportedTypes()调用改成GetTypes()

两处GetMethod(methodName, paramTypes)调用,换一种方式查找Method,否则私有类型参数的Hook 函数无法编写;示例代码如下:

源代码(有两处要改):
src = type.GetMethod(methodName, paramTypes);
改成
src = getFn(type, methodName, paramTypes);

------

		//查找最优函数,忽略参数匹配
		private static MethodInfo getFn(Type type, string name, Type[] paramTypes) {
			var all = type.GetMethods(AllFlag);
			MethodInfo rtv = null;
			var find = 0;
			foreach (var item in all) {
				if (item.Name == name) {
					rtv = item;
					find++;
				}
			}
			if (find == 0) {
				return null;
			}
			if (find == 1) {
				return rtv;
			}

			//获取最佳的
			var list = new List<fnItem>();
			foreach (var item in all) {
				if (item.Name == name) {
					int score = 0;
					var ps = item.GetParameters();
					score += 100 - Math.Abs(paramTypes.Length - ps.Length);
					//参数数量不一样基本忽略
					if (score == 100) {
						for (int i = 0, len = paramTypes.Length; i < len; i++) {
							if (paramTypes[i] == ps[i].ParameterType) {
								score += 100;
							}
						}
					}
					list.Add(new fnItem() { fn = item, score = score });
				}
			}
			list.Sort((a, b) => {
				return b.score - a.score;
			});
			return list[0].fn;
		}
		private class fnItem {
			public MethodInfo fn;
			public int score;
		}

我也改了这里,并且已支持构造函数的hook。遇到很多属性hook失败,目前尝试一个System_Net_WebHeaderCollection的CheckBadChars也不行了。有内存访问错误

@kissstudio 有些属性hook失败,也许是编译后被优化掉了,压根没有调用这个get方法,试一下hook最原始取值的地方,比如:

//AClass
string Val1{get{return BClass.Val2;}}
//BClass
string Val2{get{return val;}} //要是hook上面无效,hook这里说不定有效

我遇上了一个这样的,hook直接调用的属性get方法没有效果,hook中间返回值的属性的get也没有效果,hook最深一个返回值的get就成功了,非常奇怪。

我找到方法调用内存访问错误的原因了,是我写的方法和要hook的方法签名不一致造成的,我在hook时把静态方法声明为实例方法了。CLR对不同签名的方法的调用最后生成的汇编可能不一致。

属性反正经常遇到内存访问错误,我已经放弃了。

我还增加了内部类作为参数的方法的优雅hook,使用object声明并对参数进行特征标记:
private void PostProgressChanged(AsyncOperation asyncOp, [NonPublicParameterTypeAttribute("System.Net.WebClient+ProgressData")] object progress)

提交pull request吧,我合并下代码

@bigbaldy1128 不知道怎么开始写这个PR,感觉对当前版本代码基础上那几处改动有点伤筋动骨的感觉,Monitor换了一半血那种。

另外测试也是个比较纠结的事,刚才重新clone了一份代码,没有做任何改动,测试发现那个系统方法通不过,开调试又通的过:

测试名称: 	SystemMethod
测试全名: 	Test.MonitorTest.SystemMethod
测试源:	..... MonitorTest.cs: 第 34 行
测试结果: 	未通过
测试持续时间: 	0:00:00.0471841

结果 的消息: 	
测试方法 Test.MonitorTest.SystemMethod 引发了异常: 
System.AccessViolationException: 尝试读取或写入受保护的内存。这通常指示其他内存已损坏。

环境.NET Framework 4.5。

批量时测试不过是有原因的。仔细想想。😁

批量时测试不过是有原因的。仔细想想。😁

通过对patch的汇编代码的比对发现,也不是批量执行引起的。我的比对代码如下

public virtual bool IsDetourInstalled()
        {
            byte[] v = new byte[newInstrs.Length];
            for (int i = 0; i < v.Length; i++)
            {
                v[i] = *(rawMethodPtr + i);
            }
            return v.SequenceEqual(newInstrs);
        }

patch代码修改成这样子

public virtual bool Patch(MethodBase rawMethod/*要hook的目标函数*/, 
            MethodInfo customImplMethod/*用户定义的函数,可以调用占位函数来实现对原函数的调用*/, 
            MethodInfo placeholder/*占位函数*/)
        {
            //确保jit过了
            var typeHandles = rawMethod.DeclaringType.GetGenericArguments().Select(t => t.TypeHandle).ToArray();
            RuntimeHelpers.PrepareMethod(rawMethod.MethodHandle, typeHandles);

            rawMethodPtr = (byte*)rawMethod.MethodHandle.GetFunctionPointer().ToPointer();

            var customImplMethodPtr = (byte*)customImplMethod.MethodHandle.GetFunctionPointer().ToPointer();
            //生成跳转指令,使用相对地址,用于跳转到用户定义函数
            fixed (byte* newInstrPtr = newInstrs)
            {
                *(uint*)(newInstrPtr + 1) = (uint)customImplMethodPtr - (uint)rawMethodPtr - 5;
            }

            //因测试项目的特殊性,确保测试项目代码不会重入
            if (IsDetourInstalled())
            {
                return false;
            }

            //将对占位函数的调用指向原函数,实现调用占位函数即调用原始函数的功能
            if (placeholder != null)
            {
                MakePlacholderMethodCallPointsToRawMethod(placeholder);
            }


            //并且将对原函数的调用指向跳转指令,以此实现将对原始目标函数的调用跳转到用户定义函数执行的目的
            Patch();
            return true;
        }

vs的测试功能会启动一个执行引擎,其默认选项是复用执行引擎。
修改汇编指令会对其造成影响,这个影响不能从汇编字节对比(IsDetourInstalled方法)获取到。
因此从菜单关闭该选项Test->Test Settings ->Keep Test Execution Engine Running ,
经我测试,目前没有发现问题了。

@kissstudio 厉害了,设置一下测试就没有问题了,你那边改动的厉害点,看到你改动的代码了,也许你那可以PR 😃

提了PR,外围改动比较大。。不知道收不收😈

尝试在azure Devops进行build,有一个测试未通过。。。