在两年前发布Riru版Il2CppDumper的时候,就打算发布这篇配套的文章,结果码了一半就被我丢到了一旁,后来每次想起来的时候总是以”既然代码都发布了大概也不需要写什么所谓的教程了吧”的理由停下了准备码字的手,就这样一转眼来到了2022年。两个月前Magisk更新了v24版本,新增了Zygisk替代了Riru,所以我也顺势更新了Zygisk的版本,然后从草稿箱中把这篇文章翻了出来准备修改成Zygisk的版本发布,结果改着改着又被我丢了。这段时间疫情形势严峻,我住的小区也被封禁,不能出门的我又从草稿箱把这篇文章翻了出来,不过这次总算是难产成功了。

Zygisk和Il2Cpp Api

先简单介绍下Zygisk和Il2Cpp Api,其实接下来要做的跟我之前的使用Riru修改手游这篇文章类似,Zygisk就是一个用来注入代码到游戏里的工具,只不过这次是通过调用Il2Cpp的Api来进行修改。那么为什么要使用Il2Cpp Api呢?首先就是通用性,api作为导出函数在任何平台上使用il2cpp编译的游戏里都是存在的,所以在修改里使用api部分的代码不用考虑是移动还是pc平台都可以使用。还有就是对于函数地址这种,需要在代码里硬编码的部分也可以通过调用api获取,这样即使游戏更新只要之前修改的逻辑还能用,比如让某某函数返回0这种,就不需要更新修改的代码。而且api还能非常轻松的直接修改运行时的数据,比如字段等等。最后因为是运行时修改,也能绕过大部分壳,目前大多数游戏厂商都没有对api做保护。

使用API前的准备工作

如果你比较懒不想从头开始写,你可以直接下载Zygisk-Il2CppDumper的源码,然后从这之后写你的代码就行,使用api前的准备工作都已经做好,也有Dobby这个hook框架。需要注意dumper是直接使用最新的api声明文件,里面有些函数在老版本里是不存在的,而且没有包含Unity结构的详细定义。如果你需要特定版本的Unity api和结构体头文件,可以从这里下载替换il2cpp-api-functions.hil2cpp-class.h即可。要编译时只要运行gradle任务:module:assembleRelease就会在out文件夹生成zip包。

下面就是详细步骤了,因为已经提供源码了这里就不会再贴代码,不懂的地方请配合源码食用。

首先是Zygisk部分,从zygisk-module-sample下载源码拿到zygisk.hppexample.cpp,修改example.cpp的部分就跟之前riru那篇类似了,在preAppSpecialize中判断当前程序包名,然后在postAppSpecialize中启动新线程进行之后的操作。

在新线程里要做的事就是初始化il2cpp的api了,不过首先我们需要从Unity的安装包里偷下api的声明和用到的结构,具体参考这小节的开头。至于初始化,在安卓和ios平台,可以直接使用dlopen + dlsym。不过安卓平台游戏可能加壳,dlopen就不管用了,所以源码里是使用hook do_dlopen的方式来取handle然后再调用dlsym

顺带说下windows平台,可以通过GetModuleHandle + GetProcAddress来初始化api。

认识基础结构

在使用api之前需要认识一下两个基本结构,Il2CppClassIl2CppObject,其中Il2CppClass就相当于是一个类的定义,Il2CppObject就是类的实例,可以通过il2cpp_object_new生成,也就是C#里的new操作,在使用api的时候任何类和类的实例都可以通过Il2CppClassIl2CppObject表示而不需要关心类的具体结构,如果需要类的具体声明,可以考虑从Il2CppDumper生成的il2cpp.h中复制。

使用API

首先需要调用il2cpp_thread_attach,不然在后面进行一些操作可能会使游戏崩溃

auto domain = il2cpp_domain_get();
il2cpp_thread_attach(domain);

接下来就是基本操作,遍历il2cpp_domain_get_assemblies的返回值取出想要的dll,然后用il2cpp_assembly_get_image获取Il2CppImage

auto assemblies = il2cpp_domain_get_assemblies(domain, &size);
for (int i = 0; i < size; ++i) {
    if (strcmp(assemblies[i]->aname.name, "Assembly-CSharp") == 0) {
        auto image = il2cpp_assembly_get_image(assemblies[i]);
    }
}

有了Il2CppImage后就可以调用il2cpp_class_from_name获取Il2CppClass,想要获取某个method,就可以继续调用il2cpp_class_get_method_from_name,最后一参数是method的参数数量

auto klass = il2cpp_class_from_name(image, "Namespace", "Classname");
auto method = il2cpp_class_get_method_from_name(klass, "MethodName", 1);
auto addr = method->methodPointer;

有些情况下il2cpp_class_get_method_from_name可能无法使用,比如刚好有同名同参数数量的多个method,这个时候就使用il2cpp_class_get_methods进行迭代直到找到自己需要的。

void *iter = nullptr;
while (auto method = il2cpp_class_get_methods(klass, &iter)) {
    //TODO
}

如果想要调用函数,函数声明可以照着c#代码手抄一份,或者用Il2CppDumper生成script.json后从里面复制。需要注意的是非静态函数需要添加一个实例(Il2CppObject*)参数作为第一个参数,可以根据需求使用hook或者调用il2cpp_object_new生成,静态函数的话需要注意版本号,如果版本号大于等于2018.3,则不需要添加参数,小于2018.3一样要添加一个实例参数,只是传NULL。另外在函数声明最后还要添加一个”void* method”参数,这个参数在大多数函数里直接传NULL就行,这里就不展开讲了。

//c#函数
int Add(int a, int b)
//c声明
//是静态函数,版本大于等于2018.3
int32_t Add(int32_t a, int32_t b, void *method)
//是静态函数,版本小于2018.3
int32_t Add(Il2CppObject* _null, int32_t a, int32_t b, void *method)
//非静态函数
int32_t Add(Il2CppObject* _this, int32_t a, int32_t b, void *method)

在il2cpp中字符串都是Il2CppString结构,需要使用时,可以调用il2cpp_string_new生成,不过要从Il2CppString转回std::string或者char*就稍微有点麻烦,因为本身Il2CppString在内存中存储的格式是Utf16,所以这里就直接复制il2cpp里的代码来进行转换,使用时需要从Unity的安装路径“Editor\Data\il2cpp\libil2cpp\utils\utf8-cpp”偷utf8-cpp来使用。

//代码来自“Editor\Data\il2cpp\libil2cpp\utils\StringUtils.cpp”
std::string Utf16ToUtf8(const Il2CppChar *utf16String, int maximumSize) {
    const Il2CppChar *ptr = utf16String;
    size_t length = 0;
    while (*ptr) {
        ptr++;
        length++;
        if (maximumSize != -1 && length == maximumSize)
            break;
    }

    std::string utf8String;
    utf8String.reserve(length);
    utf8::unchecked::utf16to8(utf16String, ptr, std::back_inserter(utf8String));

    return utf8String;
}

std::string Utf16ToUtf8(const Il2CppChar *utf16String) {
    return Utf16ToUtf8(utf16String, -1);
}

std::string Il2CppStringToStdString(Il2CppString *str) {
    auto chars = il2cpp_string_chars(str);
    return Utf16ToUtf8(chars);
}

最后说下字段(field)方面的api,首先是取需要的字段,这个跟前面的method类似,使用il2cpp_class_get_field_from_name直接通过名称获取或者用il2cpp_class_get_fields遍历。拿到字段后要对值进行操作的话,就需要分下情况,对于静态字段,读取和写入直接使用il2cpp_field_static_get_valueil2cpp_field_static_set_value就行,如果是需要修改实例类的字段值,大多数情况都需要使用hook获取到你需要修改的实例类,然后再通过il2cpp_field_get_valueil2cpp_field_set_value进行读取和写入。

api的介绍就到这里了,基本会使用到的api都介绍完了,其他的api就请自行查阅il2cpp-api-functions.h文件,里面已经给api分好了类,命名也非常清晰。

一个过手游反作弊中.text段校验的方法

原本打算下个月再水的,不过转念一想,反正也不可能做到每个月都水一篇,而且现在放开以后准备到处去玩了,所以还是赶紧写完了发出来。 偷梁换柱 思路...

阅读全文

在模拟器上使用Zygisk修改游戏

前言 没想到距离上次发一篇教程性质的文章已经过去整整一年了,刚好最近搞了标题里提到的东西觉得还是可以写一下的,于是就来水了这一篇。 之前我一直...

阅读全文

使用Riru修改手游

前篇 手游的注入与修改 使用VirtualXposed修改手游 前言 时隔半年终于把这坑填上了,一开始只是打算随便写写,不过现在看来刚好可以凑出一个系列,之前的文章...

阅读全文

84 条评论

  1. “””
    可以通过il2cpp_object_new生成,也就是C#里的new操作
    “””

    大佬,这个il2cpp_object_new跟new还是有差异吧?il2cpp_object_new感觉没有执行ctor中的逻辑

  2. 大佬 Il2CppObject *instance里存着我想要的数据
    struct Il2CppObject
    {
    Il2CppClass *klass;
    void *monitor; //0
    };

    这个结构怎么分析出我想要的数据

  3. C#代码:
    “`
    {
    this._collider = base.GetComponent();
    }
    “`

    IDA反汇编得到的:
    “`
    {
    Component_object = UnityEngine_Component__GetComponent_object_(
    a1,
    Method_UnityEngine_Component_GetComponent_Collider___);
    *(_DWORD *)(a1 + 24) = Component_object;
    }
    “`

    其中 `Method_UnityEngine_Component_GetComponent_Collider___` 是静态解析出来的一个变量,值是 `0xC0069E01` :
    “`
    .data:06B7882C 01 9E 06 C0 Method$UnityEngine.Component.GetComponent_Collider___ DCD 0xC0069E01
    “`

    怎么通过 il2cpp API 来动态地获取这个变量?

  4. Thanks for your tools and tutorials, is it possible to ask you if you can make a tutorial or an update for assetstudio to be able to deserialize text assets of il2cpp games? I’ve been trying to do that for a few months but got nothing (I don’t know anything about programming :cry: ) 谢谢!

  5. 大佬你好,非常感谢你的框架.
    我在使用的时候发现个问题,并没有找到DobbyHook.
    想问下文章里面是怎么Hook il2cpp的函数呢,需要自己添加Dobby吗?

  6. Perfare 大佬,可否请教一个问题,Unity的il2cpp为什么一定要导出那些对游戏安全有隐患的API呢?我搜遍资料都没有找到说明unity这么做的原理,可否解惑?感谢

    1. 可能是因为il2cpp还没有初始化完成,我在最新的代码里加上了判断的方法,等il2cpp初始化完成后再调用就不会闪退了

    1. 最近刚好在一个游戏上遇到这个问题,检查之后发现是因为il2cpp还没初始化完成,我在最新的代码里加上了判断的方法,如果有需要你可以看看

  7. 大佬,请问下cs里面得这种地址需要怎么获取 |-RVA: 0x156C800 Offset: 0x156C800 VA: 0x156C800 |-Action.Invoke ?zygisk得il2cppdumper dump不出来这些消息

  8. 大佬 可以发个修改字段的例子嘛
    我在使用il2cpp_object_new生成类的实例时游戏会卡死

  9. 大佬,我想问一下怎么获取下面这个方法的参数,从ida看,它的调用是没有参数的,不知道il2cpp是怎么实现的
    public static Void MyAESDecrypted(ref Byte[] bytes) { }

    1. 这个我还真没研究过,不过你反编译下这段函数看代码用到bytes这个参数地方的汇编应该可以很容易知道是怎么来的

  10. 大佬
    auto assemblies = il2cpp_domain_get_assemblies(domain, &size);
    for (int i = 0; i aname.name, “Assembly-CSharp”) == 0) {
    image = il2cpp_assembly_get_image(assemblies[i]);
    }
    }
    这段代码里的 -> 报了这个错 Member access into incomplete type ‘const Il2CppAssembly’
    应该怎么修复 求教 :cry:

    1. 我在Zygisk-Il2CppDumper代码(commit:5ff73ad)基础上做修改时也碰到了这个问题,发现似乎是 il2cpp-class.h 里只有 typedef struct Il2CppAssembly Il2CppAssembly; 这样一句声明、但缺少 struct Il2CppAssembly 类型定义的的缘故。
      去大佬提到的那个 Il2CppVersions 仓库,用适当unity版本的头文件(类型声明在仓库headers目录下,il2cpp function的声明在api目录下)对 il2cpp-class.h 内容做替换就能消除这个报错的样子。

    2. 要进行类型转换,C++代码如下:
      image = const_cast(il2cpp_assembly_get_image(assemblies[i]));

      用il2cppdumpper得到的dump.cs文件里面已经标注了各dll的索引,例如:
      // Image 0: Assembly-CSharp.dll – 0
      // Image 1: mscorlib.dll – 10434
      // Image 2: ClientCommons.dll – 12215

      所以可以省去for循环的判断,直接使用
      image_AssemblyCSharp = const_cast(il2cpp_assembly_get_image(assemblies[0]));
      image_mscorlib = const_cast(il2cpp_assembly_get_image(assemblies[1]));
      image_ClientCommons = const_cast(il2cpp_assembly_get_image(assemblies[2]));

  11. 大佬你好 运行你的Zygisk-il2cppdumper脚本游戏会直接闪退 我通过frida 查看libil2cpp.so里的函数 发现没有il2cpp_assembly_get_image 这个函数 请问这种情况下我该怎么办
    游戏名:奥比岛
    Unity版本:2020.3.32f1

    1. 确实,其它api基本都在,就这个消失,应该是被魔改了。我也很想知道面对这种情况应该怎么做

  12. 没搞过IL2CPP_NEW实例化达到修改函数怎么操作,调试一次重启一次麻了,有没有示例

  13. Dictionary dicPostData是不是用Il2CppObject *dicPostData
    如:
    public void Post(string url, Dictionary dicPostData, Action funcResult) { }
    Hook C代码:
    void old_Post(Il2CppObject* _this,Il2CppString *url, Il2CppObject *dicPostData, Il2CppObject *funcResult);

    void new_Post(Il2CppObject* _this,Il2CppString *url, Il2CppObject *dicPostData, Il2CppObject *funcResult);
v
    oid new_Post(
    Il2CppObject* _this,Il2CppString *url, Il2CppObject *dicPostData, Il2CppObject *funcResult){

    std::string purl = Il2CppStringToStdString(url);

    LOGI(“dumping2 purl start %s “,purl.c_str());

    saveFile(purl.c_str(), sizeof(purl.c_str()), “purl.txt”);

    LOGI(“dumping2 purl end”);

    return old_Post(_this,url,dicPostData,funcResult);

    }

    能成功HOOk。但是我如何获取dicPostData里面的键值?然后保存

    1. 因为泛型的操作过于复杂所以我正文里没写。
      首先我最一般用的是hook Dictionary的Add函数,这样既可以抓到键值也可以修改。
      具体操作通过反射获取到Dictionary泛型实例的Il2CppClass,之后就可以使用il2cpp_class_get_method_from_name获取到Add函数的地址了。我这里就稍微贴点代码,Zygisk-Il2CppDumper后面也有具体的反射代码。其中用到了静态函数,注意版本差异
      auto corlib = il2cpp_get_corlib();
      auto assemblyClass = il2cpp_class_from_name(corlib, “System.Reflection”, “Assembly”);
      auto assemblyLoad = il2cpp_class_get_method_from_name(assemblyClass, “Load”, 1);
      auto assemblyGetType = il2cpp_class_get_method_from_name(assemblyClass, “GetType”, 1);
      typedef void *(*Assembly_Load_ftn)(void *, Il2CppString *, void *);
      typedef Il2CppReflectionType *(*Assembly_GetType_ftn)(void *, Il2CppString *, void *);
      auto reflectionAssembly = ((Assembly_Load_ftn) assemblyLoad->methodPointer)(nullptr,
      il2cpp_string_new(“mscorlib”),
      nullptr);
      auto reflectionType = ((Assembly_GetType_ftn) assemblyGetType->methodPointer)(
      reflectionAssembly, il2cpp_string_new(“System.Collections.Generic.Dictionary`2[System.String,System.String]”),
      nullptr);
      auto klass = il2cpp_class_from_system_type(reflectionType);
      然后调用il2cpp_class_get_method_from_name就行了

    2. 大佬,调用assemblyGetType的时候出现错误,无法获取到System.Collections.Generic.Dictionary`2[System.String,System.String],
      但是获取System.Collections.Generic.Dictionary`2是正常的,但是后续il2cpp_class_get_method_from_name无法获取到泛型函数的地址,请问要怎么处理

    1. 如果你不需要使用直接用void*代替就行,如果需要取值就定义成Il2CppArray *,遍历array->vector就行

    2. 大佬可以给一段简单的demo代码吗,不太熟悉c语言,照着百度的std::vector遍历方法,改了一下,报fault addr了 (val 的C#类型是byte[]) :cry: :cry: :cry: :cry:

      Il2CppArray* bytes = (Il2CppArray*)val;
      uint32_t count_1 = il2cpp_array_length(bytes);
      LD(“length %d”, count_1);
      char* chars = new char[count_1];
      for(int i = 0; i vector[i]);
      chars[i] = v;
      }

  14. 大佬,请问一下这个可以通过修改获取so的大小然后达到:Dump出解密好的so吗,因为相比于只能看到函数名,我更倾向于用IDA去分析他的一个函数的走向与处理的流程

    1. 用api的话其实也能获取调用堆栈来方便逆向,可以看看这个工具https://github.com/vfsfitvnm/frida-il2cpp-bridge

    2. 当然可以用来dump so,不过需要你自己处理map然后将数据读出来,不过既然已经root了建议直接使用gameguardian

  15. 大佬 请问一下 我想要frida hook 一个游戏的libil2cpp.so 但是列举了所有游戏加载的so 文件 发现游戏并没有加载libil2cpp.so 文件 但是已经确定这个文件是unity开发的了 游戏名叫无悔华夏

    1. 我并没有用过frida,所以没法解决你的问题,不过我测了一下Zygisk-Il2CppDumper,是没有任何问题的

    2. 大佬,你有试过用Zygisk-Il2CppDumper去dump月圆之夜的吗?我刚刚试了没dump出来(其它游戏试过了,成功),TapTap上面有这个游戏的下载

  16. il2cpp_class_get_method_from_name一调用整个游戏就卡死。
    这是什么原因呢?
    IDA中,这个函数的附近有大量DCQ。这个采取了什么措施? 大佬给指路下 :razz:

    1. il2cpp_class_get_method_from_name 这个函数从libunity.so调用过来,我用frida stalker追踪了指令,可以正常运行(它搞了花指令 IDA里一堆DCQ ),但是如果直接hook这个函数来调用,就会卡死, 在il2cpp_class_get_method_from_name(libil2cpp.so中)之前有一堆地址计算,我怀疑是从libunity.so中除了函数参数外,通过额外寄存器给出了一些初始值,以便能计算出正确的代码地址。 您之前碰到这种保护方式吧? 我想坐下确认。

    2. 感谢回复,跟您提到的问题比较不一致,该游戏有导出函数。 目前我通过hook该游戏libunity库中对il2cpp_系列函数调用过程的参数与返回值,被动获取到相关结构体。 但是发现该游戏应该是使用了Beebytes’ obfuscator(猜测该插件)进行了函数名称与字段的混淆(非全部代码都混淆,关键一些函数混淆了)。 这种情况下,请问有什么思路去梳理这些函数与字段的含义呢? 可否给几个思路。谢谢

    3. 大概只能猜测和通过跟踪调用堆栈来判断了,这个工具https://github.com/vfsfitvnm/frida-il2cpp-bridge可能对你有帮助

  17. 可是现在大量游戏都会检测,一开游戏就显示 “检测到异常,违反安全策略 Code 00000XX“
    不知道这个壳到底是啥,大部分新游戏都会加这个壳,比如少女前线,还有今天sega刚出的真·锁链战记 都是跳这个
    实在不知道要怎么过这个壳啊。。我记得多年前的魔法少女小圆也是这个壳,然后有人发布了脱壳后的版本 直接就能运行。。
    大大有办法麽

    1. 隐藏了的,用了最新的magisk的zygisk和Shamiko,开了最强检测momo,momo显示顺利通过,今天发布的真锁链战记打开还是显示检测到异常,违反安全策略..

    2. 这个游戏我今天测试过了,并没有任何检测能影响到调用il2cpp api,建议你自己检查下是不是还开了其他东西,比如xposed之类的

    3. 我是在模拟器里开的,估计是检测到模拟器了,不知道到底检测了什么模拟器的参数。。大佬有思路吗,我去试试

欢迎留言

8 + 0 =