首页
社区
课程
招聘
[原创]关于Unity游戏Mono注入型外挂的一个检测思路
发表于: 2020-12-20 23:42 17329

[原创]关于Unity游戏Mono注入型外挂的一个检测思路

2020-12-20 23:42
17329

朋友们好啊,我是混元编程门掌门人马保国,刚才有个朋友问我马老师发生什么事了,我说怎么回事?给我发了一个张截图,我一看,哦,源赖氏佐田,有一个年轻人,10多岁,塔说,塔写了个unity(SSJJ)游戏的外挂,还是注入直接调用SDK,功能十分牛逼,问我马老师你能不能检测出我?我说可以,我说你用mono注入不好用,他不服气,我说小朋友,你两个手指来跟我一根手指比敲代码,我来检测你,你来过检测,他敲不动,他说我这个检测没用,我说我这个有用,这是化劲,传统功夫讲化劲的,他说要和我试试,我说可以,我一说,他啪就站起来了,很快啊,然后上来就是一个嫖开源代码注入器dnspy反编译游戏代码调用SDK做挂,我大意了啊,没有闪。它的挂给我游戏蹭了一下,我当时就流眼泪了,但是没关系啊,我两分钟过后就好了,把他检测了。(-_-以上是废话,可以掠过)

unity游戏使用的c#虚拟机是mono,一般注入型又能调用SDK的c#dll外挂一般都会调用mono的函数来完成注入,通常来说流程是这样

在这里我们有两个方面可以努力一下:

其一是在注入时,我们可以通过hook mono的载入dll函数,验证这个dll是否属于游戏自身的dll,来达到防止注入的目的;亦或是将自己的所有dll都以自己知道的加密方式加密,然后载入dll时按照加密方式解密,这样如果一个未经加密的dll想要注入进来,经过解密流程后就变得不可用

其二便是主动遍历模块,mono载入c#模块跟windows载入dll模块类似,会维护一个全局的链表,存储所有模块的信息,我们遍历这个链表即可得到所有载入dll的信息,然后根据需要判断该dll是否为可疑dll

mono下载最新的mono源码,用VS打开,直接来到载入镜像函数mono_image_open_from_data_with_name

可以看到,没有名字的镜像调用这个函数载入时,mono会自动为其分配一个名字,这个名字格式就是data-镜像内存地址

继续往下跟进,发现调用了do_mono_image_load函数,这里就是镜像载入的核心函数,最后调用的register_image是我们关注的重点,因为执行了这个函数,image就彻底被加入到了mono维护的链表中,我们点进去看一下这个函数

注意到代码的第18-22行完成了一个 g_hash_table_insert 的操作,而在glib中,hash_table是一个类似于map的映射表,我们可以看到,这个插入操作往loaded_images这个表里面插入了一项,而这个loaded_images可以在第5行代码看到是一个全局变量

于是乎我们找到了这个全局链表的位置

image.c 47-51

他们的初始化位于mono_images_init

所以我们可以通过特征很方便的定位到这两个表的地址,然后用glib开源的方法遍历即可

另外,还有一个表也很有用,可以作为检测的途径,就是 assembly.c 中的 loaded_assemblies

assembly.c 114-117

定位变量

遍历

g_hash_table_foreachg_list_foreach等都是glib的函数,在mono的源码里有,自己去CV吧

遍历回调函数

小伙子忙说对不起对不起,我不懂规矩啊,我说他是乱打的,他可不是乱打啊,铮铮鞭腿左刺拳训练有素,后来他说他练过三四年泰拳,看来是有备而来

他说他把链表断了,然后把PE头也清掉了,还把所有字符串都清空了

我去~

这个年轻人,不讲武德

来,骗!

来,偷袭!

我69岁的老同志。

这好吗?这不好。我劝这位年轻人,耗子尾汁。好好反思。以后不要再犯这样的小聪明。武林要以和为贵,要讲武德,不要搞窝里斗。

远程写入dll数据 -> 外部获取mono导出函数表地址 -> 写入调用mono函数的shellcode -> 远程创建线程调用shellcode -> 查询加载结果
远程写入dll数据 -> 外部获取mono导出函数表地址 -> 写入调用mono函数的shellcode -> 远程创建线程调用shellcode -> 查询加载结果
 
 
MonoImage *
mono_image_open_from_data_with_name (char *data, guint32 data_len, gboolean need_copy, MonoImageOpenStatus *status, gboolean refonly, const char *name)
{
    MonoCLIImageInfo *iinfo;
    MonoImage *image;
    char *datac;
 
    if (!data || !data_len) {
        if (status)
            *status = MONO_IMAGE_IMAGE_INVALID;
        return NULL;
    }
    datac = data;
    if (need_copy) {
        datac = g_try_malloc (data_len);
        if (!datac) {
            if (status)
                *status = MONO_IMAGE_ERROR_ERRNO;
            return NULL;
        }
        memcpy (datac, data, data_len);
    }
 
    image = g_new0 (MonoImage, 1);
    image->raw_data = datac;
    image->raw_data_len = data_len;
    image->raw_data_allocated = need_copy;
    image->name = (name == NULL) ? g_strdup_printf ("data-%p", datac) : g_strdup(name);
    iinfo = g_new0 (MonoCLIImageInfo, 1);
    image->image_info = iinfo;
    image->ref_only = refonly;
    image->ref_count = 1;
 
    image = do_mono_image_load (image, status, TRUE, TRUE);
    if (image == NULL)
        return NULL;
 
    return register_image (image);
}
MonoImage *
mono_image_open_from_data_with_name (char *data, guint32 data_len, gboolean need_copy, MonoImageOpenStatus *status, gboolean refonly, const char *name)
{
    MonoCLIImageInfo *iinfo;
    MonoImage *image;
    char *datac;
 
    if (!data || !data_len) {
        if (status)
            *status = MONO_IMAGE_IMAGE_INVALID;
        return NULL;
    }
    datac = data;
    if (need_copy) {
        datac = g_try_malloc (data_len);
        if (!datac) {
            if (status)
                *status = MONO_IMAGE_ERROR_ERRNO;
            return NULL;
        }
        memcpy (datac, data, data_len);
    }
 
    image = g_new0 (MonoImage, 1);
    image->raw_data = datac;
    image->raw_data_len = data_len;
    image->raw_data_allocated = need_copy;
    image->name = (name == NULL) ? g_strdup_printf ("data-%p", datac) : g_strdup(name);
    iinfo = g_new0 (MonoCLIImageInfo, 1);
    image->image_info = iinfo;
    image->ref_only = refonly;
    image->ref_count = 1;
 
    image = do_mono_image_load (image, status, TRUE, TRUE);
    if (image == NULL)
        return NULL;
 
    return register_image (image);
}
 
static MonoImage *
register_image (MonoImage *image)
{
    MonoImage *image2;
    GHashTable *loaded_images = image->ref_only ? loaded_images_refonly_hash : loaded_images_hash;
 
    mono_images_lock ();
    image2 = g_hash_table_lookup (loaded_images, image->name);
 
    if (image2) {
        /* Somebody else beat us to it */
        mono_image_addref (image2);
        mono_images_unlock ();
        mono_image_close (image);
        return image2;
    }
    g_hash_table_insert (loaded_images, image->name, image);
    if (image->assembly_name && (g_hash_table_lookup (loaded_images, image->assembly_name) == NULL))
        g_hash_table_insert (loaded_images, (char *) image->assembly_name, image);   
    mono_images_unlock ();
 
    return image;
}
static MonoImage *
register_image (MonoImage *image)
{
    MonoImage *image2;
    GHashTable *loaded_images = image->ref_only ? loaded_images_refonly_hash : loaded_images_hash;
 
    mono_images_lock ();
    image2 = g_hash_table_lookup (loaded_images, image->name);
 
    if (image2) {
        /* Somebody else beat us to it */
        mono_image_addref (image2);
        mono_images_unlock ();
        mono_image_close (image);
        return image2;
    }
    g_hash_table_insert (loaded_images, image->name, image);
    if (image->assembly_name && (g_hash_table_lookup (loaded_images, image->assembly_name) == NULL))
        g_hash_table_insert (loaded_images, (char *) image->assembly_name, image);   
    mono_images_unlock ();
 
    return image;
}
 
 
/*
 * Keeps track of the various assemblies loaded
 */
static GHashTable *loaded_images_hash;
static GHashTable *loaded_images_refonly_hash;
/*
 * Keeps track of the various assemblies loaded
 */
static GHashTable *loaded_images_hash;
static GHashTable *loaded_images_refonly_hash;
/**
 * mono_images_init:
 *
 *  Initialize the global variables used by this module.
 */
void
mono_images_init (void)
{
    InitializeCriticalSection (&images_mutex);
 
    loaded_images_hash = g_hash_table_new (g_str_hash, g_str_equal);
    loaded_images_refonly_hash = g_hash_table_new (g_str_hash, g_str_equal);
 
    debug_assembly_unload = g_getenv ("MONO_DEBUG_ASSEMBLY_UNLOAD") != NULL;
 
    mutex_inited = TRUE;
}
/**
 * mono_images_init:
 *
 *  Initialize the global variables used by this module.
 */
void
mono_images_init (void)
{
    InitializeCriticalSection (&images_mutex);
 
    loaded_images_hash = g_hash_table_new (g_str_hash, g_str_equal);
    loaded_images_refonly_hash = g_hash_table_new (g_str_hash, g_str_equal);
 
    debug_assembly_unload = g_getenv ("MONO_DEBUG_ASSEMBLY_UNLOAD") != NULL;
 
    mutex_inited = TRUE;
}
 
 
/*
 * keeps track of loaded assemblies
 */
static GList *loaded_assemblies = NULL;
/*
 * keeps track of loaded assemblies
 */
static GList *loaded_assemblies = NULL;
PVOID LocateLoadedAssemblies() {
#ifdef ver5_6_5_x86
    /*
        Function: mono_assembly_foreach
        Byte:
            56                            push    esi
            57                            push    edi
            BF 84 B4 20 10                mov     edi, offset stru_1020B484
            57                            push    edi; lpCriticalSection
            FF 15 4C 61 14 10             call    ds : EnterCriticalSection
            FF 35 7C B4 20 10             push    loaded_assemblies
    */
    PVOID monoModule = GetModuleHandleA("mono.dll");
    PVOID mono_assembly_foreach = GetProcAddress((HMODULE)monoModule, "mono_assembly_foreach");
    vector<ULONG64> res;
    int Cnt = SearchMemory((HANDLE)-1, (char*)"56 57 BF ?? ?? ?? ?? 57 ?? ?? ?? ?? ?? ?? FF 35 ?? ?? ?? ??", (unsigned long long)mono_assembly_foreach, (unsigned long long)((DWORD)mono_assembly_foreach + 50), 10, res);
    Print("SigCnt2 = %d", Cnt);
    if (Cnt == 1) {
        ULONG64 OffsetAddress = res[0] + 16;
        int LoadedAssemblies = *(int*)OffsetAddress;
        Print("LoadedAssemblies = %X", LoadedAssemblies);
        return (PVOID)(LoadedAssemblies);
    }
    return 0;
#else
    return 0;
#endif
}
 
PVOID LocateLoadedImagesHashAddress() {
#ifdef ver5_6_5_x86
    /*
        Function: mono_image_init
        Byte:
            57                            push    edi
            56                            push    esi
            E8 FA 74 FC FF                call    sub_100012C0
            57                            push    edi
            56                            push    esi
            A3 F4 B6 20 10                mov     loaded_images_hash, eax
    */
    PVOID monoModule = GetModuleHandleA("mono.dll");
    Print("MonoModule = %X", monoModule);
    PVOID mono_image_init = GetProcAddress((HMODULE)monoModule, "mono_images_init");
    Print("mono_image_init = %X", mono_image_init);
    vector<ULONG64> res;
    int Cnt = SearchMemory((HANDLE)-1, (char*)"57 56 E8 ?? ?? ?? ?? 57 56 A3 ?? ?? ?? ??", (unsigned long long)mono_image_init, (unsigned long long)((DWORD)mono_image_init + 50), 10, res);
    Print("SigCnt = %d", Cnt);
    if (Cnt == 1) {
        ULONG64 OffsetAddress = res[0] + 10;
        int LoadedImagesHashAddress = *(int*)OffsetAddress;
        Print("LoadedImagesHashAddress = %X", LoadedImagesHashAddress);
        return (PVOID)(LoadedImagesHashAddress);
    }
    return 0;
#else
    return 0;
#endif
 
}
PVOID LocateLoadedAssemblies() {
#ifdef ver5_6_5_x86
    /*
        Function: mono_assembly_foreach
        Byte:
            56                            push    esi
            57                            push    edi
            BF 84 B4 20 10                mov     edi, offset stru_1020B484
            57                            push    edi; lpCriticalSection
            FF 15 4C 61 14 10             call    ds : EnterCriticalSection
            FF 35 7C B4 20 10             push    loaded_assemblies
    */
    PVOID monoModule = GetModuleHandleA("mono.dll");
    PVOID mono_assembly_foreach = GetProcAddress((HMODULE)monoModule, "mono_assembly_foreach");
    vector<ULONG64> res;
    int Cnt = SearchMemory((HANDLE)-1, (char*)"56 57 BF ?? ?? ?? ?? 57 ?? ?? ?? ?? ?? ?? FF 35 ?? ?? ?? ??", (unsigned long long)mono_assembly_foreach, (unsigned long long)((DWORD)mono_assembly_foreach + 50), 10, res);
    Print("SigCnt2 = %d", Cnt);
    if (Cnt == 1) {
        ULONG64 OffsetAddress = res[0] + 16;
        int LoadedAssemblies = *(int*)OffsetAddress;
        Print("LoadedAssemblies = %X", LoadedAssemblies);
        return (PVOID)(LoadedAssemblies);
    }
    return 0;
#else
    return 0;
#endif
}
 
PVOID LocateLoadedImagesHashAddress() {
#ifdef ver5_6_5_x86

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

最后于 2020-12-20 23:44 被renbohan编辑 ,原因: 原创
收藏
免费 5
支持
分享
最新回复 (10)
雪    币: 9034
活跃值: (6255)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
2

好像根本不用這樣,遊戲大部分有熱更系統,把東西丟到熱更系統就好了,需要數據直接讓遊戲內嵌的json模塊輸出來。il2cpp 導出的東西太全,基本防不住外掛。主要難點還是在調用的地方需要遊戲主線程操作。一般自己申請一個組件掛接到遊戲系統裏,這一步操作完了一路暢通。

最后于 2020-12-21 04:06 被mudebug编辑 ,原因:
2020-12-21 04:04
0
雪    币: 458
活跃值: (1882)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
3
mudebug 好像根本不用這樣,遊戲大部分有熱更系統,把東西丟到熱更系統就好了,需要數據直接讓遊戲內嵌的json模塊輸出來。il2cpp&nbsp;導出的東西太全,基本防不住外掛。主要難點還是在調用的地方需 ...
沒有明白你什麼意思,我這個是針對使用mono的unity遊戲,而不是用il2cpp的遊戲,還有是在模塊層面上進行檢測,如果要檢測GameObject組件也是有方法,但不是本文討論點
2020-12-21 07:07
0
雪    币: 92
活跃值: (508)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
2020-12-21 12:43
0
雪    币: 137
活跃值: (1380)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
好像几年前一款unity游戏的方法是扫那个unity的function然后做一个hash 服务端也保留一份hash 有注入函数进来的时候hash会变
2020-12-21 16:36
0
雪    币: 1110
活跃值: (3274)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
6

好,又来个叫 BepInEx 的小伙子,在跃跃欲试,看老同志禁不禁打

2020-12-21 16:45
0
雪    币: 237
活跃值: (29)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
请问SSJJ 的你怎么还原DLL的?记得新版某易DLL PE无法还原出来
2020-12-21 17:42
0
雪    币: 123
活跃值: (316)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
huojier 好像几年前一款unity游戏的方法是扫那个unity的function然后做一个hash 服务端也保留一份hash 有注入函数进来的时候hash会变
对比hash值似乎很容易解决掉
2021-1-17 09:48
0
雪    币: 1411
活跃值: (737)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
10
可以.net层面直接检测有哪些Component,基于程序集的逃不开这个
2021-3-8 20:59
0
雪    币: 228
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
11
楼主 方便 给个联系方式吗 
2022-1-22 10:20
0
游客
登录 | 注册 方可回帖
返回
//