使用 mitmproxy + python 实现移动端异常数据测试
mitmproxy github地址(Man-in-the-middle attack)
背景
Crash率是衡量一个App好坏的重要指标之一,如果你忽略了它的存在,它就会愈演愈烈
客户端的很大一部分的Crash是因为API返回的脏数据。比如当API返回空值、空数组或返回不是约定类型的数据,App收到这些数据,就极有可能发生空指针、数组越界和类型转换错误等Crash。而且这样的脏数据,特别容易引起线上大面积的崩溃。
异常数据测试方法
手工测试时,借助charles等抓包工具,对返回结果拦截修改数据,再进行数据的maplocal
异常数据修改规则
字符:非法字符、超长、null
数组:空数组、非法序列
接口超时:5s、10s
接口状态码:404、500、503
增加删除数据:数组、字典
mitmproxy是什么
顾名思义,mitmproxy 就是用于 MITM 的 proxy,MITM 即中间人攻击(Man-in-the-middle attack)。
mitmproxy是一个支持HTTP和HTTPS的抓包程序,有类似Fiddler、Charles的功能,只不过它是一个控制台的形式操作。mitmproxy还有两个关联组件。一个是mitmdump,它是mitmproxy的命令行接口,利用它我们可以对接Python脚本,用Python实现监听后的处理。另一个是mitmweb,它是一个Web程序,通过它我们可以清楚观察mitmproxy捕获的请求。
mitmproxy有如下几项功能
- 拦截HTTP和HTTPS请求和响应。
- 保存HTTP会话并进行分析。
- 模拟客户端发起请求,模拟服务端返回响应。
- 利用反向代理将流量转发给指定的服务器。
- 支持Mac和Linux上的透明代理。
- 利用Python对HTTP请求和响应进行实时处理。
安装mitmproxy
brew install mitmproxy
安装完成后,系统将拥有 mitmproxy、mitmdump、mitmweb 三个命令
验证安装成功mitmdump --version
应当可以看到类似于这样的输出:
1 | Mitmproxy: 5.2 |
运行
要启动 mitmproxy 用 mitmproxy
、mitmdump
、mitmweb
这三个命令中的任意一个即可,这三个命令功能一致,且都可以加载自定义脚本,唯一的区别是交互界面的不同。
mitmproxy 命令启动后,会提供一个命令行界面,用户可以实时看到发生的请求,并通过命令过滤请求,查看请求数据。形如:
启动 mitmproxy:mitmweb
应当看到如下输出:
1 | Web server listening at http://127.0.0.1:8081/ |
设置代理+https证书安装
移动设备和电脑保存在一个wifi网络下,在移动设备在网络代理填写:电脑ip+8080(端口)
在浏览器输入:mitm.it
,选择对应设备类型安装证书
脚本
完成了上述工作,我们已经具备了操作 mitmproxy 的基本能力 了。接下来开始开发自定义脚本,这才是 mitmproxy 真正强大的地方。
脚本的编写需要遵循 mitmproxy 规定的套路,这样的套路有两个,使用时选其中一个套路即可。
##第一个套路
编写一个 py 文件供 mitmproxy 加载,文件中定义了若干函数,这些函数实现了某些 mitmproxy 提供的事件,mitmproxy 会在某个事件发生时调用对应的函数,形如:
1 | from mitmproxy import http |
第二个套路
编写一个 py 文件供 mitmproxy 加载,文件定义了变量 addons,addons 是个数组,每个元素是一个类实例,这些类有若干方法,这些方法实现了某些 mitmproxy 提供的事件,mitmproxy 会在某个事件发生时调用对应的方法。这些类,称为一个个 addon,比如一个叫 Counter 的 addon:
1 | from mitmproxy import http |
执行
这里强烈建议使用第二种套路,这也是官方内置的一些 addon 的实现方式。
我们将上面第二种套路的示例代码存为 addons.py,再重新启动 mitmproxy:
1 | mitmweb -s addons.py |
这个脚本是当 request 发生时,计数器加一,并打印日志。这里对应的是 request 事件
事件
事件针对不同生命周期分为 5 类:HTTP 生命周期、 TCP 生命周期、Websocket 生命周期、网络连接生命周期、通用生命周期
这里我们只介绍HTTP 生命周期的事件:def http_connect(self, flow: mitmproxy.http.HTTPFlow):
(Called when) 收到了来自客户端的 HTTP CONNECT 请求。在 flow 上设置非 2xx 响应将返回该响应并断开连接。CONNECT 不是常用的 HTTP 请求方法,目的是与服务器建立代理连接,仅是 client 与 proxy 的之间的交流,所以 CONNECT 请求不会触发 request、response 等其他常规的 HTTP 事件。
def requestheaders(self, flow: mitmproxy.http.HTTPFlow):
(Called when) 来自客户端的 HTTP 请求的头部被成功读取。此时 flow 中的 request 的 body 是空的。
def request(self, flow: mitmproxy.http.HTTPFlow):
(Called when) 来自客户端的 HTTP 请求被成功完整读取。
def responseheaders(self, flow: mitmproxy.http.HTTPFlow):
(Called when) 来自服务端的 HTTP 响应的头部被成功读取。此时 flow 中的 response 的 body 是空的。
def response(self, flow: mitmproxy.http.HTTPFlow):
(Called when) 来自服务端端的 HTTP 响应被成功完整读取。
def error(self, flow: mitmproxy.http.HTTPFlow):
(Called when) 发生了一个 HTTP 错误。比如无效的服务端响应、连接断开等。注意与“有效的 HTTP 错误返回”不是一回事,后者是一个正确的服务端响应,只是 HTTP code 表示错误而已。
大多数情况下我们只会用到针对 HTTP 生命周期的几个事件。再精简一点,甚至只需要用到 http_connect、request、response 三个事件就能完成大多数需求了。
脚本示例
1 | """ |
常用Api有
request
1 | flow.request.headers # 获取所有头信息,包含Host、User-Agent、Content-type等字段 |
response
1 | flow.response.status_code #状态码 |
设计流程
在上面提到可以拦截request和response,那么就可以对response数据做修改,再返回修改后的数据.
基于上面提到修改数据规则,随机多拦截数据做随机修改.
保存修改前和修改后的数据,方便数据diff.