首页
论坛
课程
招聘
[原创]通过C完成对C#程序的注入与HOOK
2021-10-13 17:26 20393

[原创]通过C完成对C#程序的注入与HOOK

2021-10-13 17:26
20393

JIT临时编译现象观察

为了某个目的,我设计一些简单的实验,最后做了一下笔记,以下是笔记内容。

 

C#程序在运行时是通过JIT临时编译而成,所以每次函数编译后的代码存放在一个随机的内存地址,如果我们想使用C进行HOOK,则需要取得这个随机地址。

 

首先对这个现象进行观察:

 

1、创建观察对象,这里我选择命令行应用就足够了

 

1

 

2、将默认生成的代码稍作修改

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
using System;
using System.Reflection;
 
namespace DemoAlice
{
    class Program
    {
        static public void Main(string[] args)
        {
            test(123);
            while (true)
            {
                MethodInfo mi = typeof(Program).GetMethod("test");
                Console.WriteLine(string.Format("{0:X8}", (int)mi.MethodHandle.GetFunctionPointer()));
            }
        }
 
        static public void test(int num)
        {
            int a, b, c;
            a = 0x20;
            b = 0x10;
            c = a + b;
            Console.WriteLine("someThing : " + num.ToString());
        }
    }
}

这里用到了C#中的反射方法拿到test函数地址
这个test也是后续我们用来测试hook的函数

 

2

 

运行之后,我们可以看到这个函数地址。

 

3

 

其中0x2AA098A地址开始显然是我们的test代码逻辑,说明这个函数位置找对了,且临时编译的代码也是寻常的汇编字节码,并不是具有虚拟意义的字节码,这样也就是说寻常的二进制字节码只要插入这段内存,就可以修改原本的程序逻辑,剩下的问题就只有如何定位这段代码了。

 

在重复运行后可以观察得到,每次临时编译后的临时代码也是相同的。

 

基于上述观察不难得到两种定位的思路

 

第一种:通过C++/CLI的特性,同样使用反射也能够拿到编译后的函数地址。

 

第二种:通过快速搜索内存的方法,直接搜索临时代码的特征。

注入

为了测试我的HOOK方法是否好用,那么随便写个远程线程注入吧,毕竟重点不在这。

 

由于是做实验,我这里的路径什么的就很随意的写死了。

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
#include <Windows.h>
#include <stdio.h>
 
int main()
{
    DWORD Pid = 0;
    printf("PID : ");
    scanf_s("%d", &Pid);
 
    HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Pid);
 
    char fileName[100] = "C:\\Users\\admin\\source\\repos\\DemoAlice\\Debug\\injectordll.dll";
 
    LPVOID pszLibFileRemote = VirtualAllocEx(hProc, NULL, 0x100, MEM_COMMIT, PAGE_READWRITE);
 
    DWORD n = WriteProcessMemory(hProc, pszLibFileRemote, fileName, 60, NULL);
 
    PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(L"Kernel32"), "LoadLibraryA");
 
    HANDLE hThread = CreateRemoteThread(hProc, NULL, 0, pfnThreadRtn, pszLibFileRemote, 0, NULL);
 
    CloseHandle(hProc);
 
    system("pause");
}

定位test函数

使用C++/CLI的反射拿到函数地址

创建一个C++的DLL项目,需要修改一下项目配置

 

4

 

打开公共语言运行时支持

 

5

 

一致性模式选择NO

 

然后编写DLL代码

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
31
32
33
34
35
36
37
38
// dllmain.cpp : Defines the entry point for the DLL application.
#include "pch.h"
#include <stdio.h>
 
using namespace System;
using namespace Reflection;
 
void showTest()
{
    Type^ type = Type::GetType("DemoAlice.Program,DemoAlice");
    MethodInfo^ method = type->GetMethod("test", BindingFlags::Static | BindingFlags::Public);
    PVOID address = (PVOID)method->MethodHandle.GetFunctionPointer();
    char DebugString[1024] = { 0 };
    sprintf_s(DebugString, 1024, "[+] : 0x%x\r\n", address);
    OutputDebugStringA(DebugString);
 
    sprintf_s(DebugString, 1024, "[+] : 0x%x\r\n", *(DWORD*)address);
    OutputDebugStringA(DebugString);
}
 
BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    HANDLE hThread;
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)showTest, NULL, NULL, NULL);
        CloseHandle(hThread);
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

从输出中看到这种定位是成功的

 

6

纯C也可以使用特征定位

直接上代码

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include "pch.h"
#include <stdio.h>
 
void showTest()
{
    SYSTEM_INFO sysinfo = { 0 };
    GetSystemInfo(&sysinfo);
    char* p = (char *)sysinfo.lpMinimumApplicationAddress;
    MEMORY_BASIC_INFORMATION meminfo = { 0 };
    DWORD targetAddr = 0;
    char DebugString[1024] = { 0 };
    while (p < sysinfo.lpMaximumApplicationAddress)
    {
        size_t size = VirtualQueryEx((HANDLE)-1, p, &meminfo, sizeof(MEMORY_BASIC_INFORMATION));
        if (size != sizeof(MEMORY_BASIC_INFORMATION))break;
        if (meminfo.Protect == PAGE_EXECUTE_READWRITE)
        {
            int addr = (int)meminfo.BaseAddress;
 
            for (int i = 0; i < meminfo.RegionSize; i++)
            {
                if (*(BYTE*)(addr + i) == 0x55
                    && *(BYTE*)(addr + i + 1) == 0x8B
                    && *(BYTE*)(addr + i + 2) == 0xEC
                    && *(BYTE*)(addr + i + 3) == 0x83
                    && *(BYTE*)(addr + i + 4) == 0xEC
                    && *(BYTE*)(addr + i + 5) == 0x1C
                    && *(BYTE*)(addr + i + 6) == 0x33
                    && *(BYTE*)(addr + i + 7) == 0xC0
                    && *(BYTE*)(addr + i + 8) == 0x89
                    && *(BYTE*)(addr + i + 9) == 0x45
                    && *(BYTE*)(addr + i + 10) == 0xEC
                    && *(BYTE*)(addr + i + 11) == 0x89
                    && *(BYTE*)(addr + i + 12) == 0x45
                    && *(BYTE*)(addr + i + 13) == 0xE8
                    && *(BYTE*)(addr + i + 14) == 0x89
                    && *(BYTE*)(addr + i + 15) == 0x45
                    && *(BYTE*)(addr + i + 16) == 0xE4
                    && *(BYTE*)(addr + i + 17) == 0x89
                    && *(BYTE*)(addr + i + 18) == 0x4D
                    && *(BYTE*)(addr + i + 19) == 0xFC)
                {
                    targetAddr = addr + i;
                    break;
                }
 
            }
 
        }
        p += meminfo.RegionSize;
        if (targetAddr)break;
    }
 
    sprintf_s(DebugString, 1024, "[+] : 0x%x\r\n", targetAddr);
    OutputDebugStringA(DebugString);
}
 
BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    HANDLE hThread;
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)showTest, NULL, NULL, NULL);
        CloseHandle(hThread);
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

可以看到结果,确实能够快速定位到目标函数

 

7

最后,HOOK

HOOK部分各种HOOk姿势其实都是可以的,我这里就用我用的比较顺手的钩子库MinHook

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
#include "pch.h"
#include <stdio.h>
#include "MinHook.h"
 
#pragma comment(lib,"libMinHook.lib")
 
PVOID OriginAddr = 0;
 
void realHookThing()
{
    printf("hello C#,I'm C++\n");
}
 
void __declspec(naked)MyTest()
{
    __asm
    {
        call realHookThing;
        mov ecx, 22b8h;
        jmp OriginAddr;
    }
}
 
void showTest()
{
    SYSTEM_INFO sysinfo = { 0 };
    GetSystemInfo(&sysinfo);
    char* p = (char *)sysinfo.lpMinimumApplicationAddress;
    MEMORY_BASIC_INFORMATION meminfo = { 0 };
    DWORD targetAddr = 0;
    char DebugString[1024] = { 0 };
    while (p < sysinfo.lpMaximumApplicationAddress)
    {
        size_t size = VirtualQueryEx((HANDLE)-1, p, &meminfo, sizeof(MEMORY_BASIC_INFORMATION));
        if (size != sizeof(MEMORY_BASIC_INFORMATION))break;
        if (meminfo.Protect == PAGE_EXECUTE_READWRITE)
        {
            int addr = (int)meminfo.BaseAddress;
 
            for (int i = 0; i < meminfo.RegionSize; i++)
            {
                if (*(BYTE*)(addr + i) == 0x55
                    && *(BYTE*)(addr + i + 1) == 0x8B
                    && *(BYTE*)(addr + i + 2) == 0xEC
                    && *(BYTE*)(addr + i + 3) == 0x83
                    && *(BYTE*)(addr + i + 4) == 0xEC
                    && *(BYTE*)(addr + i + 5) == 0x1C
                    && *(BYTE*)(addr + i + 6) == 0x33
                    && *(BYTE*)(addr + i + 7) == 0xC0
                    && *(BYTE*)(addr + i + 8) == 0x89
                    && *(BYTE*)(addr + i + 9) == 0x45
                    && *(BYTE*)(addr + i + 10) == 0xEC
                    && *(BYTE*)(addr + i + 11) == 0x89
                    && *(BYTE*)(addr + i + 12) == 0x45
                    && *(BYTE*)(addr + i + 13) == 0xE8
                    && *(BYTE*)(addr + i + 14) == 0x89
                    && *(BYTE*)(addr + i + 15) == 0x45
                    && *(BYTE*)(addr + i + 16) == 0xE4
                    && *(BYTE*)(addr + i + 17) == 0x89
                    && *(BYTE*)(addr + i + 18) == 0x4D
                    && *(BYTE*)(addr + i + 19) == 0xFC)
                {
                    targetAddr = addr + i;
                    break;
                }
 
            }
 
        }
        p += meminfo.RegionSize;
        if (targetAddr)break;
    }
 
    sprintf_s(DebugString, 1024, "[+] : 0x%x\r\n", targetAddr);
    OutputDebugStringA(DebugString);
 
    if (MH_Initialize() != MH_OK)
    {
        return;
    }
 
    // Create a hook for MessageBoxW, in disabled state.
    if (MH_CreateHook((LPVOID)targetAddr, (LPVOID)MyTest, reinterpret_cast<LPVOID*>(&OriginAddr)) != MH_OK)
    {
        return;
    }
 
    if (MH_EnableHook((LPVOID)targetAddr) != MH_OK)
    {
        return;
    }
}
 
BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    HANDLE hThread;
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)showTest, NULL, NULL, NULL);
        CloseHandle(hThread);
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

为了看到效果,我们修改一下C# ,让他不停调用test函数

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
using System;
using System.Reflection;
 
namespace DemoAlice
{
    class Program
    {
        static public void Main(string[] args)
        {
 
            while (true)
            {
                test(123);
                //MethodInfo mi = typeof(Program).GetMethod("test");
                //Console.WriteLine(string.Format("{0:X8}", (int)mi.MethodHandle.GetFunctionPointer()));
            }
        }
 
        static public void test(int num)
        {
            int a, b, c;
            a = 0x20;
            b = 0x10;
            c = a + b;
            Console.WriteLine("someThing : " + num.ToString());
        }
    }
}

最后看到效果

 

8

最后一点想法

通过上述的一些简单实验,不难看出,就算是C#这种即时编译的语言,我们依旧可以从底层去做一些攻防相关的事情

 

依我拙见,接下来可以做的事情有:

 

1、由于即时编译的特性,只要函数被调用了,就会在内存的某一块地方存在相应的汇编代码。内存快速查找可以方便的定位到特征,这一点可以做很多事,比如反病毒、反木马、游戏关键逻辑修改等等。

 

2、有一点骚的想法是我自己注我自己,C#的功能或许可以通过上述的方式,在注入时把对应的目标汇编代码加密,然后把解密函数写到HOOK的部分,甚至把部分功能拆开写,一部分写到HOOK的逻辑里,这样的程序功能仍然能够保证,但是单独分析C#程序和注入用的DLL就比较难发现完整的逻辑,两个文件彼此之间也没有太强的联系。我认为这一点也同样可以用在攻防里。

 

3、。。。。

 

想做的事情很多,还得一个个实验过去,慢慢来吧。


[2022冬季班]《安卓高级研修班(网课)》月薪两万班招生中~

最后于 2021-10-14 01:56 被airshelf编辑 ,原因: 错别字订正
收藏
点赞4
打赏
分享
最新回复 (11)
雪    币: 1891
活跃值: 活跃值 (1482)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
LexSafe 活跃值 2021-10-13 17:47
2
1
第二条这个思路很骚啊
雪    币: 3548
活跃值: 活跃值 (942)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
chinasmu 活跃值 2021-10-13 22:53
3
0
jit是运行前解析成汇编吗,这样你获得了地址但是他的代码应该运行完一遍了吧,如果只运行一次的话hook了也没啥用吧,除非像实验的那种不断调用的
雪    币: 1193
活跃值: 活跃值 (347)
能力值: ( LV3,RANK:39 )
在线值:
发帖
回帖
粉丝
airshelf 活跃值 2021-10-14 01:53
4
0
chinasmu jit是运行前解析成汇编吗,这样你获得了地址但是他的代码应该运行完一遍了吧,如果只运行一次的话hook了也没啥用吧,除非像实验的那种不断调用的
确实,针对只运行一次的函数HOOK意义不大,要是想在运行前就改变原有的逻辑的话,我觉得从IL层入手是个不错的选择。
其实真实环境中还是有许多这种重复调用的例子的,比如身份认证的计算、木马的心跳、游戏的弹道计算等等这些函数都是会不停被调用的,这种情况下这种HOOK的方法还是可以一用的
雪    币: 73
活跃值: 活跃值 (676)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
hixhi 活跃值 2021-10-14 15:10
5
0
微软自己有一个叫做.net profiler我记得就可以。。。不知道跟楼主说的这个有没有啥区别。。
雪    币: 179
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_hgrbqfun 活跃值 2021-10-14 16:42
6
0
chinasmu jit是运行前解析成汇编吗,这样你获得了地址但是他的代码应该运行完一遍了吧,如果只运行一次的话hook了也没啥用吧,除非像实验的那种不断调用的

估计可以针对il执行下手,hook jit编译的逻辑的c/c++函数,这样在第一时间就能hook了。

比如mscorjit!CILJit::compileMethod 和mscorjit!jitNativeCode(这两个函数是.NET 2.0的)。


net 4.0的好像是clrjit.dll的getJit函数,这个函数会返回jit的函数表,再去表里面去找CILJit::compileMethod的函数地址。


不少net加固和逆向托阔就是hook net的jit实现的。


hook这些底层的函数就能第一时间hook目标的c#函数。

最后于 2021-10-14 17:27 被mb_hgrbqfun编辑 ,原因:
雪    币: 278
活跃值: 活跃值 (707)
能力值: ( LV7,RANK:140 )
在线值:
发帖
回帖
粉丝
yeyeshun 活跃值 2 2021-10-14 17:07
7
0
特征码这个不太可行吧,我记得每次都是有可能变化的
雪    币: 6
活跃值: 活跃值 (507)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
路易 活跃值 2021-10-14 17:22
8
0
第一个方法比较靠谱。取得的地址是准确的。只要把HOOK结构导出给c#用就好了
雪    币: 1193
活跃值: 活跃值 (347)
能力值: ( LV3,RANK:39 )
在线值:
发帖
回帖
粉丝
airshelf 活跃值 2021-10-14 18:10
9
0
yeyeshun 特征码这个不太可行吧,我记得每次都是有可能变化的
嗯,确实是会有变化,这个在我看来和几个因素有关,一个是JIT的版本、另一个是那些可变的地址和偏移。把这两个因素排除掉,特征码应该还是可以的。
雪    币: 278
活跃值: 活跃值 (707)
能力值: ( LV7,RANK:140 )
在线值:
发帖
回帖
粉丝
yeyeshun 活跃值 2 2021-10-15 09:37
10
0
airshelf 嗯,确实是会有变化,这个在我看来和几个因素有关,一个是JIT的版本、另一个是那些可变的地址和偏移。把这两个因素排除掉,特征码应该还是可以的。[em_13]
我意思是同一个jit版本,不包含可变的地址和偏移的情况下,程序多次执行的时候,同一条IL可能会被解释为不同的x86汇编。
雪    币: 981
活跃值: 活跃值 (891)
能力值: ( LV6,RANK:80 )
在线值:
发帖
回帖
粉丝
HOWMP 活跃值 1 2021-10-20 18:37
11
1

题主可以看看 https://github.com/MonoMod/MonoMod.Common/

支持各种架构和.NET平台

雪    币: 1788
活跃值: 活跃值 (852)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
拍拖 活跃值 2 2021-10-22 15:03
12
0
.NET加壳软件很多就是按你说的第二个思路来做的。HOOK了JIT编译的API,然后实现解码后编译。
游客
登录 | 注册 方可回帖
返回