
简介这是一份围绕APK静态与动态分析整理的Python项目包面向Android安全入门者、逆向分析学习者及需要评估应用风险的开发者旨在用脚本辅助完成权限检测、敏感API定位、资源提取与运行时行为观察。压缩包共46个文件、约280KB以20个Python脚本和20个dlist数据文件为主辅以Shell运行脚本、Markdown说明及敏感方法、dexdump相关数据分别承担解析分析、规则清单、环境执行与文档说明等用途。已有774人浏览学习。内容覆盖静态解析DEX与Manifest、混淆代码识别思路、动态hook与网络流量分析等场景读者可据此理解APK解压、签名验证、资源提取、调用关系梳理和敏感接口排查并参考目录组织搭建自己的分析流程。1. 拿到一个 APK先跑脚本还是先开模拟器静态先行、动态验证这是我做了几十个包之后固定下来的顺序。原因很实在动态分析的时间成本高得离谱模拟器启动、frida-server 推送、版本对齐、反调试对抗一套流程下来十几分钟就没了而静态分析往往三分钟就能告诉你这个包值不值得开设备。我的做法是先用 Python 加 androguard 跑一遍信息面把包名、权限、导出组件、明文流量开关、加固特征全部拉成一张表判断这个 APK 大概是什么档次再决定要不要上 hook。这篇内容面向正在做 APK 静态分析和动态分析的人不管你是刚接手一个自有应用的健壮性自查还是在实验室里拆一个授权样本下面这套顺序和脚本都能直接抄。2. 静态分析从解包到关键逻辑定位的四步2.1 三个工具各自管一段别指望一把梭APK 静态分析的工具有一堆但真正每天用的就三个而且它们的职责边界非常清楚。apktool 管资源和 smali它的价值在于保真反编译出来的 AndroidManifest.xml 是接近原文的 XMLres 目录里的布局、字符串、颜色都是可读的smali 目录里的字节码是一一对应的指令。你想确认某个字符串常量到底出现在哪、想改一个资源再回编译验证猜想只能靠 apktool。它的短板也很明显smali 读起来费眼睛一个几百行的 onCreate 能翻成上千行 smali人的脑子扛不住。jadx 管逻辑理解它把 dex 反编译成 Java 伪代码控制流、循环、try-catch 都能还原出来读业务逻辑的效率比 smali 高一个数量级。它的短板是遇到混淆和加固会大面积翻车出现// failed to decompile或者干脆给你一个空方法体这时候别急着骂工具很多情况下是代码根本不在这个 dex 里。androguard 管批量它是 Python 库把 APK 当成对象来读你可以写脚本一次性拉几百个包的权限、组件、签名、证书指纹做横向对比。它的短板是 API 在不同大版本之间改过网上抄来的代码经常 import 就报错用之前先确认自己装的是哪个版本。我一般这么排先用 androguard 跑批量体检拿到概览再用 apktool 解包确认 Manifest 原文和资源最后用 jadx 精读那两三个可疑的类。三步的顺序反过来也能做但效率差很多。# 解包-f 覆盖旧目录-o 指定输出--no-src 只看资源不解 smali apktool d -f -o out_smali app.apk # 只解资源速度更快适合先看 Manifest 和 strings.xml apktool d -f -o out_res --no-src app.apk # jadx 命令行导出 Java 伪代码--show-bad-code 保留反编译失败的占位 jadx -d out_jadx --show-bad-code app.apk # 只看某个包路径下的类减少噪音 jadx -d out_jadx --single-class com.example.target.net app.apk参数里--no-src是最容易被忽略的一个很多时候你只想确认 targetSdk 和 exported 组件没必要等它把 smali 全吐出来。--show-bad-code则相反它让 jadx 把失败的方法也保留成空壳你能看到方法签名和调用关系还在至少知道有这么个东西存在比整段消失强。2.2 Manifest 里这六项先过一遍能省掉一半动态工作Manifest 是 APK 的门牌号信息密度极高但新手容易从头看到尾然后什么都没记住。我固定看六项按重要性排第一是android:debuggable。它为 true 意味着你可以直接用 jdwp 附加调试动态分析的门槛瞬间降到最低为 false 也不代表没戏只是要上 hook。第二是android:allowBackup。这个开关决定了应用数据能不能被 adb backup 导出来很多应用的本地数据库、SharedPreferences 里的 token 就躺在那儿。第三是android:usesCleartextTraffic和networkSecurityConfig。前者为 true 说明允许明文 HTTP后者指向一个 XML 配置文件里面可能写了哪些域名豁免了证书校验。这两个字段是判断传输层健壮性的第一手依据。第四是 exported 组件。android:exportedtrue的 Activity、Service、Receiver、Provider 都是对外暴露的入口配合 intent-filter 看能直接推断出哪些组件可以被外部应用拉起。这部分是攻击面分析的核心写脚本批量统计比肉眼扫快得多。第五是权限清单。危险权限和自定义权限要分开看自定义权限的 protectionLevel 如果是 normal 或者 dangerous 而没做签名校验等于白送。第六是 minSdkVersion 和 targetSdkVersion。低 minSdk 往往意味着兼容老版本的加密算法和 TLS 配置高 targetSdk 则意味着系统层面的限制更多动态分析时要考虑分区存储、后台启动限制这些坑。注意Manifest 在编译后是二进制 XML直接用文本编辑器打开是乱码。要么走 apktool 反编译要么用 androguard 的get_android_manifest_xml()拿解析好的对象别硬啃二进制。2.3 定位关键逻辑靠的是关键词不是通读一个中型应用的 dex 动辄几万方法通读是不可能的。有效的做法是拿关键词去钓钓上来的再精读。我常用的几组关键词字符串层面搜 URL、域名、/api/、token、secret、key、password。这些常量在 dex 的字符串池里是明文存的即使代码混淆了字符串往往还在。apktool 解出来的 smali 里用grep -rn直接扫命中率很高。API 层面搜Cipher.getInstance、SecretKeySpec、IvParameterSpec、MessageDigest.getInstance、Mac.getInstance。这几个是 Java 层加密的必经之路找到调用点就能顺着参数看到算法、模式、填充方式以及密钥是从哪儿来的——是硬编码常量、是从 so 里返回、还是从服务端下发。网络层面搜OkHttpClient、Retrofit.Builder、HttpURLConnection、WebView的loadUrl。找到之后再去看有没有HostnameVerifier返回 true、有没有自定义X509TrustManager的空实现这两处是证书校验最常见的失效点。原生层面搜native关键字声明的方法。Java 层看到public static native byte[] a(byte[] b)这种签名就知道真正的逻辑在 .so 里静态分析到这里就该转向动态了。# 在 apktool 输出的 smali 目录里扫硬编码线索-i 忽略大小写-l 只列文件名 grep -rniE (secret|apikey|api_key|access_key)[\]?\s*[:] out_smali/ -l # 找加密 API 的调用点定位到具体 smali 文件后再用 jadx 看对应的 Java grep -rn Ljavax/crypto/Cipher;-getInstance out_smali/ | head -20 # 找证书校验的可疑实现 grep -rn X509TrustManager\|HostnameVerifier out_smali/ -l三条命令跑完基本能圈出三到五个文件。接下来的动作是拿类名去 jadx 里反查读出完整的调用链。这个来回切换的过程一开始很别扭做熟了就是肌肉记忆smali 负责定位jadx 负责理解。2.4 加固和混淆的识别特征判断要不要换打法不是所有包都能静态分析出东西来。有些包你用 jadx 打开主逻辑类全是几行的空壳方法体里只有一句return null这时候八成是被加固了。识别加固有几个稳定特征一是 Application 类被替换。Manifest 里android:name指向的那个 Application 类反编译出来只有几十行核心逻辑是反射调用另外一个不存在于当前 dex 的类。二是 assets 目录下有体积异常的文件名字可能是.bin、.dat、.so或者干脆没有扩展名几百 KB 到几 MB这些就是加密后的原始 dex。三是存在DexClassLoader、InMemoryDexClassLoader、BaseDexClassLoader的构造调用。四是 native 方法数量异常多且 .so 体积很大。混淆的识别更简单类名变成a.a.a、方法名变成a()、b()字符串被拆成多段拼接或者用简单的异或解密。混淆不影响你读逻辑只影响你读得爽不爽配合 jadx 的重命名功能快捷键 N手工标几个关键类后面就顺了。加固就不一样了静态分析能拿到的只有壳层真实代码要等运行时解密加载进内存。这时候正确的动作不是死磕 jadx而是转到动态在类加载完成之后从内存里把 dex 捞出来或者直接在运行时 hook 目标方法。判断标准很简单核心业务类的方法体普遍为空且存在 DexClassLoader 调用链就走动态。3. 动态分析从 logcat 到 hook 取证3.1 环境准备三件套和版本对齐动态分析的最小环境是三样一台能跑目标应用的设备真机或模拟器、adb、frida-server。前两样没什么可说的第三样是翻车重灾区。frida 的客户端你 pip 装的那个和 frida-server推送到设备上跑的那个二进制版本必须一致不一致的表现是frida-ps -U直接报unable to connect或者握手失败。这个错误的迷惑性在于它看起来像连接问题实际上是版本问题。所以每次换环境第一件事是frida --version和frida-server --version对一遍。推送和启动的流程固定# 1. 确认设备架构决定推哪个 frida-server 二进制 adb shell getprop ro.product.cpu.abi # 输出 arm64-v8a 就推 android-arm64 那个包 # 2. 推送到可执行目录并赋权 adb push frida-server-xx.x.x-android-arm64 /data/local/tmp/fs adb shell chmod 755 /data/local/tmp/fs # 3. 后台启动注意要 root 权限 adb shell su -c /data/local/tmp/fs # 4. 宿主机侧验证能列出进程列表就算通了 frida-ps -U | head第 3 步的su -c在非 root 设备上会失败这时候要么换一台能 root 的测试机要么退回到 logcat 和 dumpsys 这类零成本手段。模拟器方面带 root 的 AOSP 镜像比带 Google Play 的镜像好用后者通常不允许 remount。注意所有动态操作都应发生在你自己开发的应用、明确授权的测试样本或你有权处置的设备上。别人的应用不在这个范围里。3.2 logcat 和 dumpsys不写一行 hook 也能拿到大量信息很多人一上来就写 hook 脚本其实 logcat 里躺着的东西比想象中多。开发阶段遗留的调试日志、异常堆栈、第三方 SDK 的初始化信息全都会打出来。做法是先清空缓冲区再启动应用然后按关键字过滤。# 清空历史日志保证看到的都是本次运行产生的 adb logcat -c # 启动目标 Activity adb shell am start -n com.example.target/.ui.MainActivity # 抓日志并按关键字过滤-i 忽略大小写 adb logcat -v time | grep -iE crypto|http|token|sqlite|exception|leak-v time加上时间戳很关键因为它能让你把日志和界面操作对上。比如你点了登录按钮三秒后出现一条带 URL 的日志那基本就锁定了登录接口。dumpsys 是另一个免费的信息源。adb shell dumpsys activity top能看到当前栈顶 Activity 的完整视图树和 Intent 参数adb shell dumpsys package com.example.target能看到组件注册、权限授予、签名信息。这些信息的价值在于交叉验证——静态分析里你推测某个组件是导出的dumpsys 能直接确认。3.3 Frida hook 加密函数拿到明文的那一刀静态分析看到Cipher.getInstance(AES/CBC/PKCS5Padding)你知道了算法但密钥和明文还得靠动态。最直接的切入点是 hookjavax.crypto.Cipher的doFinal因为不管上层怎么封装最终都要走到这个方法。Frida 脚本基本长这样// hook_cipher.js —— 挂 Cipher.doFinal 的所有重载打印算法名与入参 Java.perform(function () { var Cipher Java.use(javax.crypto.Cipher); var JString Java.use(java.lang.String); // doFinal 有 byte[]、byte[],int,int 等多个重载逐个挂才不会漏 Cipher.doFinal.overloads.forEach(function (ov) { ov.implementation function () { var ret ov.call(this, arguments[0], arguments[1], arguments[2]); var algo this.getAlgorithm(); // 用 String 的构造把字节数组转成可读文本失败就打印长度 var plain ; try { plain JString.$new(arguments[0]).toString(); } catch (e) { plain [非文本]; } console.log([Cipher] algo algo in plain); return ret; }; }); });这段脚本有三个关键点。Java.perform保证在 Java 运行时就绪之后再执行 hook不加它脚本会在类加载前跑直接报错。overloads.forEach遍历所有重载因为doFinal至少有doFinal(byte[])、doFinal(byte[], int, int)、doFinal(byte[], int, int, byte[])这几个版本只挂一个必然漏数据。this.getAlgorithm()是实例方法能直接拿到当前 Cipher 实例注册的转换字符串比你在静态分析里猜要准确得多。启动方式有两种。attach 是应用已经跑起来之后再挂简单但会漏掉启动阶段执行的加密spawn 是先让应用挂起、注入脚本、再让它继续跑能抓到 Application 初始化阶段的所有调用。分析启动期的密钥协商必须用 spawn# 用 spawn 模式启动frida 会先挂起进程再注入 frida -U -f com.example.target -l hook_cipher.js --no-pause--no-pause表示注入完成后立即恢复运行不加的话进程会一直停在那里等你敲%resume。3.4 加固应用内存里捞 dex.so 里找入口被加固的包静态阶段只能看到壳。破局点在运行时——壳最终要调用DexClassLoader把真实 dex 加载进内存加载完成的那一刻真实代码就在进程里了。做法是在内存中扫描 dex 文件的魔数64 65 78 0A也就是 dex\n把匹配到的内存段 dump 下来。这个操作有现成的思路可以借鉴枚举进程的内存映射逐个区间读字节匹配魔数后按 dex 头部记录的 file_size 截断写出。整个过程可以在 Frida 脚本里完成也可以写一个独立的注入脚本。写出来的 dex 文件修复一下头部校验就能用 jadx 打开这时候看到的就是真实业务代码。如果关键逻辑在 native 层静态和动态的思路要换。先看 APK 里带了哪些 .so# 列出 APK 内所有原生库注意 32 位和 64 位是分开的目录 unzip -l app.apk | grep lib/ # 单独解出某个 so 看导出符号-D 显示动态符号表 unzip -o app.apk lib/arm64-v8a/* -d out_so/ readelf -D out_so/lib/arm64-v8a/libnative.so | grep -i FUNC.*GLOBAL32 位和 64 位要分开处理这是新手最常踩的坑设备是 arm64 的但应用可能优先加载 32 位的 so你在 64 位目录里分析半天运行时跑的根本不是这个文件。判断方法是在运行时打印/proc/pid/maps看实际加载的路径。4. 用 Python 把重复劳动压成一条流水线4.1 androguard 批量体检一次跑完几十个包单个包靠手工看没问题数量上去了就必须脚本化。androguard 的价值在于它把 APK 解析成了 Python 对象你可以用列表推导式处理几百个样本。# apk_triage.py —— 批量体检包名、版本、开关项、导出组件、危险权限 from androguard.core.apk import APK import csv import sys from xml.etree import ElementTree as ET # Manifest 里的属性都带这个命名空间前缀不写前缀取不到值 NS {http://schemas.android.com/apk/res/android} # 我只关心这几类高风险权限其余的不进报告避免噪音 DANGEROUS { android.permission.READ_EXTERNAL_STORAGE, android.permission.WRITE_EXTERNAL_STORAGE, android.permission.READ_SMS, android.permission.READ_CONTACTS, android.permission.ACCESS_FINE_LOCATION, android.permission.CAMERA, android.permission.RECORD_AUDIO, } COMPONENT_TAGS (activity, service, receiver, provider) def triage(path): apk APK(path) # get_android_manifest_xml 返回的是解析好的 Element可以直接 iter root apk.get_android_manifest_xml() app root.find(application) exported [] for tag in COMPONENT_TAGS: for node in root.iter(tag): if node.get(NS exported) true: exported.append(node.get(NS name)) return { 包名: apk.get_package(), 版本: apk.get_androidversion_name(), debuggable: app.get(NS debuggable) if app is not None else None, allowBackup: app.get(NS allowBackup) if app is not None else None, cleartext: app.get(NS usesCleartextTraffic) if app is not None else None, 导出组件数: len(exported), 导出组件: ;.join(exported[:8]), 危险权限: ;.join(sorted(set(apk.get_permissions()) DANGEROUS)), } if __name__ __main__: rows [triage(p) for p in sys.argv[1:]] writer csv.DictWriter(sys.stdout, fieldnameslist(rows[0].keys())) writer.writeheader() writer.writerows(rows)导入路径androguard.core.apk是 4.x 的写法老版本是androguard.core.bytecodes.apk报 ImportError 先查版本。NS这个前缀必须加因为get(exported)取不到值——Manifest 解析后所有 Android 属性都挂在{http://schemas.android.com/apk/res/android}命名空间下这是最容易卡住的一个点。app.get(...)在 application 节点不存在时会抛异常所以加了一层is not None兜底。导出组件只截前 8 个是因为有些应用几十个 Activity 全是导出的全打出来没法看。跑起来就是python apk_triage.py *.apk report.csv几十个包的横向对比一张表搞定按导出组件数排序问题最大的排在最前面。4.2 frida-python把 hook 结果落盘而不是刷屏用 frida 命令行跑脚本输出全在终端里滚过去就没了。要做分析就得落盘。frida 的 Python 绑定让你能在宿主机侧接收设备侧发来的消息直接写文件。# hook_to_file.py —— attach 目标进程把 hook 到的数据逐行写成 JSONL import frida import json import sys PKG com.example.target JS Java.perform(function () { var Cipher Java.use(javax.crypto.Cipher); Cipher.doFinal.overloads.forEach(function (ov) { ov.implementation function () { var ret ov.call(this, arguments[0], arguments[1], arguments[2]); send({ tag: cipher, algo: this.getAlgorithm(), len: arguments[0].length, data: Array.from(arguments[0]) // 转成 JS 数组才能跨进程传输 }); return ret; }; }); }); def on_message(message, data): if message[type] ! send: return # JSONL一行一条记录方便后续用 pandas 或 jq 处理 with open(hooks.jsonl, a, encodingutf-8) as f: f.write(json.dumps(message[payload], ensure_asciiFalse) \n) device frida.get_usb_device(timeout5) # spawn 而不是 attach保证启动期的加密调用也能抓到 pid device.spawn([PKG]) session device.attach(pid) script session.create_script(JS) script.on(message, on_message) script.load() device.resume(pid) sys.stdin.read() # 阻塞住CtrlC 退出Array.from(arguments[0])这行是必须的因为 Frida 的send只接受可 JSON 序列化的对象Java 的 byte[] 直接传会报错。转成普通数组之后宿主机侧收到的是整数列表再按需用bytes(data).decode()还原文本。device.spawn加device.resume的组合是抓启动期的唯一办法attach 只能看应用跑起来之后的事。JSONL 格式的好处是追加写不破坏结构脚本中途崩了前面的数据还在。4.3 正则扫描把 apktool 的输出当成文本语料apktool 解出来的 smali 是纯文本用正则扫比用任何工具都快。这招在批量排查硬编码密钥、内网地址、调试开关时特别有效。# scan_smali.py —— 扫 apktool 输出目录正则命中可疑常量 import re import pathlib # 每条规则一个名字一个正则命中就打印文件路径和行号 RULES { 疑似硬编码密钥: re.compile( rb(?i)(secret|apikey|api_key|access_key|aes_key)\s*[:]\s*[\][A-Za-z0-9_\-]{12,}[\] ), 疑似内网地址: re.compile( rbhttps?://(?:10\.|192\.168\.|172\.(?:1[6-9]|2\d|3[01])\.) ), 疑似调试开关: re.compile(rb(?i)build\.type\s*\s*[\]debug[\]), } root pathlib.Path(out_smali) for rule_name, pattern in RULES.items(): print(f {rule_name} ) hits 0 for f in root.rglob(*.smali): # 大文件跳过通常是被混淆的巨型方法扫了也没信息量 if f.stat().st_size 2 * 1024 * 1024: continue for i, line in enumerate(f.read_bytes().splitlines(), 1): if pattern.search(line): print(f{f}:{i}: {line.strip()[:120]}) hits 1 if hits 20: # 每个规则最多打 20 条防止终端被刷爆 break if hits 20: break正则用 bytes 编译前缀b因为read_bytes读出来的是 bytes混用 str 和 bytes 会抛 TypeError这是最常见的报错。rglob(*.smali)递归遍历所有子目录不用手写 os.walk。加 2MB 的文件大小上限是为了跳过被混淆工具生成的巨型方法那些文件里字符串全是拼接出来的正则扫不到有效信息还特别慢。三条规则跑完一般能捞出五到十条线索拿去 jadx 里逐个验证就行。5. 踩坑与排查五个让我返工的现场5.1 apktool 反编译报 resource not found现象apktool d app.apk跑到一半抛异常提示某个attr或者style找不到输出目录里只有半截文件。原因apktool 依赖一套框架资源framework-res.apk来解析属性引用目标应用用了比 apktool 内置框架更新的属性或者应用自己引用了系统私有的 attr。版本越新的应用越容易撞上。解决先升级 apktool 到最新版它的内置框架会跟着更新。如果还不行用--frame-path指定一个你自己从设备上拉下来的 framework-res.apk再不行就加-s跳过 smali 反编译先把资源解出来看 Manifestsmali 层改用 jadx 或者直接对 dex 用 baksmali 处理。5.2 jadx 大片 failed to decompile现象用 jadx 打开关键类的方法体显示// failed to decompile或者干脆是空的但方法签名还在调用关系也在。原因三种可能。一是代码被加固真实逻辑不在这个 dex 里二是用了 jadx 不支持的新字节码特性或者极端混淆比如控制流平坦化三是方法体确实就是空的比如接口或者被抽离到 native 的实现。解决先看 Application 类和 assets 目录确认是不是加固是的话转动态。不是加固就看 smaliapktool 解出来的 smali 一定是对的只是难读对着 jadx 的方法签名去 smali 里找同名方法逐条指令读。jadx 本身也有--decompilation-mode参数可以切换反编译引擎fallback 模式对畸形字节码更宽容。5.3 frida-ps -U 连不上或者版本不匹配现象frida-ps -U报Failed to enumerate processes: unable to connect to remote frida-server或者提示unexpectedly timed out。原因按出现频率排——frida-server 没启动、版本和客户端不一致、端口被占用、设备没 root 导致进程起不来、selinux 拦截。解决按顺序排查。adb shell ps | grep fs确认进程在不在adb shell /data/local/tmp/fs --version和宿主机frida --version对一遍换一个端口用fs -l 0.0.0.0:1234再frida -Hadb shell getenforce看是不是 Enforcing是的话临时改成 Permissive最后确认设备确实有 root。5.4 hook 时报 ClassNotFoundException现象Java.use(com.example.target.CryptoUtil)直接抛异常说找不到类。原因类名写错了混淆后类名可能是a.b.c、类还没被加载懒加载只有用到才 load、或者类被壳动态加载在另一个 ClassLoader 里。解决先用Java.enumerateLoadedClasses把当前已加载的类全打出来搜关键字。如果确实还没加载就把 hook 时机往后挪——hook Application 的onCreate或者某个已知一定会被调用 early 的方法在里面再执行Java.use。如果是多 ClassLoader 场景用Java.enumerateClassLoaders找到目标 loader再配合Java.classFactory.loader切换。5.5 应用一启动就闪退现象spawn 模式注入脚本后应用刚起来就退出logcat 里能看到SIGSEGV或者Abort message。原因应用带了反调试或者完整性校验检测到 frida 的特征进程名、端口、内存里的库名就主动退出。也有可能是 hook 的时机太早目标类还没初始化完就被改写触发了空指针。解决先确认是不是反调试用frida-ps -U看进程能不能起来起来后几秒内消失基本就是。对抗思路是延迟注入——先让应用跑起来等它过了检测窗口再 attach或者用Java.perform里套setTimeout把 hook 往后推几百毫秒。另一种情况是 hook 太早那就改成 attach 模式验证逻辑对不对确认无误再回到 spawn 并加延迟。6. 一条可复现的分析顺序先静态圈地再动态验证把上面所有东西串起来我实际执行的顺序是这样阶段动作产出判断分支一androguard 批量体检权限、组件、开关项表格导出组件多、cleartexttrue 优先深挖二apktool 解包看 Manifest组件清单、native 声明有 native 方法就标记待动态三jadx 读可疑类加密调用链、网络配置方法体为空则判定为加固四frida hook 验证算法、密钥来源、明文hook 不上则检查 ClassLoader五内存 dump 或 so 分析真实 dex 或原生入口仅在前三步都没结果时启用这个顺序的核心逻辑是成本递增第一步几秒钟第二步半分钟第三步几分钟第四步十几分钟第五步可能一小时。永远让便宜的步骤先跑用它的结果去决定要不要付昂贵的代价。我见过太多人拿到包直接开模拟器上 hook挂了两个小时发现目标方法压根不在这个 dex 里。验证环节我固定做一次闭环静态分析里得出登录请求用了 AES/CBC密钥硬编码在 CryptoUtil 类的静态字段里这个结论然后写一个 hook 脚本打印Cipher.doFinal的算法和入参跑一遍登录流程看输出是不是 AES/CBC、明文是不是和界面输入一致。对上了结论成立对不上说明静态分析的调用链读错了回去重读。最后留一个具体的技巧分析加固包时别急着上内存 dump先在 Frida 里Java.enumerateClassLoaders把所有 ClassLoader 打一遍看目标类到底被哪个 loader 加载。有时候壳只是换了 loader类名并没有混淆找到 loader 之后直接Java.use就能 hook比 dump 出几百个 dex 再逐个翻要快得多。回过头看我踩过最深的一次坑是拿着 64 位的 so 分析了整整一个下午最后发现应用运行时加载的是 32 位那个同名文件符号表完全对不上。从那以后我每次分析 native 层都强制先跑一遍adb shell cat /proc/pid/maps | grep lib确认实际加载路径再动手。希望帮到你。本文还有配套的精品资源点击获取