记一次unity的资源逆向

查看 86|回复 11
作者:fjqzlqh   
废话不多说,直奔主题~~
关于工具的用法不会讲的太细,如果有什么不懂的可以留言一起探讨
需要准备的工具:
1.IDA
2.dnSpy
3.Il2CppDumper
4.frida
5.一个root的安卓手机
将目标apk修改后缀为rar并解压


image.png (31.99 KB, 下载次数: 0)
下载附件
2022-12-5 17:44 上传

我们随意打开一个资源文件发现它是经过加密的


image.png (18.75 KB, 下载次数: 0)
下载附件
2022-12-5 18:17 上传



image.png (52.48 KB, 下载次数: 0)
下载附件
2022-12-5 18:17 上传

正常的unity资源文件是以Unity开头的


image.png (40.16 KB, 下载次数: 0)
下载附件
2022-12-5 18:18 上传

用Il2CppDumper工具导出dll(你的手机是64位的就选arm64的so,否则选32位的so)


image.png (63.28 KB, 下载次数: 0)
下载附件
2022-12-5 17:53 上传



image.png (43.11 KB, 下载次数: 0)
下载附件
2022-12-5 17:54 上传

导出后会在Il2CppDumper工具的目录下DummyDll文件夹


image.png (54.12 KB, 下载次数: 0)
下载附件
2022-12-5 17:58 上传

接下来使用dnSpy打开Assembly-CSharp.dll


image.png (69.2 KB, 下载次数: 0)
下载附件
2022-12-5 18:00 上传

使用IDA打开前面提到的libil2cpp.so并导入符号表,且等待ida分析完成(这里时间可能需要比较久,分析没完成之前,可能无法按F5转到C源码分析)


image.png (112.66 KB, 下载次数: 0)
下载附件
2022-12-5 18:03 上传

OK! 准备工作已经完成,接下里分析代码
Unity的资源加载主要是依靠Assetbundle
我们在dnspy里正好看到一个
[color=]AssetBundleUtil的类


image.png (34.11 KB, 下载次数: 0)
下载附件
2022-12-5 18:14 上传

这个类里有多个加载函数


image.png (58.45 KB, 下载次数: 0)
下载附件
2022-12-5 18:21 上传

而看起来LoadAsset 和 LoadAssetPrefab 比较像是加载资源的(猜的...)
接下来我们去IDA查看 直接搜索LoadAsset


image.png (31.57 KB, 下载次数: 0)
下载附件
2022-12-5 18:24 上传

通过查看源码发现不管是LoadAsset还是LoadAssetPrefab都是调用的GetOrLoadAssetBundleByAssetPath
LoadAsset代码


image.png (71.83 KB, 下载次数: 0)
下载附件
2022-12-5 18:27 上传

LoadAssetPrefab代码


image.png (24.08 KB, 下载次数: 0)
下载附件
2022-12-5 18:27 上传



image.png (71.69 KB, 下载次数: 0)
下载附件
2022-12-5 18:28 上传

而GetOrLoadAssetBundleByAssetPath最终调用的是AssetBundleUtil__Load


image.png (27.8 KB, 下载次数: 0)
下载附件
2022-12-5 18:32 上传

继续分析AssetBundleUtil__Load函数,经过分析发现Unity自身的API,UnityEngine_AssetBundle__LoadFromMemoryAsync和UnityEngine_AssetBundle__LoadFromMemory这两个函数都是从内存中加载资源,那么说明v25就是解密过的数据,而v25是通过CryptUtil__DecryptToBytes函数返回的!
那么DecryptToBytes就是我们要的解密函数
而GetByteArrayFromFile函数可以让我们获取到资源的路径


image.png (43.37 KB, 下载次数: 0)
下载附件
2022-12-5 19:25 上传

回到dySpy 查看GetByteArrayFromFile 和 DecryptToBytes的函数偏移地址


image.png (9.75 KB, 下载次数: 0)
下载附件
2022-12-5 19:27 上传



image.png (8.83 KB, 下载次数: 0)
下载附件
2022-12-5 19:27 上传

开始写frida代码 decode.js
主要功能就是Hook GetByteArrayFromFile 和 DecryptToBytes函数 通过参数和返回值来获取资源目录以及解密后的bytes并发送给python代码直接写入电脑的文件夹下
需要注意的是,64位手机和32位手机获取字符串和bytes数组的偏移量不一样.如var str = args[0].add(20).readUtf16String(-1) 在32位手机中就不是add(20)了
[JavaScript] 纯文本查看 复制代码"use strict";
Java.perform(function () {
    console.log("==============================================");
    //这里libil2cpp.so还没有加载完成,所以下个定时器循环获取直到libil2cpp.so加载完成
    var i = setInterval(function () {
        var addr = Module.findBaseAddress('libil2cpp.so');
        if (addr) {
            let path;
            console.log("libil2cpp 已加载~~~~~~~~~~~~");
            //GetByteArrayFromFile
            Interceptor.attach(addr.add("0xF9F68C"), {
                onEnter(args) {
                    var str = args[0].add(20).readUtf16String(-1);
                    if (str) {
                        str = str.replace("/storage/emulated/0/Android/data/com.xxx.xxx.android/files/Assets/", "Assets2/")
                        path = "/sdcard/" + str
                        console.log(path);
                        }
                },
                onLeave(retval) {
                    //console.log(retval);
                }
            });
            let ret;
            //static byte[] DecryptToBytes(byte[] bytes)
            Interceptor.attach(addr.add("0xF1C244"), {
                onEnter(args) {
                    ret = args[0];
                },
                onLeave(retval) {
                    ret = retval
                    let size = (ret.add(24).readLong());
                    let data = ret.add(32).readByteArray(size);
                    send(path, data);
                    //以下方法可以直接保存在手机的sdcard目录
                    // let file = new File(path, "wb");
                    // file.write(data);
                    // file.flush();
                    // file.close();
                }
            });
            clearInterval(i);
        }
    }, 100);
});
main.py[Python] 纯文本查看 复制代码import frida, sysimport struct
import os
def on_message(message, data):
    if message['type'] == 'send':
        strPath = message['payload']
        print(" {0}".format(strPath))
        if isinstance(data, bytes):
          n = strPath.rindex("/")
          dir = "H:" + strPath[0:n]
          if not os.path.exists(dir):
            print("创建目录:"+dir)
            os.makedirs(dir)
          fw = open('H:'+ strPath, 'wb')
          fw.write(data)
          fw.close()
    else:
        print(message)
device = frida.get_usb_device()
pid = device.spawn(["com.xxx.xxx.android"])//这里是apk的包名
device.resume(pid)
session = device.attach(pid)
# session = device.attach(pid, realm='emulated') #模拟器专用
f = open("decode.js",'r+')
script = session.create_script(f.read())
script.on('message', on_message)
print(' Running CTF')
script.load()
sys.stdin.read()
开启手机的frida-server


image.png (8.75 KB, 下载次数: 0)
下载附件
2022-12-5 19:37 上传

转发端口


image.png (7.18 KB, 下载次数: 0)
下载附件
2022-12-5 19:39 上传

执行main.py


image.png (5.95 KB, 下载次数: 0)
下载附件
2022-12-5 19:44 上传

我们就可以在指定目录下看到解密后的资源了


image.png (14.26 KB, 下载次数: 0)
下载附件
2022-12-5 19:45 上传

将sdcard文件夹拖入AssetStudioGUI就可以得到正确的资源文件啦


image.png (106.28 KB, 下载次数: 0)
下载附件
2022-12-5 19:47 上传

这个方法的缺点是,只能获取游戏当前加载的资源,如果这个资源没有加载过就获取不到
可以通过自己构造路径然后手动调用AssetBundleUtil__Load方法来解决这个问题
注:第一次写文章写的不好的地方请大家见谅,欢迎大家留言探讨~

下载次数, 下载附件

fjqzlqh
OP
  


故事散场 发表于 2022-12-6 20:48
大佬, 如果.ab格式的包也可以这样干吗?

可以的,跟后缀名没关系
fjqzlqh
OP
  


Poorwood 发表于 2023-1-6 11:16
你好,frida中的hook方法后,你 有一个  add(20) 这么一个操作。我看上文,这个貌似和64、32有关?这个20是 ...

这个主要是类头包含的信息,32位跟64位大小不一样.如果你熟悉CLI就能知道了,如果不熟悉可以通过打印字节数组的方式来找到你要的数据
fjqzlqh
OP
  

最后两张无用的附件图片怎么删除
surpasskarma   

这个是游戏引擎吗?
lfordch   

感谢楼主分享!学习了!
lu5156   

感谢分享
Clown4730   

参考参考
ltg1831   

用心学习
故事散场   

大佬, 如果.ab格式的包也可以这样干吗?
您需要登录后才可以回帖 登录 | 立即注册

返回顶部