吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 8595|回复: 29
收起左侧

[游戏安全] Unity il2cpp API 调用实践

  [复制链接]
BUGres 发表于 2022-9-27 17:00

IL2CPP API 使用

测试环境

  • Unity2019.4.38
  • il2cpp版本24.5

c# to il to cpp

在Unity4.6.1 p5以后版本中,在PlayerSettings—>Other Settings—>Scripting Backend有mono和il2cpp两个选项,它们是Unity脚本后处理(Scripting Backend)的两种方式。

经过il2cpp编译后的包体,其代码存在于运行库文件,il2cpp还需要一个非常重要的文件:global-metadata.dat文件

在默认状态下,运行库文件在Windows系统上名字为GameAssembly.dllAndroid上为libil2cpp.so这两种文件本身也是攻防要地

运行库文件本身带有导出函数,这些内容为il2cpp的API函数,可以使用IDA打开库文件,切换窗口至Exports查看导出函数,如果没有就是做过加密

下面展示一个普通的unity脚本,绑定在Camera上打包

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class il2cpptest : MonoBehaviour
{
    // Start is called before the first frame update
    void Start(){}
    // Update is called once per frame
    void Update(){}
    public int il2cppadd(int i) {
        return i + 1;
    }

    private int il2cppsub(int i) {
        return i - 1;
    }
}

下面将通过il2cppAPI对脚本内的方法进行访问,同时你也可以修改内部属性将函数指针修改到其他位置来起到替换函数的目的

无论如何你都需要提前了解一些信息!

global-metadata.dat

global-metadata.dat中保留了从class到cpp编译dll的信息,你可以使用010Editor打开dat文件直接观察可解读的信息

上面的cs脚本中int il2cppadd(int)这个函数完全可以直接在dat文件里面找到

UnityEngine.dll.Assembly-CSharp....Assembly-CSharp.dll.il2cpptest.il2cppadd.il2cppsub

这是一段ASCII码翻译的字符串,其中的“.”为原文的字节00,其中的Assembly-CSharp.dll在mono打包方式中将以C#编译dll,这个dll包含了所有用户写下的代码,你可以通过这种方式猜测类结构,上文中的il2cpptest就是类名

注入

你需要将dll注入目标程序

这里不细说

直接载入库(dll 或者 so)无法实现复刻真实程序内的情况

需要在运行中调用这些API函数

il2cpp_domain_assembly_open

此函数看名字应该是用来打开内部的Assembly,但是实际上看调用已经打开的则直接返回Il2CppAssembly*指针

这个函数不需要第一个参数工作,所以可以直接传入NULL,const char* name就是模块的名字:Assembly-CSharp

本文所有的代码中可以只关心导出函数的内容,为了方便查看就把调用列出来了,结构体在最后面

const Il2CppAssembly* il2cpp_domain_assembly_open(Il2CppDomain* domain, const char* name)//导出函数
{
    return Assembly::Load(name);
}
const Il2CppAssembly* Assembly::Load(const char* name)
{
    const size_t len = strlen(name);
    utils::VmStringUtils::CaseInsensitiveComparer comparer;

    for (AssemblyVector::const_iterator assembly = s_Assemblies.begin(); assembly != s_Assemblies.end(); ++assembly)
    {
        if (comparer(name, (*assembly)->aname.name))
            return *assembly;
    }

    if (!ends_with(name, ".dll") && !ends_with(name, ".exe"))
    {
        char* tmp = new char[len + 5];

        memset(tmp, 0, len + 5);

        memcpy(tmp, name, len);
        memcpy(tmp + len, ".dll", 4);

        const Il2CppAssembly* result = Load(tmp);

        if (!result)
        {
            memcpy(tmp + len, ".exe", 4);
            result = Load(tmp);
        }

        delete[] tmp;

        return result;
    }
    else
    {
        for (AssemblyVector::const_iterator assembly = s_Assemblies.begin(); assembly != s_Assemblies.end(); ++assembly)
        {
            if (comparer(name, (*assembly)->image->name))
                return *assembly;
        }

        return NULL;
    }
}

il2cpp_assembly_get_image

从Il2CppAssembly转换到Il2CppImage,结构体的信息可以查阅源码给出,源码位于unity引擎的libil2cpp文件夹下

const Il2CppImage* il2cpp_assembly_get_image(const Il2CppAssembly* assembly)//导出函数
{
    return Assembly::GetImage(assembly);
}

其实通过Il2CppAssembly结构体可以直接取出Il2CppImage结构体

il2cpp_class_from_name

*参数列表:Il2CppImage 指针 命名空间 类名字**

Il2CppClass* il2cpp_class_from_name(const Il2CppImage* image, const char* namespaze, const char* name)//导出函数
{
    return Class::FromName(image, namespaze, name);
}
Il2CppClass* Class::FromName(const Il2CppImage* image, const char* namespaze, const char *name)
{
    return Image::ClassFromName(image, namespaze, name);
}
Il2CppClass* Image::ClassFromName(const Il2CppImage* image, const char* namespaze, const char *name)
    {
        if (!image->nameToClassHashTable)
        {
            os::FastAutoLock lock(&s_ClassFromNameMutex);
            if (!image->nameToClassHashTable)
            {
                image->nameToClassHashTable = new Il2CppNameToTypeDefinitionIndexHashTable();
                for (uint32_t index = 0; index < image->typeCount; index++)
                {
                    TypeDefinitionIndex typeIndex = image->typeStart + index;
                    AddTypeToNametoClassHashTable(image, typeIndex);
                }

                for (uint32_t index = 0; index < image->exportedTypeCount; index++)
                {
                    TypeDefinitionIndex typeIndex = MetadataCache::GetExportedTypeFromIndex(image->exportedTypeStart + index);
                    if (typeIndex != kTypeIndexInvalid)
                        AddTypeToNametoClassHashTable(image, typeIndex);
                }
            }
        }

        Il2CppNameToTypeDefinitionIndexHashTable::const_iterator iter = image->nameToClassHashTable->find(std::make_pair(namespaze, name));
        if (iter != image->nameToClassHashTable->end())
            return MetadataCache::GetTypeInfoFromTypeDefinitionIndex(iter->second);

        return NULL;
    }

il2cpp_class_get_method_from_name

*参数列表:Il2CppClass 指针 方法名字 方法参数数量**

const MethodInfo* il2cpp_class_get_method_from_name(Il2CppClass *klass, const char* name, int argsCount)//导出函数
{
    return Class::GetMethodFromName(klass, name, argsCount);
}
const MethodInfo* Class::GetMethodFromName(Il2CppClass *klass, const char* name, int argsCount)
{
return GetMethodFromNameFlags(klass, name, argsCount, 0);
}
const MethodInfo* Class::GetMethodFromNameFlags(Il2CppClass *klass, const char* name, int argsCount, int32_t flags)
    {
        Class::Init(klass);

        while (klass != NULL)
        {
            void* iter = NULL;
            while (const MethodInfo* method = Class::GetMethods(klass, &iter))
            {
                if (method->name[0] == name[0] &&
                    !strcmp(name, method->name) &&
                    (argsCount == IgnoreNumberOfArguments || method->parameters_count == argsCount) &&
                    ((method->flags & flags) == flags))
                {
                    return method;
                }
            }

            klass = klass->parent;
        }

        return NULL;
    }

il2cpp的函数生成后会多一个参数,排在第一位,在il2cpp_class_get_method_from_name中需要以正常参数查询,但是真正调用的时候必须由MethodInfo结构体得到函数地址IDA分析查阅函数真正的参数数量!

IDA中的int il2cppadd(int i)

__int64 __fastcall sub_1804EA170(__int64 a1, int a2)
{
  return (unsigned int)(a2 + 1);
}

~我实际写的代码,测试成功后我就写下了本文~

//这块代码不属于il2cpp源码,是我的注入dll内的代码,前面的指针你可以用void *正常传递我就不发出来了
int (*func)(int, int);//对照IDA声明了函数指针
func = (int(*)(int, int)) il2cppMethod->methodPointer;//右面是MethodInfo结构体下的函数地址,强制转换给func调用

if (func(123,2) != 3)MessageBox(NULL, L"il2cppadd false", L"false", 0);//我测试了能否成功调用il2cpptest类下的il2cppadd

总结

API函数内容多种多样,可以实现很多神奇功能,但是和il2cppdumper一样地,一旦开发者直接混淆就没法从这个层面进攻

不过通过其他方法拿到信息再通过API函数修改还是比较方便的,Github上有很多大佬已经给出了解决方案,如Zygisk

更重要的是目前这些导出函数的加密还比较少,目前看来就算将来进行了修改,也能定位一些关键函数得到很多信息

下面是结构体信息:

Structs

你可以直接接着il2cpp源代码构造,也可以手动构造下面的类,实际输出发现00字节占了很多

typedef unsigned int uint32_t;//我的环境是64位
typedef int int32_t;
typedef unsigned short uint16_t;
typedef unsigned char uint8_t;
typedef struct Il2CppAssembly
{
    Il2CppImage* image;
    uint32_t token;
    int32_t referencedAssemblyStart;
    int32_t referencedAssemblyCount;
    Il2CppAssemblyName aname;
} Il2CppAssembly;
typedef struct Il2CppImage
{
    const char* name;
    const char *nameNoExt;
    Il2CppAssembly* assembly;

    TypeDefinitionIndex typeStart;
    uint32_t typeCount;

    TypeDefinitionIndex exportedTypeStart;
    uint32_t exportedTypeCount;

    CustomAttributeIndex customAttributeStart;
    uint32_t customAttributeCount;

    MethodIndex entryPointIndex;

#ifdef __cplusplus
    mutable
#endif
    Il2CppNameToTypeDefinitionIndexHashTable * nameToClassHashTable;

    const Il2CppCodeGenModule* codeGenModule;

    uint32_t token;
    uint8_t dynamic;
} Il2CppImage;
typedef struct Il2CppClass
{
    // The following fields are always valid for a Il2CppClass structure
    const Il2CppImage* image;
    void* gc_desc;
    const char* name;//类名
    const char* namespaze;//命名空间
    Il2CppType byval_arg;
    Il2CppType this_arg;
    Il2CppClass* element_class;
    Il2CppClass* castClass;
    Il2CppClass* declaringType;
    Il2CppClass* parent;
    Il2CppGenericClass *generic_class;
    const Il2CppTypeDefinition* typeDefinition; // non-NULL for Il2CppClass's constructed from type defintions
    const Il2CppInteropData* interopData;
    Il2CppClass* klass; // hack to pretend we are a MonoVTable. Points to ourself
    // End always valid fields

    // The following fields need initialized before access. This can be done per field or as an aggregate via a call to Class::Init
    FieldInfo* fields; // Initialized in SetupFields
    const EventInfo* events; // Initialized in SetupEvents
    const PropertyInfo* properties; // Initialized in SetupProperties
    const MethodInfo** methods; // Initialized in SetupMethods
    Il2CppClass** nestedTypes; // Initialized in SetupNestedTypes
    Il2CppClass** implementedInterfaces; // Initialized in SetupInterfaces
    Il2CppRuntimeInterfaceOffsetPair* interfaceOffsets; // Initialized in Init
    void* static_fields; // Initialized in Init
    const Il2CppRGCTXData* rgctx_data; // Initialized in Init
    // used for fast parent checks
    Il2CppClass** typeHierarchy; // Initialized in SetupTypeHierachy
    // End initialization required fields

    void *unity_user_data;

    uint32_t initializationExceptionGCHandle;

    uint32_t cctor_started;
    uint32_t cctor_finished;
    ALIGN_TYPE(8) size_t cctor_thread;

    // Remaining fields are always valid except where noted
    GenericContainerIndex genericContainerIndex;
    uint32_t instance_size;
    uint32_t actualSize;
    uint32_t element_size;
    int32_t native_size;
    uint32_t static_fields_size;
    uint32_t thread_static_fields_size;
    int32_t thread_static_fields_offset;
    uint32_t flags;
    uint32_t token;

    uint16_t method_count; // lazily calculated for arrays, i.e. when rank > 0
    uint16_t property_count;
    uint16_t field_count;
    uint16_t event_count;
    uint16_t nested_type_count;
    uint16_t vtable_count; // lazily calculated for arrays, i.e. when rank > 0
    uint16_t interfaces_count;
    uint16_t interface_offsets_count; // lazily calculated for arrays, i.e. when rank > 0

    uint8_t typeHierarchyDepth; // Initialized in SetupTypeHierachy
    uint8_t genericRecursionDepth;
    uint8_t rank;
    uint8_t minimumAlignment; // Alignment of this type
    uint8_t naturalAligment; // Alignment of this type without accounting for packing
    uint8_t packingSize;

    // this is critical for performance of Class::InitFromCodegen. Equals to initialized && !has_initialization_error at all times.
    // Use Class::UpdateInitializedAndNoError to update
    uint8_t initialized_and_no_error : 1;

    uint8_t valuetype : 1;
    uint8_t initialized : 1;
    uint8_t enumtype : 1;
    uint8_t is_generic : 1;
    uint8_t has_references : 1;
    uint8_t init_pending : 1;
    uint8_t size_inited : 1;
    uint8_t has_finalize : 1;
    uint8_t has_cctor : 1;
    uint8_t is_blittable : 1;
    uint8_t is_import_or_windows_runtime : 1;
    uint8_t is_vtable_initialized : 1;
    uint8_t has_initialization_error : 1;
    VirtualInvokeData vtable[IL2CPP_ZERO_LEN_ARRAY];
} Il2CppClass;
typedef struct MethodInfo
{
    Il2CppMethodPointer methodPointer;//函数地址
    InvokerMethod invoker_method;
    const char* name;
    Il2CppClass *klass;
    const Il2CppType *return_type;
    const ParameterInfo* parameters;

    union
    {
        const Il2CppRGCTXData* rgctx_data; /* is_inflated is true and is_generic is false, i.e. a generic instance method */
        const Il2CppMethodDefinition* methodDefinition;
    };

    /* note, when is_generic == true and is_inflated == true the method represents an uninflated generic method on an inflated type. */
    union
    {
        const Il2CppGenericMethod* genericMethod; /* is_inflated is true */
        const Il2CppGenericContainer* genericContainer; /* is_inflated is false and is_generic is true */
    };

    uint32_t token;
    uint16_t flags;
    uint16_t iflags;
    uint16_t slot;
    uint8_t parameters_count;
    uint8_t is_generic : 1; /* true if method is a generic method definition */
    uint8_t is_inflated : 1; /* true if declaring_type is a generic instance or if method is a generic instance*/
    uint8_t wrapper_type : 1; /* always zero (MONO_WRAPPER_NONE) needed for the debugger */
    uint8_t is_marshaled_from_native : 1; /* a fake MethodInfo wrapping a native function pointer */
} MethodInfo;

免费评分

参与人数 14威望 +2 吾爱币 +118 热心值 +14 收起 理由
CrazyNut + 3 + 1 用心讨论,共获提升!
MorichikaRinno + 1 + 1 热心回复!
xiong930626 + 1 + 1 用心讨论,共获提升!
sunlei658 + 1 + 1 谢谢@Thanks!
努力加载中 + 1 + 1 谢谢@Thanks!
二娃 + 2 + 1 谢谢@Thanks!
heikis + 1 + 1 我很赞同!
腰围两尺99 + 1 + 1 我很赞同!
happyBread + 1 + 1 谢谢@Thanks!
loo1221ool + 1 + 1 我很赞同!
fengbolee + 2 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
Panel + 1 + 1 我很赞同!
Hmily + 2 + 100 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
忆魂丶天雷 + 2 + 1 谢谢@Thanks!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

 楼主| BUGres 发表于 2022-9-28 01:26

百度搜索上文的几个API函数就可以看到很多内容
il2cpp源代码下载unity引擎即可查阅
最开始是搞一个知名游戏的dll发现他的导出函数竟然完全没任何加密,就想这样hook调用一下他的导出函数
 楼主| BUGres 发表于 2022-9-28 01:23
hackdjh 发表于 2022-9-27 18:44
看完我真是才疏学浅,我一个专业搞Unity开发的,完全看不懂

unity正常开发只需要在打包时候确定代码打包方法
mono类型容易给玩家添加mod
il2cpp运行快很多,但是mod需要开发者自己提供接口吧
感觉不到风 发表于 2022-9-27 17:51
hackdjh 发表于 2022-9-27 18:44
看完我真是才疏学浅,我一个专业搞Unity开发的,完全看不懂
Slimu 发表于 2022-9-27 19:42
也能在安卓直接调用?
无头草 发表于 2022-9-27 20:45
学习一下,看看内容
wo546400 发表于 2022-9-27 22:54
来看看大神,学习学习,
nrmyh732 发表于 2022-9-27 23:58
谢谢大佬,学习学习!!
 楼主| BUGres 发表于 2022-9-28 01:21
Slimu 发表于 2022-9-27 19:42
也能在安卓直接调用?

当然,但是需要fridahook,文中已经给出了GitHub项目名字
你也可以自己写一份,不过无论如何都需要hook才能完成
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-11-21 19:53

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表