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

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

2020-12-20 23:42
17691

前言

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

思路

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

1
远程写入dll数据 -> 外部获取mono导出函数表地址 -> 写入调用mono函数的shellcode -> 远程创建线程调用shellcode -> 查询加载结果

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

 

其一是在注入时,我们可以通过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

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
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);
}

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

 

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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;
}

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

 

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

 

image.c 47-51

1
2
3
4
5
/*
 * Keeps track of the various assemblies loaded
 */
static GHashTable *loaded_images_hash;
static GHashTable *loaded_images_refonly_hash;

他们的初始化位于mono_images_init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
 * 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;
}

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

 

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

 

[注意]看雪招聘,专注安全领域的专业人才平台!

最后于 2020-12-20 23:44 被renbohan编辑 ,原因: 原创
收藏
免费 5
支持
分享
赞赏记录
参与人
雪币
留言
时间
Youlor
为你点赞!
2024-8-17 01:27
QinBeast
看雪因你而更加精彩!
2024-6-24 05:49
PLEBFE
为你点赞~
2022-7-30 09:29
yikuaiyingbi
为你点赞~
2022-5-17 21:59
caolinkai
为你点赞~
2021-2-24 10:05
最新回复 (10)
雪    币: 9640
活跃值: (6875)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
2

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

最后于 2020-12-21 04:06 被mudebug编辑 ,原因:
2020-12-21 04:04
0
雪    币: 458
活跃值: (1917)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
3
mudebug 好像根本不用這樣,遊戲大部分有熱更系統,把東西丟到熱更系統就好了,需要數據直接讓遊戲內嵌的json模塊輸出來。il2cpp 導出的東西太全,基本防不住外掛。主要難點還是在調用的地方需 ...
沒有明白你什麼意思,我這個是針對使用mono的unity遊戲,而不是用il2cpp的遊戲,還有是在模塊層面上進行檢測,如果要檢測GameObject組件也是有方法,但不是本文討論點
2020-12-21 07:07
0
雪    币: 92
活跃值: (508)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
2020-12-21 12:43
0
雪    币: 137
活跃值: (1490)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
好像几年前一款unity游戏的方法是扫那个unity的function然后做一个hash 服务端也保留一份hash 有注入函数进来的时候hash会变
2020-12-21 16:36
0
雪    币: 1109
活跃值: (3626)
能力值: ( 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
活跃值: (361)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
huojier 好像几年前一款unity游戏的方法是扫那个unity的function然后做一个hash 服务端也保留一份hash 有注入函数进来的时候hash会变
对比hash值似乎很容易解决掉
2021-1-17 09:48
0
雪    币: 1411
活跃值: (997)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
10
可以.net层面直接检测有哪些Component,基于程序集的逃不开这个
2021-3-8 20:59
0
雪    币: 228
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
11
楼主 方便 给个联系方式吗 
2022-1-22 10:20
0
游客
登录 | 注册 方可回帖
返回

账号登录
验证码登录

忘记密码?
没有账号?立即免费注册