最近有不少朋友问我,怎么分析一个App的加密数据流。这事儿听起来高大上,其实说白了就是搞清楚App怎么把数据加密传输,再想办法把它解密出来。今天我就用某小说App的实战案例,带大家走一遍完整的逆向流程。
我选这个App有几个原因:首先它数据量够大,适合练手;其次它的加密方式比较典型,学会了这套方法,80%的App都能搞定;最重要的是,它没有特别复杂的反调试机制,对新手比较友好。咱们这次的目标很明确:找到小说内容的加密响应,定位关键URL,绕过登录和VIP限制,最终解密出原始数据。
工欲善其事必先利其器,先说说要用到的工具:
这些工具的具体安装配置网上教程很多,我就不赘述了。重点提醒一下,记得准备一台root过的安卓测试机,或者用模拟器也行。真机调试记得关掉各种安全软件,不然可能会拦截我们的调试行为。
第一步要找到小说内容是从哪个URL获取的。听起来简单,但实际找起来就像大海捞针。我一开始用JADX直接搜索"novel-content"这样的关键词,结果一无所获。这说明URL很可能不是硬编码在代码里的,而是动态获取的。
这时候Charles就派上用场了。我在App里随便点开几本小说,同时监控网络请求。果然在某个返回包里发现了content_url字段。这个发现很重要,它告诉我们小说内容是通过另一个接口动态获取的。
有了这个线索,回到JADX搜索"content_url",这次确实找到了相关代码,但问题来了——这个变量在哪被使用了?代码里没有直接调用它的地方。这种情况通常说明URL是通过反射或者动态代理的方式处理的,直接搜索字符串可能找不到。
就在我琢磨怎么定位URL处理逻辑时,App突然要求登录,而且登录后还要买VIP才能继续阅读。这可不行,我们只是想做技术分析,没必要真去充钱。
解决这个问题有两个思路:
我选择了第二种方法,因为更彻底。在JADX里搜索"登录"关键词,很快定位到CacheUtils类。这个类名起得真直白,连打码都省了。里面有两个关键方法:
用Frida写个hook脚本就能轻松绕过:
javascript复制var CacheUtilsCls = Java.use('com.xxoo.net.net.CacheUtils');
CacheUtilsCls.isVip.implementation = function(){
return true;
};
CacheUtilsCls.isLogin.implementation = function(){
return true;
};
这样App就认为我们既是登录用户又是VIP了。这种修改运行时内存的方法比直接改APK方便多了,重启App就会恢复原状,不会留下永久性修改。
现在可以畅通无阻地阅读了,但我们的目标是分析加密数据。既然知道了内容URL,下一步就是找到数据解密的地方。
我先尝试hook URL.openConnection()方法,想监控所有网络请求:
javascript复制var URL = Java.use('java.net.URL');
URL.openConnection.overload().implementation = function() {
var retval = this.openConnection();
console.log('URL openConnection' + retval);
return retval;
};
还hook了OkHttp的newCall方法:
javascript复制var OkHttpClient = Java.use("okhttp3.OkHttpClient");
OkHttpClient.newCall.implementation = function (request) {
var result = this.newCall(request);
console.log(request.toString());
return result;
};
但奇怪的是,这些hook点都没抓到小说内容的请求。这说明App可能用了自定义的网络库,或者对请求做了特殊处理。
既然常规方法不奏效,那就祭出"大海捞针"大法——监控所有字符串操作。因为不管加密多复杂,最终解密后的内容总要转换成字符串显示在界面上。
hook StringBuilder的toString方法是个不错的选择:
javascript复制var strCls = Java.use("java.lang.StringBuilder");
strCls.toString.implementation = function(){
var result = this.toString();
console.log(result.toString());
return result;
}
运行后控制台瞬间刷出大量日志,但就在这堆乱码中,我发现了一个关键线索:"AES/CBC/PKCS5Padding"。这明显是AES加密的参数,而且很可能是解密时使用的算法。
有了加密算法的线索,就可以优化我们的hook脚本,只关注相关字符串:
javascript复制var strCls = Java.use("java.lang.StringBuilder");
strCls.toString.implementation = function(){
var result = this.toString();
if(result.toString().indexOf("AES/CBC/PKCS5Padding") >= 0 ) {
console.log(result.toString());
var stack = threadinstance.currentThread().getStackTrace();
console.log("Rc Full call stack:" + Where(stack));
}
return result;
}
这次堆栈信息直接指向了NativeBds.dae1方法。从名字看这是个native方法,但堆栈显示它最终调用了javax.crypto的加解密功能。这说明虽然核心算法在so库中实现,但接口层还是用了Java的标准加密框架。
最后的步骤就简单了,直接hook这个解密方法:
javascript复制var DecodeCls = Java.use('com.baidu.searchbox.NativeBds');
DecodeCls.dae1.implementation = function(a,b){
var rc = this.dae1(a,b);
var StrCls = Java.use('java.lang.String');
var inStr = StrCls.$new(rc);
console.log(inStr);
return rc;
}
运行后,期待已久的明文小说内容终于出现在控制台。至此,我们完成了从加密数据流定位到解密的完整流程。
这次逆向过程中有几个关键点值得总结:
逆向工程最考验的不是技术,而是耐心。有时候可能要hook几十个方法才能找到关键点,但这就是逆向的乐趣所在——像解谜游戏一样,每个线索都带你更接近真相。