在两年前发布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分好了类,命名也非常清晰。

使用云服务器和云手机运行AzurLaneAutoScript

起因 事情的起因是由于无法忍受红手指的各种问题,而且刚好也快到期了,就直接趁着有优惠活动的时候换到了雷电云。红手指也直接在游戏群里送给了群友,结...

阅读全文

使用Riru修改手游

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

阅读全文

使用VirtualXposed修改手游

前篇 手游的注入与修改 前言 这篇后续文章原本是打算很快就写完的,但是不知怎么一转眼就已经12月了,眼看今年都要过了,还是赶紧把这篇文章水出来吧。 在上...

阅读全文

35 条评论

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

  2. 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就行了

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

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

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

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

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

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

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

  5. 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中除了函数参数外,通过额外寄存器给出了一些初始值,以便能计算出正确的代码地址。 您之前碰到这种保护方式吧? 我想坐下确认。

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

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

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

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

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

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

欢迎留言

5 + 4 =