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更甚。