0x00 前言

最近测试的APP里,越来越多的APP采用了加密流量的通信方式,即在原有的HTTP、HTTPS流量之上,又做了一层加密。

要对这些加密过程进行逆向,无疑要耗费大量的工作量,时间可能不允许。因此,可采用一种Hook的方式,将加密前/解密后的流量截获下来。

0x01 原理

通过加密流量与远端进行通信,势必会调用响应的加密、解密函数。因此,可通过Frida直接将未加密的流量Hook出来。

然而这样只能查看未加密的流量,不能篡改里边的数据,显然不太实用。因此,我们需要允许用户在流量进行加密/解密时,篡改里边的内容。

实现思路较为简单,如下所示:

  1. Hook加密函数,中断加密过程,将未加密的数据发送到本地搭建的一个服务器,并将HTTP代理设置为Burpsuite的代理
  2. 本地搭建的服务器原封不动地返回请求数据
  3. Hook点收到服务器返回的数据后,利用服务器返回的数据替换原有数据,并恢复加密函数的执行过程
  4. 重复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更甚。