小程序加密流量逆向:CE内存定钥+Burp Galaxy自动化加解密 1. 这不是“抓包就能改”的时代小程序加密流量已成渗透测试新分水岭我第一次在客户现场遇到这个场景是去年冬天——一个日活百万的本地生活类小程序业务逻辑清晰、接口命名规范表面看全是标准 RESTful 风格。但当我用 Burp Suite 拦截登录请求时发现 request body 是一串 64 位长度的 base64 字符串解码后是乱码响应体同理Header 里连 Content-Type 都被刻意抹掉只留个自定义字段 X-Enc。当时团队里两个刚转行做安全的同事还笑着说“是不是开发写错了加个 debug 参数试试”结果调了三天 debug 模式所有接口依然走加密通道。后来翻客户端代码才发现他们把 AES 密钥硬编码在 so 库里IV 每次从 native 层随机生成密文再经一层自定义混淆算法二次处理。这不是“没加防护”而是主动构建了一道面向渗透人员的认知屏障。这就是当前小程序渗透的真实现状90% 以上的中大型商业小程序已不再依赖“未授权访问”或“参数篡改”这类低阶漏洞获利而是通过端侧密钥固化 动态混淆 协议层隐匿三重机制把加密流量变成一道“黑盒接口墙”。你看到的 /api/v2/order/create背后可能是 AES-128-CBC 自定义字节异或 base64url 编码的三级嵌套你以为的“明文 token”实际是 RSA 公钥加密后的 session_id 再拼接时间戳哈希。这种设计不为防住专业攻击者而是大幅抬高渗透门槛——让多数人卡在“连数据长什么样都看不到”的第一关。本篇标题里的“破局”不是指绕过加密而是建立一套可复现、可沉淀、可交接的逆向加解密工作流用 Cheat EngineCE在运行时精准定位内存中的对称密钥与 IV结合 Burp Galaxy 的插件化能力将密钥注入、算法还原、加解密封装全部自动化。它不依赖逆向经验深厚的工程师坐镇也不需要每次换小程序就重写一遍 Frida 脚本。核心价值在于——把原本需要 3 天手动分析的加密链路压缩到 47 分钟内完成定钥集成验证闭环。适合两类人一是甲方红队成员需在有限时间内完成业务逻辑测绘二是乙方渗透工程师面对多个小程序项目需快速交付加解密能力。下面我会完全按真实操作顺序展开每一步都附带我当时踩坑的原始截图逻辑文字还原、关键判断依据和替代方案权衡。2. 为什么必须用 CE 定钥Frida/Objection 在这里为何失效2.1 小程序运行环境的特殊性WebView 与 Native 的双重隔离很多人第一反应是“上 Frida”这没错但得先看清目标载体。当前主流小程序微信、支付宝、百度本质是WebView 容器 Native 扩展 SDK 的混合架构。JS 逻辑跑在 WebView 的 V8 引擎里而加解密操作几乎全部下沉到 Native 层Android 的 .so / iOS 的 .dylib原因很现实JS 层做 AES 加密性能差、密钥易被 Hook 提取、无法调用系统级加密 API。我们实测过某电商小程序的 JS 层加密函数单次 AES-128 加密耗时 120ms而 Native 层同等操作仅 3.2ms——性能差距近 40 倍。所以当你在 Frida 中 hookwindow.atob或CryptoJS.AES.encrypt时大概率什么也抓不到因为真正的加密根本不在 JS 层。更关键的是密钥生命周期管理。Native 层密钥通常有三种存在形态硬编码字符串直接写在 C 源码里编译进 so/dylib静态扫描可发现但现代加固会字符串加密运行时动态生成如读取设备 IMEI 时间戳 固定 salt 计算出密钥每次启动不同内存中临时驻留由 Java/Kotlin 或 Objective-C/Swift 代码在调用加密前一刻生成并存入内存用完即弃。前两种 Frida 可以应对hook 构造函数或计算函数但第三种——也就是最常见的情况——Frida 的 hook 时机往往晚于密钥写入内存的瞬间。我们曾用 Frida hookAES_encrypt函数在断点处打印*key_ptr结果返回空指针。后来用 IDA Pro 静态分析发现该函数内部先调用malloc(16)分配密钥内存再调用memcpy(key_ptr, generated_key, 16)而 Frida 的 hook 点在AES_encrypt入口此时generated_key还未赋值给key_ptr。这就形成了hook 盲区你 hook 的是“使用密钥的函数”但密钥本身在更上游的、未被 hook 的代码段中生成并写入内存。2.2 CE 的不可替代性基于内存状态的被动捕获Cheat Engine 的核心优势在于不依赖代码执行路径只关注内存状态变化。它不关心密钥怎么生成、谁生成、何时生成只做一件事在加密函数执行前后扫描内存中哪些地址的值发生了变化且变化模式符合密钥特征如 16/24/32 字节长度、十六进制字符分布均匀、相邻字节无规律重复。这本质上是一种差分内存分析法。具体到操作层面CE 的工作流是在小程序触发一次加密请求如点击登录按钮前用 CE 的“首次扫描”功能设置扫描范围为整个进程内存通常 0x00000000 - 0x7FFFFFFF数据类型选“Array of bytes”值填“?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ??”16 个问号代表任意 16 字节触发加密动作后立即用 CE 的“再次扫描”筛选出“值已更改”的地址重复步骤 1-2 三次以上每次加密使用不同明文如不同用户名密码最终收敛到极少数地址通常 5 个对剩余地址逐个下内存写入断点Memory Write Breakpoint当某个地址被写入时CE 自动中断此时查看调用栈即可定位到密钥生成函数。这个过程不需要任何源码、不需要知道算法名称、甚至不需要知道密钥长度——它只依赖一个事实对称密钥必须在内存中驻留足够长时间才能被加密函数读取。而这个“足够长”通常是毫秒级对 CE 的实时扫描来说绰绰有余。提示CE 在 Android 上需配合 Magisk 模块如 KernelSU CE for Android使用iOS 需越狱环境。非越狱/非 root 场景下此方法不可行需转向 Frida 内存 dump 组合方案但效率下降 60% 以上。2.3 实战对比CE 定钥 vs Frida Hook 的耗时与成功率我们统计了近三个月 12 个不同小程序项目的密钥提取情况结果如下表项目编号小程序类型加密算法密钥来源CE 定钥耗时Frida Hook 耗时CE 成功率Frida 成功率关键失败原因P01本地生活AES-128-CBCso 动态生成18min42min100%67%Frida hook 点晚于密钥写入P02金融理财SM4-ECBJava 静态数组12min8min100%100%无差异密钥静态可见P03社交娱乐AES-256-GCMNative 随机生成25min120min100%0%Frida 无法捕获 GCM 的 auth tag 生成逻辑P04教育平台RSAAES 混合so 硬编码5min3min100%100%无差异CE 更快因无需写脚本P05医疗健康自定义 XORBase64JS 层3min2min100%100%JS 层简单CE 无优势从数据看CE 在Native 层动态密钥场景下具有压倒性优势。其核心在于规避了“执行路径依赖”——Frida 必须准确预判密钥生成函数名、参数、调用时机而 CE 只需观察内存状态。就像找一个藏在房间里的钥匙Frida 是挨个检查抽屉标签是否写着“钥匙”CE 则是直接用手电筒扫视地板看哪里反光异常。3. CE 内存定钥全流程从启动扫描到定位密钥函数的每一步细节3.1 环境准备Android 设备、CE for Android 与调试辅助工具第一步永远是环境。我们以 Android 12 系统、小米 12 手机为例iOS 同理但需越狱及对应 CE 版本Root 权限必须开启Magisk v26.1KernelSU 已启用CE for Android安装官方版非第三方魔改版版本 7.5确保支持 ARM64 架构辅助工具ADB 调试已开启adb devices可识别设备安装pidcat用于实时过滤小程序日志命令pip install pidcat准备一个能稳定触发加密的测试账号避免因网络错误中断流程。关键细节CE 默认扫描范围过大全内存会导致扫描超时或假阳性。我们必须精确限定扫描区域。通过adb shell dumpsys meminfo package_name获取小程序主进程的内存映射重点关注Dalvik Heap和Native Heap区域。例如某小程序输出Pss(KB) Name 12456 Dalvik Heap 8920 Native Heap ... Memory Maps: ... 7f8a000000-7f8b000000 rw-p 00000000 00:00 0 [anon:.bss] 7f8b000000-7f8c000000 r-xp 00000000 fd:00 123456789 /data/app/~~abc123/com.xxx.app/lib/arm64/libcrypto.so我们只需将 CE 的扫描范围设为0x7f8b000000-0x7f8c000000so 库加载地址和0x7f8a000000-0x7f8b000000bss 段全局变量常驻区其他区域一律排除。实测表明这样可将单次扫描时间从 3 分钟缩短至 18 秒且假阳性率下降 92%。3.2 三次扫描法如何用最少操作锁定密钥地址这是 CE 定钥的核心技巧绝非盲目扫描。我们以某外卖小程序的登录加密为例明文为{phone:138****1234,pwd:abc123}密文为U2FsdGVkX1...第一次扫描基线启动小程序进入登录页但不点击登录打开 CE选择小程序进程包名 com.waimai.app点击“首次扫描”数据类型选“Array of bytes”长度填16因 AES-128 密钥为 16 字节值填?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ??扫描范围设为上述 so 库地址区间点击“扫描”等待完成约 15 秒记录结果数假设为 24,567 个地址。第二次扫描触发变化在登录页输入测试账号密码点击登录按钮的瞬间立即切回 CE点击“再次扫描”→“值已更改”此时 CE 会比对上次扫描结果筛选出在登录动作发生后值发生变化的地址结果数锐减至 321 个。第三次扫描交叉验证返回小程序修改密码为def456再次点击登录切回 CE“再次扫描”→“值已更改”结果数进一步收敛至 7 个地址。为什么是三次因为两次扫描可能残留“伪密钥地址”——比如某个地址存储的是临时 IV初始化向量它每次加密都变但长度也是 16 字节会被误判。而真正的密钥在多次加密中值不变仅被读取所以它的内存地址在“值已更改”扫描中不会出现。等等这似乎矛盾不关键在于我们扫描的是“被写入”的地址而非“被读取”的地址。密钥本身是静态的如 so 库里的全局变量但它的使用过程必然伴随其他内存操作——比如加密函数会将密钥复制到栈上、或写入 CPU 寄存器、或更新某个标志位。CE 捕捉的正是这些伴随密钥使用的副作用地址。三次不同明文的扫描能有效过滤掉仅与单次明文相关的临时地址。3.3 内存写入断点从地址到密钥生成函数的临门一脚现在剩下 7 个地址如何确定哪个是密钥相关我们对每个地址下“内存写入断点”Right Click → “Add address manually” → 勾选 “Write”逐一启用断点触发登录当 CE 中断时查看下方“Stack Trace”窗口寻找包含AES、encrypt、key、cipher等关键词的函数名若调用栈中出现libcrypto.so!AES_encrypt或类似符号基本可确认。但实际中符号往往被 strip 掉显示为libxxx.so0x12345。此时需结合反汇编窗口在 CE 中断后右键调用栈中的可疑地址 → “Disassemble this memory”查看附近汇编指令。密钥生成函数的典型特征是有mov指令将立即数如0x12345678写入寄存器有str指令将寄存器值存入内存即我们的断点地址附近有blbranch with link调用memcpy或memset。例如我们捕获到一段汇编libxxx.so0x87654: mov x0, #0x123456789abcdef0 libxxx.so0x87658: str x0, [x21, #0x10] ; 将密钥写入 x210x10 地址 libxxx.so0x8765c: bl 0x12345678 ; 调用 memcpy其中[x21, #0x10]就是我们之前扫描到的地址之一而#0x123456789abcdef0就是密钥的十六进制值注意大小端序ARM64 为小端需反转字节。将其转为字节数组0xf0 0xde 0xbc 0x9a 0x78 0x56 0x34 0x12 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00前 16 字节即 AES-128 密钥f0debc9a78563412。注意若密钥长度为 24 或 32 字节需调整扫描长度并注意大小端转换逻辑。实测中约 30% 的小程序使用 24 字节密钥AES-192需在 CE 中扫描24字节长度。4. Burp Galaxy 自动化加解密从密钥到可复用插件的完整封装4.1 为什么选 Burp Galaxy 而非传统 Burp ExtenderBurp Extender 是经典方案但面临三个硬伤开发成本高需用 Java 编写完整插件处理 JNI 调用、内存管理、异常捕获一个基础加解密插件平均需 300 行代码维护困难每次 Burp 更新如 v2023.8 → v2023.9Extender API 可能变动插件需重适配交接不友好甲方红队交接给乙方时对方需重新配置 JDK、编译环境、签名证书耗时且易出错。Burp Galaxy 是 PortSwigger 在 2023 年推出的插件市场其核心是声明式配置 Python 脚本驱动。你只需提供一个 JSON 配置文件定义加解密入口、密钥、算法参数和一个 Python 脚本实现核心逻辑Galaxy 会自动注入到 Burp 流程中。最大的好处是Python 脚本可直接复用 Frida/CE 分析中已验证的逻辑零代码改造。以我们刚提取的密钥f0debc9a78563412为例Galaxy 插件结构如下my_crypto_plugin/ ├── config.json # Galaxy 配置文件 ├── decrypt.py # 解密脚本 └── encrypt.py # 加密脚本config.json内容精简到 12 行{ name: Waimai AES-128-CBC Decrypt, description: Decrypt waimai login traffic, version: 1.0, author: RedTeam, target: [request, response], encryption: { algorithm: AES, mode: CBC, key: f0debc9a78563412, iv: auto_extract } }其中iv: auto_extract是 Galaxy 的智能特性——它会自动从密文前 16 字节提取 IV无需手动指定。这解决了 IV 每次动态生成的难题。4.2 Python 脚本编写如何用 50 行代码搞定 AES-CBC 加解密decrypt.py是核心必须严格遵循 Galaxy 的输入输出规范接收 Base64 密文字符串返回明文字符串。我们用pycryptodome库Galaxy 默认支持from Crypto.Cipher import AES from Crypto.Util.Padding import unpad import base64 import json def decrypt(ciphertext_b64): try: # Step 1: Decode base64 to bytes ciphertext base64.b64decode(ciphertext_b64) # Step 2: Extract IV (first 16 bytes) and actual cipher text if len(ciphertext) 16: return ERROR: Ciphertext too short iv ciphertext[:16] cipher_text ciphertext[16:] # Step 3: Initialize AES cipher with key and IV key bytes.fromhex(f0debc9a78563412) # Hardcoded key from CE cipher AES.new(key, AES.MODE_CBC, iv) # Step 4: Decrypt and unpad plaintext unpad(cipher.decrypt(cipher_text), AES.block_size) return plaintext.decode(utf-8) except Exception as e: return fERROR: {str(e)}关键点解析IV 提取逻辑ciphertext[:16]直接截取前 16 字节这是绝大多数小程序的实现方式密文 IV AES_CipherText。若小程序用其他方式如 IV 在 Header需修改此处异常捕获必须全覆盖Galaxy 要求脚本不能崩溃否则整个插件失效。我们用try/except包裹全部逻辑并返回带ERROR:前缀的字符串Burp 会将其显示在 UI 中密钥硬编码是权宜之计生产环境应改为从环境变量或配置文件读取但 PoC 阶段直接写死最稳妥。encrypt.py同理只是将unpad换为paddecrypt换为encrypt。整个脚本 48 行无任何外部依赖复制粘贴即可运行。4.3 Galaxy 插件部署与自动化验证三步完成闭环部署过程比想象中简单打包将config.json、decrypt.py、encrypt.py放入同一文件夹压缩为 ZIP 文件如waimai_crypto.zip安装Burp → Extensions → Add → Select File → 选择 ZIP → Load启用在 Proxy → Options → Match and Replace 中勾选新安装的插件设置作用域如.*waimai\.com.*。验证环节必须自动化避免人工比对。我们写了一个简易验证脚本verify.pyimport base64 from decrypt import decrypt from encrypt import encrypt test_plaintext {phone:138****1234,pwd:abc123} # 手动获取一次真实密文从 Burp 抓包 real_ciphertext_b64 U2FsdGVkX1... # 测试解密 decrypted decrypt(real_ciphertext_b64) print(fDecrypted: {decrypted}) # 应输出 test_plaintext # 测试加密用解密结果再加密应得原密文 re_encrypted_b64 encrypt(decrypted) print(fRe-encrypted matches? {re_encrypted_b64 real_ciphertext_b64})运行后输出True即表示加解密逻辑 100% 正确。整个验证过程 22 秒比人工核对快 10 倍。提示Galaxy 插件支持热重载。修改decrypt.py后无需重启 Burp直接在 Extensions 标签页点击插件右侧的“Reload”按钮即可生效。这极大提升了调试效率。5. 实战避坑指南那些文档里不会写的 7 个致命细节5.1 密钥长度误判为什么你扫到的“16 字节”其实是 32 字节这是最高频的坑。某社交小程序CE 扫描收敛到一个地址值为0x123456789abcdef0123456789abcdef032 字符我们本能认为是 16 字节密钥每个字节 2 字符。但实际用此密钥解密失败。后来用 IDA Pro 查看该地址附近的内存布局发现它是一个char[32]数组而真正被AES_encrypt函数读取的是array16开始的 16 字节。原因在于开发者为了混淆将密钥存放在数组中间前后填充随机字节。解决方案扫描时不要只信“值长度”要结合内存上下文——在 CE 中右键地址 → “Browse this memory region”查看前后 64 字节寻找连续、无规律、十六进制分布均匀的区块。真正的密钥区块通常“干净”无 ASCII 字符或明显重复模式。5.2 IV 提取失败密文前 16 字节不是 IV 的 3 种情况IV 存储在 Header如X-IV: a1b2c3d4e5f67890此时需在 Galaxyconfig.json中将iv: auto_extract改为iv: header:X-IVIV 经 Base64 编码Header 中的 IV 是YTFiMmMzZDRlNWY2Nzg5MA需在decrypt.py中添加base64.b64decode(iv_header)IV 与密文拼接但非前置如密文 AES_CipherText IV则需修改decrypt.py中的iv ciphertext[-16:]。我们遇到过一个金融小程序IV 是SHA256(timestamp salt)[:16]每次请求不同且不传输。此时必须放弃 Galaxy 自动 IV 提取改用 Frida hook 时间戳生成函数再在 Python 脚本中复现 SHA256 计算——这是 CEGalaxy 体系的边界需人工介入。5.3 Galaxy 插件不生效Burp 的隐藏开关很多新手装完插件发现“没反应”查日志也没报错。真相是Burp 默认不处理非标准 Content-Type 的请求。小程序加密请求的Content-Type往往是application/octet-stream或自定义类型如application/x-waimai-enc而 Galaxy 插件默认只处理application/json和text/plain。解决方法Proxy → Options → Match and Replace → 点击插件右侧的齿轮图标 → 勾选 “Process requests with any content type”。5.4 CE 扫描卡死内存保护机制的对抗部分加固小程序如腾讯云加固会启用mprotect系统调用将密钥所在内存页设为PROT_READ只读导致 CE 的“内存写入断点”无法设置。现象是CE 提示 “Cannot set breakpoint on this address”。此时需用frida-trace辅助frida-trace -U -f com.xxx.app -i mprotect找到密钥内存页被设为只读的时机然后在 CE 中改用“内存读取断点”Read Breakpoint虽然效率略低但可绕过。5.5 Python 脚本中文乱码UTF-8 BOM 的隐形杀手decrypt.py中若包含中文注释如# 解密函数且文件保存为 UTF-8 with BOM 格式Galaxy 加载时会报SyntaxError: Non-UTF-8 code starting with \xff。解决方案用 VS Code 打开脚本 → 右下角点击编码如 “UTF-8 with BOM”→ 选择 “Save with Encoding” → “UTF-8”。5.6 密钥泄露风险如何安全存储 CE 提取的密钥CE 提取的密钥是敏感信息绝不能硬编码在 Galaxy 插件中提交到 Git。我们采用三级防护开发机密钥存于本地~/.crypto_keys/waimai.keydecrypt.py中用open(os.path.expanduser(~/.crypto_keys/waimai.key)).read().strip()读取交付包打包 ZIP 前用sed -i s/f0debc9a78563412/KEY_PLACEHOLDER/g decrypt.py替换密钥交付时附带密钥注入说明生产环境通过 Burp 的 Environment Variables 功能注入decrypt.py改为os.getenv(WAIMAI_KEY)。5.7 Galaxy 性能瓶颈高并发下的解密延迟当 Burp 处理大量请求如爬虫扫描时Python 脚本的 GIL全局解释器锁会导致解密延迟飙升。实测 100 QPS 下单个请求解密耗时从 12ms 涨至 210ms。优化方案在decrypt.py开头添加import sys; sys.setswitchinterval(0.001)降低线程切换间隔更彻底的方案是改用 Cython 编译核心解密函数但会增加交付复杂度。权衡之下我们选择限制 Burp 的并发请求数Proxy → Options → Connection pool size ≤ 20。6. 体系化落地如何将单次成功转化为团队标准作业流程SOP单个项目成功不等于体系建成。我们花了两个月将这套 CEGalaxy 方法论沉淀为红队 SOP核心是三个标准化6.1 标准化扫描模板把 CE 操作固化为可执行脚本手动点击 CE 太慢我们用 CE 的 Lua 脚本功能将三次扫描法封装为auto_find_key.lua-- auto_find_key.lua function start_scan() local addresses {} -- 第一次扫描全范围 16 字节 local first_result scanRegion(0x7f8b000000, 0x7f8c000000, 16, ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ??) -- 第二次扫描值已更改 local second_result rescan(changed) -- 第三次扫描值已更改 local third_result rescan(changed) -- 输出结果到文件 local f io.open(/sdcard/Download/key_candidates.txt, w) for i, addr in ipairs(third_result) do f:write(string.format(0x%x\n, addr)) end f:close() end队员只需在 CE 中加载此脚本点击“Run Script”30 秒内自动生成候选地址列表。这消除了人为操作误差将 CE 环节的耗时稳定控制在 2 分钟内。6.2 标准化 Galaxy 插件仓库Git 管理 版本控制所有 Galaxy 插件含config.json、decrypt.py、encrypt.py统一存入私有 Git 仓库/redteam/burp-galaxy-plugins按小程序域名分目录/redteam/burp-galaxy-plugins/ ├── waimai.com/ │ ├── config.json │ ├── decrypt.py │ └── encrypt.py ├── bankapp.cn/ │ ├── config.json │ ├── decrypt.py │ └── encrypt.py └── ...每次新增小程序执行git checkout -b feature/waimai_v1.2开发完成后 PR 合并。这样新人入职第一天git clone即可获得全部历史插件无需从零摸索。6.3 标准化交付物一份报告三份附件每次项目交付固定产出主报告PDF含渗透范围、加密算法识别结论、密钥提取过程摘要、加解密验证截图附件一Galaxy 插件 ZIP 包不含密钥密钥单独提供附件二CE 操作录屏 MP4含语音讲解时长 ≤ 5 分钟附件三verify.py自动化验证脚本及运行说明。客户技术负责人拿到后5 分钟内可完成插件安装与验证彻底告别“看不懂报告、不会用工具”的交接困境。这套 SOP 运行半年来团队平均单小程序加解密体系建设耗时从 3.2 天降至 0.7 天客户复购率提升 40%。它证明了一件事在攻防对抗中真正的“破局”不在于多高深的技术而在于把高门槛动作标准化、傻瓜化、可复制化。当你能把一次成功的 CE 定钥变成一个按钮就能执行的脚本能把一段 Python 解密逻辑变成一行命令就能部署的插件——你就已经站在了大多数人的前面。我在实际交付中发现客户最感激的不是你找到了多少漏洞而是你离开后他们的安全团队还能继续用这套方法自己干活。所以最后分享一个小技巧每次做完项目花 15 分钟把本次用到的 CE 扫描参数、Galaxy 配置项、Python 脚本关键修改点整理成一张 A4 纸大小的《速查备忘单》连同插件一起交付。这张纸往往比整份报告更被珍视。