赞
踩
Interceptor.NET 是专用于.NET托管函数的拦截器(只限 IA32/IA64 指令集平台的处理器,ARM之流不在考虑范围),通过 “ Interceptor.NET ” 你可以对 “.NET应用域” 之中任何一个托管函数进行劫持或者放行,似乎听起来很有意思不是吗?
那么 “ Interceptor.NET ” 是只可以工作在 “Windows” 平台下面?答案是否定的,MacOS下.NET Core 2.0应用本人未试过能不能行,但是X-Linux(CentOS7、Ubuntu14)是没有问题的,这也意味着其它发行版的 “Linux” 是可行的。
从我的角度而言,MacOS上面它应该也可以正确的工作,当然如果不能工作出现 “内存不可读写之类的异常问题” 那么可能就是与 “内存保护权限” 的设定相关,本人对于MacOS完全是小白白,什么都不懂;如果你们非要在 MacOS 上面强上,出现了类似的问题造成无法工作的话,那么你可能需要自己解决一下。
我们是如何获取到 “托管函数” 的函数地址的?即反射获取到函数的 “MethodBody”,在通过其 “MethodHandle” 属性的 “GetFunctionPointer()” 函数获取到 “.NET 托管函数” 的函数地址的。
函数地址指向函数内存的 “二进制机器代码” 与 C/C++ 函数的格式是不同的,准确的说这里并不是一个严格函数而是一些安全检查还有按需即时编译的串联。
.NET 托管函数一般为以下格式(但这仅仅是 “MethodHandle.GetFunctionPointer()” 所获取到 “函数地址指针”,实际真正处理实际事务的 “托管实现函数” ,仔细看下面有很多的CALL;那个RVA地址就是指向 “真实托管实现函数” 的地址(虽然看上去很多个CALL的地址都是相同的,但你需要知道它们CALL的却不是同一个函数,不要看到CALL后面跟的地址相同就认为CALL的则是同一个函数,这是非常错误的。)附注:CALL 78C35DCA(形式为 CALL RVA / 这是是段间调用指令)
// 机器代码(1型)
e8 c5 5d c3 78 5e 00 04 e8 bd 5d c3 78 5e 04 03 e8 b5 5d c3 78 5e 08 02 e9 43 40 23 05 5f 0c 01 e8 a5 5d c3 78 5e 10 00 14 34 ba 00 00 00 00
// 汇编形式(1型)
CALL 78C35DCA
POP ESI
ADD [EAX+EBP*8],AL
MOV EBP,5E78C35D
ADD AL,3
CALL 78C35DCA
POP ESI
OR [EDX],AL
JMP 05234060
POP EDI
OR AL,1
CALL 78C35DCA
POP ESI
ADC [EAX],AL
ADC AL,34
MOV EDX,0
// 机器代码(2型)
e8 bd 5d c3 78 5e 04 03 e8 b5 5d c3 78 5e 08 02 e9 43 40 23 05 5f 0c 01 e8 a5 5d c3 78 5e 10 00 14 34 ba 00 00 00 00
// 汇编代码(2型)
CALL 78C35DC2
POP ESI
ADD AL,3
CALL 78C35DC2
POP ESI
OR [EDX],AL
JMP 05234058
POP EDI
OR AL,1
CALL 78C35DC2
POP ESI
ADC [EAX],AL
ADC AL,34
MOV EDX,0
示图:
实际上,我本人在编写 “Interceptor.NET” 时,预想了无损拦截的一种方法,其原理则是在安装拦截器的时候为被拦截的托管函数动态编译一个 “Jumper gadget” 的小装置,但这需要寻找被拦截的托管函数头部必要特征,索性我们找到了特征其特征为机器码 “\xE8” = “CALL” 指令,几乎每个托管函数头部的第一句指令就是 CALL XXXX 这样的形式,但实际上本人真正的利用了这个特征构建 “Jumper” 的时候,发生了很多不太容易被解决的问题。
例如:
1、调用协议(委托无法绑定正确)
2、安全机制(执行引擎会X掉程序继续运行)
3、函数栈检查失败
因为通过CALL XXX特征构建小装置的原理就是,先复制复制这段机器代码,然后重新修饰CALL XXX的RVA地址,并且在第一个CALL执行完成之后跳到 “原目标托管函数” 相对于被复制 “CALL XXX” 机器代码之后的位置继续执行,相当于要做一个交叉跳板来解决这个问题。
从上述实现思路上谈,其本身并没有任何问题,我们只是没有找到一个恰当的办法的来设定这个 Jumper,本人坚信人们是可以寻找到办法解决这个问题的。
.NET 托管函数最大可以分配的地址空间不超过 (1 << 32) + ~0个字节之间的空间,不超出4GB/但实际上因为各种限制的缘故最多就2GB,理论上一个程序编译的机器代码要加载到内存要上2G,这个就目前公共的程序应该没有那个这么夸张吧?!。
.NET 托管函数编译后的汇编函数之间的 “堆内存分段” 都属于同一个大地址分段这里的分段指不超过 (1 << 32) + ~0字节范围,另外很直接的道理,设两者托管函数之间超出32位寻址范围的话,那么 .NET 托管函数之间调用或者跳转应该是采取64位或者绝对地址跳转,否则是无法CALL这些函数的。
1、绝对地址调用
MOV RAX, FUNC
CALL RAX
2、相对地址调用(段间调用)
CALL RVA(FUNC)
.NET运行时编译的托管函数都属于同一个大内存地址分段,相对于C/C++程序每加载其的一个DLL库模块时,每个库都有属于自己单独的一个大内存地址分段,我们想要 “拦截/hooking” 某个公共或私有DLL库模块之中的函数时,在x64的环境下我们就没有办法使用 “段间调用” 或者 “段间调用” 的汇编指令,而必须使用绝对地址“调用/转移”的办法。
基于这样的特性,我们要编写.NET托管函数拦截器,那么只需要很简单的一个办法就可以做到,因为拦截器的核心原理本身就是 “inline-hook” 这类型技术的产物,只是用到了正道上面。------>>> 参考文章:https://blog.csdn.net/liulilittle/article/details/56280052 ^^^^ .NET 内链钩子技术(inline-hook)
Interceptor.NET 实现与例子的开放源代码托管在 “https://github.com/liulilittle/Interceptor.NET” 上面,如果转载或使用代码注意著名出处站点。
以下代码为测试运行的示例代码,我们会看到其程序的运行结果显著的被拦截器在运行时改变了,纵然我们通过静态反编译工具都可以看到,“生成的代码” 与 “工程的代码” 是等价的。
- namespace Interceptor
- {
- using System;
- using Interceptor.Hooking;
-
- class Samples
- {
- public static int A(int x, int y)
- {
- Console.WriteLine($"Program.A({x}, {y})");
- return x + y;
- }
-
- public static int B(int x, int y)
- {
- Console.WriteLine($"Program.B({x}, {y})");
- return x + y;
- }
-
- public class Foo
- {
- public void A()
- {
- Console.WriteLine("Foo.A");
- }
-
- public void B()
- {
- Console.WriteLine("Foo.B");
- }
-
- public void G(int x)
- {
- Console.WriteLine($"Foo.G{x}");
- }
- };
-
- public static void G(Foo foo, int x)
- {
- Console.WriteLine($"Program.Foo.G({x})");
- }
-
- static void Main(string[] args)
- {
- Interceptor[] interceptor = new[]
- {
- new Interceptor(typeof(Samples).GetMethod("A"), typeof(Samples).GetMethod("B")),
- new Interceptor(typeof(Foo).GetMethod("A"), typeof(Foo).GetMethod("B")),
- new Interceptor(typeof(Foo).GetMethod("G"), typeof(Samples).GetMethod("G")),
- };
-
- A(1, 2);
-
- Foo foo = new Foo();
- foo.A();
-
- foo.G(3);
- interceptor[2].Invoke(() => foo.G(3));
-
- Console.ReadKey(false);
- }
- }
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。