做Android逆向的人,手头基本都有一套自己的流程:拿APK、解包、看manifest、翻dex、挂frida跑一遍。这套流程熟练之后,我心里一直冒出一个念头——能不能把它变成一段AI Agent能直接执行的逻辑?于是就有了“apk-reverse”这个项目:把Android逆向工程从“人手动点命令、人脑记上下文”变成一套可由AI Agent按步骤调用、自动决策、输出结构化报告的工作流。这篇文章就把这套东西完整拆开,包括模块设计、工具封装、Agent编排、还有我实际踩过的坑,给想玩“AI Agent + 逆向自动化”的朋友一份可以直接抄作业的参考。
我默认你会一点基础命令操作,但不需要你已经是逆向老兵。只要你知道APK大概是zip包、知道里面有个classes.dex,后面跟着做就行。我用的工具都是开源或社区常见的:apktool、jadx、frida、mitmproxy,再加上一个LLM接口(OpenAI兼容的、本地部署的都行)。Agent这套东西,我自己先用了Python写一套轻量编排器,后来又把同样逻辑抽到Dify里跑通了一遍,所以下面两种方式的实现都会提到。
1. 为什么要把 Android 逆向工程变成 AI Agent 工作流
1.1 传统逆向流程的重复劳动和思维断裂
逆向一个APK,常规动作差不多是固定的:先看包名、版本、签名,再用apktool解出资源文件和smali,用jadx反编译出Java代码,然后打开AndroidManifest.xml看权限和组件,接着在代码里搜关键字符串、找网络请求地址、找加密逻辑,最后想弄清某个行为时再上模拟器,用frida去hook。
这套动作本身不难,难在三点:
第一,大量命令是重复的。每换一个样本,都要重新执行一遍同样的解包命令、同样的搜索脚本。手动跑几遍还凑合,样本一多就烦了。
第二,分析过程中产生的“上下文”是断裂的。你静态分析时记在脑子里的关键点,等到动态分析时往往又要重新翻文件、找日志。人的短期记忆是有上限的,样本复杂一点,前面发现的线索就会忘。
第三,结果不好沉淀。分析完一个APK,最后可能只在脑子里留下一些模糊印象,过一周再问你这个样本有什么行为,你还得重新翻一遍。
这其实就是典型的“可标准化流程”。既然可标准化,那就应该写成代码。而AI Agent能做的,不只是执行这些命令,更重要的是它能把每一步的输出当作输入,自己决定“下一步该看什么”,最后把所有证据汇总成一份报告。这个价值非常大。
1.2 AI Agent 在这个场景里能承担什么角色
一开始我以为Agent只能是“按顺序执行几条命令”的脚本。真做进去才发现,它能做的比脚本多得多。
举个例子,脚本只能告诉你“AndroidManifest里有个权限是READ_PHONE_STATE”,但Agent可以结合代码上下文判断:它不光申请了这个权限,还在启动时把设备IMEI拼接进了一个请求参数,看起来是在做设备指纹采集。这种跨文件、跨工具的综合判断,以前的脚本做不了,现在大模型可以。
所以我在apk-reverse里给Agent定位成“项目总管”:我来定义工具,Agent来调用工具;我把标准检测规则写成固定脚本,Agent负责按分析目标组合这些脚本,并根据中间结果动态调整后续动作。比如静态分析发现样本动态加载了一个dex文件,Agent就会在动态分析阶段自动加上对DexClassLoader的hook;如果没发现,就跳过这个hook点。这种决策能力,比写一堆if-else靠谱得多。
1.3 我的技术选型与整体架构
先说一下最终采用的工具链和分层,免得后面看迷糊。
底层是标准逆向工具:
- apktool:解资源、解析smali、回编译测试
- jadx:把dex反编译成可读性更好的Java代码
- aapt/aapt2:提取包信息和manifest原始信息
- frida:动态插桩
- mitmproxy:网络抓包
- logcat/adb:系统和应用日志
中间层是我自己包的一层工具脚本,每个脚本只做一件事,输入输出都是JSON。这一步很关键,因为AI Agent调用工具时,最希望看到的是“结构化、可解析的结果”,而不是一段混着ANSI颜色码的终端输出。后面会详细讲。
上层是Agent编排器。我自己写了Python版的轻量ReAct循环;也把同样的一组工具封装成了HTTP服务,可以在Dify、Coze、n8n里拖流程节点调用。两种方案各有优劣,我在第三章会展开说。
整个工作流的状态是单向推进的:采集信息 -> 静态分析 -> 动态分析 -> 报告生成。中间允许根据结果回退或跳过,但整体骨架不要乱。把架构定成这样,是因为逆向分析本质上是一个“信息逐步收敛”的过程:先知道这APK是什么,再知道它里面有什么,再知道它运行时做了什么,最后才能下结论。
2. 核心模块拆解:从 APK 到结构化数据
2.1 APK 输入与元数据提取
一个APK文件拿到手,第一件事永远是确认“来源和指纹”。我一般先计算哈希,并保留原始文件名来源信息,防止后续分析时样本污染。这一步可以顺手记录文件大小、首次拿到时间、来源渠道,作为分析报告的基础元数据。
然后解析包信息。用aapt2最稳:
aapt2 dump badging sample.apk输出里关键字段包括包名(package name)、版本号、最低SDK版本、目标SDK版本、启动Activity等。Agent要从这段输出里提取的信息,最好由我提前解析好:
# tools/apk_info.py 片段 import subprocess, json, re def get_apk_basic_info(apk_path): out = subprocess.check_output(['aapt2', 'dump', 'badging', apk_path], text=True) result = { "package": re.search(r"package: name='([^']+)'", out).group(1), "version": re.search(r"versionName='([^']+)'", out).group(1), "min_sdk": re.search(r"sdkVersion:'(\d+)'", out).group(1), "target_sdk": re.search(r"targetSdkVersion:'(\d+)'", out).group(1), } return json.dumps(result, ensure_ascii=False)注意有些APK是分批zip、或者用了非常规压缩方式,aapt2会解析失败。这时退回到aapt dump badging,再不行就扔给apktool去解。
2.2 静态分析模块
静态分析是整个工作流的主体,我的模块分了四层。
第一层,资源与smali拆解:
apktool d -f -s sample.apk -o out/apktool-s表示不反编译dex成smali,只解资源。如果开头想快速看资源,可以加这个参数省时间;但Agent要判断代码逻辑时,还得完整解出smali。所以apktool我通常会完整跑一遍:
apktool d -f sample.apk -o out/apktool第二层,Manifest分析。APK解包后,AndroidManifest.xml已经是可读XML。Agent要重点看几个东西:
- 危险权限,比如READ_PHONE_STATE、RECORD_AUDIO、SEND_SMS
- 导出组件(exported=true)的Activity/Service/Receiver,这些是外部可直接触达的入口
- application的name,看看是否自定义Application类,那里常是初始化的地方
- 是否有
android:debuggable=true
我在这个模块里写了规则扫描,把这两百多行XML里值得留意的点抽出成JSON。比如发现某个Service被导出且没有权限保护,Agent就会在报告里标一个风险点。
第三层,代码反编译与敏感API扫描。jadx反编译整个工程比直接看smali要舒服:
jadx -d out/jadx sample.apk反编译完我会跑一个静态扫描,用grep和自定义正则去搜关键API调用,包括但不限于:
Runtime.exec/ProcessBuilder:执行系统命令DexClassLoader/PathClassLoader:动态加载dexCipher/AES:是否有加密逻辑Base64:可能在做数据编码传输Settings.Secure.getString+ANDROID_ID:设备指纹TelephonyManager.getDeviceId:IMEI获取NotificationListenerService:监听通知
扫描结果输出成类似下面这样:
{ "rule": "getDeviceId", "files": [ {"path": "sources/com/example/MainActivity.java", "line": 87}, {"path": "sources/com/example/App.java", "line": 42} ] }第四层,第三方SDK识别。这个我直接抄了社区的经验:维护一份常见SDK包名前缀列表,比如百度定位、腾讯bugly、友盟统计、个推、穿山甲广告等。Agent看到这些包名时就知道这个样本集成了哪些服务,也能判断某些隐私数据到底是谁采的。
2.3 动态分析模块
静态分析能给出“它可能做了什么”,但看不到“它实际做了什么”。所以我保留了动态分析模块,让Agent在拿到静态结论后决定是否执行。
环境这块,我用Android Studio的AVD起了几台不同Android版本的模拟器,ARM和x86各留一个,因为部分native库对ABI有要求。真机也能接,但模拟器更容易恢复快照。
动态分析最常用的动作是frida hook。我在项目里封了一个通用hook脚本,支持给某个类方法打印参数和调用栈:
# tools/frida_hook.py 片段 import frida, sys session = frida.get_usb_device().attach("com.example.app") script = session.create_script(""" Java.perform(function () { var Runtime = Java.use('java.lang.Runtime'); Runtime.exec.overload('java.lang.String').implementation = function (cmd) { console.log('[exec] ' + cmd); showStack(); return this.exec(cmd); }; }); """) script.load()除了hook,我还会在模拟器里提前装好mitmproxy的CA证书,把代理指向宿主机,这样样本发出去的每一个HTTP请求都会自动留档。这一步对识别数据外传特别有用:很多样本会把收集到的设备信息拼成JSON往固定域名上发。
动态分析阶段,Agent会把三方来源的证据合并:frida打印的调用栈、mitmproxy记录的流量、logcat里的运行日志。合并之后,它就能回答一些更实际的问题:这个应用启动时到底有没有向某个域名POST数据?数据内容是什么?
2.4 Agent 工具函数封装设计
我在前面强调过,Agent需要的是干净的结构化输入输出。所以apk-reverse里的每个模块脚本,都遵守以下约定:
- 输入:既有位置参数,也有可选参数,可选参数用
--key=value形式 - 输出:只输出JSON,禁止输出多余日志
- 错误:统一用非零退出码,并在stderr输出简短错误描述
- 状态:除了
ok/error,我还定义了timeout、skipped,让Agent知道不是所有工具都得永远成功
设计这些工具的初衷,是让“AI Agent调用工具”像“人调用命令”一样容易。工具多了以后,我会为每个工具生成一份JSON Schema,里面写好参数说明、用途、返回字段,然后在Prompt里把这些描述塞给Agent。
这套封装的工作量比想象中大,但绝对值得。因为一旦工具模块稳定了,后面不管换Agent框架还是换LLM,都不用动底层逻辑。
3. 把模块串成 AI Agent 可执行的工作流
3.1 工作流的两种实现方式
把上面的模块串起来,能走两条路:自研编排器,或者用现成工作流平台。
我一开始走的是自研,因为想搞清楚Agent在调用工具时到底需要哪些信息。核心循环就是经典的ReAct:Thought(想想现在缺什么信息)-> Action(调用哪个工具)-> Observation(看工具返回的JSON)-> 继续,直到收集的证据足够生成报告。
# agent_loop.py 简化版 tools = [apk_info, static_scan, dynamic_scan, generate_report] def run_agent(apk_path): messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": f"分析APK: {apk_path}"} ] max_steps = 10 for step in range(max_steps): resp = llm.chat(messages) action = parse_action(resp) if action["type"] == "call_tool": result = run_tool(action["tool"], action["params"]) messages.append({"role": "tool", "tool_call_id": action["id"], "content": result}) elif action["type"] == "finish": return resp return "达到最大步数,未完成"后来为了让不写代码的人也能调,我把同样的工具各包了一层HTTP接口,然后在Dify里搭了一条可视化工作流:
- 第一个节点:输入APK文件,调用
/apk_info拿元数据 - 第二个节点:条件判断,如果是恶意样本库里的样本,跳过动态分析;否则走静态分析
- 第三个节点:静态分析结果喂给LLM,让它判断是否有必要动态分析
- 第四个节点:动态分析工具节点
- 最后一个节点:LLM生成报告
Coze和n8n也是类似思路,只是节点名称和编排习惯略有不同。我个人的建议是:工具封装阶段先走自研,快速迭代;流程稳定后再根据需要把工具暴露成API挪到可视化平台上。直接一上来就去Dify里拖节点,调试工具细节会很痛苦。
3.2 Agent 的 Prompt 与工具描述设计
Agent能不能正确选择工具,很大程度取决于你给它的Prompt。我在系统提示里写了这几层内容:
- 角色定位:你是Android逆向分析助手,只分析APK,不执行其他操作。
- 可用工具列表:每个工具包含名称、用途、参数、返回值示例。
- 分析目标:默认目标是识别可疑行为,输出威胁评分和证据。
- 操作红线:不得对商业应用做破解、篡改、重打包;只做授权范围内的安全分析。
- 输出规范:最终报告用Markdown,包含摘要、静态分析发现、动态分析发现、威胁评分。
工具描述一定要写具体的“何时用”。不能只说“静态分析”,要说“当需要了解APK权限、组件、敏感API时调用static_scan;如果样本疑似加壳或存在未知native库,先调用apk_info查看基本信息,再调用static_scan”。这样Agent才不容易乱调。
3.3 可运行的 Agent 核心循环代码
我把自己线上的Agent循环简化一下,去掉跟具体LLM SDK绑定的部分,给你一个可以照着改的骨架:
# ai_agent_reverse 核心循环 import json import subprocess from typing import Dict, Optional def call_llm(messages) -> Dict: # 你的LLM接口封装,兼容OpenAI格式 # 返回 {"content": "...", "tool_calls": [...]} pass def run_tool(name: str, params: Dict) -> Optional[str]: if not validate_params(name, params): return json.dumps({"status": "error", "message": "参数不合法"}) cmd = build_cmd(name, params) proc = subprocess.run(cmd, capture_output=True, text=True, timeout=60) if proc.returncode != 0: return json.dumps({"status": "error", "stderr": proc.stderr[:500]}) return proc.stdout def agent_loop(apk_path: str): messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": f"开始分析: {apk_path}"} ] for step in range(8): resp = call_llm(messages) if resp.get("tool_calls"): for tool_call in resp["tool_calls"]: result = run_tool(tool_call["name"], json.loads(tool_call["arguments"])) messages.append({ "role": "tool", "tool_call_id": tool_call["id"], "content": result }) else: final_report = resp["content"] return final_report return None这套循环很简单,但对我的场景够用。固化到工具里的标准扫描规则已经把大量重复判断做掉了,Agent只需要在高一层做决策,不用每一步靠LLM硬想。
3.4 端到端运行效果
我用一个从团队样本库里拿到的“可能采集设备信息”的样本跑了一遍完整流程。这里只展示Agent最终报告里的威胁评分框架,方便你看懂输出长什么样:
## 样本报告 **基础信息** - 包名: com.example.suspicious - 版本: 2.1.0 - 目标SDK: 34 **静态分析发现** - 申请 READ_PHONE_STATE、READ_LOGS、SEND_SMS - 自定义 Application 初始化时调用 getDeviceId() - 发现 DexClassLoader 加载 /data/data/.../payload.dex **动态分析发现** - frida hook 确认 getDeviceId 返回值被拼入 POST 请求参数 - mitmproxy 捕获到向 `api.example-metrics.com` 发送设备信息 **威胁评分** 7.5 / 10全程我基本没看中间结果,Agent自己决定先跑静态、再判断需要动态、然后hook、最后写报告。这就是“可执行工作流”的真正意义。
4. 实战中的坑:常见问题与排查实录
4.1 静态分析阶段的坑
apktool 解码失败
常见原因是没有对应Android版本的framework资源。apktool在解析使用较新SDK编译的APK时,可能需要先安装框架:
apktool if framework-res.apk另外,如果APK被加固过,classes.dex会被替换成一个很小的壳dex,真正的代码在native层。这种情况Apktool能解出壳,但jadx反编译出的只是壳的代码。我的做法是先跑一遍apktool d,然后检查classes.dex文件大小:明显偏小(比如只有几百KB)就要打上“疑似加固”标签,不再走常规静态反编译。
jadx 内存溢出
大工程反编译几十万个类时,默认堆很容易爆。在脚本里显式给内存:
jadx -Xmx4g -d out/jadx sample.apk还是不够就改用jadx --deobf加混淆还原,但这一步会非常慢,我一般只在需要看某个关键类时才启用。
Manifest解析碰到的特殊字符
有些APK把XML做了特殊编码(AXML格式),直接用文本编辑器打开全是乱码。这时用apktool正常解包,解出来的Manifest就是可读的;如果apktool也失败,再试jadx --show-bad-code,让它强行反编译看看。
4.2 动态分析阶段的坑
frida 连接不上设备
模拟器通常是adb devices能看到设备,但frida attach时提示超时。我的排查顺序是:先确认frida-server版本和手机端frida版本一致;再检查模拟器的CPU架构,x86模拟器要下x86的frida-server;最后确认端口转发,真机需要adb forward tcp:27043 tcp:27043。
模拟器被检测
很多样本会检测模拟器特征,比如读/proc/cpuinfo、检查是否有goldfish、或者检测Build.FINGERPRINT。如果是做恶意样本行为分析,我一般会选一台root过的真机;如果一定要用模拟器,可以刷一下隐藏xposed模块。但这里要提醒一下:绕过检测手段本身是双刃剑,只建议用于样本分析环境,别用在普通应用或者任何非授权场景里。
TLS pinning 抓不到包
现在很多应用都做了证书固定,默认情况下mitmproxy只能看到TLS握手失败。之前我用一个frida脚本去Hook常见库的证书校验逻辑,让流量能正常解密。但这块脚本维护成本挺高,而且容易触发应用崩溃。我的建议是:优先看应用是否支持代理设置,再看能否在测试环境里关闭pin,最后才考虑用插桩去绕过。顺序很重要,上来就绕是给自己找麻烦。
4.3 Agent 编排阶段的坑
上下文超长
把一堆反编译代码、扫描结果全部塞给LLM,必然爆上下文。我踩过这个坑之后,改成“先摘要、再决策”的策略:静态扫描模块在输出前就对结果做了聚合,关键代码片段只截取前后各几十行,并且用规则提前过滤掉无关日志。这样每次调用LLM平均上下文控制在2K token左右,只有最后写报告时才可能用到长下文。
工具调用失败重试
Agent调工具不会永远一次成功,可能超时、可能命令打错。我在rerun工具里加了三个机制:
- 超时后自动重试两次,间隔指数退避
- 参数校验失败时返回错误信息而不是抛异常
- Agent看到工具返回error时,可以决定换一个参数重新调用
token 成本浪费
如果一个样本很简单,Agent还每一步都调用大模型,成本就上去了。我的办法是先跑一遍只做规则判断的快速扫描,把结果直接给Agent,同时告诉Agent“如果不确定,再调用动态工具”。这样信息密度高的路径一次就能走完,只有真正复杂的样本才会触发更多工具调用。
5. 安全边界与未来扩展
5.1 必须守住的合规红线
做这套工作流的初衷是为了提升安全研究和恶意样本分析效率,但同样一套工具,用来做什么完全看使用者。这里我必须把底线说清楚:
- 只分析你有权分析的应用,比如自己的App、公司授权测试的App、公开恶意样本。
- 不要用这套工具去绕过付费机制、破解商业App、篡改别人应用。
- 动态分析中的hook和抓包,只在隔离测试环境里做,不要对公共环境使用的应用做插桩。
我在Agent的Prompt里也固定了这些限制,但限制只能约束遵守规则的人。技术本身是中性的,写代码的开发者心里要有数。
5.2 可以继续扩展的方向
这套工作流跑通之后,我觉得有几个方向很值得接着做:
一是本地模型离线化。现在很多企业要求样本不能出内网,所以我把静态分析、特征提取全部留在本地,只在需要总结时调本地部署的Qwen或GLM模型。离线部署之后,吞吐低一些,但隐私和安全可控得多。
二是动态分析插件库。目前只封装了少数通用hook,未来可以把frida脚本做成插件仓库,让Agent按样本行为特征动态选择加载哪些脚本。比如发现样本用了某个加密算法,就自动加载对应的trace脚本。
三是把结果对接进威胁情报平台。每次分析生成的JSON报告,可以直接推送SIEM或自己的情报图谱里,这样历史样本可以自动关联,同一家族的变种也能靠相似度聚合出来。
从最开始手动敲命令,到现在一个Agent自动完成大部分工作,我的体会是:流程化比智能化更重要。把逆向动作拆成一个个“可描述、可调用、可验证”的工具,这一步做好,后面接再强的模型都顺利;反过来,工具一团乱麻,再聪明的Agent也没法帮你分析出个像样的结果。apk-reverse目前对我来说不是终点,它只是把Android逆向工程正式推进到了“自动决策型工作流”的起点。