发布一个自己写的用于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. 随便选择两个城市作为预选旅游目标。实现两个独立的线程分别显示10次城市名,每次显示后休眠一段随机时间(1000ms以内),哪个先显示完毕,就决定去哪个城市。分别用Runnable接口和Thread类实现。

    public class Testlvyou extends Thread{ @Override public void run() { test(); } private void test() { ...

  2. 今天说一下Order by 这个常规东西~

    Order by 在我们日常的数据库开发生活中是出镜率灰常高的. order by 的作用就是用于对查询出来的结果进行排序~对啊~人家就是这么接地气~比如按发生时间啊,首字母啊之类的都是相当常见. 今 ...

  3. Oracle安装前用户信息设置

    如果是重复安装,首先需要清除已经存在的软件安装记录: rm -fr /usr/local/bin/*oraenv rm -fr /usr/local/bin/dbhome rm -fr /usr/tm ...

  4. Windows下常用软件工具的命令

    Linux上主要操作是命令,懂一点linux知识的都知道,其实windows下边很多工具也是可以用命令来操作打开的,这样会提高效率,节省很多的时间.下边就记录一下常用的命令. 一.常用命令 1.远程桌 ...

  5. linux 中/proc 详解

    proc 文件系统 在Linux中有额外的机制可以为内核和内核模块将信息发送给进程-- /proc 文件系统.最初设计的目的是允许更方便的对进程信息进行访问(因此得名),现在它被每一个有有趣的东西报告 ...

  6. 类似material design的电影应用源码

    这个源码应用是一款大家比较熟悉的类似material design的电影应用源码,源码Material-Movies,material design风格的电影应用,这个应用的目的其实是为了讲解作者一篇 ...

  7. 零拷贝传输(zero-copy transfer)——sendfile()

    做Web服务器时通常需要将文件传送出去,其中一种方法是通过定义一个buffer每次读取文件发送给接收端.大多数服务器会选择sendfile的方式,nginx实现时就是采用这种方式.对于并发搞得服务器性 ...

  8. No resource found that matches the given name 'Theme.AppCompat.Light 的完美解决方案

    No resource found that matches the given name 'Theme.AppCompat.Light 的完美解决方案 首先这个问题的产生是由于缺少Theme.App ...

  9. nginx 入门

    1.nginx 如何处理一个请求 IP,域名的处理 server { listen  80  default_server;     //添加 default_server就是一个默认的server ...

  10. 用AutoCompleteTextView实现历史记录提示

    自定义AutoCompleteTextView 博客分类: android进阶 android  网上找到的都是同ArrayAdapter一起使用的,有时候需要自定义风格,咋办?follow me! ...