JS reverse engineering and algorithm analysis: complete closed loop from packet capture to Frida-RPC call

written in front

Friends who have done mobile crawlers know that hybrid apps often hide their core encryption logic in the web layer or native layer, making it almost impossible to restore parameters simply by capturing packets. This article takes you through a complete reverse route: From Web layer packet capture and debugging of the hybrid App, to Algorithm tracking and obfuscation restoration, and finally using Frida-RPC to directly call the encryption algorithm inside the App to generate a signature. The entire process is lightweight and reusable, helping you completely solve the pain point of "cannot follow the encryption parameters".

1. Getting started with packet capture for Hybrid App

The essence of a hybrid application is "WebView + native container". The Web layer is responsible for page rendering, using HTTP/HTTPS, and can usually capture packets; the native layer is responsible for calling hardware capabilities, local storage and some algorithms; the two communicate through Bridge. If you want to debug this type of application, the first step is to get access to WebView.

1.1 Quick overview of typical architecture

There is not much difference between the common Hybrid frameworks on the market. Let’s take a quick look at their characteristics:

FrameworkBusiness implementation methodsCommon communication methodsRecommended debugging tools
Apache Cordova / IonicAlmost all on the Webcordova.exec() / postMessageChrome DevTools
React Native WebViewPartially using the WebpostMessage + addEventListenerDevTools + Frida
UniAppFull Web RenderingNative Plugin CommunicationChrome DevTools + HBuilderX Plug-in

1.2 What is Bridge communication?

Before officially starting debugging, it is necessary to take a look at the data transfer method inside the hybrid application, which can help us quickly locate key functions later.

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)
);

The encryption parameters of many Apps are passed through something likebridge.invokeNative('calcApiSign', ...)The method is returned by the native layer. After understanding the Bridge communication mechanism, we can accurately intercept these calls at the Web layer.


2. Chrome DevTools remote debugging practice

The most direct tool for debugging the web layer is Chrome DevTools, but the target App must turn on the WebView debugging mode in advance** - if the App is not open, we can use Frida to temporarily "force open" it.

2.1 Preparation

  1. Android device/emulator has "USB debugging" enabled
  2. The computer has ADB and the latest version of Chrome browser installed
  3. Get the package name of the target App (you can useadb shell dumpsys window | grep mCurrentFocusquick view)

2.2 Three-step connection and enable debugging

# 第一步:将本地9222端口转发到设备的Chrome调试端口
adb forward tcp:9222 localabstract:chrome_devtools_remote

# 第二步:打开Chrome,地址栏输入
# chrome://inspect/#devices
# 勾选「Discover USB devices」或「Discover network targets」(WiFi调试时)

# 第三步:如果页面上看不到目标App的WebView入口,
# 那就是App没有开启调试开关,用下面的Frida脚本临时开启

Frida script to temporarily enable WebView debuggingenable-webview.js

Java.perform(() => {
    const WebViewCls = Java.use('android.webkit.WebView');
    // 强制所有WebView实例开启调试
    WebViewCls.setWebContentsDebuggingEnabled(true);
    console.log('[+] 所有WebView调试已全局开启!');
    // 可选:打印当前运行的Activity,方便定位
    const ActivityThread = Java.use('android.app.ActivityThread');
    const app = ActivityThread.currentApplication();
    const ctx = app.getApplicationContext();
    const am = ctx.getSystemService('activity');
    // 这里只做示意,实际获取WebView实例可以配合Xposed或更细粒度的Hook
});

How to run:

# 附加到正在运行的App并加载脚本
frida -U -f com.target.app -l enable-webview.js --no-pause

After execution, refreshchrome://inspectpage, you can see the WebView list of the target App.

2.3 Advanced WebView debugging: two essential skills

Tip 1: Global Hook packet capture (bypassing proxy restrictions and WebWorker)

Some apps disable WebView's HTTP/S proxy, or hide requests in WebWorker, making them invisible to the Network panel. At this time, you can use DevTools' Sources → Snippets to inject the global Hook script:

// 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;
    };

    console.log('[🔧 全局抓包Hook已启动!');
})();

After injection, all requests that go through XHR and Fetch will be clearly printed in the Console and are no longer restricted by agents or workers.

Tip 2: One-click search algorithm keywords

Capture packets to get encryption parameters (such as commonsigntokenauthetc.), the next step is to quickly find the code that generates them.

Operating steps:

  1. Open DevTools’ Sources → Search in files (shortcut keyCtrl+Shift+F / Cmd+Option+F
  2. Enter keyword search according to priority:
  • The parameter name itself:signcalcSigngenerateSignature
  • Common encryption libraries:CryptoJSAESMD5SHA
  • Coding related:Base64encodeURIComponent(commonly used for secondary encoding)
  1. Select the file from the search results, right-click Reveal in Navigator to quickly locate the obfuscated code location; right-click Deobfuscate source to automatically format the obfuscated code to improve readability.

Combined with call stack tracing, the core function that generates the signature can usually be locked.


3. Frida-RPC: Directly call the App’s internal algorithm

Sometimes, the JavaScript-level code is obfuscated, or the encryption logic is simply placed in the native layer..soCurry, static restoration is extremely difficult. The most labor-saving way at this time is to directly "borrow" the algorithm function that has been packaged inside the App. Frida-RPC can help you "export" these functions to external scripts (such as Python, Node.js), and you only need to pass parameters to get the results.

3.1 Build Frida-RPC service

Assume we have located the native Java class that generates the API signature:com.target.app.util.SecurityUtils, which has a static methodcalcApiSign(Map<String, String> params). Now export it as an RPC interface.

Frida side scriptfrida-rpc-server.js

// 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);
                }
                
                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;
    }
};

3.2 Python side client encapsulation

To facilitate reuse, we can encapsulate a general Python RPC client class:

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:
            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}")

        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 = 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}")

After running, even if you have no ideacalcApiSignThe internal implementation can also generate correct signatures directly in Python, seamlessly connecting with crawlers or testing tools.


4. Summary

This article provides a set of lightweight and reusable hybrid App reverse workflow:

  1. Packet capture debugging: Use Frida to force WebView debugging to be enabled, and cooperate with the global Hook script to capture all web layer requests while ignoring proxy restrictions and Worker isolation.
  2. Algorithm positioning: Use DevTools’ global search and call stack analysis to quickly find the entry point for generating encryption parameters.
  3. Quick call: If the code is too confusing or the logic is loaded in the native layer, directly "borrow" the algorithm function inside the App through Frida-RPC, and the external script can generate an encrypted signature of any parameter with one click.

If you encounter a packed .so library, you can further combine it with Unidbg to simulate the execution of Native logic on the PC; if it is WebAssembly, you can use tools such as wasm-decompile to decompile and perform static analysis. The entire solution is as flexible as building blocks and is sufficient to meet the reverse needs of most hybrid apps.