这个事情要从很久很久以前说起…
我有一个朋友,母胎单身,去年不知道从哪拱到了一棵大白菜,感觉人生到达了巅峰。
有一次,我与他相遇夜宵,凑了一局。 两杯啤酒顿顿顿下肚,只见他脸色红润,情绪激昂,只差一匹汗血宝马就可以出征长沙,血战沙场。
于是大放厥词,口出狂言道: 忆往夕,吾已鳏独二十有五载,看今朝,新春必携伴归乡探亲。
……
很可惜的是,可能是因为没有经验或者性 格不合,两人还没到过年就吹了,老死不相往来。
那几天,他悲痛欲绝,哀哀戚戚,无心工作,借酒消愁,感觉人生已经到达了低谷。
然后我去安利他看爱情公寓。 喜剧,也许能缓解一下心情。
后来突然有一天跟我说要他练级,练到 99 级!
咱也不知道咋练、练啥,咱也不敢问。。。
……
这货上网搜了一下陌生人社交软件,下载了一个比较热门的 APP。
练级第一天,点击匹配,匹配成功,开始聊天,于是:
然后就没然后了,他说这是他人生中遇到的第一次滑铁卢,感觉人生已经到达了低谷中的低谷。
练级第二天,看到社区里有漂亮的小姐姐发自拍,能收到好多赞和评论,真是一群有趣的灵魂啊,于是也决定自己发一张。
练级第三天:
不禁令人感慨,人生呐,就是如此的起起落落落落落落。
然后呢,就又来找我了,让我帮帮他。
这位胸 dei,谈恋爱俺不会,聊天俺也不会,那我只能手动帮你点个赞了。
这位仁兄一听,灵机一动,道: “那你帮我搞个软件,能自动帮别人点赞也行。高手靠实力,新手靠运气,只要我给一万个人点赞,总有那么几个人能看到我的自拍,然后被我那无处安放的魅力所吸引的。”
嗯, 他说的好像有道理。
于是有了正文。
因年代久远,当时的分析与现在 APP 的协议已经很多对不上,我只能尽量复盘。
大伙们,编故事真的不容易。
……
需求很明确:
这个 soeasy
,主要是抓获取动态和点赞的两个请求。
首先打开 Charles
,然后给把代理端口反向转发到手机里。
adb reverse tcp:8888 tcp:8888
再给手机设置代理
adb shell settings put global http_proxy 127.0.0.1:8888
结果发现一挂上代理就无法正常发送请求。
这种情况一般有几种可能:
根据我多年的经验,第一判断就是 sslpinning
,于是我用 3 秒钟时间打开 objection
, 执行 android sslpinning disable
。 关于 Frida
和 objection
的安装和使用,我的另一位母胎不单身的好友已经写过多遍,可以参阅。
然后再次抓包,然鹅不管用。
那么先观察一下抓包的情况,发现 Charles
并没有出现 SSL 错误的提示,可以完整的看到请求的报文。
然鹅, response
却返回 400。由此可得,这是一个 SSL 双向校验,因为代理可以正常拦截到发送包,说明不是代理检测,如果有代理检测,那么这个数据包并不会通过代理发送出去。而且可以正常解密,更是说明也没有做证书绑定。
因为做双向验证,就需要在客户端里放一个证书,那么直接在 APK 里的 assets
中就可以找到一个 .p12
和一个 .cer
的文件,那么这两个就是客户端的证书了。
但是想要使用这个证书来与服务端通信,还需要一个密码。
一般来说,我们可以静态分析代码,搜索 KeyStore
或者 client.p12
来逆向分析找到密码,然鹅,这个 APP 做过加固,看不到代码。 那么可以采用更加高效的方式: Hook
。
Hook 的原理不再多说,可参阅 《Android 动态分析攻防小结》 。
直接上 Frida
脚本:
Java.perform(function () { console.log("Hooking..."); var PKCS12KeyStoreSpi = Java.use("com.android.org.bouncycastle.jcajce.provider.keystore.pkcs12.PKCS12KeyStoreSpi"); var String = Java.use("java.lang.String"); PKCS12KeyStoreSpi.cryptData.implementation = function(a, b, c, d, e){ console.log(String.$new(c)); return this.cryptData(a, b ,c ,d ,e); } });
拿到密码之后,就可以打开 Charles
的 SSL Proxy Settings
里导入 p12
证书了,随后在抓包的时候, Charles
会提示你输入密码。 而想要在 Python
脚本里使用,还需要使用 openssl
转换成 pem
证书,然后就可以像这样正常发起 Https
请求了:
response = requests.post(url="https://{}{}".format(ip, path), headers=headers, data=data, verify=False, cert="./client.pem")
于是,终于跨过抓包这个坎了。
一款安全性合格的产品,请求里必然会有 sign 字段,可以防止篡改的同时还能提高逆向分析的难度。
抓包分析之后发现请求的的 headers
里头有一个 APISign
字段,每一个请求都会通过这个字段对当前的数据以及时间戳进行校验。
按照常规套路,就是反编译之后搜索相关字符串,然后找到加密算法的地方,抠出来。
然鹅,这个 APP 是加固的,首先尝试一下 快速脱壳 。
结果好像还不错, dump
下来很多 dex
,可以开始一顿搜索了。
然鹅:
搜索并没有得到结果,我以为是字符串进行了加密,然后手动打开这堆了 dex
文件,也搜索了其他的一些关键字、API,发现确实没有。 另外发现应该少了一些代码,这说明有一个重要的 dex
可能没有脱下来
逆向分析就跟股市一样,喜欢起起落落落落落落。短时间内也不纠结为什么脱不下来了,来一次不脱壳的纯动态分析逆算法。
无法脱壳下的动态分析口诀只有两个字:猜、试。 说详细一点,就是信息收集之后一步一步的进行猜想和验证。
因为 APISIGN
是一个 32 位长的 hex
字符串,那么可以初步猜想,这可能是个 MD5
, SHA1
之类的摘要算法。那么这个时候,如果开发者使用了系统自带的摘要 API
那我们就可以通过 Hook
来得到计算之前的数据,从而倒推出签名算法。
关键类: java.security.MessageDigest
, 通过对这个类的 getInstance
, update
, digest
方法进行 hook
, 可以得到所有调用系统 API 的摘要算法、摘要数据、摘要结果。 代码这里不给了,自己实现一份不难。
想象很美好,现实却很残酷,通过这个方法并没有成功 Hook
到与请求数据相符合的调用结果,那么基本上这个算法就是自己实现的了。
大部分 APP 的网络请求,都是通过调用第三方网络框架发出去的,那么,我们可以先尝试一下能不能找到这款 APP 所使用的网络请求框架。
第一个肯定是龙头: OKHttp
因为之前有写过现成的针对 OKHttp
的抓包、解密工具,所以我就直接尝试启动了一下,结果:
[*OKHttp-InfoIntercept] not found RealInterceptorChain class [*OKHttp-PacketIntercept] not found CallServerInterceptor class
找不到预设的类名,可能是因为混淆或者 APP 在集成框架的时候,很多人会选择修改包名,因为工具之前没有写智能查找的功能,那么可以使用 objection
手动查找一下。
使用命令: android hooking search classes Interceptor
, 先搜一下带 Interceptor
的类名,因为一般来说可以通过他们,直接拦截到发包的调用。
结果发现,还是可以找到 okhttp
的类的。
那么将找到的这几个类进行一个 android hooking watch class
。在发送一个请求,发现:
可以成功拦截到调用。那么,观察调用栈:
执行 android hooking watch class_method okhttp3.logging.HttpLoggingInterceptor.intercept --dump-args --dump-backtrace --dump-return
于是我们得到了 RealInterceptorChain
的真正类名
很多时候, OKHttp
对请求的加密、 Sign
都是在开发者自定义的 Interceptor
里完成的,而 RealInterceptorChain
这个类,是贯穿全部 Interceptor
的一个关键点,具体详情可以阅读 OKHttp
的源码。对其 proceed
方法进行 Hook
在调用栈中即可看到所有的 Interceptor
。
于是: android hooking watch class_method okhttp3.internal.http.f.proceed --dump-args --du mp-backtrace --dump-return
运气很不错,一下子就打出来了一条 APP 包名代码里的调用栈,而且还是 pkgname + .net
的包名,一看就是用于网络请求的,信息情报加一: 代码里有一个 .net 的包, 而且这个包并没有被脱下来。这个包基本上一看就知道很关键了。
那么再继续对找到的这个 Interceptor
类进行 hook
, 仔细观察返回结果以及调用栈。
很可惜的是,并未在返回结果里找到与请求相符合的 sign
字符串,这说明, sign
的算法方法并不在这个类里面。
调用栈里也没有什么可疑的类或者方法,可以说这条线在这里已经断了。
从二中,我们收获到了一个情报: 未脱下来的代码中有一个 pkgname + .net
的包,那么可以先搜索一下,看看这个包中都有一些什么类:
[usb] # android hooking search classes xxx.xx.xxx.net ...... # 具体的类名就不列了 ...... Found 54 classes
排除掉一些明显不相关的类,剩下的类不多,只有十几个。一个一个对其进行尝试 hook
,功夫不负有心人,在一个没有做符号混淆的类中发现了一个方法:
那么观察一下这个方法的输入输出
看到这个,我就觉得基本上已经结束了。
再详细分析一下输入参数,分别是 URL PATH、用户 ID、时间戳等信息,都是已知或者可以自己生成的参数,返回结果就是 HTTP 报文里的 APISign
。
至于为什么这么方法和类没有做符号混淆,原因是这个算法是在 SO
里面的,需要在混淆配置里面排除。
由此,我们又得到了一条新的经验: 下次 Hook
不到算法就直接看 SO
。
算法调用方法有了,那我们还需要自己逆算法么?no。
Frida-RPC
,懒人必备,在也不用手撕 OLLVM
, VMP
。
rpc.exports = { sign: function (xxx1, path, xxx2, t, xxx3, xxx4) { var result; Java.perform(function () { result = xxxxxxx.yyyyyyySign(context, xxx1, path, xxx2, t, xxx3, xxx4); }); return result; } };
sign = script.exports.sign(xxx1, path, xxx2, t, xxx3, xxx4)
完全 OK。 然后再构造一下包体,编码一下逻辑,一个无情的自动点赞机器人就诞生了。
作为一个新世纪的颜值主义者,虽然这哥们长得丑,但我还是希望他能找到一个漂亮的女朋友。于是我添加了一段人脸识别和打分的代码…
原本的逻辑是这样的:
遍历动态列表 -> 是个女的 -> 点赞
然后我加上了 F4ce+-
和 B4ldu
的 AI 颜值评分系统之后,逻辑是这样的:
遍历动态列表 -> 是个女的 -> 识别动态里的全部人脸 -> 评分 -> 大于80分 -> 点赞 -> 关注
最终效果咋样咱也不知道,反正这哥们那几天就抱着个手机在那一直傻乐,前几天跟我说准备跟新的女朋友去旅游,咱也不知道这是哪个新女朋友,咱也不敢问。
后来我拿这哥们的账号提取了一点数据,事实证明, B4ldu
的颜值评分比较靠谱。