JS逆向与算法分析

实战路线预览

本文带大家从混合App Web层抓包分析,到算法混淆破解/追踪,最后用Frida-RPC直接生成加密签名,完成一套通用的混合App逆向闭环,解决加密参数无法还原的痛点。


1. 混合应用(Hybrid App)抓包

混合应用的核心是WebView+原生容器,Web层走HTTP/S可抓包,原生层调用硬件或做本地逻辑,WebView之间用Bridge桥接。这类应用的抓包调试,关键是拿到WebView的访问权

1.1 核心架构扫盲

常见Hybrid框架与通信方式

框架名业务实现占比通信方式适用调试工具
Apache Cordova/Ionic全Web为主cordova.exec()/postMessageChrome DevTools
React Native WebView部分WebpostMessage + addEventListenerDevTools + Frida
UniApp全Web渲染Native Plugin通信同Cordova + HBuilderX插件

简化版Bridge通信示例

先看一眼混合应用的内部数据传递逻辑,方便后续Hook定位:

class MiniBridge {
    constructor() {
        this.callbackMap = {};
        this.seqId = 0;
    }
    
    // JS→原生(比如获取设备IMEI、本地加密)
    invokeNative(method, args, onResult) {
        const id = ++this.seqId;
        if (onResult) this.callbackMap[id] = onResult;

        const payload = JSON.stringify({ id, method, args });
        // 多平台适配
        if (window.AndroidBridge) AndroidBridge.handle(payload); // Android
        else if (window.webkit?.messageHandlers?.iosBridge) 
            iosBridge.postMessage(payload); // iOS
        else window.location.replace(`minibridge://call?data=${encodeURIComponent(payload)}`); // 兜底
    }
    
    // 原生→JS回调(比如加密后返回)
    invokeJS(id, data) {
        this.callbackMap[id]?.(data);
        delete this.callbackMap[id];
    }
}

// 测试:调用原生签名
const bridge = new MiniBridge();
bridge.invokeNative('calcApiSign', { uid: '123', ts: Date.now() }, (sign) => console.log('API签名:', sign));

1.2 Chrome DevTools远程调试

调试Web层最直接的工具就是Chrome DevTools,但目标App必须开启WebView调试模式——如果没开,我们可以用Frida临时补上!

前置检查

  1. Android设备/模拟器已开启「USB调试」
  2. 电脑已安装ADB、Chrome(建议最新版,兼容新版DevTools Protocol)
  3. 已拿到目标App的包名(用adb shell dumpsys window | grep mCurrentFocus快速查看)

三步开启调试

# 1. 本地9222端口转发到设备的Chrome DevTools端口
adb forward tcp:9222 localabstract:chrome_devtools_remote

# 2. 浏览器打开检查页面
# 输入:chrome://inspect/#devices
# 勾选「Discover USB devices」或「Discover network targets」(如果是WiFi调试)

# 3. 临时开启未设置的WebView调试(如果没看到目标App的WebView入口)
# 下载脚本保存为 enable-webview.js

临时开启调试的Frida脚本:

// enable-webview.js
Java.perform(() => {
    const WebViewCls = Java.use('android.webkit.WebView');
    // 强制所有WebView实例开启调试
    WebViewCls.setWebContentsDebuggingEnabled(true);
    console.log('[+] 所有WebView调试已全局开启!');
    // 可选:打印当前所有WebView的URL,确认目标入口
    const ActivityThread = Java.use('android.app.ActivityThread');
    const allActivities = ActivityThread.currentApplication().getApplicationContext().getSystemService('activity').getRunningTasks(10);
    // 这里只做示意,实际获取WebView实例可配合Xposed或更细致的Frida Hook
});

运行方式:

# 附加到已启动的App
frida -U -f com.target.app -l enable-webview.js --no-pause
# 或启动并附加
frida -U -f com.target.app -l enable-webview.js

1.3 WebView调试进阶:必用的两个技巧

1. 全局Hook抓包(绕开WebView禁用代理/缓存)

有些App会通过系统设置禁用WebView的HTTP/S代理,或者把请求藏在WebWorker里Network面板看不到——这时候直接在DevTools的「Sources → Snippets」注入Hook脚本就行:

// webview-hook.js (粘贴到Snippets后右键Run)
(function GlobalCapture() {
    // Hook XMLHttpRequest
    const OriginalXHR = window.XMLHttpRequest;
    window.XMLHttpRequest = function(...args) {
        const xhr = new OriginalXHR(...args);
        const _open = xhr.open;
        const _send = xhr.send;
        
        xhr.open = (method, url) => {
            xhr._method = method;
            xhr._url = url;
            console.log('[🚀 XHR 发送准备]', method, url);
            return _open.apply(xhr, arguments);
        };
        
        xhr.send = (body) => {
            if (body) console.log('[📦 XHR 请求体]', body);
            // 监听响应
            xhr.addEventListener('readystatechange', () => {
                if (xhr.readyState === 4) {
                    console.log('[✅ XHR 响应状态]', xhr.status);
                    console.log('[📄 XHR 响应体]', xhr.responseText);
                }
            });
            return _send.apply(xhr, arguments);
        };
        return xhr;
    };

    // Hook Fetch API
    const OriginalFetch = window.fetch;
    window.fetch = async (input, init = {}) => {
        const url = typeof input === 'string' ? input : input.url;
        console.log('[🚀 Fetch 发送准备]', init.method || 'GET', url);
        if (init.body) console.log('[📦 Fetch 请求体]', init.body);
        
        const resp = await OriginalFetch(input, init);
        const clonedResp = resp.clone(); // 必须clone,否则原流被读取后会报错
        console.log('[✅ Fetch 响应状态]', clonedResp.status);
        clonedResp.text().then(text => console.log('[📄 Fetch 响应体]', text));
        return resp;
    };

    // 可选:Hook Bridge通信(如果加密参数是原生返回的)
    console.log('[🔧 全局抓包Hook已启动!');
})();

2. 一键搜索算法关键词

抓包后会发现加密参数(比如signtokenauth),接下来要快速找到生成这些参数的代码:

  • 步骤1:打开DevTools「Sources → Search in Files」(快捷键Ctrl+Shift+F/Cmd+Option+F
  • 步骤2:输入核心关键词(按优先级排序):
    1. 加密参数名:signcalcSigngenerateSignature
    2. 加密库调用:CryptoJSAESMD5SHA
    3. 字符串编码:Base64encodeURIComponent(二次加密常用)
  • 步骤3:点击结果中的文件,右键「Reveal in Navigator」定位到混淆代码,右键「Deobfuscate source」自动格式化

2. Frida-RPC调用技术

如果遇到混淆太彻底无法还原的JS代码,或者加密逻辑放在原生层.so库,最好的办法就是直接调用App内部已经有的算法函数——Frida-RPC能帮你把这些函数“导出”到Python/Node.js的外部脚本里。

2.1 快速搭建RPC服务

1. Frida侧JS脚本(导出函数)

假设我们已经找到了生成API签名的原生Java类com.target.app.util.SecurityUtils,有个静态方法calcApiSign(Map<String, String> params),现在把它导出:

// frida-rpc-server.js
rpc.exports = {
    // 导出签名函数:注意RPC只能传JSON,所以要把外部传的JSON转成Java的HashMap
    getApiSign: function(paramsJsonStr) {
        let result = null;
        Java.perform(() => {
            try {
                const SecurityUtils = Java.use('com.target.app.util.SecurityUtils');
                const HashMap = Java.use('java.util.HashMap');
                
                // 转外部参数
                const paramsMap = HashMap.$new();
                const paramsObj = JSON.parse(paramsJsonStr);
                for (const [k, v] of Object.entries(paramsObj)) {
                    paramsMap.put(k, v);
                }
                
                // 调用App内部方法
                result = SecurityUtils.calcApiSign(paramsMap);
                console.log('[+] 签名生成成功:', result);
            } catch (err) {
                console.error('[!] 签名生成失败:', err.stack);
            }
        });
        return result;
    },

    // 可选:导出设备指纹(很多签名需要这个参数)
    getDeviceFingerprint: function() {
        let fingerprint = null;
        Java.perform(() => {
            try {
                const FingerprintUtil = Java.use('com.target.app.util.FingerprintUtil');
                fingerprint = FingerprintUtil.getDeviceId().toString();
                console.log('[+] 设备指纹获取成功:', fingerprint);
            } catch (err) {
                console.error('[!] 设备指纹获取失败:', err.stack);
            }
        });
        return fingerprint;
    }
};

2. Python侧客户端(调用RPC)

写个简单的Python封装类,以后可以复用在所有混合App逆向项目里:

import frida
import json
import time
from typing import Any, Dict, Optional

class FridaHybridRPC:
    def __init__(self, package_name: str, script_path: str, is_spawn: bool = False):
        """
        :param package_name: 目标App包名
        :param script_path: Frida RPC脚本路径
        :param is_spawn: 是否需要重新启动App(附加失败时用)
        """
        self.device = frida.get_usb_device()
        self.package_name = package_name
        self.script_path = script_path
        self.is_spawn = is_spawn
        self.session: Optional[frida.core.Session] = None
        self.script: Optional[frida.core.Script] = None

        self._connect()

    def _connect(self):
        """附加/启动App并加载RPC脚本"""
        try:
            # 尝试附加到已运行的App
            if not self.is_spawn:
                self.session = self.device.attach(self.package_name)
                print(f"[+] 已附加到运行中的 {self.package_name}")
            else:
                raise Exception("强制启动模式")
        except Exception as e:
            # 启动并附加
            print(f"[*] 附加失败,正在启动 {self.package_name}...")
            pid = self.device.spawn(self.package_name)
            self.session = self.device.attach(pid)
            self.device.resume(pid)
            time.sleep(3)  # 等待App初始化完成
            print(f"[+] 已启动并附加到 {self.package_name}")

        # 加载RPC脚本
        with open(self.script_path, "r", encoding="utf-8") as f:
            script_code = f.read()
        self.script = self.session.create_script(script_code)
        self.script.load()
        print("[+] RPC服务已加载完成")

    def call(self, func_name: str, *args) -> Any:
        """调用RPC导出的函数"""
        try:
            return getattr(self.script.exports, func_name)(*args)
        except Exception as e:
            print(f"[!] RPC调用失败[{func_name}]:{str(e)}")
            return None

# ------------------- 测试 -------------------
if __name__ == "__main__":
    # 初始化RPC客户端
    rpc = FridaHybridRPC(
        package_name="com.target.app",
        script_path="./frida-rpc-server.js",
        is_spawn=False
    )

    # 1. 获取设备指纹
    device_id = rpc.call("getDeviceFingerprint")
    print(f"[📱] 设备ID:{device_id}")

    # 2. 构造测试参数
    test_params = {
        "uid": "test_user_001",
        "ts": str(int(time.time() * 1000)),
        "deviceId": device_id
    }

    # 3. 调用API签名
    api_sign = rpc.call("getApiSign", json.dumps(test_params))
    print(f"[🔑] API签名:{api_sign}")

总结

本文整理了一套轻量级、高复用的混合App逆向方案:

  1. 抓包调试:用Frida临时开启WebView调试,配合全局Hook脚本抓全所有请求
  2. 算法定位:用DevTools关键词搜索,结合调用栈追踪找到入口
  3. 快速调用:如果代码太复杂,直接用Frida-RPC导出内部函数,外部脚本一键生成加密参数

如果遇到加壳的.so库,可以进一步结合Unidbg模拟Native执行;如果是WebAssembly,可以用wasm-decompile反编译后分析。