iOS 中利用 Frida 解密任意 APP 的流量
0x00 前言 最近测试的APP里,越来越多的APP采用了加密流量的通信方式,即在原有的HTTP、HTTPS流量之上,又做了一层加密。 要对这些加密过程进行逆向,无疑要耗费大量的工作量,时间可能不允许。因此,可采用一种Hook的方式,将加密前/解密后的流量截获下来。 0x01 原理 通过加密流量与远端进行通信,势必会调用响应的加密、解密函数。因此,可通过Frida直接将未加密的流量Hook出来。 然而这样只能查看未加密的流量,不能篡改里边的数据,显然不太实用。因此,我们需要允许用户在流量进行加密/解密时,篡改里边的内容。 实现思路较为简单,如下所示: Hook加密函数,中断加密过程,将未加密的数据发送到本地搭建的一个服务器,并将HTTP代理设置为Burpsuite的代理 本地搭建的服务器原封不动地返回请求数据 Hook点收到服务器返回的数据后,利用服务器返回的数据替换原有数据,并恢复加密函数的执行过程 重复1、2、3的步骤,Hook掉解密函数 这样一来,Burpsuite就能够发挥其原有的作用,劫持未加密的流量,并对流量内容进行篡改了。 0x02 实现 以某APP为例,首先起一个echo服务器线程,专门负责原封不动地返回客户端的请求数据;其次用Frida hook掉相关加解密函数,将未加密的流量通过Burpsuite代理发往echo服务器。相关脚本内容如下: #!/usr/bin/env python3 # coding: utf-8 from time import sleep from threading import Thread from http.server import HTTPServer, BaseHTTPRequestHandler import sys import requests import frida ECHO_PORT = 28080 BURP_PORT = 8080 class RequestHandler(BaseHTTPRequestHandler): def do_REQUEST(self): content_length = int(self.headers.get('content-length', 0)) self.send_response(200) self.end_headers() self.wfile.write(self.rfile.read(content_length)) do_RESPONSE = do_REQUEST def echo_server_thread(): print('start echo server at port {}'.format(ECHO_PORT)) server = HTTPServer(('', ECHO_PORT), RequestHandler) server.serve_forever() # start echo server first t = Thread(target=echo_server_thread) t.daemon = True t.start() session = frida.get_usb_device().attach('平安普惠') script = session.create_script(''' var reqMethod = ObjC.classes.PHNetworkAgent['- requestOperationWithHTTPMethod:requestSerializer:URLString:parameters:']; var respDecrypt = ObjC.classes.PHSecurityHelper['+ phUnSecurityAESWithAesKey:content:']; var NSString = ObjC.classes.NSString; Interceptor.attach(reqMethod.implementation, { onEnter: function (args) { var methodStr = new ObjC.Object(args[2]).toString(); var urlStr = new ObjC.Object(args[4]).toString(); var param = new ObjC.Object(args[5]); var paramStr = param['- uxy_JSONString']().toString(); var data = { method: methodStr, url: urlStr, param: paramStr, }; send({type: 'REQ', data: data}) var op = recv('NEW_REQ', function(val) { var data = val.payload; args[2] = NSString.stringWithString_(data.method); args[4] = NSString.stringWithString_(data.url); args[5] = NSString.stringWithString_(data.param)['+ __uxy_JSONObject'](); }); op.wait(); } }); Interceptor.attach(respDecrypt.implementation, { onLeave: function (retval) { var resp = new ObjC.Object(retval); var data = resp.toString(); send({type: 'RESP', data: data}) var op = recv('NEW_RESP', function(val) { var data = val.payload; var newRetval = NSString.stringWithString_(data); retval.replace(newRetval); }); op.wait(); } }); ''') def on_message(message, data): if message['type'] == 'send': payload = message['payload'] _type, data = payload['type'], payload['data'] if _type == 'REQ': r = requests.request('REQUEST', 'http://127.0.0.1:{}/'.format(ECHO_PORT), proxies={'http': 'http://127.0.0.1:{}'.format(BURP_PORT)}, data=data['param'].encode('utf-8'), headers={ 'REQ_METHOD': data['method'], 'REQ_URL': data['url'], }) new_data = { 'method': r.headers.get('REQ_METHOD', data['method']), 'url': r.headers.get('REQ_URL', data['url']), 'param': r.text } script.post({'type': 'NEW_REQ', 'payload': new_data}) elif _type == 'RESP': r = requests.request('RESPONSE', 'http://127.0.0.1:{}/'.format(ECHO_PORT), proxies={'http': 'http://127.0.0.1:{}'.format(BURP_PORT)}, data=data.encode('utf-8')) script.post({'type': 'NEW_RESP', 'payload': r.text}) script.on('message', on_message) script.load() sys.stdin.read() 0x03 总结 套用上面的模板,可以快速地对加密流量的APP进行测试。然而实际应用上的难点在于找到相关加解密的函数,对于Objective-C这类没有明显调用关系的APP更甚。 ...