Python & mitmproxy实现模拟器抓包和处理
给某国服游戏做个辅助工具,传输的包是明文,没有自定义加密。
pic from mitmproxy

模拟器

Nox(夜神)的6.x版本,可以使用52大佬的去广告绿化生成器,旧版本的模拟器可以在夜神的论坛下载,可以忍受广告的也可以直接官网下载最新版。

推荐分辨率设置到2k480dpi,不然可能会出现无法点击的错位ui。

因为Android7.0+的证书信任问题,选择5.0版本的模拟器。

mitmproxy

mitmproxy的原理是中间人攻击,名字就是中间人(Man-In-The-Middle)的缩写,感觉目前大部分的能抓https包的都是用的中间人攻击,其他的比如Charles,阿里的anyproxy等。

第一次安装用了scoop,之后为了生成文档又用pip装了一次。

官方文档非常不全,只推荐用pdoc自己生成,pdoc建议安装pdoc3,不然可能出现依赖markdown版本过低导致的错误。

pip install pdoc3
pdoc3 --html mitmproxy

安装完成之后就是常规操作,尝试先直接抓一下模拟器的包。

模拟器代理设置

设置→wlan→长按修改网络→代理:手动

主机名设置为物理机的内网IP,可使用ipconfig命令查看。

ipconfig

端口设置为mitmproxy的端口8080

安装证书抓取HTTPS流量

mitmproxy把安装时生成的证书放在C:\Users\USERNAME\.mitmproxy下,所有系统版本的均有,也可设置代理后访问mitm.it来可视化地选择下载。

设置→安全→从SD卡安装

选择对应的证书即可。

mitmweb抓包

mitmweb --set allow_hosts='(.*)-app-xxxx\.xxxxxx\.com'  --set termlog_verbosity='warn'

由于该游戏的实名认证应该是使用了固定证书,不过滤可能会出现反复要求实名认证的问题。

因为要抓的包很明确,直接使用白名单模式过滤。

Wiki中的ignore-hosts说明:

Ignore host and forward all traffic without processing it. In transparent mode, it is recommended to use an IP address (range), not the hostname. In regular mode, only SSL traffic is ignored and the hostname should be used. The supplied value is interpreted as a regular expression and matched on the ip or the hostname.
Default: []

简要意思就是说在透明代理模式下推荐使用ip代替域名,以及参数使用正则匹配。

这个名单只对SSL流量生效,并不会过滤http请求,需要过滤http流请求可以使用filter。

包处理

由于需要显示UI界面,不使用mitmproxy的脚本模式,而是在Python中后台开启mitmproxy。

#Build with tkinter
class Application(Frame):
    def __init__(self, master=None):
        self.m=None
        Frame.__init__(self, master)
        self.grid()
        self.createUI()

    def createUI(self):
        # put UI here
    
    def startDump(self):
        # dump
        self.m = DumpMaster(opts, with_termlog=False, with_dumper=True)
        self.m.server = proxy.server.ProxyServer(pconf)
        self.m.addons.add(PrintResponse())
        # start mitmdump in a new thread
        loop = asyncio.get_event_loop()
        t = threading.Thread( target=loop_in_thread, args=(loop,self.m) )
        t.start()
    
    def close(self):
        self.m.shutdown()
    
    def showData(self):
        #show dump data

app = Application()

# dump option
opts = options.Options(listen_port=8888)
opts.add_option("body_size_limit", int, 0, "")
opts.add_option("console_eventlog_verbosity", str, "warn", "")
opts.add_option("allow_hosts", list, ["(.*)-app-xxxx\\.xxxxxx\\.com"], "")
pconf = proxy.config.ProxyConfig(opts)

def loop_in_thread(loop, m):
    asyncio.set_event_loop(loop)
    m.run_loop(loop.run_forever)

class PrintResponse:
    def __init__(self):
        #set flowfilter
        self.game_host = flowfilter.parse('~d "(.*)-app-xxxx\\.xxxxxx\\.com"')

    def response(self, flow):
        if(flowfilter.match(self.game_host,flow)):
            resdict=json.loads(flow.response.content) #Parse the content as json
            app.showData(resdict)

if __name__ == '__main__':
    app.mainloop()

上次修改於 2021-01-20