从对抗到出洞:某金融APP 实战渗透与 Frida 反检测绕过(Rpc + Flask + AutoDecoder)
字数 4392 2025-10-18 11:17:50
某金融APP自定义加密协议分析与自动化加解密实战教学文档
文档概述
本文档详细解析了如何对一款采用高强度自定义加密协议(模拟TLS 1.3)的金融APP进行安全评估。核心挑战在于绕过其Frida检测机制,逆向其通信数据的加解密流程,并最终通过编写一个与Burp Suite集成的自动化解码器(AutoDecoder),实现测试过程中数据包的明文查看与修改。整个研究在授权范围内进行,所有技术仅用于安全测试与学习。
第一章:核心问题与反检测绕过
1.1 遇到的问题
- 现象:Frida可以正常附加(Attach)到目标APP进程,但一旦在Frida脚本中调用
Java.use等关键API,APP会立即闪退。 - 分析:这是APP集成了成熟的Frida检测机制的典型表现。检测点通常在于监控内存中的Frida特征、端口或线程名。
Java.use的调用触发了检测。
1.2 解决方案:编译Frida-Java-Bridge
- 原理:官方Frida的
Java.use等API在实现上存在一些容易被检测的特征。通过编译自己的frida-java-bridge模块,可以修改这些特征,从而绕过基于特征码的检测。 - 实施步骤:
- 获取Frida源码。
- 定位到
frida-java-bridge目录。 - 根据文章提到的参考视频(B站视频关键词:
frida模块化开发,frida-compile,解决frida的java的api检测相关问题,frida-java-bridge调试)中的指导,修改可能被检测的代码部分(如特定字符串、函数指针等)。 - 使用
frida-compile等工具重新编译生成新的Frida Agent脚本(如_agent.js)。
- 结果:使用编译后的Agent脚本进行注入,即可正常调用
Java.use而不再触发闪退。
1.3 替代方案:使用ZygiskFrida
- 原理:
Zygisk是Magisk的模块系统,可以在Zygote进程(所有Android应用进程的父进程)中运行代码。ZygiskFrida项目通过将Frida注入到Zygote,使得Frida在APP进程启动时就已经存在,实现了更深层次的隐藏,能够绕过大多数基于应用进程内检测的方案。 - 实施:在已Root并安装了Magisk的设备上,安装ZygiskFrida模块。
- 选择:两种方案均可,编译Bridge方案更轻量,ZygiskFrida方案更彻底。文档后续以编译Bridge方案为基础展开。
第二章:通信协议逆向分析
2.1 初探与抓包
- 抓包发现请求体(Request Body)和响应体(Response Body)均为JSON格式,且键名为
=,值为一长串看似无规律的字符。这表明数据经过了整体加密。 - 请求头中包含关键签名字段
X-Emp-Signature,用于防止数据篡改。
2.2 定位关键代码
- 使用Jadx进行静态分析:将APP的APK文件拖入Jadx,反编译为Java代码。
- 搜索签名字段:全局搜索
X-Emp-Signature,快速定位到设置此头部的代码位置,通常位于网络库的拦截器或封装层。文章定位到了initHttpRequest方法。 - 分析签名算法:在
initHttpRequest方法中,发现X-Emp-Signature的值是通过encryptHMAC方法计算得到的。该方法支持HmacSha1或国密SM3算法,对输入数据和密钥进行HMAC运算。
2.3 逆向加密流程
- 动态追踪调用栈:为了找到加密发生的位置,在静态分析代码逻辑复杂时,使用Frida的
Java.use钩住(Hook)encryptHMAC等方法,然后打印当前的Java调用堆栈($bt)。这能清晰地展示出是哪个方法调用了加密函数。 - 定位核心方法:通过堆栈追踪,向上定位到
sendRequest方法,该方法内部调用了handleRequestBody函数来处理请求体加密。 - 分析
handleRequestBody:此函数是加密的核心。通过代码分析(或借助GPT等工具解释复杂代码),其逻辑可概括为:- 生成一个32字节的随机数(RNC)。
- 将RNC与原始的JSON明文请求体拼接。
- 使用AES-CBC模式,配合密钥(
clientKey_)和初始向量(clientIv_)对拼接后的数据进行加密,并进行PKCS7填充。 - 生成一个9字节的序列号。
- 将序列号与AES加密后的数据拼接。
- 使用HMAC-SHA1算法,配合密钥(
clientHmacKey)对步骤5的拼接结果进行计算,得到20字节的签名。 - 最终数据包结构为:
[20字节HMAC签名] + [9字节序列号] + [AES加密数据]。 - 将此二进制数据包进行两次Base64编码,最终作为JSON中
=键对应的值。
2.4 揭秘密钥交换(最难且最关键的部分)
- 问题:用于加解密的
clientKey_,clientIv_,clientHmacKey等密钥并非硬编码在代码中,也不是通过普通的HTTP请求下发。每次重启APP都会变化,但服务端却能正确解密。 - 逆向过程:
- 追踪密钥赋值:在Jadx中追踪
AESCipher类和ClientHello类中这些密钥变量的赋值过程。 - 发现密钥生成点:最终定位到一个可疑方法,该方法通过
PRFCipher.PRF函数生成一个长的字节数组,然后将其分割成clientKey_,clientIv_,serverKey_,serverIv_,clientHmacKey等部分。 - 分析PRF输入:
PRF函数的输入参数是理解的关键。其中一个参数ms2是主密钥(Master Secret),另一个是用于TLS密钥生成的固定常量字符串。 - 联想TLS握手:此流程与TLS 1.3的握手流程中的“密钥推算”阶段高度相似。这表明APP模拟了一个简化的、自定义的TLS握手过程来动态生成会话密钥。
- 定位握手请求:继续向上追踪
ms2的生成,会发现它来自于一个特定的网络请求(文章中提到是一个名为handleFacilityServerHelloResponse的处理函数所对应的请求)。抓包可以证实,在主要业务请求之前,APP会与服务器进行一次密钥协商请求/响应。 - 结论:密钥材料来自于这次初始的、自定义的TLS握手。服务器和客户端使用相同的算法和输入(如预共享密钥或非对称加密交换的种子)来独立生成相同的会话密钥。因此,无需在网络中传输明文密钥。
- 追踪密钥赋值:在Jadx中追踪
第三章:构建自动化加解密工具链
既然无法通过逆向算法完全独立实现加密(因为PRF和握手过程可能很复杂),文章采用了更巧妙的方案:使用Frida直接Hook内存中的密钥对象。
3.1 Frida RPC + Flask 密钥提取服务
- 目标:编写一个Frida脚本,当APP运行并完成密钥协商后,从内存中读取所需的密钥和IV,并通过一个HTTP服务暴露出来。
- Frida脚本(_agent.js)核心代码:
Java.perform(function () { // Hook相关类,直接读取其静态变量的值 var ClientHello = Java.use("com.rytong.emp.net.ClientHello"); var mClientHmacKey = ClientHello.mClientHmacKey.value; // 读取HMAC密钥 var AESCipher = Java.use("com.rytong.emp.security.AESCipher"); var clientKey = AESCipher.clientKey_.value; // 读取客户端AES密钥 var clientIv = AESCipher.clientIv_.value; // 读取客户端AES IV var serverKey = AESCipher.serverKey_.value; // 读取服务端AES密钥(用于解密响应) var serverIv = AESCipher.serverIv_.value; // 读取服务端AES IV // 将字节数组转换为十六进制字符串,方便传输 var result = { clientKey: bytesToHex(Java.array('byte', clientKey)), clientIv: bytesToHex(Java.array('byte', clientIv)), serverKey: bytesToHex(Java.array('byte', serverKey)), serverIv: bytesToHex(Java.array('byte', serverIv)), clientHmacKey: bytesToHex(Java.array('byte', mClientHmacKey)) }; // 发送回Python端 send(JSON.stringify(result)); }); - Python Flask服务(frida_rpc_server.py):
# ... (省略导入和配置) aes_data = {} # 全局变量存储密钥 def on_message(message, data): # 接收Frida脚本发送的密钥信息 if message["type"] == "send": global aes_data aes_data = json.loads(message["payload"]) @app.route("/get_aes_key", methods=["GET"]) def get_aes_key(): # 提供HTTP接口供AutoDecoder查询密钥 if aes_data["clientKey"]: return jsonify({"status": "ok", "data": aes_data}) else: return jsonify({"status": "error", "msg": "AES key not yet captured"}) # 启动Frida并注入脚本 # ... if __name__ == "__main__": init_frida("_agent.js") # 连接APP并加载脚本 app.run(host="0.0.0.0", port=5001, debug=False) - 运行:启动这个Python脚本,它会附加到APP并开始监听5001端口。当访问
http://127.0.0.1:5001/get_aes_key时,即可获取到实时密钥。
3.2 Burp Suite AutoDecoder 集成
- 目标:编写一个Flask应用,作为Burp的AutoDecoder插件配置的端点。该应用从上述密钥服务获取密钥,实现数据包的自动加解密。
- 核心函数:
request_decode_tls13:解密请求包。- 对Burp传来的密文进行两次Base64解码。
- 按结构切割:前20字节(HMAC签名)、接着9字节(序列号)、剩余部分(AES加密体)。
- (可选)验证HMAC签名以确保数据完整性。
- 使用
clientKey和clientIv对AES加密体进行CBC模式解密。 - PKCS7解填充。
- 去掉前32字节(RNC随机数),得到原始JSON明文。
request_encode_tls13:加密请求包。- 在明文JSON前拼接32字节的RNC(复用上次解密得到的或新生成)。
- PKCS7填充。
- 使用
clientKey和clientIv进行AES-CBC加密。 - 拼接9字节序列号(复用的或新生成的)和加密数据。
- 计算整个拼接结果的HMAC-SHA1签名。
- 最终拼接签名、序列号、加密数据,并进行两次Base64编码。
response_decode:解密响应包。流程类似,但使用serverKey和serverIv,且响应包结构可能略有不同(如无序列号)。calculate_signature与update_signature_header:在加密请求后,自动重新计算X-Emp-Signature头部并更新到请求头中。
- Burp配置:
- 安装
AutoDecoder插件。 - 在插件设置中,配置URL为自建的Flask应用地址(如
http://127.0.0.1:5002)。 /decode端点用于解密。/encode端点用于加密。- 勾选“请求响应不同加解密”,并在请求中通过
requestorresponse参数区分。
- 安装
第四章:最终效果与总结
- 效果:完成以上配置后,Burp Suite的所有功能(Proxy、Repeater、Scanner、Intruder)均可正常使用。在Proxy历史记录、Repeater界面中,看到的不再是加密的乱码,而是明文的JSON数据。可以随意修改明文数据,发送时AutoDecoder会自动将其加密并更新签名。
- 技术总结:
- 绕过检测:通过定制化Frida或使用ZygiskFrida绕过运行时检测。
- 静态分析:利用Jadx快速定位签名、加密等关键代码位置。
- 动态分析:结合Frida打印调用栈,理清复杂加密流程。
- 巧取密钥:当完全逆向算法困难时,直接Hook内存中的密钥是最高效的方案。
- 自动化集成:通过Frida RPC、Flask和Burp插件,构建了一条无缝的自动化测试管道。
通过这份文档,您可以清晰地复现整个渗透测试的技术路径,其方法论对于分析其他具有类似高强度加密的移动应用具有极高的参考价值。