首页
论坛
专栏
课程

[原创]Unity3D mono模式游戏保护之浅谈

2018-8-31 23:21 15023

[原创]Unity3D mono模式游戏保护之浅谈

2018-8-31 23:21
15023

前面两篇文章都是以破解为主,因为研究Unity3D的保护已经有段时间了,目前已迭代了几版了,因为某些原因,暂时不能公开,这里就只能先将以前用过的一种简单的保护方式分享出来,希望对研究Unity3D mono模式游戏保护的童鞋有所帮助。这种保护方式需要对CLR PE文件格式有所了解,特别是其中的metadata数据项。这边分析的mono源码版本为5.6。

 

这里的保护方式通过修改mono的源码,自己编译出libmono.so的方式来保护。mono_image_open_from_data_with_name这个关键的函数就不用说了,跟随函数的执行流程,通过过代码流程跟随到do_mono_image_load函数:

static MonoImage *
do_mono_image_load (MonoImage *image, MonoImageOpenStatus *status,
            gboolean care_about_cli, gboolean care_about_pecoff)
{
    MonoCLIImageInfo *iinfo;
    MonoDotNetHeader *header;

    mono_profiler_module_event (image, MONO_PROFILE_START_LOAD);

    /* if MONO_SECURITY_MODE_CORE_CLR is set then determine if this image is platform code */
    image->core_clr_platform_code = mono_security_core_clr_determine_platform_image (image);

    mono_image_init (image);

    iinfo = image->image_info;
    header = &iinfo->cli_header;

    if (status)
        *status = MONO_IMAGE_IMAGE_INVALID;

    if (care_about_pecoff == FALSE)
        goto done;

    if (!mono_verifier_verify_pe_data (image, NULL))
        goto invalid_image;

    if (!mono_image_load_pe_data (image))
        goto invalid_image;

    if (care_about_cli == FALSE) {
        goto done;
    }

    if (!mono_verifier_verify_cli_data (image, NULL))
        goto invalid_image;

    if (!mono_image_load_cli_data (image))
        goto invalid_image;

    if (!mono_verifier_verify_table_data (image, NULL))
        goto invalid_image;

#ifndef USE_COREE
    /* if the last bit is not set, then the image is mixed mode with native code */
    if (!(iinfo->cli_cli_header.ch_flags & 1))
        goto invalid_image;
#endif

    mono_image_load_names (image);

    load_modules (image);

done:
    mono_profiler_module_loaded (image, MONO_PROFILE_OK);
    if (status)
        *status = MONO_IMAGE_OK;

    return image;

invalid_image:
    mono_profiler_module_loaded (image, MONO_PROFILE_FAILED);
    mono_image_close (image);
        return NULL;
}

其中对Assembly-CSharp.dll和Assembly-CSharp-firstpass.dll加载的关键函数为mono_image_load_pe_data和mono_image_load_cli_data。可通过自定义do_mono_image_load、mono_image_load_pe_data、mono_image_load_cli_data以及后面相关的关键函数来对自定义的文件进行加载。

 

mono_image_load_pe_data函数内容如下:

gboolean
mono_image_load_pe_data (MonoImage *image)
{
    MonoCLIImageInfo *iinfo;
    MonoDotNetHeader *header;
    MonoMSDOSHeader msdos;
    gint32 offset = 0;

    iinfo = image->image_info;
    header = &iinfo->cli_header;

#ifdef USE_COREE
    if (!image->is_module_handle)
#endif
    if (offset + sizeof (msdos) > image->raw_data_len)
        goto invalid_image;
    memcpy (&msdos, image->raw_data + offset, sizeof (msdos));

    if (!(msdos.msdos_sig [0] == 'M' && msdos.msdos_sig [1] == 'Z'))
        goto invalid_image;

    msdos.pe_offset = GUINT32_FROM_LE (msdos.pe_offset);

    offset = msdos.pe_offset;

    offset = do_load_header (image, header, offset);
    if (offset < 0)
        goto invalid_image;

    /*
     * this tests for a x86 machine type, but itanium, amd64 and others could be used, too.
     * we skip this test.
    if (header->coff.coff_machine != 0x14c)
        goto invalid_image;
    */

#if 0
    if (header->pe.pe_major != 6 || header->pe.pe_minor != 0)
        goto invalid_image;
#endif

    /*
     * FIXME: byte swap all addresses here for header.
     */

    if (!load_section_tables (image, iinfo, offset))
        goto invalid_image;

    return TRUE;

invalid_image:
    return FALSE;
}

这里主要对文件的dos头进行判断,在这里可自行定义dos头,再往下继续跟,这里加载pe头的关键函数do_load_header。在这个函数中可以自行定义PE中的数据。函数原型如下:

static int
do_load_header (MonoImage *image, MonoDotNetHeader *header, int offset)
{
    MonoDotNetHeader64 header64;

#ifdef USE_COREE
    if (!image->is_module_handle)
#endif
    if (offset + sizeof (MonoDotNetHeader32) > image->raw_data_len)
        return -1;

    memcpy (header, image->raw_data + offset, sizeof (MonoDotNetHeader));

    if (header->pesig [0] != 'P' || header->pesig [1] != 'E')
        return -1;

    /* endian swap the fields common between PE and PE+ */
    SWAP32 (header->coff.coff_time);
    SWAP32 (header->coff.coff_symptr);
    SWAP32 (header->coff.coff_symcount);
    SWAP16 (header->coff.coff_machine);
    SWAP16 (header->coff.coff_sections);
    SWAP16 (header->coff.coff_opt_header_size);
    SWAP16 (header->coff.coff_attributes);
    /* MonoPEHeader */
    SWAP32 (header->pe.pe_code_size);
    SWAP32 (header->pe.pe_uninit_data_size);
    SWAP32 (header->pe.pe_rva_entry_point);
    SWAP32 (header->pe.pe_rva_code_base);
    SWAP32 (header->pe.pe_rva_data_base);
    SWAP16 (header->pe.pe_magic);

    /* now we are ready for the basic tests */

    if (header->pe.pe_magic == 0x10B) {
        offset += sizeof (MonoDotNetHeader);
        SWAP32 (header->pe.pe_data_size);
        if (header->coff.coff_opt_header_size != (sizeof (MonoDotNetHeader) - sizeof (MonoCOFFHeader) - 4))
            return -1;

        SWAP32    (header->nt.pe_image_base);     /* must be 0x400000 */
        SWAP32    (header->nt.pe_stack_reserve);
        SWAP32    (header->nt.pe_stack_commit);
        SWAP32    (header->nt.pe_heap_reserve);
        SWAP32    (header->nt.pe_heap_commit);
    } else if (header->pe.pe_magic == 0x20B) {
        /* PE32+ file format */
        if (header->coff.coff_opt_header_size != (sizeof (MonoDotNetHeader64) - sizeof (MonoCOFFHeader) - 4))
            return -1;
        memcpy (&header64, image->raw_data + offset, sizeof (MonoDotNetHeader64));
        offset += sizeof (MonoDotNetHeader64);
        /* copy the fields already swapped. the last field, pe_data_size, is missing */
        memcpy (&header64, header, sizeof (MonoDotNetHeader) - 4);
        /* FIXME: we lose bits here, but we don't use this stuff internally, so we don't care much.
         * will be fixed when we change MonoDotNetHeader to not match the 32 bit variant
         */
        SWAP64    (header64.nt.pe_image_base);
        header->nt.pe_image_base = header64.nt.pe_image_base;
        SWAP64    (header64.nt.pe_stack_reserve);
        header->nt.pe_stack_reserve = header64.nt.pe_stack_reserve;
        SWAP64    (header64.nt.pe_stack_commit);
        header->nt.pe_stack_commit = header64.nt.pe_stack_commit;
        SWAP64    (header64.nt.pe_heap_reserve);
        header->nt.pe_heap_reserve = header64.nt.pe_heap_reserve;
        SWAP64    (header64.nt.pe_heap_commit);
        header->nt.pe_heap_commit = header64.nt.pe_heap_commit;

        header->nt.pe_section_align = header64.nt.pe_section_align;
        header->nt.pe_file_alignment = header64.nt.pe_file_alignment;
        header->nt.pe_os_major = header64.nt.pe_os_major;
        header->nt.pe_os_minor = header64.nt.pe_os_minor;
        header->nt.pe_user_major = header64.nt.pe_user_major;
        header->nt.pe_user_minor = header64.nt.pe_user_minor;
        header->nt.pe_subsys_major = header64.nt.pe_subsys_major;
        header->nt.pe_subsys_minor = header64.nt.pe_subsys_minor;
        header->nt.pe_reserved_1 = header64.nt.pe_reserved_1;
        header->nt.pe_image_size = header64.nt.pe_image_size;
        header->nt.pe_header_size = header64.nt.pe_header_size;
        header->nt.pe_checksum = header64.nt.pe_checksum;
        header->nt.pe_subsys_required = header64.nt.pe_subsys_required;
        header->nt.pe_dll_flags = header64.nt.pe_dll_flags;
        header->nt.pe_loader_flags = header64.nt.pe_loader_flags;
        header->nt.pe_data_dir_count = header64.nt.pe_data_dir_count;

        /* copy the datadir */
        memcpy (&header->datadir, &header64.datadir, sizeof (MonoPEDatadir));
    } else {
        return -1;
    }

    /* MonoPEHeaderNT: not used yet */
    SWAP32    (header->nt.pe_section_align);       /* must be 8192 */
    SWAP32    (header->nt.pe_file_alignment);      /* must be 512 or 4096 */
    SWAP16    (header->nt.pe_os_major);            /* must be 4 */
    SWAP16    (header->nt.pe_os_minor);            /* must be 0 */
    SWAP16    (header->nt.pe_user_major);
    SWAP16    (header->nt.pe_user_minor);
    SWAP16    (header->nt.pe_subsys_major);
    SWAP16    (header->nt.pe_subsys_minor);
    SWAP32    (header->nt.pe_reserved_1);
    SWAP32    (header->nt.pe_image_size);
    SWAP32    (header->nt.pe_header_size);
    SWAP32    (header->nt.pe_checksum);
    SWAP16    (header->nt.pe_subsys_required);
    SWAP16    (header->nt.pe_dll_flags);
    SWAP32    (header->nt.pe_loader_flags);
    SWAP32    (header->nt.pe_data_dir_count);

    /* MonoDotNetHeader: mostly unused */
    SWAPPDE (header->datadir.pe_export_table);
    SWAPPDE (header->datadir.pe_import_table);
    SWAPPDE (header->datadir.pe_resource_table);
    SWAPPDE (header->datadir.pe_exception_table);
    SWAPPDE (header->datadir.pe_certificate_table);
    SWAPPDE (header->datadir.pe_reloc_table);
    SWAPPDE (header->datadir.pe_debug);
    SWAPPDE (header->datadir.pe_copyright);
    SWAPPDE (header->datadir.pe_global_ptr);
    SWAPPDE (header->datadir.pe_tls_table);
    SWAPPDE (header->datadir.pe_load_config_table);
    SWAPPDE (header->datadir.pe_bound_import);
    SWAPPDE (header->datadir.pe_iat);
    SWAPPDE (header->datadir.pe_delay_import_desc);
     SWAPPDE (header->datadir.pe_cli_header);
    SWAPPDE (header->datadir.pe_reserved);

#ifdef USE_COREE
    if (image->is_module_handle)
        image->raw_data_len = header->nt.pe_image_size;
#endif

    return offset;
}

在这个函数中可以针对pe头文件中数据结构进行修改,类似于dnSpy这类的软件,都是使用标准的pe格式,在mono中标准的PE头结构定义如下:

typedef struct {
    char            pesig [4];
    MonoCOFFHeader  coff;
    MonoPEHeader    pe;
    MonoPEHeaderNT  nt;
    MonoPEDatadir   datadir;
} MonoDotNetHeader;

这里将其分成了几个部分,这样一来就可以针对不同的部分进行数据的改组了,当然,改变后需要在自己的do_load_header函数中进行相应的修改。这里跟踪完成后便可以将PE文件中的前200个字节(具体字节数可根据自己定义的pe格式来进行确定,200是我以前使用的个数)加密或者直接抹零。

 

接下来便是更重要的metadata的加载了。

gboolean
mono_image_load_cli_data (MonoImage *image)
{
    MonoCLIImageInfo *iinfo;
    MonoDotNetHeader *header;

    iinfo = image->image_info;
    header = &iinfo->cli_header;

    /* Load the CLI header */
    if (!load_cli_header (image, iinfo))
        return FALSE;

    if (!load_metadata (image, iinfo))
        return FALSE;

    return TRUE;
}

继续跟踪load_metadata函数:

static gboolean
load_metadata (MonoImage *image, MonoCLIImageInfo *iinfo)
{
    if (!load_metadata_ptrs (image, iinfo))
        return FALSE;

    return load_tables (image);
}

这里可以通过定义修改load_metadata_ptrs函数来加载自己定义的dll文件。原始函数如下:

static gboolean
load_metadata_ptrs (MonoImage *image, MonoCLIImageInfo *iinfo)
{
    guint32 offset, size;
    guint16 streams;
    int i;
    guint32 pad;
    char *ptr;

    offset = mono_cli_rva_image_map (image, iinfo->cli_cli_header.ch_metadata.rva);
    if (offset == INVALID_ADDRESS)
        return FALSE;

    size = iinfo->cli_cli_header.ch_metadata.size;

    if (offset + size > image->raw_data_len)
        return FALSE;
    image->raw_metadata = image->raw_data + offset;

    /* 24.2.1: Metadata root starts here */
    ptr = image->raw_metadata;

    if (strncmp (ptr, "BSJB", 4) == 0){
        guint32 version_string_len;

        ptr += 4;
        image->md_version_major = read16 (ptr);
        ptr += 2;
        image->md_version_minor = read16 (ptr);
        ptr += 6;

        version_string_len = read32 (ptr);
        ptr += 4;
        image->version = g_strndup (ptr, version_string_len);
        ptr += version_string_len;
        pad = ptr - image->raw_metadata;
        if (pad % 4)
            ptr += 4 - (pad % 4);
    } else
        return FALSE;

    /* skip over flags */
    ptr += 2;

    streams = read16 (ptr); 
    ptr += 2;

    for (i = 0; i < streams; i++){
        if (strncmp (ptr + 8, "#~", 3) == 0){
            image->heap_tables.data = image->raw_metadata + read32 (ptr);
            image->heap_tables.size = read32 (ptr + 4);
            ptr += 8 + 3;
        } else if (strncmp (ptr + 8, "#Strings", 9) == 0){
            image->heap_strings.data = image->raw_metadata + read32 (ptr);
            image->heap_strings.size = read32 (ptr + 4);
            ptr += 8 + 9;
        } else if (strncmp (ptr + 8, "#US", 4) == 0){
            image->heap_us.data = image->raw_metadata + read32 (ptr);
            image->heap_us.size = read32 (ptr + 4);
            ptr += 8 + 4;
        } else if (strncmp (ptr + 8, "#Blob", 6) == 0){
            image->heap_blob.data = image->raw_metadata + read32 (ptr);
            image->heap_blob.size = read32 (ptr + 4);
            ptr += 8 + 6;
        } else if (strncmp (ptr + 8, "#GUID", 6) == 0){
            image->heap_guid.data = image->raw_metadata + read32 (ptr);
            image->heap_guid.size = read32 (ptr + 4);
            ptr += 8 + 6;
        } else if (strncmp (ptr + 8, "#-", 3) == 0) {
            image->heap_tables.data = image->raw_metadata + read32 (ptr);
            image->heap_tables.size = read32 (ptr + 4);
            ptr += 8 + 3;
            image->uncompressed_metadata = TRUE;
            mono_trace (G_LOG_LEVEL_INFO, MONO_TRACE_ASSEMBLY, "Assembly '%s' has the non-standard metadata heap #-.\nRecompile it correctly (without the /incremental switch or in Release mode).\n", image->name);
        } else {
            g_message ("Unknown heap type: %s\n", ptr + 8);
            ptr += 8 + strlen (ptr + 8) + 1;
        }
        pad = ptr - image->raw_metadata;
        if (pad % 4)
            ptr += 4 - (pad % 4);
    }

    g_assert (image->heap_guid.data);
    g_assert (image->heap_guid.size >= 16);

    image->guid = mono_guid_to_string ((guint8*)image->heap_guid.data);

    return TRUE;
}

其中最主要的就是image->raw_metadata这个数据的位置了,因为这里是指针,所以可以在image->raw_metadata的位置之前塞入一定量的垃圾数据,然后通过image->raw_metadata+offset(也就是塞入垃圾数据的字节数)的方式来修改image->raw_metadata的位置。

 

总结一下这种保护方式的主要思路:

  1. 自定义PE头文件格式,通过mono_image_open_from_data_with_name中的name来进行分流处理,Assembly-CSharp.dll和Assembly-CSharp-firstpass.dll文件的加载完全走自己定义的那个流程。加载完PE头以后,将一定量的数据进行抹零或者加密。
  2. 通过image->raw_metadata的位置来填充垃圾数据,并根据垃圾数据的个数来修正image->raw_matadata的正确位置。

方法思路比较简单,望大牛们轻喷。



[公告]安全服务和外包项目请将项目需求发到看雪企服平台:https://qifu.kanxue.com

打赏 + 1.00
打赏次数 1 金额 + 1.00
收起 
赞赏  junkboy   +1.00 2018/09/01
最新回复 (3)
supersoar 2018-9-9 22:17
2
0
感谢分享。
miyuecao 2018-12-12 16:22
4
0
感谢分享,mark下
游客
登录 | 注册 方可回帖
返回