某手游il2cpp逆向分析----libtprt保护

查看 64|回复 11
作者:无问且问   
最近在玩个游戏,发现是由il2cpp进行打包的,就打算用il2cppdumper来dump看看游戏内容
开干
说干就干,提取游戏安装包,在lib/arm64-v8a路径提取出libil2cpp.so,在assets/bin/Data/Managed/Metadata路径提取出global-metadata.dat
直接打开il2cppdumper,选择这两个文件,发现报错:


原版dump报错.png (43.87 KB, 下载次数: 0)
下载附件
2025-3-2 16:55 上传

那应该是有加密的,用010Editor打开global-metadata.dat文件,发现熵值很高,很明显的加密了


原版global-metadata.png (241.24 KB, 下载次数: 0)
下载附件
2025-3-2 16:56 上传

ok了,既然安装包中的global-metadata.dat被加密了,那我直接去内存中dump到的,应该就没问题吧!
既然要从内存中获取到global-metadata.dat,那肯定要根据libil2cpp.so中的逻辑来找出加载global-metadata.dat的地方,当然也可以通过在内存中搜寻魔数头的方式来找到文件头(ps:这个例子的魔数头也被抹除了,所以只能采取分析libil2cpp.so中的逻辑了@_@;)
事情果然没这么简单,当我用IDA打开libil2cpp.so后,发现libil2cpp.so也被加固了,导出表被抹除完了


原版导出表.png (12.66 KB, 下载次数: 1)
下载附件
2025-3-2 16:58 上传

并且我看到依赖库中包含libtprt.so


依赖so.png (18.56 KB, 下载次数: 1)
下载附件
2025-3-2 16:58 上传

网上搜索得知,libtprt.so是属于某讯的加固,好吧,看来还是有难度的,继续分析吧!
既然安装包中的libil2cpp.so也被加固了,那也只能去内存中拿了,写了一个frida脚本去获取libil2cpp.so:
[JavaScript] 纯文本查看 复制代码function dump_so() {
    Java.perform(function() {
        var currentApplication = Java.use("android.app.ActivityThread").currentApplication();
        var dir = currentApplication.getApplicationContext().getFilesDir().getPath();
        var libso = Process.getModuleByName("libil2cpp.so");
        var file_path = dir + "/" + libso.name + "_" + libso.base + "_" + ptr(libso.size) + ".so";
        var file_handle = new File(file_path, "wb");
        if (file_handle && file_handle != null) {
            Memory.protect(ptr(libso.base), libso.size, 'rwx');
            var libso_buffer = ptr(libso.base).readByteArray(libso.size);
            file_handle.write(libso_buffer);
            file_handle.flush();
            file_handle.close();
            console.log("[dump]:", file_path);
        }
    });
}
var isCalled = false;
function hookdlopen() {
    var dlopen = Module.findExportByName(null, "dlopen");
    Interceptor.attach(dlopen, {
        onEnter: function (args) {
            var path = args[0].readCString();
            if (path && path.indexOf('libil2cpp.so') !== -1) {
                this.path = path;
            }
        },
        onLeave: function (retval) {
            if (this.path && this.path.indexOf('libil2cpp.so') !== -1 && !isCalled) {
                dump_so();
                isCalled = true;
            }
        }
    });
}
hookdlopen();
frida使用spawn模式选择脚本并打开游戏,然后根据打印出来的地址找到dump下来的so,尝试将其使用ida打开,发现报错


dump后的so报错.png (17.59 KB, 下载次数: 1)
下载附件
2025-3-2 17:00 上传

使用SoFixer工具进行修复,然后再次通过ida打开,发现导出表都正常了,终于迈出万里长征的第一步了!


正常导出表.png (159.36 KB, 下载次数: 1)
下载附件
2025-3-2 17:00 上传

想要找到global-metadata.dat的内存地址,则需要通过将ida分析出的反汇编代码和unity il2cpp的源码进行对比来快速得到结果,通过分析源码发现,加载metadata会使用一个字符串global-metadata.dat


加载metadata的源码.png (31.88 KB, 下载次数: 1)
下载附件
2025-3-2 17:00 上传

尝试在ida中搜索这个字符串


ida查询metadata字符串.png (10.83 KB, 下载次数: 1)
下载附件
2025-3-2 17:01 上传

通过交叉引用获取到它的使用地址(图片中的变量名是我重命名后的,并不是原版)


metadata交叉引用获取.png (47.17 KB, 下载次数: 1)
下载附件
2025-3-2 17:01 上传

F5进行反汇编分析(图片中的变量名是我重命名后的,并不是原版)


metadata头伪c分析.png (70.97 KB, 下载次数: 1)
下载附件
2025-3-2 17:02 上传

发现sub_1685100和源码中LoadMetadataFile的作用很相近,直接跟进去看看


sub_1685100伪c.png (11.5 KB, 下载次数: 1)
下载附件
2025-3-2 17:02 上传

继续跟sub_2F0


sub_2F0伪c.png (12.72 KB, 下载次数: 1)
下载附件
2025-3-2 17:03 上传

F5处理有问题,没关系,继续跟下去吧


0x7281F68地址汇编.png (35.57 KB, 下载次数: 1)
下载附件
2025-3-2 17:04 上传

跟到最后发现原来是调用了libtprt里面的导出函数来进行的加载metadata,这里面肯定会涉及到加密或者解密了,还是要跟进去看看
仔细观察汇编,知道最终跳转使用的是BR X2,查看X2寄存器之前的赋值记录,只有一条LDR X2, [X8,#0x128],X8寄存器又是直接赋值g_tprt_pfn_array_ptr_0这个导入函数的地址,所以最终需要分析的地址为:libtprt.so中g_tprt_pfn_array_ptr_0导出函数的地址偏移0x128后的地址
从安装包中提取出libtprt.so,使用ida打开进行分析
找到g_tprt_pfn_array_ptr_0导出函数


g_tprt_pfn_array_ptr_0导出函数.png (21.15 KB, 下载次数: 1)
下载附件
2025-3-2 17:05 上传

根据它的地址,偏移0x128后看看


偏移0x128后的地址.png (17.03 KB, 下载次数: 1)
下载附件
2025-3-2 17:06 上传

进去看看


1BDD9C地址汇编.png (16.12 KB, 下载次数: 1)
下载附件
2025-3-2 17:06 上传

继续跟进去


1BCE3C汇编.png (115.69 KB, 下载次数: 1)
下载附件
2025-3-2 17:07 上传

F5看下伪c吧


1BCE3C伪c.png (38.43 KB, 下载次数: 1)
下载附件
2025-3-2 17:07 上传

这个函数大致流程就是先调用偏移为0x277DA0处的函数指针,然后根据这个函数的返回值进行if分支,了解global-metadata.dat的朋友应该知道,正常的魔数头就是AF1BB1FA,这说明0x277DA0处的函数应该就是加载metadata的函数,不然后面应该是不会判断这个魔数的,当然话不可以说的这么满,还是继续看后续代码吧,else里面是两个函数调用,大致功能为先调用sub_1BDC9C来获取需要调用的函数,然后将函数指针传递给v5,最后调用v5里存储的函数
函数大致流程分析的差不多了,先去看看0x277DA0处的函数指针吧


277DA0汇编.png (40.08 KB, 下载次数: 1)
下载附件
2025-3-2 17:08 上传

可以看到0x277DA0属于bss段,这是一个存储未初始化的全局和静态变量的段,查询交叉引用也没有其余调用,那么静态分析行不通,就只能通过动态分析了
写了一个frida脚本去获取0x277DA0处的函数指针,考虑到不知道它什么时候完成初始化,所以我们直接在调用sub_1BCE3C的时候才进行获取指针内容
[JavaScript] 纯文本查看 复制代码function print_arg(){
        var libtprtaddr = Module.findBaseAddress("libtprt.so");
       
        console.log("libtprt基址: ",libtprtaddr);
    console.log("libil2cpp基址: ",Module.findBaseAddress("libil2cpp.so"));
       
        var function_addr = libtprtaddr.add(0x1BCE3C);
       
        Interceptor.attach(function_addr,{
                onEnter:function (args) {
            console.log("0x277DA0: ",Memory.readPointer(libtprtaddr.add(0x277DA0)));
                },
                onLeave:function (returnValue) {
                }
        })
}
var isCalled = false;
function hookdlopen() {
    var dlopen = Module.findExportByName(null, "dlopen");
    Interceptor.attach(dlopen, {
        onEnter: function (args) {
            var path = args[0].readCString();
            if (path && path.indexOf('libil2cpp.so') !== -1) {
                this.path = path;
            }
        },
        onLeave: function (retval) {
            if (this.path && this.path.indexOf('libil2cpp.so') !== -1 && !isCalled) {
                print_arg();
                isCalled = true;
            }
        }
    });
}
hookdlopen();
运行后查看打印情况


277DA0frida打印.png (15.37 KB, 下载次数: 1)
下载附件
2025-3-2 17:09 上传

可以明显看到,0x277DA0处的函数指针并不是libtprt内的函数,而是libil2cpp中的,将得到的地址减去libil2cpp的基址,得到0x7281F04,去ida中查看


原版0x7281F04.png (37.9 KB, 下载次数: 0)
下载附件
2025-3-2 17:10 上传

数据并没有解析出来,我们按"C"键来将其主动转化成汇编


反汇编后0x7281F04.png (24.19 KB, 下载次数: 0)
下载附件
2025-3-2 17:11 上传

可以看到他跳转了一个函数,进去跟进去吧


1685104伪c开头.png (79.52 KB, 下载次数: 0)
下载附件
2025-3-2 17:11 上传

可以看到有一个明显的Metadata字符串,这和源码中的LoadMetadataFile函数很类似


LoadMetadataFile源码.png (61.09 KB, 下载次数: 0)
下载附件
2025-3-2 17:11 上传

继续往下看,发现还有类似的字符串,如"ERROR: Could not open %s"


1685104伪c结尾.png (90.23 KB, 下载次数: 0)
下载附件
2025-3-2 17:12 上传

那看来函数应该是找对了,继续对照着看,发现sub_165588C和os::File::Open很类似,都是6个参数,而且v42也和error很像,那么v32就可以认为是源码中的handle了。继续对照源码,源码中只有两个地方调用了handle,分别是utils::MemoryMappedFile::Map和os::File::Close,而ida中的伪c代码也只有两处,分别是sub_16DC91C和sub_1655C7C,故而直接推论,sub_16DC91C就是utils::MemoryMappedFile::Map,那么直接跟进去看看实现


sub_16DC91C伪c.png (12.11 KB, 下载次数: 1)
下载附件
2025-3-2 17:12 上传

跟进去看看


sub_16DCB14伪c.png (32.24 KB, 下载次数: 1)
下载附件
2025-3-2 17:13 上传

如图所示,整个sub_16DCB14只调用了三个函数,我们分别对着三个函数进行分析


sub_16EC43C伪c.png (13.01 KB, 下载次数: 1)
下载附件
2025-3-2 17:13 上传

很明显,sub_16EC43C只是一个计算长度的,直接跳过


sub_165A548伪c.png (23.75 KB, 下载次数: 0)
下载附件
2025-3-2 17:14 上传

同样的,通过sub_165A548的返回值也能看出来并不是主要函数
那就只能是sub_165A6FC了,跟进去看看


sub_165A6FC伪c.png (12.14 KB, 下载次数: 0)
下载附件
2025-3-2 17:14 上传

F5分析的有问题,直接看汇编吧


sub_165A6FC汇编-1.png (57.7 KB, 下载次数: 0)
下载附件
2025-3-2 17:15 上传

果然有问题,BL指令调用完全后是会执行后续指令的,这是带LR寄存器的跳转,所以后续的那个函数也应该包含在sub_165A6FC函数里面,直接去看0x165A740+4,也就是0x165A744处的函数实现


165A744伪c.png (75.97 KB, 下载次数: 0)
下载附件
2025-3-2 17:15 上传

提示栈有问题,不用管,能分析出来就行,查看逻辑,发现返回的result只与sub_F1E0B0有关,那行,跟进去看看


sub_F1E0B0伪c.png (12.5 KB, 下载次数: 0)
下载附件
2025-3-2 17:15 上传

继续跟


off_6C9AF40汇编.png (19.22 KB, 下载次数: 0)
下载附件
2025-3-2 17:16 上传



loc_728198C汇编.png (30.19 KB, 下载次数: 0)
下载附件
2025-3-2 17:16 上传

又看到了熟悉的g_tprt_pfn_array_ptr_0,继续去libtprt里面去找吧,不过这次的偏移量是0xA0


g_tprt_pfn_array_ptr_0偏移A0.png (86.22 KB, 下载次数: 0)
下载附件
2025-3-2 17:17 上传

跟进去,是个B跳转,继续跟,看到了一个函数


sub_1BCA50-1.png (103.12 KB, 下载次数: 0)
下载附件
2025-3-2 17:17 上传



sub_1BCA50-2.png (78.01 KB, 下载次数: 0)
下载附件
2025-3-2 17:17 上传

我们注意到函数内有几个判断值的if语句:
  if ( buf[0] != 0x94 )
    return mmap(addr, len, prot, flags, fd, offset);
  if ( buf[1] != 0x43 )
    return mmap(addr, len, prot, flags, fd, offset);
  if ( buf[2] != 0x72 )
    return mmap(addr, len, prot, flags, fd, offset);
  if ( buf[3] != 0x12 )
    return mmap(addr, len, prot, flags, fd, offset);
        
这与我们开头看到的安装包内的global-metadata.dat的头一模一样,所以基本可以判定,这个就是解密的函数,我们直接hook这个函数的返回值看看:
[JavaScript] 纯文本查看 复制代码function print_arg(){
        var libtprtaddr = Module.findBaseAddress("libtprt.so");
    var libil2cppaddr = Module.findBaseAddress("libil2cpp.so");
        console.log("\n");
        console.log("libtprt基址:",libtprtaddr);
    console.log("libil2cpp基址:",libil2cppaddr);
       
        var function_addr = libtprtaddr.add(0x1BCA50);
    var hooked = false;
        Interceptor.attach(function_addr,{
                onEnter:function (args) {
            this.len = parseInt(this.context.x1);
                },
                onLeave:function (returnValue) {
            if(!hooked){
                hooked = true;
                var currentApplication = Java.use("android.app.ActivityThread").currentApplication();
                var dir = currentApplication.getApplicationContext().getFilesDir().getPath();
                var file_path = dir + "/global-metadata.dat";
                var file_handle = new File(file_path, "wb");
                if (file_handle && file_handle != null) {
                    var buffer = ptr(this.context.x0).readByteArray(this.len);
                    file_handle.write(buffer);
                    file_handle.flush();
                    file_handle.close();
                    console.log("[dump]:", file_path);
                }
            }
                }
        })
}
var isCalled = false;
function hookdlopen() {
    var dlopen = Module.findExportByName(null, "dlopen");
    Interceptor.attach(dlopen, {
        onEnter: function (args) {
            var path = args[0].readCString();
            if (path && path.indexOf('libil2cpp.so') !== -1) {
                this.path = path;
            }
        },
        onLeave: function (retval) {
            if (this.path && this.path.indexOf('libil2cpp.so') !== -1 && !isCalled) {
                print_arg();
                isCalled = true;
            }
        }
    });
}
hookdlopen();
这里需要注意,我是测试过这个函数是第一个加载global-metadata的,所以添加了个hooked变量去控制,如果不清楚是什么时候加载global-metadata的话,可以打印this.len看看,一般来说和安装包内的大小差不多,可能会有些许差距
看看内存dump出来的global-metadta吧


内存global-metadta.png (218.45 KB, 下载次数: 0)
下载附件
2025-3-2 17:18 上传

可以看到,文件头是被抹除了的,但是基本上的内容都还在,我们用UnityMetadata.bt模板跑一遍看看


UnityMetadata跑一遍.png (30.38 KB, 下载次数: 0)
下载附件
2025-3-2 17:19 上传

是报错了的,看来内存dump出来的还是有问题,然后我hook了最开始的sub_1684EF0函数,看看会不会在中途继续解密,结果是没有,最后返回的内容还是和之前hook的一样的
继续分析吧,我们看看Il2CppGlobalMetadataHeader是什么样子


Il2CppGlobalMetadataHeader模板查看.png (52.13 KB, 下载次数: 0)
下载附件
2025-3-2 17:19 上传

可以看到,除了文件头的四个魔数被抹除了之外,其余的信息是全的,那么问题出在哪里呢,通过Il2CppGlobalMetadataHeader的内容我们可以看到,stringLiteralOffset的值为256,即0x100,那么表示文件内容是从0x100开始的,我们查看0x100处的内容,通过与正常的global-metadata.dat文件进行对比,可以确认这里肯定存在加密(因为正常的global-metadata.dat 0x104处的值必须为0)
那怎么办呢?我想到了查看源码,看看源码中有没有调用stringLiteralOffset的地方,通过源码来实现逆向分析。
找完整个源码,发现只有一处调用了stringLiteralOffset


stringLiteralOffset源码调用.png (59.5 KB, 下载次数: 0)
下载附件
2025-3-2 17:20 上传

如何快速定位到这个地址呢?这个函数并没有什么字符串特征,所以并不好通过字符串实现快速定位
这里参考了这位大佬的分析思路,通过il2cpp::vm::String::NewLen来找到对应的函数
https://notion-blog-wine-gamma.vercel.app/article/genshin_analyze_1


il2cpp_string_new_len函数.png (27.23 KB, 下载次数: 0)
下载附件
2025-3-2 17:20 上传

查一下他的交叉引用


il2cpp_string_new_len交叉引用.png (96.82 KB, 下载次数: 0)
下载附件
2025-3-2 17:21 上传

一个个对比,最终定位到sub_16852F0


sub_16852F0-1.png (97.84 KB, 下载次数: 0)
下载附件
2025-3-2 17:21 上传



sub_16852F0-2.png (74.11 KB, 下载次数: 0)
下载附件
2025-3-2 17:21 上传

很好,它在调用stringLiteralOffset的时候肯定是进行解密了的,所以我们直接hook这个情况下的GlobalMetadataHeader。我尝试hook加载后的地址,遗憾的是,它并没有走这条路径,也就是说它自实现了一些解密和加载的函数,并没有选择调用原生函数,所以只能另寻出路了
这个时候其实已经很难分析了,因为它魔改了的话,对比源码已经没太大效果了。
后面我突然想到,他如果进行解密的话,肯定会访问GlobalMetadataHeader的地址,为什么不用监听内存试试呢?说干就干,我首先尝试使用frida的MemoryAccessMonitor来进行监听内存,发现还是hook不到,因为MemoryAccessMonitor原理是使用mprotect来禁止读写执行,进而触发异常被frida监听到,但是mprotect只能针对一整页的内存(大小为0x1000),数据量太大了,并不会有什么效果,所以又要换一种思路,想要单独监听一个内存地址,就只能使用调试器之类的软件了,例如GDB和LLDB,因为我之前并没有使用过这两个调试器,所以选择了我比较熟悉的pwatch,写了个frida脚本来配合pwatch
[JavaScript] 纯文本查看 复制代码function stop(){
    var libtprtaddr = Module.findBaseAddress("libtprt.so");
    var libil2cppaddr = Module.findBaseAddress("libil2cpp.so");
       
        console.log("\n");
        console.log("libtprt基址:",libtprtaddr);
    console.log("libil2cpp基址:",libil2cppaddr);
       
        var function_addr = libil2cppaddr.add(0x1684F68);
        Interceptor.attach(function_addr,{
                onEnter:function (args) {
            console.log(`./arm_64 -t -b ${Process.getCurrentThreadId()} rw8 ${this.context.x0.add(0x100)}`)
                        console.log("开始暂停");
                        // 暂停当前线程 10 秒
            const startTime = Date.now();
            while (Date.now() - startTime
为什么hook 0x1684F68呢,因为这是在前面sub_1685100函数运行成功后的下一个地址,在刚加载完就进行hook,可以有效避免其他情况影响
frida打印为:


stop函数打印.png (20.9 KB, 下载次数: 0)
下载附件
2025-3-2 17:24 上传

pwatch打印为:


pwatch打印.png (100.65 KB, 下载次数: 0)
下载附件
2025-3-2 17:24 上传

距离tprt和il2cpp最近的地址是0x7e906848e8,减去libtprt的基址0x7e904c3000,得到0x1C18E8,直接去tprt里面看看


0x1C18E8处汇编.png (59.96 KB, 下载次数: 0)
下载附件
2025-3-2 17:25 上传

查看一下当前地址所在的函数sub_1C1884吧


sub_1C1884伪c.png (67.09 KB, 下载次数: 0)
下载附件
2025-3-2 17:25 上传

因为堆栈中显示的是lr寄存器,也就是调用的地址+4,所以可知读取stringLiteral的函数是sub_1BDB94,这样其实看伪c已经能看出来很多东西了,因为v5 + v7 + 8LL * a2这个结构,很类似于((const char*)s_GlobalMetadata + s_GlobalMetadataHeader->stringLiteralOffset) + index,进去sub_1BDB94里面看看


sub_1BDB94伪c.png (27.84 KB, 下载次数: 0)
下载附件
2025-3-2 17:26 上传

直接看跟返回值唯一有关的函数sub_1C1C48


sub_1C1C48伪c.png (18.95 KB, 下载次数: 0)
下载附件
2025-3-2 17:26 上传

终于找到解密点了,查看该函数,容易分析出参数1是加密的内容,参数2是长度,参数3是加密值,打印一下看看


sub_1C1C48参数.png (23.99 KB, 下载次数: 0)
下载附件
2025-3-2 17:27 上传

看来分析的没错,长度应该固定为8,前面解释过了,加密值怎么获取的呢?往上层分析,在sub_1BDB94中可以看到,加密值为v9 ^ a4,v9 = sub_9241C(v8, 0LL),a4则为sub_1BDB94的参数
先打印看看这两个是不是固定值,hook后发现v9为固定值,a4则为当前的偏移量,最后根据sub_1C1C20写一个相同的脚本就行了,解密出来后发现都恢复了


修复stringLiteralOffset.png (134.1 KB, 下载次数: 0)
下载附件
2025-3-2 17:27 上传

注意到sub_1C1884中,通过sub_1BDB94获取到v8后,在下面还进行了一处调用,通过对比源码,可以猜测下面的函数中包括stringLiteralData的解密函数,跟进去看了确实如此,同样写一个解密脚本进行还原即可

后记
这篇文章年前就准备写了,只是一直偷懒导致拖了许久。文章中写的都是我最开始尝试时用到的方法,其实还有很多地方可以进行优化,比如在定位解密函数时,是可以hook il2cpp_string_new_len这个导出函数通过打印堆栈来定位到的,当然,这个都是后话了,hook il2cpp_string_new_len并不如我原文中写的方法具体代表性,因为它完全可以自实现这个函数,只不过并没有罢了。文章写到这里其实是并没有完结的,此时使用il2cppdumper还是会报错,metadata里的数据并没有高熵了,那么有问题的地方应该就是il2cpp.so了,但是在写完这篇文章前,我已经没有在玩那个游戏了,耗费这个精力对我来说并不值得。如果评论区有知道的朋友,望不吝赐教

下载次数, 下载附件

tugang0731   

在使用010Editor查看文件熵值时,可以通过以下几种方式具体判断熵值是否很高:
1. 打开010Editor,加载需要分析的文件(如global-metadata.dat)。
在菜单栏中选择“分析”(Analysis)选项,然后选择“熵值分析”(Entropy Analysis)。
软件会生成一个熵值图表,同时会显示文件的整体熵值。
熵值的范围和含义:
熵值范围:熵值的取值范围是0到8(对于二进制文件)。0表示数据完全有序,8表示数据完全随机。
高熵值的判断:
如果熵值接近8(例如7.5以上),则表明文件数据高度随机,很可能经过加密或压缩。
如果熵值较低(例如低于3),则表明文件数据具有一定的规律性,通常是未加密的普通文件。
2. 熵值图表
010Editor生成的熵值图表可以直观地显示文件不同区域的熵值变化:
图表显示:熵值图表通常是一个折线图或柱状图,横轴表示文件的偏移量(位置),纵轴表示熵值。
高熵值区域:
如果图表中大部分区域的熵值都很高(接近8),则表明文件整体是高度随机的。
如果图表中某些区域熵值较低,而其他区域熵值很高,可能表明文件中部分数据未加密,而部分数据经过加密。
3. 对比未加密文件的熵值
为了更准确地判断熵值是否很高,可以对比已知未加密的类似文件的熵值:
找到一个未加密的同类型文件(例如另一个游戏的global-metadata.dat文件)。
使用010Editor对未加密文件进行熵值分析。
对比两个文件的熵值:
如果加密文件的熵值明显高于未加密文件(例如未加密文件熵值为3,加密文件熵值为7.5),则可以判断加密文件的熵值很高。
Luoli28351   

大佬,我买了课程买的不是游戏逆向,课程教的是协议逆向我对此并不敢兴趣,我对游戏逆向比较感兴趣在考虑要不要重新买课,虽然课花不了几块钱,但是视频多了比较耗我内存,
我再某些招聘软件上看到的 大部分都是搞协议逆向的,请问学会了游戏逆向后能解决温饱吗?
mscsky   

il2cppdumper更新到最新版了吗
dengchang   

支持,学习下
Poorwood   

一旦这种对抗起来,就很耗时间了。
.KK   

牛的 分析流程很细节~
jackyyue_cn   

静态动态手段都用上了值得学习
加密保护就是各种混淆、隐藏
调试分析又得各种猜测、还原
唉 程序员何苦要为难程序员
a527573171   

牛的 分析流程很细节~
无问且问
OP
  


mscsky 发表于 2025-3-4 09:22
il2cppdumper更新到最新版了吗

是最新版的
您需要登录后才可以回帖 登录 | 立即注册

返回顶部