案例一、某财经 APP 的订阅杂志
这个杂志是我最喜欢的阅读内容没有之一,我买了三年的订阅了。上周过期了,却想看我离线了的去年杂志中的一篇文章,结果发现打不开。瞬间不爽,让我动了杀心,就用著名网络调试工具 Surge 直接在手机上用中间人攻击抓了一下她的包。
结果发现他的杂志列表是这样的:
注意 package_path!这玩意绝绝绝绝对不应该露出来的。
随手 wget 了杂志链接,发现开发者貌似考虑到了这种情况,杂志是加密的。对于 zip 的加密,肯定要有个地方传输 password。然而我来来回回的找,结果也没找到类似的 API,也没看到 socket 之类的存在。开始怀疑是不是有需要付费后才可看到的 API,于是我去看了眼代码。
对于我临时兴起,身边没有准备越狱机器。只能用我不擅长的 Android 逆向来找问题了。首先用 jadx 把 APK 中的代码提取出来,注意!这里 APK 没有做任何加固!
在代码中 grep zip 相关内容,结果我们有:
public native String getZipKey(Context paramContext);
这个 API 简直可疑到飞起,以及我们发现一个有趣的事情:native
,也就是说这个 API 是 ndk 编译的。结果,在 lib 文件夹下看到了一个 libZipKey.so
。这是我见过的最蠢的命名,仿佛在说 key 在我这里,快来逆向我吧。
凭这一套 API 的设计,我们可以知道这开发者的的水平高不到哪去。于是我先用 strings
命令,扫描一下这个动态链接库中对应的字符串符号。看到了一个 e811xxxxxxxxxxxxxxxxxxxxx
的字符串非常奇怪。就随手去解压一下,bingo…然后就没然后了。这一套东西花了五分钟不到,连动态调试都没用上就搞定了。
案例二、某资讯类 APP feed 流爬取
这个 APP 也是我每日必看内容,他文章质量高到飞起。有很多大局观的长文非常值得一看,这也是我最想先搬到 telegram 上的。
然而他的手机版、PC版的主页格式比较复杂,feed 里面嵌 banner。有的 banner 还和 feed 重复做重点曝光,很复杂。我就想先去抓它家 app 的 json API,结果不断撞墙开始了。
HTTPS 证书校验
作为分析网络包的第一步,肯定是用 Surge 做中间人攻击,结果发现客户端居然做了证书校验你敢信!直接抛出来了个 handshake error。为了确认我 Surge 的使用姿势是没错的,我又用了 Charles 重复了一遍,结果一样。
在这个情况下,不越狱的 iPhone 就无计可施了,我们连他请求的 path 都不知道,只能继续静态分析。
静态分析域名
老样子下载 APK,逆向拉出来 jar,同样这个 APP 也没有加固,也没有做混淆,我们又可以不断去 grep 内容了。通过 grep 域名,我们找到了突破点,一个叫做 component/net/NetworkConstants.java
的类。里面有一大堆的 path 如:
于是我们通过拼接 host + path 得到了完整的 url 感觉有门!于是我们请求一下可得:
先不去吐槽为什么明明返回个 JSON, Content-Type 却是:text/html 这件事,但他告诉我们了一个事情,居然连 GET banner 的 API 都要了签名,安全意识真好。既然你要了签名,那我就搜签名。
静态分析签名
通过 grep, signature 发现真的中奖了,我们得到了这样的一个返回值:
感觉这个 CommonParams 有门啊,阅读一下里面的代码:
于是我们就很轻松愉快的构造出来了签名结果:
最后一步就是找到目标请求了,回到上面提到的 NetworkConstants 里面,翻了一遍得到了:feed/commonFeeds
这个 path。模拟请求成功!看了一眼表,共用时 30 分钟。
小结
这里举了两个非常初级的例子,但确实能反映出很多客户端开发需要注意的问题:
- Android 记得加固、混淆,千万别偷懒
- 一定要用 HTTPS 通信,如果可以记得做证书校验,能拦住一大堆小白攻击者
- 和后端商量 API 要去做签名
- 签名的生成算法要混淆,尽量少用常量字符串,如果想用 C++ 来混淆的话,记得 NDK 部分命名、函数签名是无法自动混淆的,要手动处理。
- API 尽量和 Cookie 绑定,做好权限管理,如果出现爬虫,后端可以针对 Cookie 封账号。
反爬虫只能是见招拆招,没有通杀的解决方案,本质上就是不断提高攻击门槛罢了。