告别HttpCanary:基于Frida RPC与Burp Suite的安卓加密流量实时篡改实战
1. 项目概述:为什么我们需要告别HttpCanary?
如果你做过安卓应用的渗透测试或者逆向分析,尤其是面对那些请求体被加密得一塌糊涂的App,HttpCanary 这类抓包工具大概率是你的老朋友,也是你的“痛点”。它能抓到包,但面对AES、RSA或者各种自定义算法加密后的数据,往往只能显示一堆乱码或十六进制字符串。你想在Burp Suite里实时修改一个加密参数?传统流程是:抓包 -> 解密 -> 修改 -> 再加密 -> 重放。步骤繁琐,效率低下,而且一旦加密逻辑稍有变动,整个流程就得重来。
这就是我们今天要解决的问题核心。通过Python搭建一个Frida RPC服务器,我们能在应用进程的内存层面,直接Hook到加密/解密函数,在数据“明文”状态下进行拦截和篡改,然后无缝对接到Burp Suite。这意味着,你在Burp里看到的、修改的,就是应用逻辑处理前后的原始数据,加解密过程对我们完全透明。这不仅仅是替代HttpCanary的抓包功能,更是实现了对加密流量的“上帝视角”操控。
这套方案特别适合安全研究人员、安卓逆向工程师以及对应用数据流有深度分析需求的开发者。它不依赖于网络层抓包,因此能绕过一些证书绑定(SSL Pinning)的初级防御,直击应用的核心业务逻辑。接下来,我会手把手带你从零搭建这套系统,并分享我在实战中踩过的坑和总结的技巧。
2. 核心架构与工具选型解析
2.1 为什么是Frida + Python RPC + Burp这个组合?
在动手之前,理解我们选择的每样工具背后的逻辑至关重要。这决定了方案的可行性和效率。
Frida:它是整个体系的基石。Frida是一个动态代码插桩框架,我们可以把它的脚本(JavaScript)注入到目标应用的进程中。这意味着我们能直接访问应用的内存、调用其函数、修改其逻辑。对于加密请求,我们不需要去逆向整个网络库,只需要找到那个关键的encrypt(data)和decrypt(data)函数,Hook住它们,就能在数据加密前、解密后拿到明文。
Python RPC服务器:Frida脚本运行在目标应用的上下文里,而我们的分析工具(如Burp)运行在电脑上。我们需要一个桥梁来通信。这就是RPC(远程过程调用)服务器的用武之地。我们用Python写一个简单的HTTP/JSON RPC服务,Frida脚本通过发送HTTP请求,将拦截到的明文数据“报告”给这台服务器,同时也可以从服务器“获取”修改后的数据。Python的http.server或更高效的Flask框架非常适合快速搭建这样一个轻量级接口。
Burp Suite:行业标准的Web安全测试工具。我们搭建的Python RPC服务器将扮演一个“透明代理”的角色。它收到Frida发来的明文请求后,不是直接发给目标服务器,而是转发给Burp(配置为上游代理)。这样,这个请求就会出现在Burp的Proxy history里,你可以任意修改其参数、头部。修改完成后,Burp将请求发往真实服务器,并将响应先传回我们的Python服务器,再由Python服务器传回给Frida脚本进行加密和返回给应用。Burp强大的重放、扫描、Intruder等功能因此得以在加密流量上施展。
这个组合的优势在于:
- 实时性:请求/响应的拦截、篡改、转发几乎是实时的,体验接近明文抓包。
- 灵活性:所有逻辑(Hook点、数据处理)都用JavaScript和Python编写,可根据不同App快速调整。
- 强大生态:利用了Burp Suite的全部功能,无需重复造轮子。
2.2 环境准备清单与避坑指南
工欲善其事,必先利其器。以下是经过实战检验的环境配置清单:
1. 基础环境:
- Python 3.8+:建议使用3.8或3.9,避免最新版本可能存在的库兼容性问题。安装时务必勾选“Add Python to PATH”。
- ADB (Android Debug Bridge):用于连接手机或模拟器。确保
adb devices能列出你的设备。 - 一部已Root的安卓手机或模拟器:这是Frida工作的前提。推荐使用雷电模拟器(Android 7.1/9.0版本),它Root方便,且对Frida支持较好。注意:某些应用(如新版微信、银行App)会在运行时检测Root或Frida环境,需要额外的对抗手段,这超出了本篇基础搭建的范围,但文末会给出方向。
2. 核心工具安装:
Frida:分为电脑端(Client)和手机端(Server)。
- 电脑端:在命令行执行
pip install frida-tools。这会安装frida、frida-ps、frida-ls-devices等工具。 - 手机端:去Frida的GitHub Release页面,根据你设备的CPU架构(通常是
arm64)下载对应的frida-server-xx.x.x-android-arm64.xz。解压后得到frida-server文件。- 通过
adb push frida-server /data/local/tmp/推送至设备。 adb shell进入设备shell,执行chmod 755 /data/local/tmp/frida-server赋予执行权限。- 在
adb shell中,切换到后台运行:cd /data/local/tmp && ./frida-server &。验证是否运行:电脑端执行frida-ps -U,应能列出手机进程。
- 通过
- 电脑端:在命令行执行
Burp Suite:下载社区版或专业版。安装后,首先配置代理监听:
- 打开Burp,进入
Proxy->Options。确保Proxy Listeners下有一个运行在127.0.0.1:8080的监听器(这是默认设置)。如果被占用,可以换其他端口,但后续脚本配置需同步修改。
- 打开Burp,进入
3. Python依赖库:创建一个requirements.txt文件,内容如下:
frida requests flask然后执行pip install -r requirements.txt。
frida: 用于在Python代码中连接和控制Frida。requests: 用于在Python服务器中向Burp转发HTTP请求。flask: 比Python内置的http.server更强大、更易用,用于构建我们的RPC服务器。我们将使用Flask。
实操心得一:关于Frida版本匹配Frida的电脑端(Client)和手机端(Server)版本必须严格一致,否则会出现连接失败或协议错误。用
frida --version查看电脑端版本,然后下载完全相同版本的frida-server。这是新手最容易踩的坑。
实操心得二:模拟器与Burp的代理设置为了让模拟器内的App流量经过Burp,需要在模拟器的WIFI设置中手动配置代理:服务器为电脑的IP地址(不是127.0.0.1,因为对模拟器来说电脑是另一个主机),端口为Burp的监听端口(如8080)。同时,需要在模拟器浏览器中访问
http://burp下载并安装Burp的CA证书。雷电模拟器通常可以通过adb shell settings put global http_proxy your_pc_ip:8080来设置,但有些App会忽略系统代理,这就是我们采用Frida方案的根本原因——我们不走系统代理,而是从代码层拦截。
3. Frida Hook脚本深度剖析与编写
这是整个项目的灵魂。脚本的目标是精准地Hook住应用进行网络请求时的加密函数和收到响应后的解密函数。
3.1 定位关键Hook点
在没有源码的情况下,如何找到这些函数?通常有几种思路:
- 关键词搜索:使用
jadx-gui等工具反编译APK,在代码中搜索encrypt、decrypt、encode、decode、AES、RSA、DES、Cipher、getEncryptedData、sign等关键词。 - 网络库分析:查看App使用的网络库,如OkHttp、Retrofit、HttpURLConnection。Hook这些库的发送请求和接收响应的方法。例如,OkHttp的
RealCall.execute()或Call.enqueue(),以及Interceptor接口。 - 黑盒Trace:使用Frida的
Stalker或frida-trace工具,对疑似类的方法进行批量跟踪,观察输入输出。
假设我们通过分析,确定目标App使用一个名为com.example.app.SecurityHelper的类,其中有两个关键方法:
public static String encryptRequest(String plainText)public static String decryptResponse(String encryptedText)
3.2 编写Frida JavaScript Hook脚本
我们将脚本保存为hook.js。
// hook.js Java.perform(function () { console.log("[*] Starting Frida Hook Script..."); // 1. Hook 加密函数 var SecurityHelper = Java.use("com.example.app.SecurityHelper"); SecurityHelper.encryptRequest.implementation = function (plainText) { console.log("[+] encryptRequest called!"); console.log("[+] PlainText (Input): " + plainText); // 调用原方法获取加密结果 var encryptedResult = this.encryptRequest(plainText); console.log("[+] EncryptedResult (Output): " + encryptedResult); // 关键步骤:将明文和加密后的数据发送给我们的Python RPC服务器 // 我们通过发送HTTP请求来实现 sendToRPC('encrypt', { 'plain': plainText, 'encrypted': encryptedResult, 'timestamp': new Date().getTime() }); return encryptedResult; // 返回原加密结果,不干扰应用正常逻辑 }; // 2. Hook 解密函数 SecurityHelper.decryptResponse.implementation = function (encryptedText) { console.log("[+] decryptResponse called!"); console.log("[+] EncryptedText (Input): " + encryptedText); // 调用原方法获取解密结果 var decryptedResult = this.decryptResponse(encryptedText); console.log("[+] DecryptedResult (Output): " + decryptedResult); // 将加密数据和解密后的明文发送给RPC服务器 sendToRPC('decrypt', { 'encrypted': encryptedText, 'decrypted': decryptedResult, 'timestamp': new Date().getTime() }); return decryptedResult; }; // 3. 与Python RPC服务器通信的函数 function sendToRPC(event, data) { // 这里我们使用Java的HttpURLConnection来发送HTTP POST请求到本地Python服务器 // Python服务器地址默认为 http://127.0.0.1:5000/api/hook var rpcUrl = "http://10.0.2.2:5000/api/hook"; // 注意:在模拟器中,127.0.0.1指向自己,需用10.0.2.2指向宿主电脑。 var url = new java.net.URL(rpcUrl); var connection = url.openConnection(); connection.setRequestMethod("POST"); connection.setRequestProperty("Content-Type", "application/json; utf-8"); connection.setRequestProperty("Accept", "application/json"); connection.setDoOutput(true); var payload = JSON.stringify({ 'event': event, 'data': data, 'process': 'com.example.app' // 可以带上进程名用于区分 }); try { var outputStream = connection.getOutputStream(); var writer = new java.io.OutputStreamWriter(outputStream, "UTF-8"); writer.write(payload); writer.flush(); writer.close(); var responseCode = connection.getResponseCode(); // console.log("[*] RPC Response Code: " + responseCode); } catch (e) { console.log("[!] Error sending to RPC: " + e.message); } finally { connection.disconnect(); } } console.log("[*] Hooks installed successfully."); });脚本解析与注意事项:
Java.perform:确保代码在Java虚拟机上下文中执行。implementation:替换原方法的实现,这是Frida Hook的核心。sendToRPC函数:这是实现“RPC”的关键。我们在Hook函数内部,将捕获到的数据(明文、密文)通过一个HTTP POST请求发送到我们即将用Python搭建的服务器(运行在电脑上,地址10.0.2.2:5000)。- 地址问题:在安卓模拟器(特别是Android Studio AVD或Genymotion)中,
127.0.0.1或localhost指向模拟器自身。要访问宿主机的服务,需要使用特殊的IP地址:10.0.2.2(对于标准Android模拟器)。雷电模拟器可能有所不同,有时是10.0.2.2,有时需要查看电脑在局域网的真实IP。如果通信失败,首先检查这个地址。 - 异步与性能:
sendToRPC是同步HTTP请求,可能会轻微拖慢应用速度。在生产脚本中,可以考虑将其改为异步或使用队列,但对于测试目的,同步通常足够。
实操心得三:Hook点的稳定性直接Hook业务类的方法可能因为混淆或版本更新而失效。一个更稳健的方法是Hook底层加密库,如
javax.crypto.Cipher的doFinal方法。这样无论上层业务逻辑如何变化,只要它使用标准Cipher,我们就能抓到。但这就需要更精细的过滤,比如通过参数判断是加密还是解密操作。这可以作为进阶优化方向。
4. Python RPC服务器与Burp桥接实现
现在,我们需要一个“中间人”服务器来接收Frida脚本发来的数据,并与Burp交互。我们将使用Flask搭建一个轻量级API服务器。
4.1 搭建Flask RPC服务器
创建文件rpc_server.py:
# rpc_server.py from flask import Flask, request, jsonify, Response import requests import json import threading import time from queue import Queue import logging # 配置日志 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) app = Flask(__name__) # 配置 BURP_PROXY = "http://127.0.0.1:8080" # Burp Suite代理地址 PYTHON_SERVER_HOST = "0.0.0.0" PYTHON_SERVER_PORT = 5000 # 用于存储待篡改的请求和响应 (简单内存队列,生产环境建议用Redis等) request_queue = Queue() response_queue = Queue() # 一个简单的内存存储,用于关联请求和响应 # key: request_id (可以用timestamp+process生成), value: 相关数据 request_context = {} def forward_to_burp(http_method, url, headers, body): """ 将请求转发给Burp Suite代理。 """ proxies = { "http": BURP_PROXY, "https": BURP_PROXY, } # 移除一些可能引起问题的头,如Content-Length,requests库会自己处理 filtered_headers = {k: v for k, v in headers.items() if k.lower() not in ['content-length', 'host']} try: # 根据方法转发请求 if http_method.upper() == 'GET': resp = requests.get(url, headers=filtered_headers, proxies=proxies, verify=False, data=body if body else None) elif http_method.upper() == 'POST': # 注意:需要根据实际的Content-Type处理body if body: if 'application/json' in headers.get('Content-Type', ''): resp = requests.post(url, json=json.loads(body), headers=filtered_headers, proxies=proxies, verify=False) elif 'application/x-www-form-urlencoded' in headers.get('Content-Type', ''): resp = requests.post(url, data=body, headers=filtered_headers, proxies=proxies, verify=False) else: # 其他类型,如二进制,直接传递bytes resp = requests.post(url, data=body.encode('utf-8') if isinstance(body, str) else body, headers=filtered_headers, proxies=proxies, verify=False) else: resp = requests.post(url, headers=filtered_headers, proxies=proxies, verify=False) # 可以添加PUT, DELETE等其他方法... else: resp = requests.request(http_method, url, headers=filtered_headers, proxies=proxies, verify=False, data=body) logger.info(f"Forwarded to Burp. Status: {resp.status_code}, URL: {url}") return resp except Exception as e: logger.error(f"Error forwarding to Burp: {e}") return None @app.route('/api/hook', methods=['POST']) def handle_hook(): """ 接收来自Frida脚本的Hook数据。 这里我们简化处理:当收到加密前的明文请求时,我们将其放入队列,并返回一个“待处理”的标识。 实际场景中,Frida脚本可能需要阻塞等待我们返回修改后的数据。 我们采用另一种更通用的模式:Frida脚本将明文发给我们,我们转发给Burp,Burp修改后请求真实服务器, 得到响应后,我们再通过另一个API将响应明文传回给Frida脚本。 这需要Frida脚本能够等待或我们能够回调。为了简化,本例采用“请求-响应”配对模式。 """ data = request.json event = data.get('event') hook_data = data.get('data') process = data.get('process') if event == 'encrypt': # 这是应用即将发送的请求,加密前的明文 plain_text = hook_data.get('plain') encrypted_text = hook_data.get('encrypted') logger.info(f"[ENCRYPT] From {process}: PlainText captured.") # 这里,plain_text 可能就是完整的HTTP请求体(JSON或表单)。 # 我们需要将其与一个“虚拟”的HTTP请求关联,转发给Burp。 # 生成一个唯一ID用于关联 request_id = f"{process}_{int(time.time()*1000)}" request_context[request_id] = { 'plain_body': plain_text, 'encrypted_body': encrypted_text, 'process': process, 'timestamp': time.time() } # 将请求ID放入队列,等待一个“控制台”或“交互界面”来触发转发和篡改 request_queue.put(request_id) # 立即返回一个响应,告诉Frida脚本我们收到了,并返回这个request_id。 # Frida脚本需要修改原逻辑,使用我们返回的加密数据(即我们从Burp修改后再加密的数据)。 # 这需要更复杂的交互协议。作为第一步,我们先只记录。 return jsonify({'status': 'intercepted', 'request_id': request_id, 'action': 'forward'}) elif event == 'decrypt': # 这是应用收到的响应,解密后的明文 encrypted_text = hook_data.get('encrypted') decrypted_text = hook_data.get('decrypted') logger.info(f"[DECRYPT] From {process}: Response decrypted.") # 同样,将解密后的响应放入队列或直接处理 response_id = f"resp_{process}_{int(time.time()*1000)}" request_context[response_id] = { 'encrypted_body': encrypted_text, 'decrypted_body': decrypted_text, 'process': process } response_queue.put(response_id) return jsonify({'status': 'intercepted', 'response_id': response_id}) return jsonify({'status': 'unknown_event'}), 400 @app.route('/api/forward_request', methods=['POST']) def forward_request(): """ 一个手动触发的端点,用于从队列中取出一个被拦截的请求,构造HTTP请求,转发给Burp, 并将Burp的响应返回(这个响应是明文的,需要后续加密返回给App)。 在实际自动化工具中,这个步骤可以自动完成。 """ if request_queue.empty(): return jsonify({'error': 'No pending requests'}), 404 request_id = request_queue.get() req_ctx = request_context.get(request_id) if not req_ctx: return jsonify({'error': 'Request context not found'}), 404 plain_body = req_ctx['plain_body'] # 假设 plain_body 是一个JSON字符串,代表请求体。 # 我们需要知道目标URL、方法、头部。这些信息Frida脚本没有提供。 # 因此,我们需要改进Frida脚本,让它Hook网络库,捕获完整的请求信息(URL, Method, Headers, Body)。 # 这里为了演示,我们假设一个固定的测试URL和POST方法 target_url = "https://api.example.com/v1/test" # 这个需要从Frida脚本获取 method = "POST" headers = { "Content-Type": "application/json", "User-Agent": "Frida-RPC-Agent" } logger.info(f"Forwarding request {request_id} to Burp: {target_url}") response = forward_to_burp(method, target_url, headers, plain_body) if response: # 存储Burp返回的明文响应,关联到request_id req_ctx['burp_response'] = { 'status_code': response.status_code, 'headers': dict(response.headers), 'body': response.text } # 这里,我们应该将这个明文响应加密,然后通过另一个API让Frida脚本获取并返回给应用。 # 我们先存储起来。 return jsonify({ 'request_id': request_id, 'burp_response': req_ctx['burp_response'] }) else: return jsonify({'error': 'Failed to forward request'}), 500 @app.route('/api/get_modified_request/<request_id>', methods=['GET']) def get_modified_request(request_id): """ Frida脚本在发送请求前,可以轮询这个接口,获取我们修改后的请求体(明文)。 拿到后,用它替换原本要加密的数据。 """ req_ctx = request_context.get(request_id) if not req_ctx: return jsonify({'error': 'Not found'}), 404 # 假设我们有一个界面修改了 `req_ctx['plain_body']` modified_body = req_ctx.get('modified_body', req_ctx['plain_body']) # 如果没有修改,返回原数据 return jsonify({'modified_body': modified_body}) def start_flask_server(): app.run(host=PYTHON_SERVER_HOST, port=PYTHON_SERVER_PORT, debug=False, threaded=True) if __name__ == '__main__': logger.info(f"Starting Frida RPC Server on {PYTHON_SERVER_HOST}:{PYTHON_SERVER_PORT}") logger.info(f"Burp Proxy is set to: {BURP_PROXY}") start_flask_server()这个服务器提供了几个关键端点:
/api/hook: 接收Frida发送的加密/解密事件数据。/api/forward_request: (手动)触发,将一个拦截的请求转发给Burp。/api/get_modified_request/<request_id>: Frida脚本在加密前,从此处获取可能被我们修改过的请求明文。
当前模式的局限性:这是一个简化的“半自动”模型。Frida脚本拦截到明文后,通知服务器,服务器等待人工或自动指令将其转发至Burp。更理想的“全自动”模型需要Frida脚本拦截整个网络请求(包括URL、头、方法),并暂时挂起该请求,等待Python服务器与Burp交互完成并返回修改后的数据后,再继续执行。这需要更复杂的Frida脚本(使用setTimeout或Promise模拟异步等待)和交互协议。
4.2 改进版:全自动拦截与篡改模型
为了实现真正的实时篡改,我们需要升级架构。思路如下:
- Frida脚本:Hook网络库的底层调用(如OkHttp的
Call.execute()),在请求发出前,将完整的请求信息(方法、URL、头、体)发送给Python服务器,并阻塞等待服务器返回。 - Python服务器:收到请求后,立即构造一个HTTP请求发给Burp,并阻塞等待Burp的响应(用户可能在Burp里修改请求并Forward)。
- Burp Suite:用户在其界面修改请求后,放行。
- Python服务器:收到Burp转发回来的真实服务器响应后,将其返回给正在等待的Frida脚本。
- Frida脚本:用服务器返回的响应(可能是修改后的请求对应的响应)继续执行,返回给应用。
这涉及到Frida脚本中的同步等待,可以通过Java.scheduleOnMainThread配合setTimeout模拟,或者使用Promise(如果环境支持)。由于实现较为复杂,这里给出核心概念代码:
Frida脚本改进片段 (概念):
var realCallExecute = OkHttpClient.Call.execute.overload('...'); realCallExecute.implementation = function() { var originalRequest = this.request(); // 获取原始请求 var url = originalRequest.url().toString(); var method = originalRequest.method(); var headers = {}; // ... 提取headers var body = null; if (originalRequest.body()) { // ... 提取body内容 } // 发送到Python服务器并等待结果 var modifiedResponse = sendAndWaitForModification(url, method, headers, body); // 根据modifiedResponse构造一个OkHttp的Response对象并返回 // 这样应用收到的是我们篡改后的响应 return constructResponseFromData(modifiedResponse); }Python服务器需要实现一个阻塞的端点,等待Burp的交互完成。这通常需要长轮询或WebSocket。对于入门,我们可以先用简单的轮询模式:Frida脚本发送请求后,轮询一个状态接口,直到Python服务器返回“处理完成”的信号。
实操心得四:性能与稳定性考量这种“拦截-等待-返回”的模式会显著增加请求延迟,可能引起应用超时或行为异常。在生产测试中,建议:
- 只针对关键请求开启拦截。
- 在Python服务器端设置超时,避免Frida脚本无限期等待。
- 做好异常处理,如果RPC服务器无响应,应让请求按原逻辑进行,避免卡死应用。
5. 整合与实战操作流程
现在,让我们把所有的部分串联起来,完成一次完整的实战操作。
5.1 完整操作步骤
启动环境:
- 确保安卓设备(模拟器)已Root,并运行
frida-server。 - 启动Burp Suite,确保
127.0.0.1:8080代理监听正常。 - 在电脑上运行我们的Python RPC服务器:
python rpc_server.py。控制台应显示启动日志。
- 确保安卓设备(模拟器)已Root,并运行
注入Frida脚本:
- 首先,找到目标应用的进程名(包名),例如
com.example.app。 - 使用Frida命令注入脚本:
frida -U -f com.example.app -l hook.js --no-pause-U: 连接USB设备。-f: 启动应用。-l: 加载脚本文件。--no-pause: 启动后不暂停。
- 如果应用已在运行,可以用
frida -U com.example.app -l hook.js附加。
- 首先,找到目标应用的进程名(包名),例如
触发网络请求:
- 在手机上操作目标App,触发一个需要测试的加密网络请求(如登录、提交数据)。
观察日志:
- 在Frida注入的控制台,你应该能看到
[*] encryptRequest called!和明文、密文的输出。 - 在Python服务器的控制台,应该能看到
[ENCRYPT] From com.example.app: PlainText captured.的日志。
- 在Frida注入的控制台,你应该能看到
与Burp交互:
- 此时,被拦截的请求信息(
request_id)存储在Python服务器的request_queue中。 - 你可以手动向
http://127.0.0.1:5000/api/forward_request发送一个POST请求(用curl或Postman),触发该请求被转发到Burp。 - 更优的做法:写一个简单的控制台程序或Web界面,自动消费队列并转发。例如,在
rpc_server.py中开一个后台线程,持续检查request_queue,并自动调用forward_to_burp。
- 此时,被拦截的请求信息(
在Burp中篡改:
- 请求转发后,它就会出现在Burp的
Proxy->Intercept标签页(如果拦截开启)或HTTP history中。 - 你可以像处理普通HTTP请求一样,修改其参数、头部。然后点击
Forward。
- 请求转发后,它就会出现在Burp的
获取篡改后响应:
- Burp将修改后的请求发往真实服务器,并将响应返回给Python服务器。
- Python服务器将响应存储在
request_context中。 - Frida脚本需要实现轮询机制,调用
/api/get_modified_request/<request_id>或类似的端点,获取服务器返回的响应明文,并将其作为解密函数的结果返回给应用。
5.2 核心配置文件与脚本优化
为了让项目更易用,我们可以创建配置文件config.py:
# config.py BURP_PROXY = "http://127.0.0.1:8080" RPC_SERVER_HOST = "0.0.0.0" RPC_SERVER_PORT = 5000 # 模拟器访问宿主机的IP EMULATOR_HOST_IP = "10.0.2.2" # Hook的目标类和方法 TARGET_CLASS = "com.example.app.SecurityHelper" ENCRYPT_METHOD = "encryptRequest" DECRYPT_METHOD = "decryptResponse"然后在hook.js和rpc_server.py中导入配置。同时,我们可以将Frida脚本中硬编码的RPC URL改为配置项:var rpcUrl = "http://" + Config.EMULATOR_HOST_IP + ":" + Config.RPC_SERVER_PORT + "/api/hook";(这需要在脚本中通过某种方式注入配置,例如作为参数传递)。
6. 常见问题排查与实战技巧实录
在实际搭建和测试过程中,你几乎一定会遇到下面这些问题。这里是我踩过坑后的解决方案。
6.1 Frida连接与脚本注入失败
- 症状:
frida-ps -U无输出或报错Failed to enumerate processes: unable to connect to remote frida-server。 - 排查:
- 设备未连接:执行
adb devices确认设备在线且已授权。 - frida-server未运行:
adb shell进入设备,执行ps | grep frida-server查看进程。如果没有,检查是否已赋予执行权限 (chmod 755) 并在后台运行 (&)。 - 端口冲突:frida-server默认使用TCP端口27042。确保该端口未被占用。可以尝试
adb forward tcp:27042 tcp:27042建立端口转发。 - 版本不匹配:严格检查电脑端
frida和手机端frida-server版本号是否完全一致。
- 设备未连接:执行
6.2 Hook脚本生效但收不到RPC服务器数据
- 症状:Frida脚本打印了Hook日志,但Python服务器没有收到POST请求。
- 排查:
- 网络连通性:在模拟器的浏览器中访问
http://10.0.2.2:5000(或你的电脑IP:端口),看是否能打开(Flask默认会有404页面)。如果不能,说明模拟器访问不到你的电脑。 - 防火墙:关闭电脑的防火墙或为Python端口(5000)添加入站规则。
- IP地址错误:确认
EMULATOR_HOST_IP配置正确。对于雷电模拟器,可能需要用电脑的局域网IP(如192.168.x.x)。可以在模拟器里adb shell然后ping 电脑IP测试。 - 脚本错误:检查Frida脚本中
sendToRPC函数内的URL拼接是否正确,以及异常捕获是否吞掉了错误信息。可以在函数内多加一些console.log调试。
- 网络连通性:在模拟器的浏览器中访问
6.3 Burp抓不到转发过来的请求
- 症状:Python服务器日志显示已转发,但Burp的History中空空如也。
- 排查:
- Burp代理监听:确认Burp的代理监听器是开启状态,并且监听在
127.0.0.1:8080。 - 代理设置:在Python的
requests调用中,proxies参数是否正确设置为Burp的地址。 - HTTPS流量:如果目标URL是HTTPS,
requests需要设置verify=False来忽略证书验证(已设置)。同时,Burp需要安装其CA证书到系统信任库,但这里requests是直接信任Burp作为代理,通常没问题。如果遇到SSL错误,可能需要额外处理。 - 请求构造:检查
forward_to_burp函数中构造的请求(方法、URL、头、体)是否完整正确。特别是请求体(Body)的格式,需要和原始请求一致(JSON、Form-data等)。用Burp抓一个正常的明文请求对比。
- Burp代理监听:确认Burp的代理监听器是开启状态,并且监听在
6.4 应用检测到Frida或代理导致崩溃
- 症状:注入Frida脚本后,应用闪退或提示“安全警告”。
- 对策:
- 反Frida检测:Frida会留下一些特征(如端口、进程名、文件、内存映射等)。可以使用对抗脚本,如
frida-detection-demo中的一些bypass方法。常见手段包括:重命名frida-server文件、隐藏端口、抹去内存中的frida字符串等。 - SSL Pinning(证书绑定):应用可能校验服务器证书,导致Burp的CA证书不被信任。这就需要使用Frida Hook来绕过证书校验。网上有大量现成的脚本(如
Universal Android SSL Pinning Bypass with Frida),可以HookOkHttp、TrustManager、X509Certificate等类相关方法,使其验证始终通过。 - 代理检测:有些应用会检测是否设置了系统代理。我们的方案不依赖系统代理,而是内存Hook,所以通常能绕过。但如果应用检测了网络栈的某些属性,可能仍需对抗。
- 反Frida检测:Frida会留下一些特征(如端口、进程名、文件、内存映射等)。可以使用对抗脚本,如
6.5 性能问题与请求超时
- 症状:应用网络请求变慢,甚至超时。
- 优化:
- 精简Hook范围:只Hook最必要的函数,避免Hook过于频繁的函数(如每个字符加密)。
- 优化RPC通信:将Frida脚本中的同步HTTP请求改为异步,或者批量发送数据。
- Python服务器性能:使用生产级WSGI服务器(如
gunicorn)替代Flask开发服务器。对于高并发场景,考虑使用gevent或asyncio。 - 设置超时:在Frida脚本和Python服务器的请求中,合理设置超时时间,避免无限等待。
最后,这套体系的威力在于其灵活性。一旦打通,你可以轻松地:
- 实时篡改任何参数:在Burp里直接修改加密请求中的金额、ID、状态。
- 自动化测试:将Python服务器与Burp的API(
Burp Extender API)结合,实现自动重放、扫描加密接口。 - 协议分析:无需解密算法,直接观察明文业务逻辑,快速理解应用协议。
它确实比单纯使用HttpCanary多了不少搭建步骤,但换来的能力是质的飞跃。从“看得见但改不了”到“看得见且任意改”,这其中的差距,在真正的渗透测试和深度逆向中,往往是突破的关键。希望这篇详尽的指南能帮你成功搭建起属于自己的加密流量实时篡改系统。