背景
不知从什么版本后,对快手进行简单抓包似乎“不可行”了。表现就是使用常规的 HTTP 正向代理抓包工具(charles、mitmproxy、fiddler 之类)并且把自签名证书种到系统证书里后,数据依然能刷出来,也能抓到一些零星的报文,但是关键出数据的那些接口报文都没有。
一般来说,常规方法无法抓安卓应用的 https 包,通常有以下几种可能:
- 证书信任问题。在 Android 7 以上,应用会默认不信任用户证书,只信任系统证书,如果配置不得当则是抓不到包的。
- 应用配置了 SSL pinning,强制只信任自己的证书。这样一来由于客户端不信任我们种的证书,因此也无法抓包。
- 应用使用了纯 TCP 传输私有协议(通常也会套上一层 TLS)。由于甚至都不是 HTTP 协议,当然就抓不到包了。
- 应用使用 WebSocket 长链接,将不同的接口封装在这个长链接里。在 WebSocket 里承载的协议一般是用某种自定义方式来模拟 http 请求,因此也难以抓包。
- 应用使用了基于 UDP 的 http3.0协议等。大部分代理工具目前还不支持 quic 等协议,所以这样一般是抓不到包的。
- 应用配置了 Proxy.NO_PROXY ,强制不走系统代理。这样http流量就绕过我们配置的代理,自然抓不到包。
当前的现象是数据能刷出来,那就说明并不是证书信任相关的问题。接下来就需要验证它究竟是使用了什么样的传输方式,对症下药。
最稳妥的验证方式当然是白盒测试看源码,不过快手反编译的代码看起来也费劲,于是考虑直接当成黑盒来测试看看。以下验证方式均以 快手 8.2.31.17191 版本为例。
分析
环境部署
准备自签名CA证书
需要在 Linux 主机上使用 openssl 工具生成一波证书。当然,这一步可以忽略,直接使用mitmproxy生成的证书。只是手动配置一下能够加深一下对 openssl 的理解。
代码语言:javascript复制# 生成RSA私钥 cert.key
openssl genrsa -out cert.key 2048
# 生成有效期为15年的X509证书 cert.crt(DN信息随便填填似乎都行,但是不能全空)
openssl req -new -x509 -key cert.key -out -days 5480 cert.crt
# 私钥和证书放一起,构成PEM格式的 cert.pem
cat cert.key cert.crt > cert.pem
# 将之前生成的pem证书作为mitmproxy使用的根证书
cp cert.pem ~/.mitmproxy/mitmproxy-ca.pem
# 以普通正向代理模式启动在8000端口,并配合curl简单验证证书可用性
mitmproxy -p 8000
# 能正常访问没有SSL相关报错,就说明之前生成的自签名证书是OK的
curl -x localhost:8000 --cacert ~/.mitmproxy/mitmproxy-ca.pem https://www.baidu.com
# 从证书文件中计算出用于放置在安卓中的文件hash名,假设结果为 a5176621
openssl x509 -subject_hash_old -in cert.crt -noout
# 将cert.crt复制出一个上述hash结果的新文件
cp cert.crt a5176621.0
值得注意的是,不要尝试使用 mitmproxy --certs
来配置证书,这种方式只能配置 leaf 证书,而不能配置根 CA 证书。因此还是老老实实的把根证书放在默认路径下。
准备设备
为了方便测试,我在 arm 服务器上使用 redroid 准备了一台安卓虚拟机。
代码语言:javascript复制docker run -itd --rm --memory-swappiness=0
--privileged --name redroid
--mount type=bind,source=/home/myths/exp/a5176621.0,target=/system/etc/security/cacerts/a5176621.0
-p 5555:5555
redroid/redroid:11.0.0-arm64
redroid.width=720
redroid.height=1280
redroid.fps=15
redroid.gpu.mode=guest
其中 /home/myths/exp/a5176621.0 替换成实际文件所在路径。
然后在arm主机上用 adb 连接安卓的 tcpip 端口,下载并安装快手 8.2.31.17191 版本。
代码语言:javascript复制# 在arm服务器上用adb连接安卓虚拟机
adb connect localhost:5555
# 安装快手
adb install kuaishou.apk
为了方便远程操作,需要在本地机器上连接 arm 服务器上的安卓虚拟机,并用scrcpy操作。
代码语言:javascript复制# 在本地主机上连接远程arm服务器上的安卓虚拟机
adb connect <ip for arm server>:5555
# 启动scrcpy
scrcpy
到这一步骤时,可以检测安卓中的网络应该都已经是通的了。
复现抓包问题
先尝试使用传统正向代理的方式进行抓包。
代码语言:javascript复制# 在arm服务器上用正向代理启动mitmproxy
mitmproxy -p 8000
# 对安卓设置正向代理,其中 172.17.0.1 为安卓下访问arm主机的ip
adb shell settings put global http_proxy 172.17.0.1:8000
设置完成后,观察手机的网络状况,现象如下:
- 使用浏览器访问普通网站,返回均正常,mitmproxy也能抓到包。
- 刷快手推荐页,返回也正常,但是mitmproxy只能抓到一些静态资源,无法抓到接口。
Ban掉不走代理的所有流量
有数据但是抓不到包,怀疑应当是有些流量漏掉了,于是尝试把这些流量Ban掉看看效果。
代码语言:javascript复制# 首先需要打开内核的 ip_forward 功能
echo 1 > /proc/sys/net/ipv4/ip_forward
# 依然在arm服务器上用正向代理启动mitmproxy
mitmproxy -p 8000
# 继续在手机上配置http代理,其中172.17.0.1为安卓下访问arm主机的ip
adb shell settings put global http_proxy 172.17.0.1:8000
# 在 arm 服务器上配置iptables,将来源于安卓虚拟机但目的地不是arm服务器的流量重定向到一个无用端口。
# 其中 172.17.0.12 位安卓虚拟机的ip,1234为一个无用端口。
sudo iptables -t nat -I PREROUTING -p tcp -s 172.17.0.12 ! -d 172.17.0.1 -j REDIRECT --to-ports 1234
# 如果上述操作报了 Couldn't load match `owner':No such file or directory 的错,那么需要在 arm 服务器上启动下相关模块。
sudo modprobe ipt_owner
# 记得测试完成后,将上面这个记录干掉
iptables -t nat -D PREROUTING 1
设置完成后,观察手机的网络状况,现象如下:
- 使用浏览器访问普通网站,返回均正常,mitmproxy也能抓到包。
- 刷快手推荐页,显示“无网络连接“。
说明这里的确是有流量漏了,没有走正向代理,但是依然出了外网。
Ban掉不走代理的443/80流量
那么这些流量到底是私有的四层TCP流量、还是没走正向代理的80/443流量呢?因此尝试把非80/443的流量放开试试。
代码语言:javascript复制# 依然在arm服务器上用正向代理启动mitmproxy
mitmproxy -p 8000
# 继续在手机上配置http代理
adb shell settings put global http_proxy 172.17.0.1:8000
# 在 arm 服务器上配置iptables,将来源于安卓虚拟机但目的端口不是80/443的流量重定向到一个无用端口。
# 其中 172.17.0.12 位安卓虚拟机的ip,1234为一个无用端口。
sudo iptables -t nat -I PREROUTING -p tcp -s 172.17.0.12 --dport 80 -j REDIRECT --to-ports 1234
sudo iptables -t nat -I PREROUTING -p tcp -s 172.17.0.12 --dport 443 -j REDIRECT --to-ports 1234
# 记得测试完成后,将上面这两条记录干掉
iptables -t nat -D PREROUTING 1
iptables -t nat -D PREROUTING 1
设置完成后,观察手机的网络状况,现象如下:
- 使用浏览器访问普通网站,返回均正常,mitmproxy也能抓到包。
- 刷快手推荐页,依然显示“无网络连接“。
这就说明,控制快手推荐页的流量并不是所谓私有流量,而就是走的80/443端口,只是没有走正向代理。
改用透明代理模式
既然七层的代理配置会被忽略,那就尝试使用四层的透明代理,将流量强制转到透明代理服务器上即可。
代码语言:javascript复制# 在arm服务器上以透明代理模式启动mitmproxy
mitmproxy -p 8000 -m transparent
# 将手机上的http代理移除
adb shell settings put global http_proxy :0
# 在arm服务器上配置将来源于安卓虚拟机的的80/443流量直接路由到mitmproxy
# 其中 172.17.0.12 位安卓虚拟机的ip
sudo iptables -t nat -I PREROUTING -p tcp -s 172.17.0.12 --dport 80 -j REDIRECT --to-ports 8000
sudo iptables -t nat -I PREROUTING -p tcp -s 172.17.0.12 --dport 443 -j REDIRECT --to-ports 8000
# 记得测试完成后,将上面这两个记录干掉
iptables -t nat -D PREROUTING 1
iptables -t nat -D PREROUTING 1
设置完成后,观察手机的网络状况,现象如下:
- 使用浏览器访问普通网站,返回均正常,mitmproxy也能抓到包。
- 刷快手推荐页,能成功刷出,并且mitmproxy也抓到了包。
总结
目前看来,快手并非像很多网上传的那样,大多数接口都走了 kquic 协议导致无法抓包。其实很多接口只是做了一个禁止走系统代理的设置,简单使用透明代理模式进行抓包即可轻松绕过。当然,不排除有些关键接口也做了SSL pinning、走私有协议之类的。。。
参考
https://docs.mitmproxy.org/stable/concepts-certificates/
https://stackoverflow.com/questions/56830858
https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/-builder/proxy/