Live2dViewerEx,是一个安卓 Live2D 动态壁纸软件,这篇文章记录了对它的破解过程

Live2dViewerEx 是一个老牌软件了,相当于 PC 端的 WallpaperEngine,还移植了 Steam 的创意工坊,安卓 5.1 时代我就用他当手机壁纸软件(谁能拒绝在桌面养一直萌娘呢)。我在中学生时代曾尝试过破解这个软件(无限点数,创意工坊的 kawaii 模型不就可以随便下了吗),但最后以失败告终(当时只会看 Java 层,改了几个类只能修改表层点数,真正的点数并没有办法修改)。如今逆向技术有成重新回过头来分析乐趣无穷。

桌面

# 软件框架

Live2dViewerEx 并不是原生应用,他核心由 Unity 编写,再与 Java 层通信渲染 UI 界面,软件逆向的核心是安卓 Unity 逆向。

# 逆向工具

Frida,Ida64,Il2CppDumper,Jadx-gui

# Java 层分析

通过 Java 层字符串索引,和 OnClick 函数调用堆栈,锁定到 UnityMessenger 类。这个类是处理 Unity 和 Java 层的消息传递的,
onReceive 类接收了字符串通过 base64 解密转化为 JSON 对象

Java.perform(function () {
    let UnityMessenger = Java.use("com.pavostudio.exlib.unity.UnityMessenger");
    UnityMessenger["onReceive"].implementation = function (str) {
        var Base64 = Java.use("android.util.Base64");
        // 你要解码的 Base64 编码字符串
        var base64Str = str; // 这是 "Hello world!" 的 Base64 编码
        // 使用 Base64 解码
        var decodedBytes = Base64.decode(base64Str, 2);
        // 将字节数组转换为字符串
        var decodedStr = Java.use("java.lang.String").$new(decodedBytes);
        var jsonObj = JSON.parse(decodedStr);
        //// 修改 JSON 对象
        // if (jsonObj.msg == 1006 || jsonObj.msg == 1093 || jsonObj.msg == 1094) {
        //     jsonObj.i = 100;  // 例如,修改 someKey 的值
        // }
        if (jsonObj.msg == 1006) {
            let innerJsonStr = jsonObj.s;
            let innerJsonObj = JSON.parse(innerJsonStr);
            // 修改 DownloadCostPoint 字段
            innerJsonObj.DownloadCostPoint = 1;  // 修改为你想要的新值
            innerJsonObj.downloadCostPoint=1;
            innerJsonObj.minWatchReward = 1000000;
            innerJsonObj.maxWatchReward = 1000001;
            // 将修改后的内嵌 JSON 对象转换回字符串
            jsonObj.s = JSON.stringify(innerJsonObj);
        }
        if (jsonObj.msg == 1008) {
            let innerJsonStr = jsonObj.s;
            let innerJsonObj = JSON.parse(innerJsonStr);
           
            let innerJsonObj1 =innerJsonObj.setting;
            // 修改 DownloadCostPoint 字段
            innerJsonObj1.DownloadCostPoint = 1;  // 修改为你想要的新值
            innerJsonObj1.downloadCostPoint=1;
            innerJsonObj1.minWatchReward = 1000000;
            innerJsonObj1.maxWatchReward = 1000001;
            // 将修改后的内嵌 JSON 对象转换回字符串
            innerJsonObj.setting = JSON.stringify(innerJsonObj1);
            jsonObj.s = JSON.stringify(innerJsonObj);
        }
        // 将修改后的对象转回 JSON 字符串
        var modifiedJsonStr = JSON.stringify(jsonObj);
        // 输出解码后的字符串
        var bytes = Java.use("java.lang.String").$new(modifiedJsonStr).getBytes();
        // 使用 Base64 编码字节数组
        var base64Encoded = Base64.encodeToString(bytes, 2);
        if (jsonObj.msg == 1093) {
            console.log('修改后的 JSON 字符串: ' + modifiedJsonStr);
           console.log(`UnityMessenger.onReceive is called: str=${base64Encoded}`);
       }
        this["onReceive"](base64Encoded);
    };
})

尝试 hook 并修改消息内容,和以前效果一样,只是安卓端 UI 界面改变,下载模型点数任然按受限。
Java 层的 Jadx 逆向数据虽然只是前端显示,但我们可以借此分析消息协议。
如:消息号 1006 为同步点数的消息,消息号 1093 为看广告增加的点数消息

# So 层符号恢复

libil2cpp.so 里是 Unity 代码的主逻辑,ida 直接分析缺失符号信息,assets 里的 global-metadata.dat 含有字符串和符号信息。
用 Il2CppDumper 可以生成符号导入脚本和 C# 的类信息。
IDA 使用运行符号导入脚本,等待重新分析完成即可,时间有点长,这个时候可以看看 Il2CppDumper 生成的 DLL 文件。用 dnspy 打开 Assembly-CSharp.dll 看看,这里只有类结构和空的函数头,函数主体并不在内,但是我们可以借此看到函数偏移和类的结构信息。搜索 UnityMessenger,发现一个 onReceive 函数,还有一个 SendUnityMessage 函数,感觉很可疑。Java 层的 onReceive 收到来自 Unity 的消息来渲染前端,那么发送这个信息的一定是一个 Send。

# Frida 分析 Unity 消息系统

等等 ida 分析完成进行 hook`UnityMessenger_SendUnityMessage'。

var mInterval = setInterval(function () {
    var sgame = Process.findModuleByName("libil2cpp.so");
    if (sgame == null) {
        console.log("无");
        return;
    }
    clearInterval(mInterval);
   
  var  addr7= sgame.base.add(0x1C2D3F4 )
    console.log("" +addr7);
    console.log(Instruction.parse(addr7).toString());
    Interceptor.attach(addr7, {
        onEnter: function (args) {
             console.log("Send2 arg1: \n"+hexdump(ptr(args[0]), { length: 100, ansi: true }));
            console.log("Send2 arg2: \n"+hexdump(ptr(args[1]), { length: 100, ansi: true }));
        }
    })
}, 1)

hook 到的消息正是 Java 层分析时 onReceive 接收到的消息,可以判断 Unity 消息由此函数发送。
交叉索引这个函数可以得到很多调用,那到底是那个调用发送点数消息呢,借助 Java 层分析可知消息号 1006 是同步增加的点数消息,我们在 hook 里打印 SendUnityMessage 函数调用堆栈,再找到消息号 1006 下的调用堆栈就可以知道 1006 同步点数消息从哪个地方被发送。

var mInterval = setInterval(function () {
    var sgame = Process.findModuleByName("libil2cpp.so");
    if (sgame == null) {
        console.log("无");
        return;
    }
    clearInterval(mInterval);
   
  var  addr7= sgame.base.add(0x1C2D3F4 )
    console.log("" +addr7);
    console.log(Instruction.parse(addr7).toString());
    Interceptor.attach(addr7, {
        onEnter: function (args) {
            // console.log("Send2 arg1: \n"+hexdump(ptr(args[0]), { length: 100, ansi: true }));
            console.log("Send2 arg2: \n"+hexdump(ptr(args[1]), { length: 100, ansi: true }));
            console.log("Backtrace:\n" + Thread.backtrace(this.context, Backtracer.ACCURATE)
            .map(DebugSymbol.fromAddress).join("\n"));
       }
    })
}, 1)

frida 打印如下消息

Send2 arg2:
           0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
aae4f990  80 ae 56 f5 00 00 00 00 4c 01 00 00 7b 00 22 00  ..V.....L...{.".
aae4f9a0  6d 00 73 00 67 00 22 00 3a 00 31 00 30 00 30 00  m.s.g.".:.1.0.0.
aae4f9b0  36 00 2c 00 22 00 69 00 22 00 3a 00 31 00 30 00  6.,.".i.".:.1.4.
aae4f9c0  32 00 34 00 2c 00 22 00 69 00 32 00 22 00 3a 00  0.0.,.".i.2.".:.
aae4f9d0  30 00 2c 00 22 00 69 00 33 00 22 00 3a 00 30 00  0.,.".i.3.".:.0.
aae4f9e0  2c 00 22 00 69 00 61 00 22 00 3a 00 5b 00 5d 00  ,.".i.a.".:.[.].
aae4f9f0  2c 00 22 00                                      ,.".
Backtrace:
0xb5a01950 libil2cpp.so!0x1e01950
0xb5a012c4 libil2cpp.so!0x1e012c4
0xb583c0ac libil2cpp.so!0x1c3c0ac
0xb5bed380 libil2cpp.so!0x1fed380
0xb5bedc9c libil2cpp.so!0x1fedc9c
0xb408a128 libil2cpp.so!0x48a128
0xb3fb69c0 libil2cpp.so!0x3b69c0
0xb9da76c7 libunity.so!0x1e66c7
0xb9da9eaf libunity.so!0x1e8eaf
0xb9db823b libunity.so!0x1f723b
0xb9cd3c3d libunity.so!0x112c3d
0xb9d41cd3 libunity.so!0x180cd3
0xb9d41cf1 libunity.so!0x180cf1
0xb9d41e8d libunity.so!0x180e8d
0xb9dfc93b libunity.so!0x23b93b
0xb9e0a8a9 libunity.so!0x2498a9

消息是 json 字符串,其中 msg 是消息号 1006, i 正是我当前点数 14。
可以锁定点数同步调用地址 0x1e01950 ,IDA 查看伪代码,核心如下。

v45 = UnityMessage__FPMMBDMKELA(1006);
  v46 = *(unsigned __int8 *)(GMAOFAGNCIO_TypeInfo + 187);
  if ( (v46 & 2) != 0 )
  {
    v46 = *(_DWORD *)(GMAOFAGNCIO_TypeInfo + 116);
    if ( !v46 )
      j_il2cpp_runtime_class_init_0(GMAOFAGNCIO_TypeInfo, 0, v44);
  }
  v47 = GMAOFAGNCIO__LJGKLPOHDKL(0, v46, v44);
  if ( !v45 )
    null(v47);
  v48 = UnityMessage__JDPPCOGEFJE(v45, v47);
  v49 = *(_DWORD *)(GMAOFAGNCIO_TypeInfo + 92);
  v50 = *(_DWORD *)(v49 + 16);
  if ( !v50 )
    null(v49);
  v51 = *(_DWORD **)(v50 + 76);
  if ( !v51 )
    null(v49);
  v53 = AdData_AdSetting__FDIJNPDBFNH(v51);
  if ( (*(_BYTE *)(EOEAIKOEFDF_TypeInfo + 187) & 2) != 0 && !*(_DWORD *)(EOEAIKOEFDF_TypeInfo + 116) )
    j_il2cpp_runtime_class_init_0(EOEAIKOEFDF_TypeInfo, 0, v52);
  v54 = toJSon((int)v53, 1, 0);
  if ( !v48 )
    null(v54);
  v55 = UnityMessage__CIJAGENPKCD(v48, v54);
  if ( !v55 )
    null(0);
  return UnityMessage__OPBMOAEIAIF(v55, 0);

UnityMessage__FPMMBDMKELA(1006) 应该是消息号声明,后面一段直到发送消息为止都是构建消息内容的调用,我们只要分析这几行代码就行了。
GMAOFAGNCIO__LJGKLPOHDKL(0, v46, v44) 进行函数的基本礼仪(交叉引用)。查看其中一个引用 UnityMessageHandler__LAJNIAIBCOK 时代码如下

if ( GMAOFAGNCIO__LJGKLPOHDKL(0, v7, a3) < a2 * a1 )
    {
      v8 = UnityMessage__FPMMBDMKELA(1091);
      if ( !v8 )
        null(0);
      v6 = 0;
      UnityMessenger__SendUnityMessage(v8, 0);
    }

在消息接收处理函数里居然有对这行返回值大小的判断,可以大胆猜测这是 Java 传递的消耗点数的消息。
为了验证这一点,我们 hook 这里就好了

var mInterval = setInterval(function () {
    var sgame = Process.findModuleByName("libil2cpp.so");
    if (sgame == null) {
        console.log("无");
        return;
    }
    clearInterval(mInterval);
   
  var  addr7= sgame.base.add(0x1C2D3F4 )
    console.log("" +addr7);
    console.log(Instruction.parse(addr7).toString());
    Interceptor.attach(addr7, {
        onEnter: function (args) {
            // console.log("Send2 arg1: \n"+hexdump(ptr(args[0]), { length: 100, ansi: true }));
            console.log("Send2 arg2: \n"+hexdump(ptr(args[1]), { length: 100, ansi: true }));
            console.log("Backtrace:\n" + Thread.backtrace(this.context, Backtracer.ACCURATE)
            .map(DebugSymbol.fromAddress).join("\n"));
       }
    })
        var  addr7= sgame.base.add(0x022F84F4)
    console.log("" +addr7);
    console.log(Instruction.parse(addr7).toString());
    Interceptor.attach(addr7, {
        onEnter: function (args) {
        //   console.log("gess arg1: "+hexdump(ptr(this.context.r5), { length: 100, ansi: true }))
        },
        onLeave: function (retval) {
           console.log("FunRet: "+retval)
        }
    })
}, 1)

hook 在 frida 控制台返回 0xe,这正是我当前的点数,写个代码修改返回值。

var mInterval = setInterval(function () {
    var sgame = Process.findModuleByName("libil2cpp.so");
    if (sgame == null) {
        console.log("无");
        return;
    }
    clearInterval(mInterval);
   
  var  addr7= sgame.base.add(0x1C2D3F4 )
    console.log("" +addr7);
    console.log(Instruction.parse(addr7).toString());
    Interceptor.attach(addr7, {
        onEnter: function (args) {
            // console.log("Send2 arg1: \n"+hexdump(ptr(args[0]), { length: 100, ansi: true }));
            console.log("Send2 arg2: \n"+hexdump(ptr(args[1]), { length: 100, ansi: true }));
            console.log("Backtrace:\n" + Thread.backtrace(this.context, Backtracer.ACCURATE)
            .map(DebugSymbol.fromAddress).join("\n"));
       }
    })
        var  addr7= sgame.base.add(0x022F84F4)
    console.log("" +addr7);
    console.log(Instruction.parse(addr7).toString());
    Interceptor.attach(addr7, {
        onEnter: function (args) {
        //   console.log("gess arg1: "+hexdump(ptr(this.context.r5), { length: 100, ansi: true }))
        },
        onLeave: function (retval) {
           console.log("FunRet: "+retval);
           retval.replace(10000)
        }
    })
}, 1)

打开软件查看点数真变成 10000 了,而且在创新工坊,15 点数的模型下载也没有问题。大喜过望,修改成功了!!!
GMAOFAGNCIO__LJGKLPOHDKL 就是软件返回点数数据的函数。

# 修改程序

因为一开始的目的就是奔着破解软件去的,hook 后还没结束,应该修改代码。
查看 GMAOFAGNCIO__LJGKLPOHDKL 的 arm 汇编。
ARM 架构返回值通常会存储在 R0 寄存器。
GMAOFAGNCIO__LJGKLPOHDKL 最后跳转到 UnityEngine.Mathf$$Max_10639724 函数,交叉引用后感觉并不是很重要的函数。直接修改函数汇编代码,让返回值改变。

il2cpp:00A2596C                 MOV             R1, #0x400
il2cpp:00A25970                 MOV             R0, #0x400
il2cpp:00A25974                 BX              LR

如此修改即可返回 1024 个点数。Apply Patch 后覆盖原本的 lib。

# 破解签名验证

修改了 lib 安装完成发现软件打开就闪退。经过 Hook 发现闪退位置还是在 Java 层的 SendUnityMessage , 仔细查看有下面的可疑代码。

case 1007:
                    UnityMessage.create(1007).setString(AppUtil.getSignature(ExApplication.get())).send();
                    return;

分析函数是 APk 签名验证的,哈哈,那我就安装原来没修改的软件,然后 Hook 签名数据,再修改 Smali,伪装一个正确的签名。
定位 Smali 代码,并修改数据如下。

invoke-static {v0}, Lcom/pavostudio/exlib/util/AppUtil;->getSignature(Landroid/content/Context;)Ljava/lang/String;
    move-result-object v0
    const-string v0, "3082019f30820108a00302010202045365a59b300d06092a864886f70d010105050030133111300f060355040313086f756b6169746f753020170d3134303530343032323733395a180f32313134303431303032323733395a30133111300f060355040313086f756b6169746f7530819f300d06092a864886f70d010101050003818d0030818902818100b2e1d67613a0fbe26fd3ea512fd7787c93d9b7053ef295b8099414cd3049916567fc1b4601ded6e18263b2a2d2b309067a912048eee368216a5dcee5ad987550de4373f7fababac7a0e3826c07b3463c10ea4a08b6986adce751b5a86f8bd062bcf866ec732fe3773d72a60144879881979d147d1c04eacdb7d7c5e89c492e2f0203010001300d06092a864886f70d0101050500038181004d5d127b7320f37271b594b59f2bb44a2fa96ca9324074268829f0ace9efc157d5844ddc5c5d0a9c71a8758a05d8544f99287ba458081abe02c91d51a6531243a31a23e1aa3cfff4b5abe6cfbdba20f34eb3c0de873b913ebdcafd0daa8c8a4a8ea17d28a76fa34f1418d201faf27305989bd2458d91b7277859fda40ee687b4"

到此重新签名软件,安装,软件正常打开,没有闪退。
打开软件测试,不管怎么下载模型,点数都被锁定为 1024,软件破解成功,美滋滋。

# 总结

熟练掌握 Frida 后分析软件真是事半功倍,逆向乐趣无穷。
想要 Live2dViewerEx 破解版的朋友可以在评论区留下邮箱,哈哈哈哈。

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

PangBai 微信支付

微信支付

PangBai 支付宝

支付宝

PangBai 贝宝

贝宝