吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 3865|回复: 87
上一主题 下一主题
收起左侧

[Android 原创] 某手游il2cpp逆向分析----libtprt保护

  [复制链接]
跳转到指定楼层
楼主
无问且问 发表于 2025-3-2 17:32 回帖奖励
最近在玩个游戏,发现是由il2cpp进行打包的,就打算用il2cppdumper来dump看看游戏内容

开干
说干就干,提取游戏安装包,在lib/arm64-v8a路径提取出libil2cpp.so,在assets/bin/Data/Managed/Metadata路径提取出global-metadata.dat

直接打开il2cppdumper,选择这两个文件,发现报错:


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


ok了,既然安装包中的global-metadata.dat被加密了,那我直接去内存中dump到的,应该就没问题吧!

既然要从内存中获取到global-metadata.dat,那肯定要根据libil2cpp.so中的逻辑来找出加载global-metadata.dat的地方,当然也可以通过在内存中搜寻魔数头的方式来找到文件头(ps:这个例子的魔数头也被抹除了,所以只能采取分析libil2cpp.so中的逻辑了@_@;)

事情果然没这么简单,当我用IDA打开libil2cpp.so后,发现libil2cpp.so也被加固了,导出表被抹除完了

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


网上搜索得知,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打开,发现报错


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


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


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


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


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


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


继续跟sub_2F0


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


跟到最后发现原来是调用了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导出函数


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


进去看看


继续跟进去


F5看下伪c吧


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

函数大致流程分析的差不多了,先去看看0x277DA0处的函数指针吧


可以看到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();


运行后查看打印情况


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



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


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


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


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


那看来函数应该是找对了,继续对照着看,发现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_16DCB14只调用了三个函数,我们分别对着三个函数进行分析


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


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


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


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


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


继续跟






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



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





我们注意到函数内有几个判断值的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吧


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


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

继续分析吧,我们看看Il2CppGlobalMetadataHeader是什么样子


可以看到,除了文件头的四个魔数被抹除了之外,其余的信息是全的,那么问题出在哪里呢,通过Il2CppGlobalMetadataHeader的内容我们可以看到,stringLiteralOffset的值为256,即0x100,那么表示文件内容是从0x100开始的,我们查看0x100处的内容,通过与正常的global-metadata.dat文件进行对比,可以确认这里肯定存在加密(因为正常的global-metadata.dat 0x104处的值必须为0)

那怎么办呢?我想到了查看源码,看看源码中有没有调用stringLiteralOffset的地方,通过源码来实现逆向分析。
找完整个源码,发现只有一处调用了stringLiteralOffset



如何快速定位到这个地址呢?这个函数并没有什么字符串特征,所以并不好通过字符串实现快速定位

这里参考了这位大佬的分析思路,通过il2cpp::vm::String::NewLen来找到对应的函数
https://notion-blog-wine-gamma.vercel.app/article/genshin_analyze_1



查一下他的交叉引用


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





很好,它在调用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 < 10000) {

            }

            console.log("恢复线程");
		},
		onLeave:function (returnValue) {
			
		}
	})
}


为什么hook 0x1684F68呢,因为这是在前面sub_1685100函数运行成功后的下一个地址,在刚加载完就进行hook,可以有效避免其他情况影响

frida打印为:


pwatch打印为:


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


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


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



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


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


看来分析的没错,长度应该固定为8,前面解释过了,加密值怎么获取的呢?往上层分析,在sub_1BDB94中可以看到,加密值为v9 ^ a4,v9 = sub_9241C(v8, 0LL),a4则为sub_1BDB94的参数

先打印看看这两个是不是固定值,hook后发现v9为固定值,a4则为当前的偏移量,最后根据sub_1C1C20写一个相同的脚本就行了,解密出来后发现都恢复了



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


后记

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

免费评分

参与人数 32吾爱币 +32 热心值 +28 收起 理由
nullsci + 1 谢谢@Thanks!
安尼大大 + 1 + 1 我很赞同!
海水很咸 + 1 + 1 我很赞同!
RF52PJ + 1 用心讨论,共获提升!
Sydyanlei0 + 1 + 1 用心讨论,共获提升!
zklkk + 1 + 1 我很赞同!
chukr11 + 1 + 1 鼓励转贴优秀软件安全工具和文档!
zbsex + 1 我很赞同!
devil2334 + 1 用心讨论,共获提升!
Jerry187 + 1 用心讨论,共获提升!
freeeeeG + 1 + 1 用心讨论,共获提升!
InfiniteBoy + 1 + 1 用心讨论,共获提升!
ww5500231 + 1 谢谢@Thanks!
SanCaiOjisang + 1 用心讨论,共获提升!
huanghui9969 + 1 + 1 我很赞同!
LinJue22 + 1 + 1 我很赞同!
RikimaruMarlon + 1 + 1 厉害~~~
1amfree + 1 + 1 我很赞同!
sujifei + 1 + 1 我很赞同!
lst13145920 + 1 + 1 谢谢@Thanks!
CrazyNut + 3 + 1 膜拜大佬
jackyyue_cn + 1 + 1 用心讨论,共获提升!
umbella + 1 + 1 用心讨论,共获提升!
流年 + 1 + 1 用心讨论,共获提升!
Weirdo + 1 + 1 谢谢@Thanks!
Yao2903 + 1 + 1 谢谢@Thanks!
A_DUST + 1 + 1 谢谢@Thanks!
allspark + 1 + 1 用心讨论,共获提升!
Minesa + 1 + 1 用心讨论,共获提升!
helian147 + 1 + 1 热心回复!
ngiokweng + 2 + 1 谢谢@Thanks!
iamshy520 + 1 + 1 用心讨论,共获提升!

查看全部评分

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

推荐
 楼主| 无问且问 发表于 2025-3-19 19:48 |楼主
jy3399 发表于 2025-3-18 22:18
有时候真好奇大佬们的脑袋怎么长的,同样都吃了这么多年饭,感觉自己跟个白痴一样!

我还是建议直接实战的,但是难度要一点一点提上来,不能好高鹭远,建议先往java层的方向实战,然后再往native层进军,有不懂的可以问AI,但是思路是要自己实践出来的,AI大部分情况只能用作分析,最好把AI的分析多琢磨琢磨,然后形成一套具体的思路
推荐
zkbutt 发表于 2025-3-20 16:54
太复杂了,这得费多在的精力,最近研究il2cpp  发现,不需要 费很大劲去弄 global-metadata.dat ,直接内存解析 il2cpp.so即可!
沙发
mscsky 发表于 2025-3-4 09:22
3#
dengchang 发表于 2025-3-4 09:34
支持,学习下
4#
Poorwood 发表于 2025-3-4 10:41
一旦这种对抗起来,就很耗时间了。
5#
.KK 发表于 2025-3-4 11:21
牛的 分析流程很细节~
6#
jackyyue_cn 发表于 2025-3-4 12:48
静态动态手段都用上了值得学习

加密保护就是各种混淆、隐藏
调试分析又得各种猜测、还原

唉 程序员何苦要为难程序员
7#
a527573171 发表于 2025-3-4 15:57

牛的 分析流程很细节~
8#
 楼主| 无问且问 发表于 2025-3-4 17:09 |楼主
mscsky 发表于 2025-3-4 09:22
il2cppdumper更新到最新版了吗

是最新版的
9#
jianguo85 发表于 2025-3-4 18:01


牛的 分析流程很细节~
10#
AIRForce 发表于 2025-3-4 18:31
逆向大佬
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

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

GMT+8, 2025-3-25 03:01

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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