当前位置:首页>学习笔记>Frida学习笔记(十五):算法自吐 · Native 层 OpenSSL/BoringSSL Hook

Frida学习笔记(十五):算法自吐 · Native 层 OpenSSL/BoringSSL Hook

  • 2026-05-28 15:15:15
Frida学习笔记(十五):算法自吐 · Native 层 OpenSSL/BoringSSL Hook

本篇目标:解决「Java 层算法自吐没有输出,但 App 确实在做加密」的问题。第14篇的 crypto_monitor.js 覆盖了 Java 标准加密 API,但有些 App 的加密操作完全在 Native 层——直接调用 OpenSSL/BoringSSL 的 C 函数,或者自己用 C 实现加密算法。本篇教你在 Native 层建立同等的「算法自吐」能力:覆盖 EVP 高级 API 和 AES_/RSA_/MD5_* 低级 API 两套路径,识别自研算法的特征,最终交付一个 native_crypto_monitor.js 拿到密钥和明文。

配套脚本:本篇产出的 native_crypto_monitor.js(单文件 1100+ 行、7 个模块开关、EVP/HMAC/AES/RSA/MD5/Base64 全覆盖、多副本 SO 自动装钩、TLS 噪声过滤、Spawn 模式 dlopen 兜底、ANSI 256 色对齐第 14 篇视觉)以及配套的 hook_registernatives.js / find_crypto_constants.js / check_app_crypto_imports.js 等已整理打包。关注本公众号后私信回复关键词「脚本」 即可获取,本系列与后续 Unidbg / SO 逆向 / ARM 汇编 等系列脚本会统一在此发放并持续更新。

一、什么时候需要 Native 层加密分析

1.1 判断加密是否在 Native 层

加载第14篇的 crypto_monitor.js,在 App 中执行目标操作(如登录),观察输出:

  • 有 Cipher/HMAC 输出 → 加密在 Java 层,第14篇已解决
  • 没有任何加密输出,但抓包确认有加密数据 → 加密在 Native 层,本篇的内容

进一步确认:用第06篇的模块枚举脚本看 App 加载了哪些 SO,重点关注:

SO 文件说明
libcrypto.so / libssl.soOpenSSL/BoringSSL(系统或 App 自带)
libnative-lib.so / libapp.soApp 自己的 Native 库(可能含自研加密)
libflutter.soFlutter 框架(使用 BoringSSL)
libsqlcipher.so加密数据库
// 快速检查:App 是否导入了 OpenSSL 加密函数
(function() {
    var found = [];
    ["libcrypto.so""libssl.so"].forEach(function(lib) {
        var mod = Process.findModuleByName(lib);
        if (!mod) return;
        mod.enumerateExports().forEach(function(exp) {
            if (exp.name.match(/^EVP_|^AES_|^HMAC|^SHA[0-9]|^MD5/)) {
                found.push(lib + "!" + exp.name);
            }
        });
    });
    if (found.length > 0) {
        console.log("[*] 发现 " + found.length + " 个加密相关导出:");
        found.slice(020).forEach(function(f) { console.log(" " + f); });
    } else {
        console.log("[*] 未发现标准加密库导出,App 可能使用自研加密");
    }
})();

关于本文的真机样例:为了让方法落地可见、避免"全是抽象代码",本文贯穿一个具体真机样例——酷狗音乐 v20.6.4(Pixel 6 Pro · Android 13 · frida-server 17.9.10)。这只是一个 BoringSSL App 的具体例子,本文讲的所有方法对任何走 BoringSSL/OpenSSL 的 App 同样适用,酷狗不是本文要分析/突破的对象,只是说明性载体。读者拿自己的目标 App 替换样例里的 SO 名即可。

在这个样例上跑第 1.1 节的扫描脚本,输出长这样——一眼能看出 libcrypto_kg.so 是 App 自带的定制 BoringSSL(1668 KB,596 个加密导出,包含完整 AES_* / RSA_* / BN_mod_exp_mont 低级 API 套):

libcrypto_kg 导出扫描

1.2 Native 加密的三种形式

形式一:调用系统 OpenSSL/BoringSSL。 Android 系统自带 BoringSSL(Google 的 OpenSSL 分支),提供了 libcrypto.solibssl.so。App 的 Native 代码可以直接 #include <openssl/evp.h> 调用系统库。Flutter 和 Chromium 内核的 WebView 也使用 BoringSSL。

形式二:App 自带 OpenSSL。 有些 App 把自编译的 OpenSSL/BoringSSL 打包在 APK 中(如 lib/arm64-v8a/libcrypto.so),而不是使用系统的。函数名可能相同,但版本和编译选项不同。

形式三:自研加密算法。 少数高安全需求的 App 自己用 C 实现 AES/SM4 等算法,不调用任何标准库。这种情况下导入表中没有 EVP/AES 相关函数,需要通过 IDA 分析识别算法。

二、OpenSSL/BoringSSL 的两套 API · 选择策略

OpenSSL/BoringSSL 对外暴露两套 API,定位不同:

API 风格例子何时遇到
EVP 高级 APIEVP_EncryptInit_ex / EVP_DigestUpdate / HMAC_Init_ex新代码、Conscrypt、Flutter、Chromium 默认走这条
低级 APIAES_set_encrypt_key / AES_cbc_encrypt / RSA_public_encrypt / MD5_Update老代码、嵌入式、自带定制 BoringSSL 的大型 App、为追求性能直接调底层算法

Hook 策略:实战中两套都要 Hook——你不知道目标 App 走哪条。三步:第三章定位到 SO 里的 crypto 函数 → 第四章加载综合脚本 native_crypto_monitor.js 一次抓全 → 第五章学会读输出(key 形态识别、公钥提取、ctx 累积模式)。

2.1 EVP 是什么

EVP(Envelope)是 OpenSSL 的高级加密 API。它提供了一套统一的接口来使用各种加密算法——不管底层是 AES、DES、ChaCha20 还是 SM4,上层调用的 API 格式都相同。这和 Java 的 Cipher 类设计理念一致。

EVP 的对称加密工作流程:

// 1. 创建上下文
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();

// 2. 初始化(指定算法、密钥、IV)
EVP_EncryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, key, iv);

// 3. 加密数据(可多次调用)
EVP_EncryptUpdate(ctx, out, &outl, in, inl);

// 4. 完成(处理填充)
EVP_EncryptFinal_ex(ctx, out + outl, &final_len);

// 5. 释放上下文
EVP_CIPHER_CTX_free(ctx);

解密使用对应的 EVP_DecryptInit_ex / EVP_DecryptUpdate / EVP_DecryptFinal_ex

2.2 JCE ↔ OpenSSL/BoringSSL 函数对照表

Hook 时按需查表即可——左列是 Java 层调用、右列是对应的 Native 函数:

Java JCEOpenSSL/BoringSSL 函数抓什么
Cipher.init (AES)AES_set_encrypt_key(userKey, bits, AES_KEY*)原始 key、bits
Cipher.doFinal (AES-CBC)AES_cbc_encrypt(in, out, len, key, iv, enc)明文、密文、IV、方向
Cipher.doFinal (AES 单块)AES_encrypt(in, out, key)明文 16B、密文 16B
Cipher.init/doFinal (DES)DES_set_key_unchecked + DES_cbc_encryptkey(8B)、IV(8B)、明密文
Cipher.init/doFinal (3DES)DES_ede3_cbc_encrypt三组 key、IV、明密文
Cipher.doFinal (EVP 高层)EVP_EncryptInit_ex + EVP_EncryptUpdate + EVP_EncryptFinal_exkey、iv、明密文
Cipher.doFinal (RSA 加密)RSA_public_encrypt(flen, from, to, rsa, padding)明文、密文、padding
Signature.sign (RSA)RSA_sign(type, m, mlen, sig, siglen, rsa)摘要、签名
MessageDigest.update + digestMD5_Update / MD5_FinalSHA256_Update / SHA256_Final 或 EVP_Digest*输入(可多次)、最终输出
Mac.doFinal (HMAC)HMAC_Init_ex / Update / Final 或一次性 HMACkey、消息、tag

补充

  • 新写的 Native 代码更倾向走 EVP 系列,而不是直接调 AES_* / DES_* / RSA_* 低级 API。EVP_EncryptInit_ex(ctx, type, impl, key, iv) 一次拿到 key 和 iv,EVP_EncryptUpdate 拿到明密文,一组 Hook 覆盖所有对称算法。优先尝试 EVP;只有当 SO 里没有 EVP 导出符号时,再退回低级 API。
  • RSA_public_encrypt 底下最终会调 BN_mod_exp_mont 完成 c = m^e mod n,第 3、4 参数(指数 e、模数 n)直接暴露 RSA 公钥本体——IDA 里看不到明文公钥常量(key 是从 PEM 解析出来或堆上分配)时,这是最稳的公钥提取路径,第 4 章脚本段 6 内置 + 第 5.2 节讲怎么读出来的公钥。

三、三步定位法:从 Java 到 SO 函数

定位 Native 加密入口的标准流程:

第一步,在 jadx 里找到声明为 native 的方法,记下完整签名(类名 + 方法名 + JNI 签名)。

第二步,Hook libart.so 的 RegisterNatives 拿到函数地址。Java native 方法通过 RegisterNatives 绑定到 SO 里的具体地址。

// hook_registernatives.js · 跨 Android 版本统一脚本
// 关键点:
// 1) 枚举 libart.so 符号表抓 *所有* RegisterNatives 变体 (A13 模板特化的 Lb0/Lb1 不会漏)
// 2) Java.vm.tryGetEnv().getClassName(jclass) 把 jclass 解成 "android.net.NetworkUtils" 类名
// 3) DebugSymbol.fromAddress(this.returnAddress) 拿到 callee, 通常是某 SO 的 JNI_OnLoad

function find_RegisterNatives(params) {
    let addrRegisterNatives = null;
    if (Frida.version != undefined) {
        const major = parseInt(Frida.version.split('.')[0], 10);
        if (major >= 17) {
            Process.findModuleByName("libart.so").enumerateSymbols().map(symbol => {
                if (symbol.name.indexOf("art">= 0 &&
                    symbol.name.indexOf("JNI">= 0 &&
                    symbol.name.indexOf("RegisterNatives">= 0 &&
                    symbol.name.indexOf("CheckJNI"< 0) {
                    addrRegisterNatives = symbol.address;
                    console.log("RegisterNatives is at ", symbol.address, symbol.name);
                    hook_RegisterNatives(addrRegisterNatives)
                }
            })
            return;
        }
    }
    // frida 16 兼容路径
    let symbols = Module.enumerateSymbolsSync("libart.so");
    for (let i = 0; i < symbols.length; i++) {
        let symbol = symbols[i];
        if (symbol.name.indexOf("art">= 0 &&
            symbol.name.indexOf("JNI">= 0 &&
            symbol.name.indexOf("RegisterNatives">= 0 &&
            symbol.name.indexOf("CheckJNI"< 0) {
            addrRegisterNatives = symbol.address;
            console.log("RegisterNatives is at ", symbol.address, symbol.name);
            hook_RegisterNatives(addrRegisterNatives)
        }
    }
}

function hook_RegisterNatives(addrRegisterNatives) {
    if (addrRegisterNatives != null) {
        Interceptor.attach(addrRegisterNatives, {
            onEnter: function (args) {
                console.log("[RegisterNatives] method_count:", args[3]);
                let java_class = args[1];
                let class_name = Java.vm.tryGetEnv().getClassName(java_class);

                let methods_ptr = ptr(args[2]);
                let method_count = parseInt(args[3]);
                for (let i = 0; i < method_count; i++) {
                    let name_ptr  = ptr(methods_ptr.add(i * Process.pointerSize * 3)).readPointer();
                    let sig_ptr   = ptr(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize)).readPointer();
                    let fnPtr_ptr = ptr(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize * 2)).readPointer();

                    let name = ptr(name_ptr).readCString();
                    let sig  = ptr(sig_ptr).readCString();
                    let symbol = DebugSymbol.fromAddress(fnPtr_ptr)
                    console.log("[RegisterNatives] java_class:", class_name,
                                "name:", name, "sig:", sig,
                                "fnPtr:", fnPtr_ptr,
                                " fnOffset:", symbol,
                                " callee:", DebugSymbol.fromAddress(this.returnAddress));
                }
            }
        });
    }
}

setImmediate(find_RegisterNatives);

在样例 App(Android 13)上 spawn 跑出来是这样的。

hook_registernatives 真机命中

接下来每条 [RegisterNatives] 命中包含 5 个维度:

字段例子用途
java_classandroid.NetworkUtilsJava 类全限定名
namebindProcessToNetworkHandleJava 方法名
sig(J)Z(I)ZJNI 签名(参数类型 + 返回类型)
fnPtr + fnOffset0x6e4a3e860libframework-connectivity-jni.so!_ZN7android..._utils_bindProcessToNetworkHandleEP7_JNIEnv+0x60函数实现地址 + SO 偏移
calleelibframework-connectivity-jni.so!_ZN7android33register_android_net_NetworkUtilsEP7_JNIEnv+0x60谁在 JNI_OnLoad 里调 RegisterNatives 注册的

第三步,用 IDA/Ghidra 打开对应 SO,跳到那个偏移,看调用了哪个 crypto API。看到 AES_cbc_encryptRSA_public_encryptMD5_Update 这类名字,直接 Hook 这些标准函数即可,业务函数本身可以不深入逆向。看不到名字(stripped)时需要用常量特征定位(S-box、0x10001、MD5 IV 67 45 23 01),那部分见 第七章 自研算法识别。

四、完整的 native_crypto_monitor.js

上一章把"从 Java 到 SO 函数"的入口定位讲清楚了;本章给一个综合脚本 native_crypto_monitor.js,把 EVP 高级 API、HMAC、AES + RSA + MD5 低级 API、Base64 编解码一次性覆盖,加载即用。这个脚本是本篇的核心交付物。

脚本结构 · 7 段拼装

脚本按 7 段 给出——这是结构骨架,按顺序拼起来就是一份能跑的 native_crypto_monitor.js。配置开关在段 1,可按需关闭模块以减少日志噪音。本章最后一节列出生产环境下还需要的几项工程化能力(多副本装钩、TLS 噪声过滤、ANSI 着色 ……);完整版(单文件 1100+ 行,文末关注公众号回复关键词获取)已合入这些能力,复制即用。

段 1 · 入口 + 配置 + 辅助函数

CONFIG 集中六个模块开关 + dump 字节上限 + 调用栈控制。辅助函数中 getEvpCipherName 是个小亮点——通过 EVP_CIPHER_CTX_cipher + EVP_CIPHER_nid + OBJ_nid2sn 三步反查算法名(aes-128-cbc / sm4-cbc 等),后续 EVP Hook 输出的算法标签全靠它。

本段重点:CONFIG 开关 / getCryptoExport 跨 SO 多名查找 / safeDump 限长 + 编码兼容 hex / getNativeStack 可选 backtrace / getEvpCipherName 三步反查。后面 6 段只调用这些 helper,不再展开。

// native_crypto_monitor.js
// Native 层加密操作全自动监控
// 用法: frida -U -f <包名> -l native_crypto_monitor.js --no-pause

(function() {
    "use strict";

    // ==================== 配置 ====================
    var CONFIG = {
        hookEvpCipher: true,      // EVP_Encrypt*/EVP_Decrypt*
        hookEvpDigest: true,      // EVP_Digest* (SHA/MD5)
        hookHmac: true,           // HMAC / HMAC_Init/Update/Final
        hookAesLowLevel: true,    // AES_set_encrypt_key / AES_cbc_encrypt
        hookRsaLowLevel: true,    // RSA_public_encrypt + BN_mod_exp_mont (公钥提取)
        hookMd5LowLevel: true,    // MD5_Update / MD5_Final (按 ctx 累积输入)
        maxDumpBytes: 128,        // hexdump 最大字节数
        showBacktrace: true,      // 是否显示 Native 调用栈
        backtraceDepth: 5         // 调用栈深度
    };

    // ==================== 辅助函数 ====================

    // 在 BoringSSL/OpenSSL SO 里找导出符号 (frida 17 兼容写法)
    // Flutter / Cronet 等内嵌 BoringSSL 的场景见第 6.3 节,扩展这里的 SO 列表即可
    var CRYPTO_SOS = ["libcrypto.so"];
    function getCryptoExport(name) {
        for (var i = 0; i < CRYPTO_SOS.length; i++) {
            var mod = Process.findModuleByName(CRYPTO_SOS[i]);
            if (!mod) continue;
            var addr = mod.findExportByName(name);
            if (addr) return addr;
        }
        return null;
    }

    function getNativeStack(ctx) {
        if (!CONFIG.showBacktrace) return "";
        try {
            return Thread.backtrace(ctx, Backtracer.ACCURATE)
                .slice(0, CONFIG.backtraceDepth)
                .map(function(addr) {
                    return " │ " + DebugSymbol.fromAddress(addr).toString();
                })
                .join("\n");
        } catch(e) { return ""; }
    }

    function safeDump(ptr, size) {
        if (ptr.isNull() || size <= 0return "(null)";
        var dumpSize = Math.min(size, CONFIG.maxDumpBytes);
        try {
            var hex = [];
            for (var i = 0; i < dumpSize; i++) {
                var b = ptr.add(i).readU8().toString(16);
                hex.push(b.length === 1 ? "0" + b : b);
            }
            var result = hex.join("");
            if (size > CONFIG.maxDumpBytes) result += "...(" + size + " bytes)";
            return result;
        } catch(e) {
            return "(读取失败)";
        }
    }

    function safeReadString(ptr, maxLen) {
        if (ptr.isNull()) return "(null)";
        try {
            return ptr.readUtf8String(maxLen || 64);
        } catch(e) {
            return "(非字符串)";
        }
    }

    function getEvpCipherName(ctx) {
        // 从 EVP_CIPHER_CTX 中提取算法名称
        try {
            var EVP_CIPHER_CTX_cipher = new NativeFunction(
                getCryptoExport("EVP_CIPHER_CTX_cipher"||
                getCryptoExport("EVP_CIPHER_CTX_get0_cipher"),
                "pointer", ["pointer"]);
            var cipherPtr = EVP_CIPHER_CTX_cipher(ctx);
            if (cipherPtr.isNull()) return "unknown";

            var EVP_CIPHER_nid = new NativeFunction(
                getCryptoExport("EVP_CIPHER_nid"||
                getCryptoExport("EVP_CIPHER_get_nid"),
                "int", ["pointer"]);
            var nid = EVP_CIPHER_nid(cipherPtr);

            var OBJ_nid2sn = new NativeFunction(
                getCryptoExport("OBJ_nid2sn"),
                "pointer", ["int"]);
            var namePtr = OBJ_nid2sn(nid);
            return namePtr.isNull() ? "nid:" + nid : namePtr.readUtf8String();
        } catch(e) {
            return "unknown";
        }
    }

段 2 · EVP 对称加密 · EVP_Encrypt* / EVP_Decrypt*

覆盖 Init / Update / Final 三步。Init 拿算法名 + 密钥 + IV;Update 拿明文 + 密文,顺便尝试把输入解为可读字符串(JSON、URL 等场景下 hex 看起来无意义但文本很直观);Final 拿最后一块填充。三步都用 EVP_CIPHER_CTX_cipher 反查算法名,日志里直接显示 aes-256-cbc 而不是 NID 数字。

本段重点:三个 forEach 循环分别 hook Init / Update / Final,每个 onLeave 都用 retval !== 1 过滤失败调用;密钥长度根据算法名后缀 128/192/256 推断;IV 固定 16 字节。

    // ==================== EVP 对称加密 ====================

    if (CONFIG.hookEvpCipher) {
        // --- EVP_EncryptInit_ex / EVP_DecryptInit_ex ---
        ["EVP_EncryptInit_ex""EVP_DecryptInit_ex",
         "EVP_EncryptInit""EVP_DecryptInit"].forEach(function(funcName) {
            var addr = getCryptoExport(funcName);
            if (!addr) return;

            var isEncrypt = funcName.indexOf("Encrypt"!== -1;
            var mode = isEncrypt ? "ENCRYPT" : "DECRYPT";

            Interceptor.attach(addr, {
                onEnter: function(args) {
                    // int EVP_EncryptInit_ex(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *type,
                    // ENGINE *impl, const unsigned char *key,
                    // const unsigned char *iv);
                    this.ctx = args[0];
                    this.key = args[3];
                    this.iv = args[4];
                },
                onLeave: function(retval) {
                    if (retval.toInt32() !== 1return;  // 失败

                    var algoName = getEvpCipherName(this.ctx);

                    console.log("\n┌─[EVP] " + algoName + " | " + mode + " | Init");
                    if (!this.key.isNull()) {
                        // 密钥长度根据算法推断
                        var keyLen = 16;  // 默认
                        if (algoName.indexOf("256"!== -1) keyLen = 32;
                        else if (algoName.indexOf("192"!== -1) keyLen = 24;
                        else if (algoName.indexOf("128"!== -1) keyLen = 16;
                        console.log("│ 密钥: " + safeDump(this.key, keyLen));
                    }
                    if (!this.iv.isNull()) {
                        console.log("│ IV: " + safeDump(this.iv, 16));
                    }
                    var stack = getNativeStack(this.context);
                    if (stack) console.log("│ 调用栈:\n" + stack);
                    console.log("└─");
                }
            });
            console.log("[OK] " + funcName);
        });

        // --- EVP_EncryptUpdate / EVP_DecryptUpdate ---
        ["EVP_EncryptUpdate""EVP_DecryptUpdate"].forEach(function(funcName) {
            var addr = getCryptoExport(funcName);
            if (!addr) return;

            var isEncrypt = funcName.indexOf("Encrypt"!== -1;

            Interceptor.attach(addr, {
                onEnter: function(args) {
                    // int EVP_EncryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out,
                    // int *outl, const unsigned char *in, int inl);
                    this.ctx = args[0];
                    this.outBuf = args[1];
                    this.outlPtr = args[2];
                    this.inBuf = args[3];
                    this.inLen = args[4].toInt32();
                },
                onLeave: function(retval) {
                    if (retval.toInt32() !== 1return;

                    var outLen = this.outlPtr.readS32();
                    var algoName = getEvpCipherName(this.ctx);
                    var label = isEncrypt ? "Encrypt" : "Decrypt";

                    console.log("\n┌─[EVP] " + algoName + " | " + label + "Update");
                    console.log("│ 输入 (" + this.inLen + " bytes): " + safeDump(this.inBuf, this.inLen));
                    console.log("│ 输出 (" + outLen + " bytes): " + safeDump(this.outBuf, outLen));

                    // 尝试将输入/输出读为字符串
                    if (this.inLen > 0 && this.inLen < 256) {
                        var text = safeReadString(this.inBuf, this.inLen);
                        if (text && text.length > 2 && text.indexOf("("=== -1) {
                            console.log("│ 输入(text): \"" + text + "\"");
                        }
                    }
                    console.log("└─");
                }
            });
            console.log("[OK] " + funcName);
        });

        // --- EVP_EncryptFinal_ex / EVP_DecryptFinal_ex ---
        ["EVP_EncryptFinal_ex""EVP_DecryptFinal_ex",
         "EVP_EncryptFinal""EVP_DecryptFinal"].forEach(function(funcName) {
            var addr = getCryptoExport(funcName);
            if (!addr) return;

            Interceptor.attach(addr, {
                onEnter: function(args) {
                    this.outBuf = args[1];
                    this.outlPtr = args[2];
                },
                onLeave: function(retval) {
                    if (retval.toInt32() !== 1return;
                    var outLen = this.outlPtr.readS32();
                    if (outLen > 0) {
                        console.log("[EVP] Final: " + safeDump(this.outBuf, outLen) + " (" + outLen + " bytes)");
                    }
                }
            });
        });
    }

段 3 · EVP 哈希 · EVP_Digest*

只 Hook EVP_DigestUpdate + EVP_DigestFinal_ex——不 Hook Init 是因为算法名要从 ctx 反查,需要的额外代码与 Init 本身价值不匹配;真正需要时根据输出长度(MD5=16 / SHA-1=20 / SHA-256=32 / SHA-512=64)反推即可。

    // ==================== EVP 哈希(Digest)====================

    if (CONFIG.hookEvpDigest) {
        // --- EVP_DigestUpdate ---
        var digestUpdate = getCryptoExport("EVP_DigestUpdate");
        if (digestUpdate) {
            Interceptor.attach(digestUpdate, {
                onEnter: function(args) {
                    // int EVP_DigestUpdate(EVP_MD_CTX *ctx, const void *d, size_t cnt);
                    this.data = args[1];
                    this.len = args[2].toInt32();
                },
                onLeave: function(retval) {
                    if (this.len > 0 && this.len < 4096) {
                        console.log("\n┌─[EVP Hash] DigestUpdate (" + this.len + " bytes)");
                        console.log("│ 数据: " + safeDump(this.data, this.len));
                        var text = safeReadString(this.data, Math.min(this.len, 128));
                        if (text && text.length > 2) {
                            console.log("│ 文本: \"" + text + "\"");
                        }
                        console.log("└─");
                    }
                }
            });
            console.log("[OK] EVP_DigestUpdate");
        }

        // --- EVP_DigestFinal_ex ---
        var digestFinal = getCryptoExport("EVP_DigestFinal_ex");
        if (digestFinal) {
            Interceptor.attach(digestFinal, {
                onEnter: function(args) {
                    // int EVP_DigestFinal_ex(EVP_MD_CTX *ctx, unsigned char *md, unsigned int *s);
                    this.mdBuf = args[1];
                    this.sizePtr = args[2];
                },
                onLeave: function(retval) {
                    if (retval.toInt32() !== 1return;
                    var size = this.sizePtr.readU32();
                    console.log("[EVP Hash] DigestFinal: " + safeDump(this.mdBuf, size) + " (" + size + " bytes)");
                }
            });
            console.log("[OK] EVP_DigestFinal_ex");
        }
    }

段 4 · HMAC · 一次性 + 三步

OpenSSL HMAC 有两种用法:一次性 HMAC() 把 key+消息+输出一次塞过去,或 HMAC_Init_ex / HMAC_Update / HMAC_Final 三步链。两种都 Hook。API 签名场景里的密钥常常是可读字符串(如 "app_secret_key_2024"),所以 key 输出同时给 hex 和 utf8 两种形态。

本段重点:四个独立 hook —— 一次性 HMAC()(key + 数据 + 摘要一次性拿到)、HMAC_Init_ex(key)、HMAC_Update(每段消息)、HMAC_Final(摘要)。Update 每段独立打印,看 ctx 聚合输出的形态见第 5.4 节截图。

    // ==================== HMAC ====================

    if (CONFIG.hookHmac) {
        // --- HMAC(一次性调用)---
        var hmacAddr = getCryptoExport("HMAC");
        if (hmacAddr) {
            Interceptor.attach(hmacAddr, {
                onEnter: function(args) {
                    // unsigned char *HMAC(const EVP_MD *evp_md,
                    // const void *key, int key_len,
                    // const unsigned char *d, size_t n,
                    // unsigned char *md, unsigned int *md_len);
                    this.keyPtr = args[1];
                    this.keyLen = args[2].toInt32();
                    this.dataPtr = args[3];
                    this.dataLen = args[4].toInt32();
                    this.mdPtr = args[5];
                    this.mdLenPtr = args[6];
                },
                onLeave: function(retval) {
                    if (retval.isNull()) return;

                    var mdLen = 32;  // 默认 SHA-256
                    try { mdLen = this.mdLenPtr.readU32(); } catch(e) {}

                    console.log("\n┌─[HMAC] Native");
                    console.log("│ 密钥 (" + this.keyLen + " bytes): " + safeDump(this.keyPtr, this.keyLen));
                    console.log("│ 数据 (" + this.dataLen + " bytes): " + safeDump(this.dataPtr, this.dataLen));

                    var keyText = safeReadString(this.keyPtr, this.keyLen);
                    if (keyText && keyText.length > 1) console.log("│ 密钥(text): \"" + keyText + "\"");
                    var dataText = safeReadString(this.dataPtr, Math.min(this.dataLen, 256));
                    if (dataText && dataText.length > 1) console.log("│ 数据(text): \"" + dataText + "\"");

                    console.log("│ 输出 (" + mdLen + " bytes): " + safeDump(retval, mdLen));
                    var stack = getNativeStack(this.context);
                    if (stack) console.log("│ 调用栈:\n" + stack);
                    console.log("└─");
                }
            });
            console.log("[OK] HMAC");
        }

        // --- HMAC_Init_ex ---
        var hmacInit = getCryptoExport("HMAC_Init_ex");
        if (hmacInit) {
            Interceptor.attach(hmacInit, {
                onEnter: function(args) {
                    // int HMAC_Init_ex(HMAC_CTX *ctx, const void *key, int len,
                    // const EVP_MD *md, ENGINE *impl);
                    var keyPtr = args[1];
                    var keyLen = args[2].toInt32();
                    if (keyLen > 0 && !keyPtr.isNull()) {
                        console.log("\n┌─[HMAC] Init");
                        console.log("│ 密钥 (" + keyLen + " bytes): " + safeDump(keyPtr, keyLen));
                        var keyText = safeReadString(keyPtr, keyLen);
                        if (keyText && keyText.length > 1) console.log("│ 密钥(text): \"" + keyText + "\"");
                        console.log("└─");
                    }
                }
            });
            console.log("[OK] HMAC_Init_ex");
        }

        // --- HMAC_Update ---
        var hmacUpdate = getCryptoExport("HMAC_Update");
        if (hmacUpdate) {
            Interceptor.attach(hmacUpdate, {
                onEnter: function(args) {
                    var data = args[1];
                    var len = args[2].toInt32();
                    if (len > 0 && len < 4096) {
                        console.log("[HMAC] Update (" + len + " bytes): " + safeDump(data, len));
                        var text = safeReadString(data, Math.min(len, 256));
                        if (text && text.length > 1) console.log(" text: \"" + text + "\"");
                    }
                }
            });
            console.log("[OK] HMAC_Update");
        }

        // --- HMAC_Final ---
        var hmacFinal = getCryptoExport("HMAC_Final");
        if (hmacFinal) {
            Interceptor.attach(hmacFinal, {
                onEnter: function(args) {
                    this.mdPtr = args[1];
                    this.mdLenPtr = args[2];
                },
                onLeave: function(retval) {
                    var mdLen = 32;
                    try { mdLen = this.mdLenPtr.readU32(); } catch(e) {}
                    console.log("[HMAC] Final (" + mdLen + " bytes): " + safeDump(this.mdPtr, mdLen));
                }
            });
            console.log("[OK] HMAC_Final");
        }
    }

段 5 · AES 低级 API · AES_set_encrypt_key / AES_cbc_encrypt

针对不走 EVP 直接调用 AES_* 的代码——自带定制 BoringSSL 的大型 App、嵌入式 SDK、老代码常见。关键AES_cbc_encrypt 的第 4 参是 key schedule 展开后的扩展密钥(176/240 字节,反推不出来),必须同时 hook AES_set_encrypt_key 抓原始 key——输出的具体含义见第 5.1 节解读。

本段重点:三个 hook —— AES_set_encrypt_key(原始 key + bits)、AES_set_decrypt_key(同上但解密方向)、AES_cbc_encrypt(IV + 明密文 + enc/dec 方向)。前两个对应第 5.1 节截图中的"双行密钥"事件。

    // ==================== AES 低级 API ====================

    if (CONFIG.hookAesLowLevel) {
        // 有些代码不走 EVP,直接调用 AES_set_encrypt_key + AES_cbc_encrypt

        var aesSetKey = getCryptoExport("AES_set_encrypt_key");
        if (aesSetKey) {
            Interceptor.attach(aesSetKey, {
                onEnter: function(args) {
                    // int AES_set_encrypt_key(const unsigned char *userKey, const int bits, AES_KEY *key);
                    var keyPtr = args[0];
                    var bits = args[1].toInt32();
                    var keyLen = bits / 8;

                    console.log("\n┌─[AES] set_encrypt_key (" + bits + " bits)");
                    console.log("│ 密钥: " + safeDump(keyPtr, keyLen));
                    var stack = getNativeStack(this.context);
                    if (stack) console.log("│ 调用栈:\n" + stack);
                    console.log("└─");
                }
            });
            console.log("[OK] AES_set_encrypt_key");
        }

        var aesSetDecKey = getCryptoExport("AES_set_decrypt_key");
        if (aesSetDecKey) {
            Interceptor.attach(aesSetDecKey, {
                onEnter: function(args) {
                    var keyPtr = args[0];
                    var bits = args[1].toInt32();
                    console.log("[AES] set_decrypt_key (" + bits + " bits): " + safeDump(keyPtr, bits / 8));
                }
            });
            console.log("[OK] AES_set_decrypt_key");
        }

        // AES_cbc_encrypt
        var aesCbc = getCryptoExport("AES_cbc_encrypt");
        if (aesCbc) {
            Interceptor.attach(aesCbc, {
                onEnter: function(args) {
                    // void AES_cbc_encrypt(const unsigned char *in, unsigned char *out,
                    // size_t length, const AES_KEY *key,
                    // unsigned char *ivec, const int enc);
                    this.inPtr = args[0];
                    this.outPtr = args[1];
                    this.length = args[2].toInt32();
                    this.ivPtr = args[4];
                    this.enc = args[5].toInt32();
                },
                onLeave: function(retval) {
                    var mode = this.enc === 1 ? "ENCRYPT" : "DECRYPT";
                    console.log("\n┌─[AES] cbc_encrypt | " + mode);
                    console.log("│ IV: " + safeDump(this.ivPtr, 16));
                    console.log("│ 输入 (" + this.length + " bytes): " + safeDump(this.inPtr, this.length));
                    console.log("│ 输出 (" + this.length + " bytes): " + safeDump(this.outPtr, this.length));
                    console.log("└─");
                }
            });
            console.log("[OK] AES_cbc_encrypt");
        }
    }

段 6 · RSA + MD5 低级 API · RSA_public_encrypt / BN_mod_exp_mont / MD5_Update / MD5_Final

针对不走 EVP 直接调用 RSA_* / MD5_* 的代码。RSA 段双 hook:RSA_public_encrypt 抓明密文,BN_mod_exp_mont 解 BIGNUM 拿公钥本体(e + n)——具体读法见第 5.2 节。MD5 段按 MD5_CTX* 索引累积 Update 输入,Final 时一次性吐出所有分段 + 摘要——即便业务方复用同一 ctx 也不串(见第 5.5 节的 ctx 累积模式)。BN_mod_exp_mont 也被 DH/DSA 调用,BIGNUM 读不出时静默跳过避免噪音。

本段重点:四个 hook —— readBigNumHex helper(按 BoringSSL {d, top} 布局读 BIGNUM 转大端 hex)、RSA_public_encrypt(flen / padding / 明密文)、BN_mod_exp_mont(args[2] 指数 e、args[3] 模数 n,公钥本体)、MD5_Update + MD5_Final 配对(按 args[0] ctx 指针索引累积 chunks,Final 时一次性吐出)。MD5 这套 ctx-key 累积模式可直接套到 EVP_Digest / HMAC_Update 上做相同的"分段聚合"输出。

    // ==================== RSA 低级 API ====================

    if (CONFIG.hookRsaLowLevel) {
        // BIGNUM 经典布局: { BN_ULONG* d; int top; ... }; BoringSSL 32/64 位实测都能用
        function readBigNumHex(bnPtr) {
            try {
                var dPtr = bnPtr.readPointer();
                var top  = bnPtr.add(Process.pointerSize).readU32();
                if (top <= 0 || top > 128return null;  // 不像 RSA modulus,跳过
                var le = new Uint8Array(dPtr.readByteArray(top * Process.pointerSize));
                var hex = [];
                for (var i = le.length - 1; i >= 0; i--) {
                    var b = le[i].toString(16);
                    hex.push(b.length === 1 ? "0" + b : b);
                }
                return hex.join("");
            } catch (e) { return null; }
        }

        var rsaEnc = getCryptoExport("RSA_public_encrypt");
        if (rsaEnc) {
            Interceptor.attach(rsaEnc, {
                onEnter: function (args) {
                    this.flen = args[0].toInt32();
                    this.from = args[1];
                    this.to = args[2];
                    this.padding = args[4].toInt32();
                },
                onLeave: function (retval) {
                    var outLen = retval.toInt32();
                    var padMap = { 1: "PKCS1"3: "NoPadding"4: "OAEP" };
                    console.log("\n┌─[RSA] public_encrypt | " + (padMap[this.padding] || this.padding));
                    console.log("│ 明文 (" + this.flen + " B): " + safeDump(this.fromthis.flen));
                    if (outLen > 0) console.log("│ 密文 (" + outLen + " B): " + safeDump(this.to, outLen));
                    var stack = getNativeStack(this.context);
                    if (stack) console.log("│ 调用栈:\n" + stack);
                    console.log("└─");
                }
            });
            console.log("[OK] RSA_public_encrypt");
        }

        // BN_mod_exp_mont(r, a, p, m, ...) — args[2]=指数 e, args[3]=模数 n (公钥本体)
        var modExp = getCryptoExport("BN_mod_exp_mont");
        if (modExp) {
            Interceptor.attach(modExp, {
                onEnter: function (args) {
                    var n = readBigNumHex(args[3]);
                    if (!n) return;  // 读不出则跳过 (DH/DSA 也调用此函数,布局不同)
                    var e = readBigNumHex(args[2]);
                    console.log("\n┌─[RSA] BN_mod_exp_mont (公钥)");
                    console.log("│ 指数 e: 0x" + e);
                    console.log("│ 模数 n: 0x" + n);
                    console.log("└─");
                }
            });
            console.log("[OK] BN_mod_exp_mont");
        }
    }

    // ==================== MD5 低级 API ====================

    if (CONFIG.hookMd5LowLevel) {
        // 按 MD5_CTX* 指针索引累积 Update 输入,Final 时一并吐出
        var md5Update = getCryptoExport("MD5_Update");
        var md5Final  = getCryptoExport("MD5_Final");

        if (md5Update && md5Final) {
            var md5CtxInputs = {};

            Interceptor.attach(md5Update, {
                onEnter: function (args) {
                    var ctxKey = args[0].toString();
                    var len = args[2].toInt32();
                    if (!md5CtxInputs[ctxKey]) md5CtxInputs[ctxKey] = [];
                    try { md5CtxInputs[ctxKey].push(args[1].readByteArray(len)); } catch (e) {}
                }
            });

            Interceptor.attach(md5Final, {
                onEnter: function (args) {
                    this.outPtr = args[0];
                    this.ctxKey = args[1].toString();
                },
                onLeave: function () {
                    var chunks = md5CtxInputs[this.ctxKey] || [];
                    console.log("\n┌─[MD5] " + chunks.length + " 次 Update → Final");
                    chunks.forEach(function (c, i) {
                        var arr = new Uint8Array(c);
                        var preview = "";
                        for (var j = 0; j < Math.min(arr.length, 64); j++) {
                            var ch = arr[j];
                            preview += (ch >= 32 && ch <= 126? String.fromCharCode(ch) : ".";
                        }
                        console.log("│ Update[" + i + "] (" + arr.length + " B): " + preview);
                    });
                    console.log("│ 摘要: " + safeDump(this.outPtr, 16));
                    console.log("└─");
                    delete md5CtxInputs[this.ctxKey];  // 清理避免内存累积
                }
            });
            console.log("[OK] MD5_Update + MD5_Final");
        }
    }

段 7 · 启动信息

加载完成横幅。和第 14 篇 Java 层算法自吐保持同一视觉风格——单边横线 + emoji,避免中英文混排导致右侧框线对不齐。

    // ==================== 启动信息 ====================
    // 不使用右侧闭合边框,规避中英文混排的对齐问题
    console.log("");
    console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
    console.log(" 🧬 Native 算法自吐 native_crypto_monitor.js v1.0");
    console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
    console.log(" 📡 监控范围");
    console.log(" • EVP Cipher (EVP_Encrypt* / EVP_Decrypt*)");
    console.log(" • EVP Digest (SHA / MD5 via EVP_Digest*)");
    console.log(" • HMAC (一次性 HMAC 与三步 Init/Update/Final)");
    console.log(" • AES 低级 (AES_set_encrypt_key / AES_cbc_encrypt)");
    console.log("");
    console.log(" ⚙️ 配置提示");
    console.log(" backtrace 默认开启,性能敏感时关 CONFIG.showBacktrace");
    console.log(" Flutter App 见第 6.3 节,需替换 libcrypto.so → libflutter.so");
    console.log("");
    console.log(" 🩺 无输出排查");
    console.log(" 1) 启动是否打印 [OK] 各 Hook 加载日志");
    console.log(" 2) SO 没有 EVP_* 导出 → 段 5/6 的低级 API 应该补上");
    console.log(" 3) 完全无输出 → 见第七章自研算法识别");
    console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
    console.log("");

})();

零代码替代方案 frida-trace:如果只想快速验证目标 SO 是否走 EVP / AES / RSA 路径、不需要明密文,frida-trace -U -f <包名> -i 'AES_*' -i 'RSA_*' -i 'EVP_*' 一条命令即可——自动生成模板 handler,函数命中就打印。本节脚本的价值是抓全参数 + 智能格式 + 调用栈 + ctx 累积,frida-trace 只告诉你"有没有调"。两者互补:先用 frida-trace 看哪些函数被命中,再用本脚本拿数据。

完整版补强清单

上面 7 段把 Hook 时机和参数布局讲清楚,但直接放到生产 App 上会撞到几堵墙。完整版(文末关注公众号回复关键词获取)在 7 段之上额外合入了下面这些能力,每一项都对应一个真机踩过的坑:

能力解决的问题关键接口
safeAttach 多副本装钩腾讯系 App 一个进程内 3+ 份 libcrypto.sofindModuleByName 只返第一个,业务真实加密的副本可能整体 0 命中enumerateModules + addr 去重
Base64 段JWT / cookie / 自定义协议高频走 BoringSSL 单步 base64,与第 14 篇 Java 层 android.util.Base64 形成同一事件的双观察点EVP_EncodeBlock / EVP_DecodeBlock
isTlsNoise 握手噪声过滤TLS 1.2 的 tls1_P_hash PRF、TLS 1.3 的 HKDF_extract/expand + SSLTranscript::GetFinishedMAC 会触发海量派生,淹没业务事件caller LR + 符号名双层匹配
BN_mod_exp_mont 完整四元组只 dump modulus N + e 没法验证 hook 抓到的真是这次 RSA 运算r = a^p mod m 四个 BIGNUM + caller LR
ANSI 256 色 + buf 累积多线程并发时字段级输出会被其它线程割开,与第 14 篇 Java 版的视觉风格保持一致事件累积成单次 console.log
android_dlopen_ext spawn 兜底spawn 模式下 setImmediate 早于 SO 加载,getCryptoExport 全返 null,hook 全装不上dlopen 命中后 installAllHooks()
一次性 export 单独 hookBoringSSL 把 MD5(d,l,m) / HMAC(...) / EVP_Digest(...) 内部 Init/Update/Final 编译期内联,只 hook Update/Final 会出现"0 段 Update"怪象直接 hook 一次性 export 本身
性能控制三件套TLS 解密路径上 Update 类 hook 一帧上千次,不限速直接 ANRskipHugeUpdates + rateLimitPerSecond + showBacktrace 默认关
hideTestVectors 自检过滤AES / DES / RSA 库启动自检会用全零 key 与 NIST 测试向量打几次,污染输出key 内容匹配后隐藏

每一项的实现都不复杂,但踩过一遍才知道哪几项是必须的——上面的清单就是踩过的坑的索引。

五、输出格式解读

脚本跑起来后会吐出大量日志。真正的功夫是「怎么从日志读出业务模式」——不是去看每条 hook 命中,而是识别几种典型形态。下面五节按 AES / RSA / SHA-256 / HMAC / MD5 顺序展开五种输出形态,每节给一张真机截图 + 关键字段读法。

5.1 AES 输出

样例 App 跑出来一条典型的 AES 输出长这样:

native_crypto_monitor AES 命中

两条相关事件配对 —— 第一条 AES set_encrypt_key · 256 bits 抓密钥装载,第二条 AES_cbc_encrypt · ENCRYPT · 160 bytes 抓实际加密:

密钥字段的双行设计 —— 第一行用引号包裹显示 "fd387891254e6cedc4019ca0061ea6d9",因为 32 个字节全部落在 ASCII 可打印范围(0x30-0x66),脚本的 smartFormat 自动识别成 text 视图。第二行是同样字节的 hex 展开 6664333837...(首字节 0x66 = 'f'、0x64 = 'd'、0x33 = '3' ...)。

5.2 RSA 输出

RSA 比 AES 多一步——除了明密文,还要拿到公钥才能在 Python 里独立加密任意 payload。

native_crypto_monitor RSA 命中

这是 BN_mod_exp_mont · r = a^p mod m 的命中 —— BN_mod_exp_mont 是 BoringSSL 内所有 RSA 运算(公钥加密 / 公钥验签 / 私钥解密 / 私钥签名)的底层"模幂运算" helper。覆盖率比 RSA_public_encrypt 高一个数量级——后者只在业务直接调高级 API 时才命中,前者所有路径都走。

四个字段一一对应到 RSA 数学:

字段含义这条记录的值
模数 mRSA modulus N2048 bit,开头 0xd33a629f...
指数 p公钥 e 或私钥 d0x10001 = 65537 = 标准公钥指数
输入 a被加密 / 被验签的数据(已 padding)0x387c3db8...
输出 r计算结果 = a^p mod m0x47c7ce3d...

指数判读约定 —— 脚本对 指数 p 短于 16 个 hex 字符的视为公钥(e 通常是 65537),更长的认为是私钥 d(2048 bit)。这里 0x10001 是教科书级的公钥指数。

抓到 m + e 就可以直接构造 Python 公钥(任意 PKCS1 / NoPadding 明文都可加密),不需要 X.509 / PEM 序列化:

from Crypto.PublicKey import RSA
= int("d33a629f0777b018f3fffeccc9a2c23a..."16)
= 0x10001
pubkey = RSA.construct((n, e))

5.3 SHA-256 输出

native_crypto_monitor SHA-256 命中

标签 SHA-256 (EVP) · 5 段 Update —— (EVP) 后缀表示业务走的是 EVP 高级 API(EVP_DigestInit_ex + EVP_DigestUpdate × N + EVP_DigestFinal_ex),脚本通过 EVP_DigestFinal_ex 拿到最终摘要长度(32 字节 → SHA-256),通过 EVP_DigestUpdate 按 ctx 累积所有 Update 输入,Final 时一次性 dump。

5 个 ①②③④⑤ 圈数字是 printMultiInput 的多段标记。这里的输入序列展示了 图片缓存 key 派生的常见模式

  1. "false" ——某个布尔属性序列化
  2. ② 完整 URL(图片真实地址)
  3. ③ 32 字节 hex —— 大概率是 thumbnail 之类的 hash prefix
  4. -com.tencent.image.rcbitmap.a#200767... —— 业务组件 ID
  5. android.support.rastermill.FrameSequenceDrawable —— Drawable 类名

复合 hash → 缓存 key。摘要 fc3ca943... 就是这个图片在本地缓存里的文件名前缀

5.4 HMAC 输出

native_crypto_monitor HMAC 命中

同一帧抓到 HMAC 的三种形态 —— 这是脚本对 BoringSSL HMAC 系列 API 全覆盖的产物:

副标题来源 hook含义
Native · oneshot业务直接调一次性 API HMAC(evp_md, key, klen, data, dlen, md, &mlen)一行代码出签名,典型业务调用
Native · N 段 Update业务三步:HMAC_Init_ex + HMAC_Update × N + HMAC_Final大块数据 / 流式签名
Native · 0 段 UpdateHMAC_Final 命中但 HMAC_Update 没抓到BoringSSL 内部 Update 被 inline,我们的 export hook 只拦到 Final

5.5 MD5 输出

native_crypto_monitor MD5 命中

输入与摘要字段含义同第 5.3 节 SHA-256,按 ctx 累积 N 段 Update + Final 16 字节摘要,不再赘述。

六、BoringSSL 与标准 OpenSSL 的差异

6.1 为什么需要单独讨论 BoringSSL

BoringSSL 是 Google 的 OpenSSL 分支,Android 系统自带的 libcrypto.so / libssl.so 实际上就是 BoringSSL。Flutter、Chromium WebView、Cronet 也使用各自编译的 BoringSSL。

BoringSSL 与标准 OpenSSL 的关键差异影响 Frida Hook:

差异点OpenSSLBoringSSL
HMAC APIOpenSSL 1.1.0+ 推荐 HMAC_CTX_new / HMAC_CTX_free保留 OpenSSL 1.0.x 老风格的 HMAC_CTX_init / HMAC_CTX_cleanup
符号可见性大部分函数有导出符号很多内部函数被 static 隐藏,不在导出表中
SO 名称libcrypto.so.1.1 / libssl.so.1.1libcrypto.so / libssl.so(无版本后缀)
EVP_CIPHER_nid通过 EVP_CIPHER_nid 获取同上,但部分版本改名为 EVP_CIPHER_get_nid
内部函数名ssl3_read_bytes名称可能不同,如 ssl_read_buffer_extend

6.2 Hook BoringSSL 的注意事项

兼容查找的逻辑已经在第 4.1 节段 1 主脚本的 getEvpCipherName 里实现——它内部就是按以下顺序依次尝试:

EVP_CIPHER_CTX_cipher      ← OpenSSL 1.x
EVP_CIPHER_CTX_get0_cipher ← OpenSSL 3.x / BoringSSL 新版
EVP_CIPHER_nid ← OpenSSL 1.x
EVP_CIPHER_get_nid ← OpenSSL 3.x

如果想看当前 SO 命中的是哪个名字,在 getEvpCipherName 里加一行 console.log('[diag] cipher getter =', getCryptoExport("EVP_CIPHER_CTX_cipher") ? "cipher" : "get0_cipher") 即可;自己另写脚本时把 getEvpCipherName 整段抄过去复用 fallback 即可,不必重新搭这套。想直接在 REPL 里枚举命名:

Process.findModuleByName("libcrypto.so").enumerateExports()
    .filter(e => e.name.match(/^EVP_CIPHER/))
    .map(e => e.name)
    .sort();

6.3 Flutter App 的特殊处理

Flutter 内嵌的 BoringSSL 在 libflutter.so 中而不是系统的 libcrypto.so,本篇 native_crypto_monitor.js 默认只搜索 libcrypto.so,对 Flutter App 必须改搜索目标:

// 把第 4.1 段 1 主脚本里 CRYPTO_SOS 列表扩展为这个 helper
function findCryptoExport(name) {
    var SOS = ["libcrypto.so""libflutter.so""libcronet.so"];
    for (var i = 0; i < SOS.length; i++) {
        var mod = Process.findModuleByName(SOS[i]);
        if (!mod) continue;
        var addr = mod.findExportByName(name);
        if (addr) return addr;
    }
    return null;
}

判断 App 是否为 Flutter 的最快方法:

console.log(Process.findModuleByName("libflutter.so"? "[*] Flutter App" : "[*] 非 Flutter");

Dart 端的 package:cryptography / package:pointycastle 最终都落到内嵌 BoringSSL,所以 Flutter App 的所有 Native 加密都集中在 libflutter.so。Cronet(Chromium 网络栈)同样内嵌一份 BoringSSL,部分大厂 App 用 Cronet 做网络层时也要看 libcronet.so

七、自研算法的识别与分析

7.1 如何判断是自研算法

当以上所有 Hook 都没有输出,但 App 确实在做加密(抓包可见密文)时,很可能是自研加密。

进一步确认:

// 检查 App 的 SO 是否导入了标准加密库函数
(function() {
    var appModules = Process.enumerateModules().filter(function(m) {
        return m.path.indexOf("/data/"!== -1;
    });

    appModules.forEach(function(mod) {
        var cryptoImports = [];
        mod.enumerateImports().forEach(function(imp) {
            if (imp.name.match(/EVP_|AES_|SHA|MD5|HMAC|DES_|RSA_/)) {
                cryptoImports.push(imp.name);
            }
        });
        if (cryptoImports.length > 0) {
            console.log("[" + mod.name + "] 加密导入 (" + cryptoImports.length + " 个):");
            cryptoImports.forEach(function(f) { console.log(" " + f); });
        } else {
            console.log("[" + mod.name + "] 无加密库导入 → 可能自研加密");
        }
    });
})();

7.2 IDA 中识别常见算法

当确认是自研加密后,需要用 IDA/Ghidra 打开 SO 文件进行静态分析:

AES 的特征

  • 包含 AES S-Box 常量表(256 字节的查找表,以 0x63, 0x7c, 0x77, 0x7b 开头)
  • 有 10/12/14 轮循环(分别对应 AES-128/192/256)
  • IDA 插件 FindCrypt 可以自动识别

DES/3DES 的特征

  • 包含 DES 初始置换表(IP 表)和最终置换表(FP 表)
  • S-Box 是 8 个 4×16 的二维数组

SM4 的特征

  • 包含 SM4 S-Box(以 0xD6, 0x90, 0xE9, 0xFE 开头,与 AES 不同)
  • 32 轮循环
  • 系统密钥 FK = {0xa3b1bac6, 0x56aa3350, 0x677d9197, 0xb27022dc}

MD5 的特征

  • 初始值 0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476
  • 64 个 T 表常量

SHA-256 的特征

  • 初始值 0x6a09e667, 0xbb67ae85, 0x3c6ef372, ...
  • 64 个 K 表常量
# 使用 IDA FindCrypt 插件
# 安装: 将 findcrypt-yara 插件复制到 IDA plugins 目录
# 使用: Edit → Plugins → FindCrypt
# 它会自动扫描 SO 中的算法常量并标记

7.3 Memory.scan 搜索算法常量

不用 IDA 也能做初步判断——在 Frida 中搜索已知的算法常量:

// find_crypto_constants.js
// 在 SO 内存中搜索常见加密算法的特征常量

(function() {
    var targets = {
        "AES S-Box": "63 7c 77 7b f2 6b 6f c5 30 01 67 2b fe d7 ab 76",
        "SM4 S-Box": "d6 90 e9 fe cc e1 3d b7 16 b6 14 c2 28 fb 2c 05",
        "MD5 Init":  "01 23 45 67 89 ab cd ef fe dc ba 98 76 54 32 10",
        "DES IP":    "3a 32 2a 22 1a 12 0a 02"
    };

    var appModules = Process.enumerateModules().filter(function(m) {
        return m.path.indexOf("/data/"!== -1;
    });

    appModules.forEach(function(mod) {
        console.log("\n[*] 扫描 " + mod.name + " (" + (mod.size / 1024).toFixed(1+ " KB)");

        Object.keys(targets).forEach(function(name) {
            var matches = Memory.scanSync(mod.base, mod.size, targets[name]);
            if (matches.length > 0) {
                matches.forEach(function(m) {
                    var offset = m.address.sub(mod.base);
                    console.log(" [FOUND] " + name + " @ " + mod.name + "+0x" + offset.toString(16));
                });
            }
        });
    });
})();

如果搜索到 AES S-Box 但没有 EVP 导入——说明 SO 自己实现了 AES。接下来你需要在 IDA 中找到引用该 S-Box 的函数,那个函数就是 AES 加密/解密的入口。然后用 Frida 的 Interceptor.attach 通过偏移地址 Hook 它,捕获参数。

在样例 App 上跑这个脚本,能看到一个典型的「商业加固 SO + 现代 BoringSSL」扫描结果:

find_crypto_constants 在 QQ 音乐上的真机扫描结果

输出解读 —— 头部块 [*] 扫描 libxxx.so (NNN KB) 后跟两类结果:

  • [SKIP] AES S-Box / SM4 S-Box / MD5 Init / DES IP — access violation accessing 0xXXXX —— 商业加固 SO(libturingbase / libtmesec / libDownloadProxy 等)把 .rodata 段标成 PROT_NONEMemory.scan 触发访问异常,脚本 try/catch 静默跳过。这是预期行为,不是脚本 bug,扫不动的 SO 不阻塞后续扫描。
  • [FOUND] AES S-Box @ libstar.so+0x961f8 / [FOUND] AES S-Box @ libostar.so+0x98a1b / [FOUND] SM4 S-Box @ libTPCore-master.so+0x94c328 —— 真正命中:libstar.so 内含 AES S-Box(自实现 AES)、libTPCore-master.so 内含 SM4 S-Box(国密算法)。

7.4 Hook 自研加密函数

假设通过 IDA 分析确定了自研 AES 函数的偏移和参数:

// hook_self_research_aes.js
// IDA 分析结果(本例):
// 函数偏移 0x4A8C,签名 int encrypt(const u8* key, const u8* iv,
// const u8* input, int input_len,
// u8* output)
//
// 本片段使用 Frida 内置 hexdump,无外部依赖,可独立运行。
// 想要带调用栈输出?见底部注释行;想用 §4.1 段 1 主脚本的 safeDump/getNativeStack,
// 把那两个函数复制过来即可。

var mod = Process.findModuleByName("libnative.so");
if (!mod) {
    console.log("[!] libnative.so 未加载,请确认 SO 名(用 Process.enumerateModules() 列出)");
else {
    var funcAddr = mod.base.add(0x4A8C);

    Interceptor.attach(funcAddr, {
        onEnter: function(args) {
            this.key      = args[0];
            this.iv       = args[1];
            this.input    = args[2];
            this.inputLen = args[3].toInt32();
            this.output   = args[4];
        },
        onLeave: function(retval) {
            var outLen = retval.toInt32();
            console.log("\n[自研 AES] libnative.so+0x4A8C len=" + this.inputLen + "->" + outLen);
            console.log(" 密钥:");
            console.log(hexdump(this.key, { length: 16, header: false }));
            console.log(" IV:");
            console.log(hexdump(this.iv, { length: 16, header: false }));
            console.log(" 输入(前 128 字节):");
            console.log(hexdump(this.input, { length: Math.min(this.inputLen, 128), header: false }));
            console.log(" 输出(前 128 字节):");
            console.log(hexdump(this.output, { length: Math.min(outLen, 128), header: false }));
            // 想看调用栈?去掉下面三行注释:
            // console.log(" 调用栈:");
            // console.log(Thread.backtrace(this.context, Backtracer.ACCURATE)
            // .map(DebugSymbol.fromAddress).join("\n"));
        }
    });

    console.log("[*] 自研 AES Hook 已加载 @ libnative.so+0x4A8C");
}

如何从 IDA 拿到这个 0x4A8C 偏移? 第 7.3 节扫到 AES S-Box 在 SO 内某个地址后,在 IDA 中右键 Jump to xref to operand,跳到引用该 S-Box 的函数,函数入口的 RVA(相对于 SO 基址的偏移)就是这里的 0x4A8C。把参数签名按 IDA 的类型推断(Y 键)猜出来,就能写 Hook。

八、Java + Native 双层监控

实战中最好的做法是同时加载 Java 和 Native 两个监控脚本,确保不遗漏任何加密操作:

frida -U -f com.example.app \
    -l crypto_monitor.js \         # 第14篇:Java 层
    -l native_crypto_monitor.js \  # 本篇:Native 层
    --no-pause

两个脚本不冲突——一个 Hook Java 类,一个 Hook Native 函数,各管各的层。

九、适用边界:三类例外场景

本篇覆盖的是「标准库 + 符号未 strip + SO 未加固」的理想场景。实战中你会遇到三类例外:

第一类,符号被 strip。SO 里 AES_cbc_encryptMD5_UpdateEVP_* 等函数不在导出表里,findExportByName 返回 null。识别信号:本篇主脚本启动横幅中 [OK] 行寥寥无几,enumerateExports() 输出里看不到任何 AES_* / EVP_* / RSA_* 命名,但 enumerateImports() 还能见到——说明业务 SO 引用了加密符号、但目标 SO 把它们藏起来了。需要用算法常量特征定位:AES S-box 首字节 63 7C 77 7B、Rcon 表 01 02 04 08 10 20 40 80 1B 36、MD5 IV 67 45 23 01、SHA-256 IV 6A 09 E6 67。IDA 配 FindCrypt 插件能自动标注这些常量,再通过交叉引用回溯到使用它们的函数。第七章已给出 Frida 端 Memory.scanSync 在运行时直接搜索常量的方案。

第二类,SO 被加固(360、爱加密、梆梆等)。SO 文件磁盘内容是壳代码,真正的加密逻辑解密后才出现在内存里,findExportByName 拿到的可能是壳的桩函数甚至 null。识别信号Process.enumerateModules() 里看到 libjiagu(360)/ libshell(腾讯乐固)/ libDexHelper(梆梆)/ libnesec(网易易盾)/ libsec2023 / libsec2024(爱加密年份命名)/ libtmesec 之类的"壳" SO 与业务 SO 并存;或者 Memory.scan 在业务 SO 的 .rodata 上触发 access violation(参考第 7.3 节 [SKIP] 输出形态)。需要先做内存 dump、修复 ELF 头(常用 010 Editor 把原始加固 SO 的头复制过来)、再用 SoFixer 还原成可静态分析的形态。

第三类,算法被魔改或自实现。S-box 被换了、轮数被改了、用了查表法白盒,常量识别也失效。识别信号:第 7.3 节常量扫描全无命中、抓包却看到明显的加密数据;或扫到 S-box 但偏移落在非标准位置(如紧贴一段循环展开的 NEON 汇编、或字节序与官方表恰好不一致)。这种场景下 Hook 思路不变(还是定位函数、抓输入输出),但需要先把魔改逻辑搞清楚才能在 Python 里复现。

这三种进阶场景的完整脱壳和反魔改流程,留给第21篇《Native Hook 入门》专门讲。

总结

Java 层算法自吐没输出但抓包确认有加密,加密就在 Native 层。一条标准路径:

  1. 两套 API 都要覆盖:EVP 高级(现代代码、Conscrypt、Flutter)+ AES_* / RSA_* / MD5_* 低级(老代码、定制 BoringSSL)——目标 App 走哪条事先不知道,主脚本同时挂上。
  2. 入口定位用 RegisterNatives:jadx 找 native 方法 → Hook libart.so!RegisterNatives 拿到 SO 偏移 → IDA 跳过去看调用的是哪个 crypto API。看到标准函数名直接 Hook,不必深入业务代码。
  3. native_crypto_monitor.js 一次覆盖六类:EVP + HMAC + AES + RSA + MD5 + Base64,配 safeAttach 多副本装钩、isTlsNoise 握手噪声过滤、BN_mod_exp_mont 四元组 RSA 公钥提取、ANSI 256 色对齐第 14 篇视觉——加载即用,是日常分析的第一枪。
  4. 加载脚本没输出 = 自研或加固:IDA FindCrypt + Frida Memory.scanSync 走 AES S-Box / SM4 S-Box / MD5 IV / SHA-256 IV 常量定位;strip / 加固 / 魔改三类进阶场景见第 21 篇《Native Hook 入门》。
  5. 双层加载crypto_monitor.js(Java 层,第 14 篇)+ native_crypto_monitor.js(Native 层,本篇)同时挂上,是确认"加密在哪一层"最快的姿势。

📦 获取本篇脚本

native_crypto_monitor.js 完整版(单文件 1100+ 行,EVP / HMAC / AES / RSA / MD5 / Base64 六类全覆盖,含 safeAttach 多副本装钩、isTlsNoise TLS 噪声过滤、android_dlopen_ext spawn 兜底、BN_mod_exp_mont 完整四元组、ANSI 256 色对齐第 14 篇视觉、性能控制三件套)以及配套的 hook_registernatives.js / find_crypto_constants.js / check_app_crypto_imports.js / enum_evp_cipher_exports.js / hook_self_research_aes.js + README 使用说明,已统一打包:

  1. 关注本公众号
  2. 私信回复关键词「脚本」

回复内含本系列与其它系列(Unidbg / SO 逆向 / ARM 汇编 ……)的脚本汇总,长期维护更新。


最新文章

随机文章

基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-05-28 16:06:03 HTTP/2.0 GET : https://67808.cn/a/491342.html
  2. 运行时间 : 0.078643s [ 吞吐率:12.72req/s ] 内存消耗:4,674.88kb 文件加载:140
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=3e75bf0a39cd380e3bb08ff61c22db54
  1. /yingpanguazai/ssd/ssd1/www/no.67808.cn/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/composer/autoload_static.php ( 4.90 KB )
  7. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  10. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  11. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  12. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  13. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  14. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  15. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  16. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  17. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  18. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  19. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  21. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  22. /yingpanguazai/ssd/ssd1/www/no.67808.cn/app/provider.php ( 0.19 KB )
  23. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  24. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  25. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  26. /yingpanguazai/ssd/ssd1/www/no.67808.cn/app/common.php ( 0.03 KB )
  27. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  28. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  29. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/app.php ( 0.95 KB )
  30. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/cache.php ( 0.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/console.php ( 0.23 KB )
  32. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/cookie.php ( 0.56 KB )
  33. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/database.php ( 2.48 KB )
  34. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  35. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/filesystem.php ( 0.61 KB )
  36. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/lang.php ( 0.91 KB )
  37. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/log.php ( 1.35 KB )
  38. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/middleware.php ( 0.19 KB )
  39. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/route.php ( 1.89 KB )
  40. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/session.php ( 0.57 KB )
  41. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/trace.php ( 0.34 KB )
  42. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/view.php ( 0.82 KB )
  43. /yingpanguazai/ssd/ssd1/www/no.67808.cn/app/event.php ( 0.25 KB )
  44. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  45. /yingpanguazai/ssd/ssd1/www/no.67808.cn/app/service.php ( 0.13 KB )
  46. /yingpanguazai/ssd/ssd1/www/no.67808.cn/app/AppService.php ( 0.26 KB )
  47. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  48. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  49. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  50. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  51. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  52. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/services.php ( 0.14 KB )
  53. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  54. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  55. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  56. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  57. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  58. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  59. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  60. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  61. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  62. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  63. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  64. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  65. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  66. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  67. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  68. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  69. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  70. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  71. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  72. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  73. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  74. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  75. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  76. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  77. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  78. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  79. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  80. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  81. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  82. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  83. /yingpanguazai/ssd/ssd1/www/no.67808.cn/app/Request.php ( 0.09 KB )
  84. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  85. /yingpanguazai/ssd/ssd1/www/no.67808.cn/app/middleware.php ( 0.25 KB )
  86. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  87. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  88. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  89. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  90. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  91. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  92. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  93. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  94. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  95. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  96. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  97. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  98. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  99. /yingpanguazai/ssd/ssd1/www/no.67808.cn/route/app.php ( 1.72 KB )
  100. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  101. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  102. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  103. /yingpanguazai/ssd/ssd1/www/no.67808.cn/app/controller/Index.php ( 4.81 KB )
  104. /yingpanguazai/ssd/ssd1/www/no.67808.cn/app/BaseController.php ( 2.05 KB )
  105. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  106. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  108. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  109. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  110. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  111. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  112. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  113. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  114. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  115. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  116. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  117. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  118. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  119. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  120. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  121. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  122. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  123. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  124. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  125. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  126. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  127. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  128. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  129. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  130. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  131. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  132. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  133. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  134. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  135. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  136. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  137. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  138. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  139. /yingpanguazai/ssd/ssd1/www/no.67808.cn/runtime/temp/6df755f970a38e704c5414acbc6e8bcd.php ( 12.06 KB )
  140. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.000452s ] mysql:host=127.0.0.1;port=3306;dbname=no_67808;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.000680s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.000293s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000278s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.000477s ]
  6. SELECT * FROM `set` [ RunTime:0.000188s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.000570s ]
  8. SELECT * FROM `article` WHERE `id` = 491342 LIMIT 1 [ RunTime:0.001891s ]
  9. UPDATE `article` SET `lasttime` = 1779955563 WHERE `id` = 491342 [ RunTime:0.002311s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 65 LIMIT 1 [ RunTime:0.000231s ]
  11. SELECT * FROM `article` WHERE `id` < 491342 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.000436s ]
  12. SELECT * FROM `article` WHERE `id` > 491342 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.000418s ]
  13. SELECT * FROM `article` WHERE `id` < 491342 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.000735s ]
  14. SELECT * FROM `article` WHERE `id` < 491342 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.000812s ]
  15. SELECT * FROM `article` WHERE `id` < 491342 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.001334s ]
0.080213s