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

发布一个自己写的用于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虚拟机的方式来实现会更简单一些,或者网络上已经有了现成的解决方案我没有找到,总之,抛砖引玉,希望大家能共同探讨

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: 在C语言中编一个hook(钩子)可以实现对特定函数或代码块的拦截、修改或扩展功能。下面是一个简单的用C编hook的示例: ```c #include <stdio.h> #include <stdlib.h> #include <dlfcn.h> // 要拦截的函数的原始定义 void original_func() { printf("原始函数被调用\n"); } // hook函数,用于代替原始函数 void hooked_func() { printf("Hook函数被调用\n"); // 调用原始函数 original_func(); } int main() { // 获取要拦截的函数的地址 void *handle = dlopen(NULL, RTLD_LAZY); void (*original)() = dlsym(handle, "original_func"); // 修改原始函数为hook函数 *((void **)(&original)) = (void *)hooked_func; // 调用原始函数(实际上会调用hook函数) original(); dlclose(handle); return 0; } ``` 这个示例中的hook函数使用`dlopen()`和`dlsym()`获取了要拦截的函数的地址,然后通过修改函数指针的方式将原始函数修改为了hook函数。最终,当原始函数被调用时,实际上会执行hook函数。 需要注意的是,hook技术在一些场景中可以用来进行调试、代码注入或代码修改等操作,但在实际应用中要谨慎使用,以免引发软件安全性或稳定性问题。 ### 回答2: 在C语言中编一个hook,可以通过拦截API调用来修改或者监控系统的行为。 首先,我们需要了解一个基本概念,即hook的实现依赖于动态链接(DLL)注入技术。DLL注入是将自定义的DLL文件加载到目标进程中,并将DLL中的函数替换为自己定义的函数,从而改变程序的行为。 以下是一个简单的C语言hook的示例: ```c #include <windows.h> // 定义一个自定义函数指针类型,用于替换原始API函数 typedef int(WINAPI* ORIGINAL_FUNCTION)(int); // 定义一个指向原始API函数的指针 ORIGINAL_FUNCTION OriginalFunction; // 自定义的Hook函数,替代原始API函数的功能 int WINAPI HookedFunction(int arg) { // 在这里进行你想要的操作 // 调用原始API函数 return OriginalFunction(arg); } // DLL入口函数 BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: // 获取原始API函数的地址 OriginalFunction = (ORIGINAL_FUNCTION)GetProcAddress(GetModuleHandle("target_module.dll"), "target_function"); // 替换原始API函数的地址为自定义Hook函数的地址 // 注意:这里需要禁用线程保护机制(DEP)和代码签名验证(Digital Signature Verification)才能进行内存入操作。 DWORD oldProtect; VirtualProtect(OriginalFunction, sizeof(HookedFunction), PAGE_EXECUTE_READWRITE, &oldProtect); memcpy(OriginalFunction, &HookedFunction, sizeof(HookedFunction)); VirtualProtect(OriginalFunction, sizeof(HookedFunction), oldProtect, &oldProtect); break; case DLL_PROCESS_DETACH: // 恢复原始API函数的地址 DWORD oldProtect; VirtualProtect(OriginalFunction, sizeof(HookedFunction), PAGE_EXECUTE_READWRITE, &oldProtect); memcpy(OriginalFunction, &HookedFunction, sizeof(HookedFunction)); VirtualProtect(OriginalFunction, sizeof(HookedFunction), oldProtect, &oldProtect); break; } return TRUE; } ``` 以上代码以动态链接的形式进行编,通过DllMain函数在目标进程中加载并注入hook函数。等到目标进程调用原始API函数时,实际上会执行我们自定义的HookedFunction函数。在这个函数中,你可以做任何你想要的操作,比如修改函数参数,监控函数调用等等。 注意,由于hook操作需要对目标进程进行内存入操作,因此在某些情况下可能导致目标进程奔溃或者产生其他不可预知的错误。所以在实际使用中,需要对目标进程进行充分的测试和评估,以确保hook操作的稳定性和安全性。 ### 回答3: 在 C 语言中,编一个 hook(钩子)实际上是指在程序运行过程中拦截和处理特定事件的方式。下面是一个简单的示例,用 C 语言编一个钩子函数: ```c #include <stdio.h> #include <stdlib.h> void hookFunction() { printf("Hook function called!\n"); // 在这里添加处理逻辑 // ... } void originalFunction() { printf("Original function called!\n"); // 在这里添加原始函数的逻辑 // ... } int main() { // 将钩子函数与原始函数绑定 void (*original)() = originalFunction; originalFunction = hookFunction; // 调用原始函数,实际上会触发钩子函数 originalFunction(); // 恢复原始函数 originalFunction = original; // 再次调用原始函数,不会触发钩子函数 originalFunction(); return 0; } ``` 在以上示例中,我们先定义了一个 hookFunction() 函数,它是我们的钩子函数。然后,我们定义了一个原始函数 originalFunction(),它是我们要绑定钩子的目标函数。 在 main() 函数中,我们将原始函数的地址赋给一个函数指针 original,然后将钩子函数 hookFunction() 绑定到 originalFunction。 当我们调用 originalFunction() 时,实际上会触发钩子函数 hookFunction(),从而输出 "Hook function called!"。 接着,我们将 original 函数指针重新赋给 originalFunction,从而恢复原始函数。 最后,再次调用 originalFunction(),输出 "Original function called!",此时不会触发钩子函数。 这就是一个简单的用 C 语言编的钩子示例。根据实际需求,可以根据事件来编不同的钩子函数和处理逻辑。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值