自己写的一个可以hook .net方法的库

bigbaldy 2016-05-27 12:09:12
加精
发布一个自己写的用于Hook .Net方法的类库,代码量不大,完全的C#代码实现,是一个比较有趣的功能,分享出来希望能和大家共同探讨

安装:Install-Package DotNetDetour
源码:https://github.com/bigbaldy1128/DotNetDetour

1.为何想做这个
说到hook大家都应该不陌生,就是改变函数的执行流程,让本应该执行的函数跑到另一个函数中执行,这是个很有用也很有趣的功能(例如获取函数参数信息,改变函数执行流程,计算函数执行时间等等),杀软中主防的原理就是hook,通过hook拦截函数获取参数信息来判断是否是危险行为,但这类程序大多是C++的,一直以来我都想实现可以hook .net函数的库,网上搜索了很多,但都不理想,所以想自己实现一个

2.实现原理
我采用的是inline hook的方式,因为我对.net虚拟机以及一些内部的结构并不是很熟悉,并且有些东西的确找不到任何文档,所以就采用原生代码的inline hook的方式来实现。

首先说一下inline hook的基本原理,它是通过修改函数的前5字节指令为jmp xxxxxxxx来实现的,例如一个C#方法:

用windbg调试查看方法信息:

查看已经jit了的原生代码:

这里的地址(0x008c0640)可以通过MethodInfo.MethodHandle.GetFunctionPointer().ToPointer()方法获取

到了这里,我们就知道了修改从push ebp开始的5个字节为jmp跳转指令,跳入我们自己的函数就可以达到hook的目的,但执行到我们的函数后,如果我们并不是要拦截执行流程,那么我们最终是需要再调用原函数的,但原函数已经被修改了,这会想到的办法就是恢复那修改的5字节指令,但这又会引发另一个问题,就是当我们恢复时,正好另一个线程调用到这个函数,那么程序将会崩溃,或者说漏掉一次函数调用,修改时暂停其他线程并等待正跑在其中的CPU执行完这5字节再去恢复指令也许是个不错的办法,但感觉并不容易实现,而且影响性能,所以我放弃了这种办法

那么如何才能调用修改前的函数呢,我首先想到是C中写裸函数的方式,即自己用汇编拼出来一个原函数再执行:
原函数前5字节指令+jmp跳转指令
但其实这也是不可行的,聪明的人已经发现,图中所示的函数的前5字节并不是一个完整的汇编指令,不同的函数,长度都不一样,.net的函数并不像某些原生函数那样,会预留mov edi,edi这样的正好5字节的指令,我先想到的是复制函数的所有汇编指令生成新的函数,但这样也会出问题,因为像E8,E9这样的相对跳转指令,如果指令地址变了,那么跳转的位置也就变了,程序就会崩溃,所以这也不可行。

到了这里,我有些不耐烦了,毕竟我是要hook所有函数的,而不是某个固定的函数,而函数入口的指令又不相同,这可怎么办,难道我需要计算出大于等于5字节的最小完整汇编指令长度

按照这个思路,最终找到了一个用C写的反汇编库(BlackBone),其中提供了类似的方法,我稍作了修改后试用了下,的确不错,可以准确求出汇编指令长度,例如

push ebp
mov ebp,esp
mov eax,dword ptr ds:[33F22ACh]


求出值是9,这样我根据求出的值动态拼接一个函数出来即可,哈哈,到了这里,感觉实现的差不多了,但没想到64位下又给了我当头一棒,之前的原函数指令可以写成:

大于等于5字节的最小完整汇编指令+jmp跳转指令即可构成我们的原函数

但我们知道,C#中要想执行汇编,是需要用Marshal.AllocHGlobal来分配非托管空间的,而这样分配的地址与我们要跳转到的原函数的地址在64位下是超过2GB地址范围的,一般的跳转指令是无法实现的,所以想到了用ret指令实现,而64位地址又不能直接push,所以最后写出如下汇编:

push rax
mov rax,target_addr
push rax
mov rax,qword ptr ss:[rsp+8]
ret 8


由于某些C#函数竟然第一行就是修改rax寄存器的值,所以只能是先保存rax,推入堆栈后再恢复,这里汇编操作就方便多了,之前实现另一个东西,用到IL指令,但发现只有dup这种复制栈顶元素的指令,却没有获取堆栈中某个非栈顶元素值的指令,所以说还是汇编灵活啊,想怎么写就怎么写,啥都能实现。

最后就是这个原函数的调用过程了,因为是动态拼接的函数,所以想到的就是用Marshal.GetDelegateForFunctionPointer转成委托来执行,后来发现不对,因为我虽然拼接的是汇编,而这个汇编是C#方法jit后的汇编,这个并不是C方法编译后的汇编,通过把非托管指针转换为委托的方式运行函数是会添加很多不需要的操作的,例如托管类型与非托管类型的转换,但我拼接出的函数是不需要这些过程的,这个怎么办,看来只能用调用C#普通函数的方式调用,这个怎么实现呢,其实很好办,只需写一个空壳函数,然后修改这个函数的方法表中的原生指令指针即可,具体方法如下:
*((ulong*)((uint*)method.MethodHandle.Value.ToPointer() + 2)) = (ulong)ptr;

method是空壳函数的MethodInfo, ptr是动态拼接的原函数的地址

好,到了这里就基本完成核心功能了,最不好处理的就是这个原函数调用,我的完整的64位原函数指令拼接就实现了,代码很少,如下所示:

byte[] jmp_inst =
{
0x50, //push rax
0x48,0xB8,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90, //mov rax,target_addr
0x50, //push rax
0x48,0x8B,0x44,0x24,0x08, //mov rax,qword ptr ss:[rsp+8]
0xC2,0x08,0x00 //ret 8
};

protected override void CreateOriginalMethod(MethodInfo method)
{
uint oldProtect;
var needSize = NativeAPI.SizeofMin5Byte(srcPtr);
byte[] src_instr = new byte[needSize];
for (int i = 0; i < needSize; i++)
{
src_instr[i] = srcPtr[i];
}
fixed (byte* p = &jmp_inst[3])
{
*((ulong*)p) = (ulong)(srcPtr + needSize);
}
var totalLength = src_instr.Length + jmp_inst.Length;
IntPtr ptr = Marshal.AllocHGlobal(totalLength);
Marshal.Copy(src_instr, 0, ptr, src_instr.Length);
Marshal.Copy(jmp_inst, 0, ptr + src_instr.Length, jmp_inst.Length);
NativeAPI.VirtualProtect(ptr, (uint)totalLength, Protection.PAGE_EXECUTE_READWRITE, out oldProtect);
RuntimeHelpers.PrepareMethod(method.MethodHandle);
*((ulong*)((uint*)method.MethodHandle.Value.ToPointer() + 2)) = (ulong)ptr;
}


3.类库开发所用到的语言
之前我说,我的这个库是完全用C#实现的,但其中的确用到了一个C写的反汇编库,于是我用C#把那个库重写了一遍,说来也简单,C的代码粘过来,C#启用unsafe代码,改了10分钟就好了,真心是非常方便,毕竟C#是支持指针和结构体的,而且基础类型非常丰富,这里得给C#点个赞!

4.具体使用
使用非常简单,首先新建控制台程序并添加一个类,继承接口IMethodMonitor,Get是你自己的函数,Ori是原函数会在运行时动态生成,在Get中你可以干你想干的任何事情

public class CustomMonitor : IMethodMonitor //自定义一个类并继承IMethodMonitor接口
{
[Monitor("TargetNamespace", "TargetClass")] //你要hook的目标方法的名称空间,类名
public string Get() //方法签名要与目标方法一致
{
return "B" + Ori();
}

[MethodImpl(MethodImplOptions.NoInlining)]
[Original] //原函数标记
public string Ori() //方法签名要与目标方法一致
{
return null; //这里写什么无所谓,能编译过即可
}
}

然后定义目标函数,例如

public string Get()
{
return "A";
}

最后调用Monitor.Install()安装监视器,例如:

Console.WrtieLine(Get());
Monitor.Install()
Console.WrtieLine(Get());

你会发现第一次调用Get输出的值是"A",第二次是"BA"

当然这个库只是hook,但hook一般都需要dll注入来配合,因为hook自身进程没什么意义,hook别人的进程才有意义,我之后会发布一个用于.net程序远程注入的类库,注入的是.net的dll哦,不是C++的

好了,讲了这么多,其实这个库代码量并不大,但主要是自己研究的一个成果,很多东西都是自己琢磨出来的,所以觉得这个过程很有意思,也希望高手能指出改进方案,毕竟感觉目前这种方法虽然实现了功能,但是并不是很好,总觉得以hook .net虚拟机的方式来实现会更简单一些,或者网络上已经有了现成的解决方案我没有找到,总之,抛砖引玉,希望大家能共同探讨
...全文
8904 67 4 打赏 收藏 举报
写回复
67 条回复
切换为时间正序
当前发帖距今超过3年,不再开放新的回复
发表回复
moshuiD 2021-07-25

  • 打赏
  • 举报
回复
星空你好 2020-01-19
谢谢分享!!!
  • 打赏
  • 举报
回复
zyg9108 2017-11-17
结贴了?有问题怎么请教楼主啊
  • 打赏
  • 举报
回复
夏虫1 2017-11-10
请教: 想实现播放器是边解密边播放,相当于内存里解密播放,而不是生产一个解密文件之后再打开播放。播放器用迅雷aplayer sdk它只提供了void open(String filename),只能播放未加密视频,参数仅有文件路径。 既然sdk没有接口就想到了用hook,但hook它的open是没有用的,应该要hook readfile之类的函数才有用,把他读取到的byte直接替换成解密后的byte应该就能实现目的了,但是不知道如何找到hook哪个函数,如何找到读取byte文件方法呢? 希望高手给点提示?感谢!
  • 打赏
  • 举报
回复
Aisht 2016-08-24
引用 65 楼 CCDDzclxy 的回复:
[quote=引用 64 楼 bigbaldy 的回复:] 不要调试运行,直接debug目录下双击运行,我直接改的内存,所以调试有可能有问题
果然如此,感谢[/quote] 你成功了吗? 我按照你的例子.还是一样输出A. 怪事
  • 打赏
  • 举报
回复
Aisht 2016-08-24
不知楼主或者有木有人给出一个完整的例子呢? 用楼主的DLL. 貌似没效果,直接一个 Monitor.Install() 就是了?
  • 打赏
  • 举报
回复
bigbaldy 2016-06-29
引用 63 楼 CCDDzclxy 的回复:
class zzzzz 改成 public class zzzzz 点击"启动",反而报错了,如下图: 我的vs2015 版本信息为:
不要调试运行,直接debug目录下双击运行,我直接改的内存,所以调试有可能有问题
  • 打赏
  • 举报
回复
CCDDzclxy 2016-06-29
引用 64 楼 bigbaldy 的回复:
不要调试运行,直接debug目录下双击运行,我直接改的内存,所以调试有可能有问题
果然如此,感谢
  • 打赏
  • 举报
回复
CCDDzclxy 2016-06-28
class zzzzz 改成 public class zzzzz

点击"启动",反而报错了,如下图:


我的vs2015 版本信息为:
  • 打赏
  • 举报
回复
CCDDzclxy 2016-06-26
小白路过,求指点:
1、我在你的 解决方案里面 新建了一个项目,名为“zzz”,代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using DotNetDetour;
using System.Runtime.CompilerServices;

namespace zzz
{
public class CustomMonitor : IMethodMonitor //自定义一个类并继承IMethodMonitor接口
{
[Monitor("zzz", "zzzzz")] //目标方法的名称空间,类名
public string Get() //方法签名要与目标方法一致
{
return "B" + Ori();
}

[MethodImpl(MethodImplOptions.NoInlining)]
[Original] //原函数标记
public string Ori() //方法签名要与目标方法一致
{
return null; //这里写什么无所谓,能编译过即可
}
}

class zzzzz
{
public string Get()
{
return "A";
}

public void z()
{
//CustomMonitor c = new CustomMonitor();

Console.WriteLine(Get());
Monitor.Install();
Console.WriteLine(Get());
}
}

class Program
{
static void Main(string[] args)
{
zzzzz z = new zzzzz();
z.z();
}
}
}

引用 里面添加了 DotNetDetour,编译OK,但是 显示 两次都是 打印出 "A",没有 "BA"...

2、上面没有 做“Install-Package DotNetDetour”操作,于是 尝试 vs2015-->工具--> NuGet 包管理器-->程序包管理器控制台-->然后输入命令:Install-Package DotNetDetour --> 安装成功
但是,我运行 项目zzz 的时候,有如下报错:



求指点~~~
  • 打赏
  • 举报
回复
AdamSearcher 2016-06-03
虽然我看不懂楼主在说什么,但是我还是很需要要楼主这样的大牛的!!!![img=https://forum.csdn.net/PointForum/ui/scripts/csdn/Plugin/003/monkey/5.gif][/img我要.NET的,我要.NET,我要.NET,薪资支持正常涨幅都可以!不管你现在多少钱,都可以涨的起!很狠、、
  • 打赏
  • 举报
回复
showjim 2016-06-03
引用 58 楼 bigbaldy 的回复:
说的很对,这个不知道怎么办,还有上面我提的那个函数地址问题,某些方法用MethodHandle.GetFunctionPointer()获取不到正确的地址,不知你有什么解决的思路吗?
另外JIT编译之前与之后会是不同的,我看你代码里面有RuntimeHelpers.PrepareMethod处理,应该是知道这个的。
  • 打赏
  • 举报
回复
showjim 2016-06-03
引用 58 楼 bigbaldy 的回复:
[quote=引用 57 楼 sbwwkmyd 的回复:] 对于某些被内联优化了的函数,获取到地址也没有意义。
说的很对,这个不知道怎么办,还有上面我提的那个函数地址问题,某些方法用MethodHandle.GetFunctionPointer()获取不到正确的地址,不知你有什么解决的思路吗?[/quote] 能不能举个,比如.NET类库中的代码的例子。 我觉得MethodHandle.GetFunctionPointer()直接指向目标函数地址应该是没有问题的,这是JIT编译应该做的优化工作。 我觉的,可能问题出在你的MethodInfo是怎么产生的,也就是说区别应该在于它是一个根据元数据产生的原生MethodInfo还是类似与dynamicMethod包装性的临时MethodInfo。如果是后者,可能就没有什么好办法处理的,因为包装方式可能比较多而且存在不确定性。
  • 打赏
  • 举报
回复
bigbaldy 2016-06-03
引用 57 楼 sbwwkmyd 的回复:
对于某些被内联优化了的函数,获取到地址也没有意义。
说的很对,这个不知道怎么办,还有上面我提的那个函数地址问题,某些方法用MethodHandle.GetFunctionPointer()获取不到正确的地址,不知你有什么解决的思路吗?
  • 打赏
  • 举报
回复
showjim 2016-06-02
对于某些被内联优化了的函数,获取到地址也没有意义。
  • 打赏
  • 举报
回复
showjim 2016-06-02
引用 55 楼 bigbaldy 的回复:
对了,我还有一个问题,我在用src.MethodHandle.GetFunctionPointer()获取函数地址时,发现某些函数是不准确的,例如:System.Data.SQLite.SQLiteCommand.set_CommandText,获取的地址就不对,最后我只能通过hook clrjit.dll中的compileMethod方法并得到BYTE **nativeEntry参数的值来抓取函数真正的地址,不知你是否有什么办法能够解决这个问题
没有用过SQLite,看起来CommandText应该是个属性,用Delegate.CreateDelegate得到的委托应该是可行的,比如试试这样
((Func<System.Data.SQLite.SQLiteCommand, string>)Delegate.CreateDelegate(typeof(Func<System.Data.SQLite.SQLiteCommand, string>), typeof(CustomMonitorTest).GetProperty("CommandText", BindingFlags.Instance | BindingFlags.Public).GetGetMethod(false)))).MethodHandle.GetFunctionPointer()
  • 打赏
  • 举报
回复
bigbaldy 2016-06-02
引用 51 楼 sbwwkmyd 的回复:
[quote=引用 49 楼 bigbaldy 的回复:] 我也想用DynamicMethod,但不知如何获取函数地址,getMethodPtrAux是个啥?能公开源代码吗?
[Serializable, ClassInterface(ClassInterfaceType.AutoDual), ComVisible(true), __DynamicallyInvokable]
public abstract class Delegate : ICloneable, ISerializable
{
    [SecurityCritical]
    internal object _methodBase;
    [SecurityCritical]
    internal IntPtr _methodPtr;
    [SecurityCritical]
    internal IntPtr _methodPtrAux;
    [SecurityCritical]
    internal object _target;
}
就是委托的两个私有字段,普通反射也能获取到的。[/quote] 嗯,看了源码已经知道了。 对了,我还有一个问题,我在用src.MethodHandle.GetFunctionPointer()获取函数地址时,发现某些函数是不准确的,例如:System.Data.SQLite.SQLiteCommand.set_CommandText,获取的地址就不对,最后我只能通过hook clrjit.dll中的compileMethod方法并得到BYTE **nativeEntry参数的值来抓取函数真正的地址,不知你是否有什么办法能够解决这个问题
  • 打赏
  • 举报
回复
bigbaldy 2016-06-02
引用 52 楼 sbwwkmyd 的回复:
[quote=引用 50 楼 bigbaldy 的回复:]找到了,fastCSharp,好东西啊
fastCSharp到这里下载,然后可以通过TFS获取最新版本,代码写得烂,见笑了。[/quote] 已经下了源码,真是好东西啊
  • 打赏
  • 举报
回复
wonderfulpeng 2016-06-02
讲的挺不错的 很好
  • 打赏
  • 举报
回复
showjim 2016-06-01
引用 50 楼 bigbaldy 的回复:
找到了,fastCSharp,好东西啊
fastCSharp到这里下载,然后可以通过TFS获取最新版本,代码写得烂,见笑了。
  • 打赏
  • 举报
回复
加载更多回复
相关推荐
发帖
C#
加入

10.6w+

社区成员

.NET技术 C#
申请成为版主
帖子事件
创建了帖子
2016-05-27 12:09
社区公告

让您成为最强悍的C#开发者