首页
社区
课程
招聘
[原创]通过C完成对C#程序的注入与HOOK
发表于: 2021-10-13 17:26 24698

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

2021-10-13 17:26
24698

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

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

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

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

1

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

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

2

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

3

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

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

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

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

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

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

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

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

4

打开公共语言运行时支持

5

一致性模式选择NO

然后编写DLL代码

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

6

直接上代码

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

7

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

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

最后看到效果

8

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

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

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

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

3、。。。。

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

 
 
 
 
 
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());
        }
    }
}
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());
        }
    }
}
 
 
 
 
 
 
 
 
 
#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");
}
#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");
}
 
 
 
 
 
// 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;
}
// 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;
}
 
#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;
}
#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

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2021-10-14 01:56 被zx_730666编辑 ,原因: 错别字订正
收藏
免费 6
支持
分享
最新回复 (11)
雪    币: 2325
活跃值: (2304)
能力值: ( LV6,RANK:89 )
在线值:
发帖
回帖
粉丝
2
第二条这个思路很骚啊
2021-10-13 17:47
1
雪    币: 6270
活跃值: (3335)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
jit是运行前解析成汇编吗,这样你获得了地址但是他的代码应该运行完一遍了吧,如果只运行一次的话hook了也没啥用吧,除非像实验的那种不断调用的
2021-10-13 22:53
0
雪    币: 1195
活跃值: (419)
能力值: ( LV3,RANK:39 )
在线值:
发帖
回帖
粉丝
4
chinasmu jit是运行前解析成汇编吗,这样你获得了地址但是他的代码应该运行完一遍了吧,如果只运行一次的话hook了也没啥用吧,除非像实验的那种不断调用的
确实,针对只运行一次的函数HOOK意义不大,要是想在运行前就改变原有的逻辑的话,我觉得从IL层入手是个不错的选择。
其实真实环境中还是有许多这种重复调用的例子的,比如身份认证的计算、木马的心跳、游戏的弹道计算等等这些函数都是会不停被调用的,这种情况下这种HOOK的方法还是可以一用的
2021-10-14 01:53
0
雪    币: 73
活跃值: (923)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
微软自己有一个叫做.net profiler我记得就可以。。。不知道跟楼主说的这个有没有啥区别。。
2021-10-14 15:10
0
雪    币: 237
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
6
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编辑 ,原因:
2021-10-14 16:42
0
雪    币: 775
活跃值: (3420)
能力值: ( LV7,RANK:140 )
在线值:
发帖
回帖
粉丝
7
特征码这个不太可行吧,我记得每次都是有可能变化的
2021-10-14 17:07
0
雪    币: 6
活跃值: (866)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
第一个方法比较靠谱。取得的地址是准确的。只要把HOOK结构导出给c#用就好了
2021-10-14 17:22
0
雪    币: 1195
活跃值: (419)
能力值: ( LV3,RANK:39 )
在线值:
发帖
回帖
粉丝
9
yeyeshun 特征码这个不太可行吧,我记得每次都是有可能变化的
嗯,确实是会有变化,这个在我看来和几个因素有关,一个是JIT的版本、另一个是那些可变的地址和偏移。把这两个因素排除掉,特征码应该还是可以的。
2021-10-14 18:10
0
雪    币: 775
活跃值: (3420)
能力值: ( LV7,RANK:140 )
在线值:
发帖
回帖
粉丝
10
airshelf 嗯,确实是会有变化,这个在我看来和几个因素有关,一个是JIT的版本、另一个是那些可变的地址和偏移。把这两个因素排除掉,特征码应该还是可以的。[em_13]
我意思是同一个jit版本,不包含可变的地址和偏移的情况下,程序多次执行的时候,同一条IL可能会被解释为不同的x86汇编。
2021-10-15 09:37
0
雪    币: 2835
活跃值: (2643)
能力值: ( LV6,RANK:80 )
在线值:
发帖
回帖
粉丝
11

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

支持各种架构和.NET平台

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