本文首发于稀土掘金 (juejin.cn/post/717988…),同步发表于 SegmentFault、开源中国等平台。
本文涉及的所有内容仅供交流学习,不公开具体 App 信息、不提供具体代码,应用名、包名、请求域名等均使用「马赛克」或「星号」隐去。严禁将本文内容用于任何商业用途或非法用途,否则产生的一切后果与本文作者无关。
平时偶尔会使用某大厂的金融 App 看基金板块的帖子,但是在 App 上的翻页体验很不好,每次只能从第一页开始翻页,而且翻页过多会非常卡顿。所以就想着能不能将基金评论数据爬下来,于是就有了这篇文章。
使用的工具: Jadx, Frida, Charles。
0x01: Charles 抓包
首先使用 Charles 的 SSL MITM 功能抓包,但是发现 App 会拒绝 Charles 的连接,并提示没有网络连接。
我首先猜测是 App 会做了 SSL Pinning,于是祭出了 Frida 和 多合一 SSL Pinning Bypass 脚本 。然后发现现在可以抓到部分请求,但是能抓到的请求都是资源请求,具体的数据请求仍然会被拦截,这说明 App 还做了其他的防抓包手段。
0x02: Hook SSL_read
多合一脚本仍然无效的情况下,我就开始尝试 Hook OpenSSL 的 SSL_read 函数,毕竟所有的 HTTPS 请求都要通过 OpenSSL 的 SSL_read 函数来读取数据。
我看到 App 中包含了 libopenssl.so 这个库,于是同时 Hook 了 libopenssl.so 和系统的 libssl.so:
function hookSslRead(dylibName) {
Interceptor.attach(Module.findExportByName(dylibName, "SSL_read"), {
onEnter(args) {
// args[0] : SSL *ssl
// args[1] : void *buf
// args[2] : int num
let message = {};
message["function"] = `${dylibName}:SSL_read`;
this.message = message;
this.buf = args[1];
info(
`SSL_read() called: args[0]=${args[0]} args[1]=${args[1]} args[2]=${args[2]}`
);
},
onLeave(retval) {
const retcode = retval.toInt32();
info(`SSL_read() returned: ${retcode}`);
if (retcode > 0) {
send(this.message, Memory.readByteArray(this.buf, retcode));
}
},
});
}
hookSslRead("libssl.so");
hookSslRead("libopenssl.so");
这里通过 send 函数将数据发送给 Python 脚本,然后 Python 脚本将 SSL_read 读取到的数据打印出来。
但是发现这样 Hook 之后,仍然只能抓到资源请求,数据请求仍然被拦截。这说明数据接口的请求很有可能并非是标准的 HTTPS 协议,那这样就很麻烦了,只能通过 Jadx 分析。
0x03: Jadx 分析
首先定位到评论页面的 Activity,然后通过 Jadx 查看相关的代码。 这里发现整个基金评论板块都是运行在 WebView (libwebviewuc.so) 中的。尝试 Hook libwebviewuc.so 中的 ssl_impl_read 函数,但是发现这个函数并没有被调用。
到这里就有点陷入僵局了,因为我不知道 App 是如何处理数据请求的,也不知道数据请求的协议是什么。但是猜测 App 应该是使用了自己的网络库,而不是系统的网络库。
0x04: 日志分析
这时候我就想到了日志分析,通过 logcat 查看 App 的日志,发现有一条日志是这样的:
1671002832384 D/AriverEngine:NativeBridge:[9049:URGENT_BIZ_SPECIFIC_THREAD_RPC_56] executeNative jsapi rep name={rpc} 16710028315860.10241515923604427 {\"ariverRpcTraceId\":\"client`Y5GgbOlLX4sDAEVHiCw0Rf5XOKFXxmH_511099\",\"expire\":1671002842883,\"externalData\" [...具体数据太长,省略...]}, keepCallback: false, stat: total(651)|dispatch(4)"
这里的日志是 App 通过 RPC 请求数据的日志,我在日志中找到了评论对应的数据,确认了这里打印的日志就是评论数据。
然后尝试定位到 RPC 请求的代码,在 Jadx 中搜索关键字 jsapi rep name,发现了一个方法 a 中包含了这样一段代码:
RVLogger.d(DefaultNativeBridge.TAG, "executeNative jsapi rep name={" + nativeCallContext.getName() + "} " + nativeCallContext.getId() + " " + jSONUtils + ", keepCallback: " + z2 + ", stat: " + nativeCallContext.getStatData().print());
可以看出这里打印的日志格式与上面的日志格式一致,因此可以确定这里就是 RPC 请求的代码。然后尝试 Hook 这段代码,发现果然拦截到了 RPC 响应的数据。
0x05: 顺藤摸瓜
找到了打印响应日志的代码之后,就可以顺藤摸瓜了,通过 Jadx 查看这个方法的调用栈,依次定位,发现调用链如下:
com.*****.ariver.engine.common.bridge.DefaultNativeBridge:acom.*****.mobile.common.transport.rpc.RpcHttpWorker:executeRequestcom.*****.mobile.common.transport.http.HttpManager:executecom.*****.mobile.framework.service.common.impl.MpaasHttpTransportSevice:executecom.*****.mobile.common.rpc.transport.http.HttpCaller:bcom.*****.mobile.common.rpc.RpcInvoker:a
最终确定了 RPC 请求的代码在 com.*****.mobile.common.rpc.RpcInvoker:a 中。
这里的代码比较长,但是很容易看出来,这里就是 RPC 请求的代码,并且包含了签名等请求预处理的逻辑。到这里,就找到了 RPC 请求的代码,接下来就可以 Hook 了。
0x06: Hook RPC 请求
通过 Frida 调试这个方法,提取出请求的数据:
[
{
"lastTime" : 0,
"pageNo" : 0,
"pageSize" : 10,
"param" : {
"SCENE" : "FORUM",
"commentNeedInterestingReplyList" : "true",
"needInterestingInfo" : "true",
"needUserRelationData" : "false"
},
"showProduction" : true,
"topicId" : "20150718000230030000000000000354",
"topicType" : "FUND"
}
]
可以发现这里的请求包含了分页信息,因此可以通过修改这里的参数来实现翻页。
于是我尝试修改这里的参数,发现能够实现翻页。然后就可以通过翻页来获取所有的评论了。
至此,就完成了整个逆向的过程。
总结
由于这个 App 是一个大厂的 App,因此在逆向的过程中,我先入为主的认为接口请求的代码一定是被混淆过的,因此在逆向的过程中,我一直在寻找混淆的代码,但是最终发现,其实这个 App 并没有混淆,甚至 App 请求的数据都直接暴露在了日志中。因此在逆向的过程中,我浪费了很多时间。
如果一开始就能够正确的判断,就能够节省很多时间,因此在逆向的过程中,一定不要先入为主,要多尝试,多验证,才能够找到正确的结果。