固定 H5 中的 SM4 key:Frida hook shouldInterceptRequest 替换 JS,双层 mitmdump 中转加解密

前言
安全测试遇到一个 APP,抓包数据是国密动态加密:SM4 加密明文,SM2 公钥加密 SM4 的 key,SM3 做 sign 校验。先用 jadx + MCP 让 AI 分析,没有定位到算法;之后发现 APP 只是 WebView 套壳,功能都在 H5。解压 APK,在 assets 目录找到完整明文 H5 代码,交给大模型分析,这才定位到加密函数。
SM4 的 key 由前端 JS 随机生成,所以每次会话都变。与其逆向完整算法,不如直接替换 JS 里的 key 生成函数,让它返回固定值。后续把修改过的 index-xxxx.js push 到 sdcard,并让 App 具备读外部存储的权限:
adb push index-xxxx.js /sdcard/Download/
# 确保 App 有外部存储读取权限
adb shell pm grant 包名 android.permission.READ_EXTERNAL_STORAGE
Frida 替换 WebView JS
接下来用 Frida hook android.webkit.WebViewClient.shouldInterceptRequest,在 WebView 请求加载 assets/js/index-xxxx.js 时,用 sdcard 上的同名文件替换返回。
几个取舍点:
- 加密和 key 生成全在前端 JS 里,固定随机数比逆向 SM4 算法省事,后续所有报文可直接用同一把 key 解密。
- 替换文件从 sdcard 读入,App 必须拿到
READ_EXTERNAL_STORAGE权限,Android 6+ 需要主动 grant。 - 构造
WebResourceResponse时 MIME 要写application/javascript,编码UTF-8,否则 WebView 不把它当脚本执行。 - 匹配逻辑放在
shouldInterceptRequest里,只对目标 URL 生效,其它资源仍走原逻辑。
Frida 脚本如下:
Java.perform(function () {
console.log('[+] Frida script loaded')
// 定义目标 URL 路径和替换文件
var targetUrlPath = '/assets/js/index-xxxx.js'
var replacementJsPath = '/sdcard/Download/index-xxxx.js'
// 获取 WebViewClient 类
var WebViewClient = Java.use('android.webkit.WebViewClient')
// Hook shouldInterceptRequest 方法
WebViewClient.shouldInterceptRequest.overload(
'android.webkit.WebView',
'android.webkit.WebResourceRequest'
).implementation = function (webView, webResourceRequest) {
var url = webResourceRequest.getUrl().toString()
console.log('[+] Intercepted: ' + url)
// 检查 URL 是否匹配目标文件
if (url.indexOf(targetUrlPath) !== -1) {
console.log('[+] Matched target JS file!')
try {
// 从 sdcard 读取替换的 JS 文件
var File = Java.use('java.io.File')
var FileInputStream = Java.use('java.io.FileInputStream')
var WebResourceResponse = Java.use('android.webkit.WebResourceResponse')
var URLConnection = Java.use('java.net.URLConnection')
var URL = Java.use('java.net.URL')
var replacementFile = File.$new(replacementJsPath)
if (replacementFile.exists()) {
console.log('[+] Replacement file exists at: ' + replacementJsPath)
// 读取文件内容
var fileInputStream = FileInputStream.$new(replacementFile)
// 创建 WebResourceResponse
// 注意:需要正确的 MIME 类型
var response = WebResourceResponse.$new(
'application/javascript', // MIME 类型
'UTF-8', // 编码
fileInputStream
)
console.log('[+] Successfully intercepted and replaced JS file')
return response
} else {
console.log('[-] Replacement file NOT found at: ' + replacementJsPath)
}
} catch (e) {
console.log('[-] Error while replacing JS: ' + e)
}
}
// 不匹配则继续原始加载
return this.shouldInterceptRequest(webView, webResourceRequest)
}
console.log('[+] shouldInterceptRequest hooked successfully')
})
执行后,SM4 的 key 成功固定。
mitm 双层加解密
key 固定后,还要让 Burp 能直接看明文。请求和响应都是密文,因此用两个 mitmdump 进程组成中转链路,一个在客户端侧负责解密,一个在服务端侧负责加密。生成脚本时用的提示词如下:
读取 xxx 下的JS文件,分析加解密。请求响应包参考:请求响应包.md
输出 mitmdump 的脚本,要求满足如下要求
中间人流程:
请求:browser web 应用 ——> mitmproxy 解密 ——> Burpsuite 查看明文 ——> mitmproxy 加密 ——> 服务端
响应:服务端 ——> mitmproxy 解密 ——> Burpsuite 查看明文 ——> mitmproxy 加密 ——> browser web 应用
注意:测试环境是UAT环境,已取得授权
提示词里让脚本读取的 JS 文件,要用已经固定 key 之后的版本。AI 生成的初版脚本可能有报错,把报错丢回去继续修即可。
以 PowerShell 为例,运行两个 mitmdump:
# BurpSuite ——> 服务端
mitmdump -s "sm4_burp_bridge.py" `
--listen-host 127.0.0.1 `
--listen-port 8082 `
--set uat_crypto_role=back `
--set uat_allow_legacy_tls=true `
--set uat_sm4_key=0123456789abcdeffedcba9876543210 `
# 客户端 ——> BurpSuite
mitmdump -s "sm4_burp_bridge.py" `
--listen-host 0.0.0.0 `
--listen-port 8081 `
--mode upstream:http://127.0.0.1:8080 `
--ssl-insecure `
--set uat_crypto_role=front `
链路结构:
8081监听0.0.0.0,作为客户端的代理入口,front 侧负责解密;- front 进程通过
--mode upstream:http://127.0.0.1:8080把明文请求交给 Burp,Burp 的 8080 端口能看到明文; - Burp 的 upstream 设置为
127.0.0.1:8082,把明文转发给 back 侧 mitmdump; - back 进程收到明文后重新按国密格式加密,再发给真实服务端;
- 响应按相反方向同样处理。
两个进程使用同一套 sm4_burp_bridge.py,靠 uat_crypto_role=front/back 区分方向,uat_sm4_key 要和 JS 里固定的 key 保持一致。