Android抓包及爬虫研究 - 1

Android抓包及爬虫研究 - 1

又忙活了一阵子项目,期间简单回顾了下原先对于Andorid的抓包,以前研究的都比较零散,这里写个总结,为什么是1,因为抓包还是门技术活,这里介绍得是一些通用原理,以及通用方案,后面有机会再学习总结一下更高级的方法。至于爬虫则是最近搞了个小目标,所以记录下怎么写爬虫。

Android抓包原理

https是在Http加了一层SSL/TLS,即在HTTP通信前,进行身份验证,同时对HTTP报文进行加密处理

在此过程中会涉及到对双方的证书认证,客户端对服务端的证书认证是必须的,服务端对客户端的证书认证则是选择的,证书文件是由第三方可信任机构颁发的。

image-20220526211538444

由于HTTP报文是明文的,而且不涉及到任何身份认证,因此只要简单设置代理,即可完成抓包。

对于HTTPS的抓包,也是基于中间人的方式,如下图

image-20220526171942529

以Charles为例,首先需要让我们的客户端(Android系统)信任Charles,即将Charles证书添加到系统信任证书列表中,这样当Charles被设置成代理后,其会代替我们客户端访问服务器,并将返回服务器返回的内容,此时从客户端的视角来看,Charles是目标服务器,但是由于我们已经信任了Charles,所以可以正常完成HTTPs的通信过程。

对抗策略

SSL Pinning

证书绑定,不仅会校验服务器证书是否是系统中的可信凭证,而只信任APP指定的证书,一旦发现服务器证书为非指定证书即停止通信,所以即便将Charles证书安装到系统信任凭据中也无法生效

由于SSL Pinning的功能是开发者自定义的,并不存在一个通用的解决方案,所以反制手段需要通过Hook校验服务器的代码,使得校验失效

一些相对通用的方法,对常见的校验方式的反制

在objection中可以直接执行下面命令

android sslpinning disable

另外开源项目DroidSSLUnpinning也添加了一部分Objection中没有的Bypass证书校验的方式

服务端校验客户端

这种方式发生Https 验证身份阶段,服务端会验证客户端的证书,如果不是信任的客户端证书就终止通信

Charles操作

Charles安装

Charles破解工具 (zzzmode.com)

Charles证书安装

image-20220526173816209

远程安装在手机上需要先设置Charles的代理,然后访问chls.pro/ssl,即可安装证书到Android系统

image-20220526174038391

Charles代理设置

image-20220526174323597

在这里设置代理

image-20220526174416067

这里设置SSL 代理

image-20220526174625588

image-20220526174634995

要抓Https的包,这里要开启SSL Proxying,同时添加ip过滤里加上**,就可以抓全部的Https包

有时候可能误点了Deny ip地址,导致抓不到包,可以在Proxy-> AccessControlSettings里面看到

image-20220526175423805

image-20220526175547981

这里的三个按钮,分别是清空历史,开始抓包,开启SSL

Charles客户端证书

image-20220526175708904

从App中提取出客户端证书后,可以在这里把客户端证书添加进去

手机代理设置

代理原理

代理一般分为两种,Https代理和Socks代理

具体Android是怎么设置代理的,我现在还是太清楚,但是根据现有的知识,Https代理是把代理设置到应用层,假设代理方式是修改hosts,把所有的网址重定向到Charles上,那么App可以轻易检测出当前https被重定向了,

而socks代理则是把代理设置到协议层,也就是TCP/UDP这一层,把所有socket的链接建立重定向到Charles上,Charles这边应该是自己实现的识别上层协议是否是Https,如果是就进行代理,那App进行的一些应用层检测可能就会失效。

总的来说就是代理的越深,能过得检测就越多。

http代理

可以直接通过adb用下面指令设置代理/关闭代理

1
2
3
adb shell settings put global http_proxy ip:端口
adb shell settings put global http_proxy :0
adb shell settings delete global http_proxy

通过网络,高级设置里面也能设置代理,但是时灵时不灵

ip相关问题

设置代理的前提得部署charles的电脑跟手机是通的,如果是真机的话,只要手机跟电脑连到同一个wifi就行了,当然也可以电脑开热点(没法开热点的电脑需要单独再配个无线网卡)

image-20220526202957681

手机只要连上电脑开的热点就行

手机ip设置比较简单,用模拟器比较麻烦,以雷电模拟器为例

需要先在这里选择桥接模式,然后要装个驱动,选择正确的网卡,当选择桥接模式后,要注意此时不能连到带有那种网页认证的网络上(比如校园网),桥接不一定能连上分配到ip,这种解决方案是手机去连,然后手机开热点,电脑再连手机热点开桥接模式。

image-20220526203611695

另外adb连上shell后,可以直接用ifconfig查看ip

socks代理

socks代理就需要借助软件了,Brook就可以设置socks代理

image-20220526210345601

手机证书设置

Android 7.0以上,SSL证书划分成系统分区和用户分区了,直接访问Charles证书的网址,只会把证书安装到用户分区,如果抓不到包,就需要把Charles证书移到系统分区

1
2
3
4
5
6
7
8
adb shell
su
# cd /data/misc/user/0/cacerts-added/
# mount -o remount,rw /system
# cp * /etc/security/cacerts
# chmod 777 /etc/security/cacerts/*
# mount -o remount,ro /system
# reboot

这个修改系统分区权限的操作,需要刷的系统是userdebug版本的,要不然会修改不了(自己手动编译的系统就是userdebug版本的),别的下载的系统可能就会出现问题

以夸克浏览器为例,打开charles的网址之后会下载pem证书到/sdcard/Quark/Download

这时候需要进入设置 -> 安全性和位置信息 -> 加密与凭据 -> 从存储设备中安装 -> 选中这个PEM安装

之后再执行上述指令,如果出现下面错误,

1
2
3
walleye:/ # mount -o remount,rw /system
mount -o remount,rw /system
mount: '/system' not in /proc/mounts

先尝试下面命令

1
2
$adb root
$adb disable-verity

如果还是不行,那就尝试修改整个/ 而不是/system

1
2
3
4
5
6
# cd /data/misc/user/0/cacerts-added/
# mount -o remount,rw /
# cp * /etc/security/cacerts
# chmod 777 /etc/security/cacerts/*
# mount -o remount,ro /
# reboot

Android10导入系统证书,这个还没实践过,目前还没刷过Andorid10.0的系统

(32条消息) Android10导入系统证书的方法。fjh1997的博客-CSDN博客安卓导入根证书

这里还有个项目

Magisk-Modules-Repo/movecert: movecert (github.com)

简单爬虫

爬虫库非常的多,只用urllib就能完成请求了

1
2
import urllib.request
import urllib.parse

Get请求示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def getMsgUnionId():
global curUnionId

url = "https://reservation.bupt.edu.cn/index.php/Wechat/Booking/confirm_booking?area_id={0}&td_id={1}_{2}&query_date={3}&country_id=0".format(areaid,roomid,timeid,dataid)
header = {
"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"x-requested-with":"com.tencent.wework",
"user-agent":"Mozilla/5.0 (Linux; Android 7.1.2; GM1900 Build/N2G47O; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/86.0.4240.198 Mobile Safari/537.36 wxwork/4.0.6 ColorScheme/Light MicroMessenger/7.0.1 NetType/WIFI Language/zh Lang/zh",
"origin":"https://reservation.bupt.edu.cn",
"sec-fetch-site":"same-origin",
"sec-fetch-mode":"cors",
"sec-fetch-dest":"empty",
"referer":"https://reservation.bupt.edu.cn/index.php/Wechat/Booking/choose_template/template/1/area_id/{0}/country_id/0".format(areaid),
"accept-language":"zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
"cookie":"think_language=zh-cn",
"cookie":"PHPSESSID="+phpSessionId
}
request = urllib.request.Request(url,headers=header,method="GET")
response = urllib.request.urlopen(request)
#print(response.read().decode("utf8","ignore"))
html_text = response.read().decode("utf8","ignore")
#print(html_text)
split_res = html_text.split("form_valid_code_value")
if len(split_res) < 3:
return
#print(split_res[2][9:19])
curUnionId = split_res[2][9:19]
print(curUnionId)
get_room_softs()
get_room_devices()

url 和header我们要先准备好,基本上爬虫就没用不需要定制header的,一般是抓包之后一摸一样的拷贝过来,少字段可能都会导致检测到,get请求不涉及到data,所以一般修改的就是cookie,

1
2
request = urllib.request.Request(url,headers=header,method="GET")
response = urllib.request.urlopen(request)

这两句就是构造request,urlopen就是发起请求

获取返回结果要用到下面这个,有的返回是html,有的返回就是json,这里是html,获取正文内容,还需要进行一次utf8解码,主要必须指定ignore,因为有的解码不了(疑惑)就会直接抛出异常,指定ignore之后解码失败的字节就会忽略而不是报错,这里就是不知道为什么会有的字节解码不了????

1
html_text = response.read().decode("utf8","ignore")

python - urllib.request.urlopen return bytes, but I cannot decode it - Stack Overflow

还有种情况是,返回的结果是经过gzip压缩的,那么就需要先解压一下

POST请求示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
def RegisterNow():
global curUnionId,areaid,roomid,timeid,boundary

url = "http://reservation.bupt.edu.cn/index.php/Wechat/Register/register_show"

data = {
"form_valid_code_value":curUnionId,
"area_id":areaid,
"room_id":roomid,
"device_id":0,
"soft_id":0,
"time_id":timeid,
"mixed_payment_type":"wechat_pay",
"to_use_vip_id":0,
"occupy_quota":1,
"sign_and_login_type":1
}
#json_text = json.dumps(data)
#print(json_text)
multipart_text = encode_multipart(data)
header = {
"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"x-requested-with":"com.tencent.wework",
"user-agent":"Mozilla/5.0 (Linux; Android 7.1.2; GM1900 Build/N2G47O; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/86.0.4240.198 Mobile Safari/537.36 wxwork/4.0.6 ColorScheme/Light MicroMessenger/7.0.1 NetType/WIFI Language/zh Lang/zh",
"origin":"https://reservation.bupt.edu.cn",
"sec-fetch-site":"same-origin",
"sec-fetch-mode":"cors",
"sec-fetch-dest":"empty",
"referer":"http://reservation.bupt.edu.cn/index.php/Wechat/Booking/confirm_booking?area_id={0}&td_id={1}_{2}&query_date={3}&country_id=0".format(areaid,roomid,timeid,dataid),
"accept-language":"zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
"Content-Length":len(multipart_text),
"Content-Type":"multipart/form-data; boundary="+boundary,
"cookie":"think_language=zh-cn",
"cookie":"PHPSESSID="+phpSessionId
}
request = urllib.request.Request(url, headers=header, method="POST")
#print(request.)
response = urllib.request.urlopen(request,data=multipart_text.encode())
html_text = response.read().decode("utf8", "ignore")
print(html_text)

post首先得注意data的类型,一般常见的有json和form-data,像这里就是form-data,有时候给服务器发什么类型都想,完全看服务器是怎么实现的,form-data就要在Content-type里写multipart/form-data,

form-data也比较特殊,需要根据data构造,urllib没提供相关方法,所以这里从网上抄了一段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def encode_multipart(params_dict):
'''
Build a multipart/form-data body with generated random boundary.
'''
global boundary
boundary = '----------%s' % hex(int(time.time() * 1000))
data = []
for k, v in params_dict.items():
data.append('--%s' % boundary)
if hasattr(v, 'read'):
filename = getattr(v, 'name', '')
content = v.read()
decoded_content = content.decode('ISO-8859-1')
data.append('Content-Disposition: form-data; name="%s"; filename="hidden"' % k)
data.append('Content-Type: application/octet-stream\r\n')
data.append(decoded_content)
else:
data.append('Content-Disposition: form-data; name="%s"\r\n' % k)
data.append(v if isinstance(v, str) else str(v))
data.append('--%s--\r\n' % boundary)
return '\r\n'.join(data)

最后要注意data-length的修改

综上就是怎么实现Get和Post,爬虫还有非常多的技术,以后再研究吧

Refs

《Android Frida 逆向与抓包实战》

实用FRIDA进阶:内存漫游、hook anywhere、抓包 - 安全客,安全资讯平台 (anquanke.com)

HTTPS原理和通信流程 - 知乎 (zhihu.com)