乐于分享
好东西不私藏

某超百万下载量APP逆向:从 Java 到 JNI 底层

某超百万下载量APP逆向:从 Java 到 JNI 底层

0、前言

在平时的安全测试与逆向分析中,遇到加密是家常便饭。今天我们将以某款拥有超百万下载量的知名APP为例,分享一次从抓包分析到Java层定位,再深入JNI底层(SO层)破解核心加密逻辑的完整逆向实战过程。

一、 数据包分析

逆向的第一步永远是观察。我们首先对APP进行抓包,对比不同业务场景下的数据形态:

1. 登录功能数据包

观察登录接口的请求,可以明显看到关键数据进行了加密处理。

2. 非登录功能数据包

作为对比,我们抓取了普通业务请求,发现依然存在加密。

分析总结:

综合以上两个数据包的特征,我们可以提炼出两个关键的逆向突破口:

  • 关键路由定位:/appindex/index
  • 核心关键字:statistics_app_paramssign

二、 Java层逆向与动态Hook

拿到突破口后,我们正式进入代码层。注意:该APP存在加固,需要先进行脱壳处理(网上相关脱壳教程非常丰富,本文不再赘述)。

我们在反编译后的源码中全局搜索路由/appindex/index。运气不错,搜索结果只有一个,这基本可以断定此处就是与协议加解密强相关的逻辑分发点。

结合AI辅助分析响应包的解密点(这里分享一个经验:在APP逆向中,加解密逻辑通常是成对出现且共用底层架构的,搞定了加密,解密往往也就迎刃而解)。

我们跟进a()方法,发现内部存在一个分支判断:要么直接返回明文,要么解密密码后再返回。这非常符合该APP“部分接口加密,部分接口明文”的设计逻辑。

继续步入b()方法,发现这里封装了两种解密算法:RSA和AES。而且,这两个解密方法均指向了JCC这个类。

进入JCC类后,真相大白:所有的加解密方法全都在这里,但类顶部加载了一个名为framework的原生库。这说明Java层只是一层JNI桥接,真正的核心代码逻辑被藏在了SO层中

由于目前还不确定实际通信使用的是AES还是RSA,盲目去分析SO会浪费时间。因此,我们先编写一段Hook脚本,通过监控函数调用来动态判定加密模式。

注意:因为目标APP需要动态加载SO文件,所以我们在脚本外层包裹了一个setTimeout延时,确保类加载完成后再进行Hook。

//char[]转换为Stringfunction charArrayToString(charArray) {    if (!charArray) {        return "null";    }    // 获取 Java 的 String 类    var JavaString = Java.use("java.lang.String");    // 实例化 String 并将其强转为 JS 标准字符串    return JavaString.$new(charArray).toString();}//byte[]转换为Stringfunction byteArrayToString(byteArray) {    if (!byteArray) {        return "null";    }    var JavaString = Java.use("java.lang.String");    // 相当于 Java 中的 new String(byte[])    return JavaString.$new(byteArray).toString();}setTimeout(function () {    Java.perform(function () {        var JCC = Java.use("cn.mama.framework.jnibridge.JCC");        JCC.decryptByAes.implementation = function (cArr, bArr) {            console.log(`JCC.decryptByAes:cArr=${cArr}, bArr=${bArr}`);            return this.decryptByAes(cArr, bArr);        };        JCC.decryptByRsa.implementation = function (str, i10, bArr) {            console.log(`JCC.decryptByRsa:str=${str}, i10=${i10}, bArr=${bArr}`);            return this.decryptByRsa(str, i10, bArr);        };        JCC.encryptByAes.implementation = function (cArr, bArr) {            console.log(`JCC.encryptByAes:cArr=${charArrayToString(cArr)}, bArr=${byteArrayToString(bArr)}`);            return this.encryptByAes(cArr, bArr);        };        JCC.encryptByAesWithKey.implementation = function (cArr, bArr) {            console.log(`JCC.encryptByAesWithKey:cArr=${charArrayToString(cArr)}, bArr=${byteArrayToString(bArr)}`);            return this.encryptByAesWithKey(cArr, bArr);        };        JCC.encryptByAesWithSKKey.implementation = function (str, bArr) {            console.log(`JCC.encryptByAesWithSKKey:str=${str}, bArr=${byteArrayToString(bArr)}`);            return this.encryptByAesWithSKKey(str, bArr);        };        JCC.encryptByRsa.implementation = function (str, i10, bArr) {            console.log(`JCC.encryptByRsa:str=${str}, i10=${i10}, bArr=${byteArrayToString(bArr)}`);            return this.encryptByRsa(str, i10, bArr);        };    });}, 5000)

运行结果如下,在登录功能成功调用的加解密分别是:encryptByAesdecryptByAes

三、SO层深挖与“降维打击”

确定了目标后,我们将APK文件解压,在\lib\arm64-v8a\目录下提取出目标库libframework.so

1. IDA 静态分析

将SO文件拖入IDA Pro中,在导出表中搜索我们刚刚Hook到的对应的JNI函数名。虽然出现了两个结果,但交叉引用后发现它们最终指向的都是同一个底层方法,任选其一跟进即可。

顺着伪代码的逻辑一路向下分析,经过层层剥茧,我们最终定位到了核心加密点。有趣的是,这里的C++代码并没有自己手搓一套AES算法,而是通过JNI环境反向调用了Android原生的Java标准密码库(javax.crypto)

2. 通用Hook提取密钥

既然底层依然是调用标准的Java库,那我们就可以直接实施“降维打击”——不需要在SO层死磕汇编指令,直接去Hook原生的Java加密类,通杀拦截!

编写针对javax.crypto的通 Hook脚本,直接抓取初始化时的Key、IV以及加密前后的明密文,这个hook脚本可以通用,可以理解为aes的自吐算法

//char[]转换为Stringfunction charArrayToString(charArray) {    if (!charArray) {        return "null";    }    // 获取 Java 的 String 类    var JavaString = Java.use("java.lang.String");    // 实例化 String 并将其强转为 JS 标准字符串    return JavaString.$new(charArray).toString();}//byte[]转换为Stringfunction byteArrayToString(byteArray) {    if (!byteArray) {        return "null";    }    var JavaString = Java.use("java.lang.String");    // 相当于 Java 中的 new String(byte[])    return JavaString.$new(byteArray).toString();}function byteArrayToHex(byteArray) {    if (!byteArray) {        return "null";    }    var hexString = "";    for (var i = 0; i < byteArray.length; i++) {        // Java 的 byte 是有符号的 (-128 到 127),需要 & 0xFF 转为无符号的 0-255,再转 16 进制        var hex = (byteArray[i] & 0xFF).toString(16);        if (hex.length === 1) {            hex = "0" + hex; // 补零对齐        }        hexString += hex;    }    return hexString;}setTimeout(function () {  Java.perform(function () {      var SecretKeySpec = Java.use('javax.crypto.spec.SecretKeySpec');      SecretKeySpec.$init.overload('[B''java.lang.String').implementation = function (keyBytes, algorithm) {          console.log("\n[*] 发现 SecretKeySpec 实例化 (抓取到 Key!)");          console.log("[-] 算法: " + algorithm);          console.log("[-] Key (Hex): " + byteArrayToHex(keyBytes));          console.log("[-] Key (String): " + byteArrayToString(keyBytes));          return this.$init(keyBytes, algorithm);      };      //Hook IvParameterSpec 抓取底层传上来的 IV      var IvParameterSpec = Java.use('javax.crypto.spec.IvParameterSpec');      IvParameterSpec.$init.overload('[B').implementation = function (ivBytes) {          console.log("\n[*] 发现 IvParameterSpec 实例化 (抓取到 IV!)");          console.log("[-] IV (Hex): " + byteArrayToHex(ivBytes));          console.log("[-] IV (String): " + byteArrayToString(ivBytes));          return this.$init(ivBytes);      };      //Hook Cipher.doFinal 抓取加密前后的明文和密文      var Cipher = Java.use('javax.crypto.Cipher');      Cipher.doFinal.overload('[B').implementation = function (data) {          console.log("\n[*] 发现 Cipher.doFinal 被调用");          console.log("[-] 输入数据 (明文/密文 Hex): " + byteArrayToHex(data));          console.log("[-] 输入数据 (String): " + byteArrayToString(data));          var result = this.doFinal(data);          //console.log("[-] 输出数据 (明文/密文 Hex): " + bytesToHex(result));          return result;      };  });}, 5000)

重新运行脚本,完美捕获!最终的密钥、IV向量及完整的明密文流全部呈现眼前。至此,该APP的核心加密被成功逆向突破。

进行解密,成功解开

总结

其实针对这种原生密码库的调用,完全可以使用“算法助手”等现成的Xposed模块来实现Key和IV的自动化获取。但本着知其然更要知其所以然,完整地走一遍从Java到SO层再回到底层的逆向分析流程,对提升我们的汇编阅读能力和逆向直觉有着不可替代的帮助。

当你习惯了扒开底层的迷雾去探寻真相,再复杂的代码加固也只是时间问题。

免责声明

本文中所涉及的技术、思路和工具仅供安全技术交流和学习研究使用。请勿将文章内的相关技术用于任何非法用途(包括但不限于未经授权的渗透测试、数据爬取、黑产牟利等)。读者在阅读本文后产生的一切行为及其引发的法律后果,均由操作者本人承担,文章作者及发布平台概不负责。维护网络安全,从合规做起!

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » 某超百万下载量APP逆向:从 Java 到 JNI 底层

猜你喜欢

  • 暂无文章