发布一个自己写的用于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#普通函数的方式调用,这个怎么实现呢,其实很好办,只需写一个空壳函数,然后修改这个函数的方法表中的原生指令指针即可,具体方法如下:

C# code

 

?

1
*((ulong*)((uint*)method.MethodHandle.Value.ToPointer() + 2)) = (ulong)ptr;

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

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

C# code

 

?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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中你可以干你想干的任何事情

C# code

 

?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CustomMonitor : IMethodMonitor //自定义一个类并继承IMethodMonitor接口
{
    [Monitor("TargetNamespace""TargetClass")] //你要hook的目标方法的名称空间,类名
    public string Get() //方法签名要与目标方法一致
    {
        return "B" + Ori();
    }
 
    [MethodImpl(MethodImplOptions.NoInlining)]
    [Original] //原函数标记
    public string Ori() //方法签名要与目标方法一致
    {
        return null//这里写什么无所谓,能编译过即可
    }
}

然后定义目标函数,例如

C# code

 

?

1
2
3
4
public string Get()
 {
    return "A";
 }

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

C# code

 

?

1
2
3
Console.WrtieLine(Get());
Monitor.Install()
Console.WrtieLine(Get());

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

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

好了,讲了这么多,其实这个库代码量并不大,但主要是自己研究的一个成果,很多东西都是自己琢磨出来的,所以觉得这个过程很有意思,也希望高手能指出改进方案,毕竟感觉目前这种方法虽然实现了功能,但是并不是很好,总觉得以hook .net虚拟机的方式来实现会更简单一些,或者网络上已经有了现成的解决方案我没有找到,总之,抛砖引玉,希望大家能共同探讨

来源

纯C#实现Hook功能的更多相关文章

  1. 什么是HOOK功能?

    HOOK API是一个永恒的话题,如果没有HOOK,许多技术将很难实现,也许根本不能实现.这里所说的API,是广义上的API,它包括DOS下的中断,WINDOWS里的API.中断服务.IFS和NDIS ...

  2. APICloud开发者进阶之路 |纯手工编写日程表功能

    本文出自APICloud官方论坛, 感谢论坛版主 赵永亮 的分享. 最近看论坛内关于极光推送的问题有很多, 本想写一个关于极光的详细教程的,无奈已经有很多大牛分享过了,所以只得纯手工写了一个日程表,可 ...

  3. PyQt(Python+Qt)学习随笔:纯文本编辑器QPlainTextEdit功能详解

    专栏:Python基础教程目录 专栏:使用PyQt开发图形界面Python应用 专栏:PyQt入门学习 老猿Python博文目录 一.概述 QPlainTextEdit是用于纯文本的一个高级文档编辑器 ...

  4. 在Linux主机上搭建SVN,用于同步提交修改,实现本地提交线上预览(SVN Hook功能实现)

    原文地址: http://blog.csdn.net/ROVAST/article/details/44887707?ref=myread 注:上文中钩子使用中有错误,正确的使用如下,上文中忘记了up ...

  5. 纯JS实现图片验证码功能并兼容IE6-8

    最近要搞一个图片验证码功能,但是又不想自己写后台代码.于是自己准备搞一个纯前端的验证码功能,于是网上搜索了一下,找到一个插件gVerify.js,简单好用,实现完美.不过后面接到说要兼容IE8,想想也 ...

  6. 话说C++中的左值、纯右值、将亡值

    写在前面 C++中有“左值”.“右值”的概念,C++11以后,又有了“左值”.“纯右值”.“将亡值”的概念.关于这些概念,许多资料上都有介绍,本文在拾人牙慧的基础上又加入了一些自己的一些理解,同时提出 ...

  7. CI框架源码阅读笔记6 扩展钩子 Hook.php

    CI框架允许你在不修改系统核心代码的基础上添加或者更改系统的核心功能(如重写缓存.输出等).例如,在系统开启hook的条件下(config.php中$config['enable_hooks'] = ...

  8. 基于Android的ELF PLT/GOT符号和重定向过程ELF Hook实现(by 低端农业代码 2014.10.27)

    介绍 技术原因写这篇文章,有两种: 一个是在大多数在线叙述性说明发现PLT/GOT第二十符号重定向过程定向x86的,例<Redirecting functions in shared ELF l ...

  9. android inline hook

    最近终于沉下心来对着书把hook跟注入方面的代码敲了一遍,打算写几个博客把它们记录下来. 第一次介绍一下我感觉难度最大的inline hook,实现代码参考了腾讯GAD的游戏安全入门. inline ...

随机推荐

  1. Mysql查询阻塞初探

    第一次值班,报警打电话给我说,数据库复制延时一个多小时,那个时候是半夜啊,但我还是很清醒的起来,开机.vpn.登录.show processlist,结果发现情况是这样的: 红线框表示的是当前每个线程 ...

  2. 十五天精通WCF——第十二天 说说wcf中的那几种序列化

    我们都知道wcf是由信道栈组成的,在我们传输的参数走到传输信道层之前,先需要经过序列化的过程,也就是将参数序列化为message,这篇 我们就来说说这里的序列化,蛮有意思的,可能初学者也明白,在wcf ...

  3. XP系统下IIS常见的几个问题

    随笔说明: 个人笔记.仅供参考 根据日常遇到的相关问题不定期增改 时间:2015年1月7日23:09 Soft:Microsoft .NET Framework 4(独立安装程序) Microsoft ...

  4. 在JazzyViewPager中调用其它layout布局xml并使用

    开源地址:https://github.com/jfeinstein10/JazzyViewPager 发现网上的例子使用的是直接创建的一个TextView来做的.但是实际上使用,不可能只有这一个控件 ...

  5. linux 目录结构及其相关命令

    目录也是一个文件,它的唯一功能是用来保存文件及其相关信息.所有的文件,包括普通文件.设备文件和目录文件,都会被保存到目录中. 主目录 登录后,你所在的位置就是你的主目录(或登录目录),如果是root用 ...

  6. HDU 4866 Shooting(持久化线段树)

    view code//第二道持久化线段树,照着别人的代码慢慢敲,还是有点不理解 #include <iostream> #include <cstdio> #include & ...

  7. stanford coursera 机器学习编程作业 exercise 3(使用神经网络 识别手写的阿拉伯数字(0-9))

    本作业使用神经网络(neural networks)识别手写的阿拉伯数字(0-9) 关于使用逻辑回归实现多分类问题:识别手写的阿拉伯数字(0-9),请参考:http://www.cnblogs.com ...

  8. HDU 4630 No Pain No Game 树状数组+离线操作

    题意:给一串数字,每次查询[L,R]中两个数的gcd的最大值. 解法:容易知道,要使取两个数让gcd最大,这两个数最好是倍数关系,所以处理出每个数的所有倍数,两两间根据倍数关系形成一条线段,值为该数. ...

  9. Eclipse的 JSON Edit插件

    1. Json-Eclipse-Plugin https://github.com/boothen/Json-Eclipse-Plugin 2. 另外一个JSON Edit工具 https://tfe ...

  10. 一句话爆破速度提升一千倍python脚本

    这个脚本是接地提供的思路,一句话爆破速度提升一千倍,看了他的帖子然而没有看到工具,思路很牛逼,我提供一个Python脚本本地测试了下,十万密码只需要3秒,速度还是可以的 # coding:utf-8 ...