使用 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
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
2
3
4
Mitmproxy: 5.2
Python: 3.7.7
OpenSSL: OpenSSL 1.1.1g 21 Apr 2020
Platform: Darwin-19.6.0-x86_64-i386-64bit

运行

要启动 mitmproxy 用 mitmproxymitmdumpmitmweb 这三个命令中的任意一个即可,这三个命令功能一致,且都可以加载自定义脚本,唯一的区别是交互界面的不同。
mitmproxy 命令启动后,会提供一个命令行界面,用户可以实时看到发生的请求,并通过命令过滤请求,查看请求数据。形如:
启动 mitmproxy:
mitmweb
应当看到如下输出:

1
2
Web server listening at http://127.0.0.1:8081/
Proxy server listening at http://*:8080

设置代理+https证书安装

移动设备和电脑保存在一个wifi网络下,在移动设备在网络代理填写:电脑ip+8080(端口)
在浏览器输入:mitm.it,选择对应设备类型安装证书

脚本

完成了上述工作,我们已经具备了操作 mitmproxy 的基本能力 了。接下来开始开发自定义脚本,这才是 mitmproxy 真正强大的地方。
脚本的编写需要遵循 mitmproxy 规定的套路,这样的套路有两个,使用时选其中一个套路即可。
##第一个套路
编写一个 py 文件供 mitmproxy 加载,文件中定义了若干函数,这些函数实现了某些 mitmproxy 提供的事件,mitmproxy 会在某个事件发生时调用对应的函数,形如:

1
2
3
4
5
6
7
8
9
10
from mitmproxy import http
from mitmproxy import ctx

num = 0


def request(flow: http.HTTPFlow):
global num
num = num + 1
ctx.log.info("We've seen %d flows" % num)

第二个套路

编写一个 py 文件供 mitmproxy 加载,文件定义了变量 addons,addons 是个数组,每个元素是一个类实例,这些类有若干方法,这些方法实现了某些 mitmproxy 提供的事件,mitmproxy 会在某个事件发生时调用对应的方法。这些类,称为一个个 addon,比如一个叫 Counter 的 addon:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from mitmproxy import http
from mitmproxy import ctx


class Counter:
def __init__(self):
self.num = 0

def request(self, flow: http.HTTPFlow):
self.num = self.num + 1
ctx.log.info("We've seen %d flows" % self.num)


addons = [
Counter()
]

执行

这里强烈建议使用第二种套路,这也是官方内置的一些 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"""
拦截请求的request
拦截返回的response
"""

from mitmproxy import http

def request(flow: http.HTTPFlow):
ctx.log.info("========================== intercept request start ==========================")
ctx.log.info("========================== host is:{} ========================== ".format(flow.request.host))
ctx.log.info("========================== url is:{} ========================== ".format(flow.request.url))
ctx.log.info("========================== method is:{} ========================== ".format(flow.request.method))
ctx.log.info("========================== intercept request end ==========================\n\n\n")

def response(flow: http.HTTPFlow):
code = flow.response.status_code

常用Api有

request

1
2
3
4
5
6
7
8
9
10
11
12
13
flow.request.headers # 获取所有头信息,包含Host、User-Agent、Content-type等字段
flow.request.url # 完整的请求地址,包含域名及请求参数,但是不包含放在body里面的请求参数
flow.request.pretty_url # 同flow.request.url目前没看出什么差别
flow.request.host # 域名
flow.request.method # 请求方式。POST、GET
flow.request.scheme # 什么请求 ,如https
flow.request.path # 请求的路径,url除域名之外的内容
flow.request.get_text() # 请求中body内容,有一些http会把请求参数放在body里面,那么可通过此方法获取,返回字典类型
flow.request.query # 返回MultiDictView类型的数据,url直接带的键值参数
flow.request.get_content() # bytes,结果如flow.request.get_text()
flow.request.raw_content # bytes,结果如flow.request.get_content()
flow.request.urlencoded_form # MultiDictView,content-type:application/x-www-form-urlencoded时的请求参数,不包含url直接带的键值参数
flow.request.multipart_form # MultiDictView,content-type:multipart/form-data时的请求参数,不包含url直接带的键值参数

response

1
2
3
4
flow.response.status_code #状态码
flow.response.text#返回内容,已解码
flow.response.content #返回内容,二进制
flow.response.setText()#修改返回内容,不需要转码

设计流程

在上面提到可以拦截request和response,那么就可以对response数据做修改,再返回修改后的数据.
基于上面提到修改数据规则,随机多拦截数据做随机修改.
保存修改前和修改后的数据,方便数据diff.
设计流程