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

2026-09-02 21:25:38

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

cover

前言

安全测试遇到一个 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 保持一致。

推荐文章

程序员茄子在线接单