首页 / 资讯中心 / 文章详情

REA:AI Agent 驱动的从应用表象到二进制深处的全链路逆向追踪

REA:AI Agent 驱动的从应用表象到二进制深处的全链路逆向追踪 ★ FEATURED ARTICLE
1. 从应用表象到二进制深处REA 到底在解决什么问题第一次看到“REA”这个缩写是在一个做移动安全分析的小圈子里。有人丢了一张截图出来说他们用一套叫 REA 的流程把一个 App 从界面上的按钮一路追到了底层动态库的某个导出函数中间没有断链。当时我的第一反应是这不就是逆向工程里最理想的那种“全链路可追溯”吗做过逆向的人都知道从应用层往下走每过一层就掉一次线索Java 层到 Native 层断一次Native 层到二进制指令再断一次最后能追到具体函数的人要么是运气好要么是工具链极其成熟。REA 这个标题里的三个关键词——AI Agent、应用表象、二进制深处——其实已经把它的定位说得很清楚了。它不是某一个单点工具而是一套让 AI Agent 具备“跨层追踪能力”的方法论或框架。应用表象指的是我们能看到的东西界面、按钮、网络请求、日志、文件读写。二进制深处指的是我们看不到的东西so 库、Mach-O 文件、PE 结构、汇编指令、寄存器状态。中间隔着的是编译、混淆、加固、动态加载这几道墙。这套东西能做什么简单说它让一个 AI Agent 不再只是“帮你搜一下这个函数在哪”而是能自己规划路径先看应用层的行为推断可能涉及的模块再定位到具体的二进制文件最后在指令级别找到关键逻辑。解决的问题是分析链条断裂——传统工具各管一段IDA 管静态反汇编Frida 管动态插桩抓包工具管网络但没有人把它们串成一条自动化的线。适合谁看如果你做过移动端逆向、恶意代码分析、或者只是对“AI 怎么帮我做底层分析”感兴趣这篇内容应该能给你一些可以直接抄作业的思路。我会尽量把每一步为什么这么做讲清楚包括我踩过的坑和实测下来比较稳的参数配置。2. 整体架构设计为什么是“Agent 驱动”而不是“工具堆叠”2.1 传统逆向工作流的三个断点在聊 REA 的设计之前先说说传统流程为什么容易断。我自己的习惯是先用抓包工具看网络行为再用 Frida hook 一些关键 API然后把 so 库拖进 IDA 看静态逻辑。这个流程听起来很顺但实际操作中至少有三次“线索丢失”。第一次断点在应用层到 Native 层。Java 层调了一个 native 方法你知道方法名但不知道它对应哪个 so 库里的哪个函数。如果应用做了动态注册连方法名和函数地址的映射关系都是运行时才建立的静态看根本找不到。第二次断点在Native 层到二进制指令。你找到了 so 库也找到了导出函数但函数内部可能经过控制流平坦化、指令替换、虚假分支等混淆处理。IDA 的 F5 出来的伪代码可能完全不可读你得手动跟汇编。第三次断点在二进制指令到实际行为。你读懂了汇编知道它在做某种解密或校验但不知道输入是什么、输出是什么、在什么条件下触发。这时候需要动态调试但动态调试又要重新建立上下文。REA 的思路是用一个 AI Agent 来维护整个分析过程中的“上下文状态”。Agent 不是替代 IDA 或 Frida而是坐在它们上面决定下一步该调用哪个工具、传什么参数、拿到结果后怎么更新自己的认知。2.2 Agent 的核心能力拆解一个能跑通全链路的 Agent至少需要四种能力。我用一个表格来对比传统脚本和 REA 式 Agent 的差异能力维度传统自动化脚本REA 式 Agent路径规划固定流程线性执行根据中间结果动态调整下一步工具调用硬编码命令根据上下文选择工具和参数结果理解正则匹配关键词语义理解 结构化提取断点恢复失败即终止记录状态尝试替代路径这个对比不是要贬低脚本而是说明 Agent 的价值在于处理不确定性。逆向分析中充满了“这个函数看不懂”“这个地址跳不过去”“这个字符串解不出来”的情况脚本遇到这些就卡住了Agent 可以换一条路继续追。2.3 为什么选择“从表象到二进制”这个方向有人可能会问为什么不直接从二进制开始分析答案很简单——成本。一个 App 的二进制文件可能几十兆包含成千上万个函数。你不可能一上来就全部反汇编。应用层的行为是一个天然的过滤器用户点了哪个按钮、触发了哪个网络请求、写了哪个文件这些信息能帮你把注意力缩小到几个模块。REA 的设计哲学是“由外向内逐层收敛”。先看表象形成假设再用假设去指导二进制分析分析结果反过来验证或修正假设。这个循环跑几轮就能从“这个 App 大概在干什么”收敛到“这个函数在校验某个字段”。3. 核心细节解析从应用表象到二进制的四层追踪3.1 第一层应用表象采集与行为建模应用表象的采集听起来简单但要做细不容易。我一般分三个维度来采集交互维度界面元素、按钮点击、页面跳转。Android 上用uiautomator dump或者 Accessibility ServiceiOS 上用 XCUITest 的层级描述。目的是建立“用户操作 → 界面响应”的映射。网络维度请求 URL、参数、响应体、加密方式。抓包工具很多但关键是把请求和触发它的界面操作关联起来。我习惯在抓包工具里给每个请求打时间戳然后和界面操作日志对齐。系统调用维度文件读写、进程创建、动态库加载。Android 上看/proc/[pid]/maps和straceiOS 上看dtrace或者fs_usage。这一层能告诉你 App 在运行时加载了哪些 so 库这是后续追踪的关键线索。采集完之后Agent 需要把这些原始数据整理成结构化的“行为图”。节点是操作、请求、文件、库边是它们之间的因果关系。这个图不需要很精确但必须能回答一个问题如果我想追某个行为应该从哪个二进制文件入手注意采集阶段不要贪多。我试过把所有系统调用都记录下来结果数据量爆炸Agent 处理不过来。后来改成只记录和网络、文件、动态加载相关的调用效率高了很多。3.2 第二层Native 层入口定位有了行为图下一步是找到 Native 层的入口。这里分两种情况静态注册的 JNI 方法方法名和 so 库的对应关系在编译时就确定了。用javap或者nm就能看到。Agent 可以直接根据方法名去 so 库里搜符号。动态注册的 JNI 方法应用在JNI_OnLoad里调用RegisterNatives来注册。这时候符号表里可能只有JNI_OnLoad具体的方法映射是运行时才建立的。Agent 需要动态插桩hookRegisterNatives把映射关系 dump 出来。我实测下来动态注册的情况在加固应用里非常普遍。一个比较稳的做法是用 Frida 脚本 hookRegisterNatives把(类名, 方法名, 函数地址)三元组打印出来。这个脚本很短但能解决大问题// Frida hook RegisterNatives 示例 var RegisterNatives Module.findExportByName(null, RegisterNatives); if (RegisterNatives) { Interceptor.attach(RegisterNatives, { onEnter: function(args) { var env args[0]; var clazz args[1]; var methods args[2]; var count args[3].toInt32(); // 遍历 methods 数组提取 name 和 fnPtr for (var i 0; i count; i) { var namePtr methods.add(i * Process.pointerSize * 3).readPointer(); var sigPtr methods.add(i * Process.pointerSize * 3 Process.pointerSize).readPointer(); var fnPtr methods.add(i * Process.pointerSize * 3 Process.pointerSize * 2).readPointer(); console.log(Registered: namePtr.readCString() - fnPtr); } } }); }拿到函数地址后Agent 需要把这个地址映射回具体的 so 库和偏移。用/proc/[pid]/maps就能做到找到地址落在哪个模块区间然后计算地址 - 模块基址 文件偏移。3.3 第三层二进制文件解析与函数定位拿到文件偏移后Agent 需要解析二进制文件找到对应的函数。这一步的难点在于不同平台的文件格式不一样。Android 是 ELFiOS 是 Mach-OWindows 是 PE。Agent 需要根据平台选择对应的解析器。我一般用lief这个库来做跨平台解析它支持 ELF、Mach-O、PEAPI 也比较统一。下面是一个用lief解析 ELF 并查找符号的示例import lief # 加载二进制文件 binary lief.parse(target.so) # 如果是 ELF遍历符号表 if binary.format lief.Binary.FORMATS.ELF: for symbol in binary.symbols: if symbol.name target_function: print(fFound: {symbol.name} at 0x{symbol.value:x}) # 进一步获取函数所在的节区 section binary.section_from_virtual_address(symbol.value) if section: print(fSection: {section.name})如果符号被剥离了就需要用函数签名或者字符串引用来定位。Agent 可以结合 IDA 的批量反汇编结果或者用radare2的aaa分析后导出函数列表。我比较喜欢用radare2的命令行模式因为它的脚本化能力很强# 用 radare2 分析并导出函数列表 r2 -A -q -c afl target.so functions.txtafl命令会列出所有识别到的函数包括地址、大小、名称。Agent 拿到这个列表后可以根据地址范围判断目标函数是哪个。3.4 第四层指令级追踪与语义还原到了指令级就是最硬核的部分了。Agent 需要做两件事追踪数据流和还原语义。数据流追踪的目的是回答“这个值从哪来、到哪去”。比如你发现某个函数在比较两个字符串你需要知道这两个字符串是怎么生成的。这时候可以用动态插桩在关键指令处打印寄存器和内存状态。Frida 的Stalker模块可以做指令级追踪但性能开销很大一般只用于小范围。语义还原更依赖 Agent 的推理能力。我试过让 Agent 读汇编然后生成伪代码描述。效果取决于指令的复杂程度简单的算术和跳转没问题遇到间接跳转和自修改代码就很容易出错。一个折中的做法是Agent 只负责标注关键指令比如call、cmp、test把详细的语义还原交给 IDA 的 F5 或者 Ghidra 的反编译器。实操心得指令级追踪不要一上来就全量跑。我一般先用静态分析缩小范围找到最可能的几个函数再对这些函数做动态追踪。全量追踪一个中型 so 库Frida 的 Stalker 能把应用卡死。4. 实操过程搭建一个最小可用的 REA 流程4.1 环境准备与工具选型要跑通一个最小可用的 REA 流程你需要以下工具。我列了一个表包括选型理由和替代方案工具用途选型理由替代方案Frida动态插桩脚本化能力强社区活跃Xposed仅 Androidradare2静态分析命令行友好适合自动化IDA Pro商业、Ghidralief二进制解析跨平台Python API 简洁pyelftools、macholibmitmproxy网络抓包可编程支持脚本扩展Charles、FiddlerPython 3.10胶水语言生态丰富适合做 Agent 逻辑-Agent 本身可以用任何语言写我选 Python 是因为上面这些工具都有 Python 绑定。如果你习惯用 JavaScript也可以用 Node.js 调 Frida 的 API。4.2 第一步采集应用行为并生成行为图假设我们要分析一个模拟的 App叫TargetApp。先启动抓包和界面采集# 启动 mitmproxy把流量保存到文件 mitmproxy -w traffic.mitm --set save_stream_filetraffic.log # Android 上采集界面层级 adb shell uiautomator dump /sdcard/ui.xml adb pull /sdcard/ui.xml然后手动操作 App触发你想要分析的功能。操作完成后把流量和界面日志对齐。我一般写一个简单的 Python 脚本把 mitmproxy 的流量日志和 uiautomator 的 XML 按时间戳合并import json from datetime import datetime def align_traffic_and_ui(traffic_log, ui_xml): # 解析流量日志提取请求时间、URL、参数 requests [] with open(traffic_log, r) as f: for line in f: entry json.loads(line) requests.append({ time: entry[timestamp], url: entry[request][url], method: entry[request][method] }) # 解析 UI XML提取界面元素 # 这里省略 XML 解析细节假设得到一个元素列表 ui_elements parse_ui_xml(ui_xml) # 按时间窗口对齐 aligned [] for req in requests: # 找请求时间前后 2 秒内的界面操作 nearby_ui [e for e in ui_elements if abs(e[time] - req[time]) 2.0] aligned.append({request: req, ui: nearby_ui}) return aligned这个对齐结果就是行为图的雏形。Agent 可以从中提取出“哪个界面操作触发了哪个网络请求”的映射。4.3 第二步定位 Native 入口并 dump 映射假设行为图显示某个请求的参数是加密的加密逻辑很可能在 Native 层。用 Frida 附加到目标进程hookRegisterNatives和常见的加密函数// hook 常见的加密库函数 var targets [ AES_encrypt, DES_encrypt, MD5_Update, SHA256_Update ]; targets.forEach(function(name) { var addr Module.findExportByName(null, name); if (addr) { Interceptor.attach(addr, { onEnter: function(args) { console.log(Called: name); // 打印参数具体取决于函数签名 } }); } });运行后如果看到AES_encrypt被调用就说明加密逻辑在某个 so 库里。然后用/proc/[pid]/maps找到这个函数的模块和偏移# 在 Android 上查看进程的内存映射 adb shell cat /proc/$(pidof com.target.app)/maps | grep \.so把 Frida 打印的函数地址和 maps 里的模块区间对比就能确定是哪个 so 库、哪个偏移。4.4 第三步静态解析二进制并定位函数拿到 so 库和偏移后用lief和radare2做静态分析import lief import subprocess def analyze_so(so_path, target_offset): # 用 lief 解析 binary lief.parse(so_path) # 找到偏移对应的节区 section binary.section_from_offset(target_offset) if section: print(fOffset 0x{target_offset:x} is in section: {section.name}) # 用 radare2 反汇编目标函数 # 先找到包含目标偏移的函数 result subprocess.run( [r2, -A, -q, -c, faf 0x{target_offset}; pdf, so_path], capture_outputTrue, textTrue ) print(result.stdout) analyze_so(libtarget.so, 0x1234)af命令让 radare2 在目标地址处分析一个函数pdf打印反汇编。Agent 拿到反汇编后可以进一步分析指令模式比如查找cmp指令来定位比较逻辑。4.5 第四步动态验证与语义标注静态分析给出假设后用动态插桩验证。比如你怀疑某个函数在做字符串比较可以在比较指令处 hook// hook 目标地址的比较指令 var targetAddr Module.findBaseAddress(libtarget.so).add(0x1234); Interceptor.attach(targetAddr, { onEnter: function(args) { // 打印寄存器状态 console.log(R0: this.context.r0); console.log(R1: this.context.r1); // 如果寄存器指向字符串打印字符串内容 try { console.log(String: this.context.r0.readCString()); } catch (e) {} } });运行后如果看到两个字符串被比较就能确认这个函数的作用。Agent 可以把这些动态验证的结果标注到静态分析的结果上形成“指令 → 语义”的映射。5. 常见问题与排查技巧实录5.1 动态注册找不到符号怎么办这是最常见的问题。应用用了动态注册符号表里只有JNI_OnLoad。解决办法是 hookRegisterNatives把映射关系 dump 出来。但有些应用会自己实现注册逻辑不调用标准的RegisterNatives。这时候需要更底层的方法hookdlopen和dlsym看应用加载了哪些库、查找了哪些符号。// hook dlopen 和 dlsym var dlopen Module.findExportByName(null, dlopen); var dlsym Module.findExportByName(null, dlsym); if (dlopen) { Interceptor.attach(dlopen, { onEnter: function(args) { console.log(dlopen: args[0].readCString()); }, onLeave: function(retval) { console.log(Handle: retval); } }); } if (dlsym) { Interceptor.attach(dlsym, { onEnter: function(args) { console.log(dlsym: args[1].readCString()); }, onLeave: function(retval) { console.log(Address: retval); } }); }5.2 反汇编结果不可读怎么办混淆是常态。我遇到过控制流平坦化、指令替换、虚假分支等各种手段。一个比较通用的应对策略是先动态后静态先用动态插桩记录实际执行的指令序列再把这个序列和静态反汇编对比忽略那些从未执行的虚假分支。Frida 的Stalker可以记录指令序列但开销大。我一般只对关键函数开启// 对特定函数开启 Stalker var targetFunc Module.findBaseAddress(libtarget.so).add(0x1234); Stalker.follow(Process.getCurrentThreadId(), { events: { call: true, ret: false, exec: false }, onReceive: function(events) { // 解析 events打印调用的函数地址 var parsed Stalker.parse(events); parsed.forEach(function(event) { console.log(Call to: event[2]); }); } });5.3 Agent 推理出错怎么回滚Agent 不是万能的它可能基于错误假设走到死胡同。我的做法是给 Agent 加一个“置信度”机制每个结论都有一个置信度分数低于阈值的结论不用于后续推理。如果某条路径连续失败Agent 自动回滚到上一个高置信度的节点尝试替代路径。这个机制实现起来不复杂用一个栈来记录分析状态就行class AnalysisState: def __init__(self): self.stack [] self.confidence_threshold 0.7 def push(self, conclusion, confidence): if confidence self.confidence_threshold: self.stack.append({ conclusion: conclusion, confidence: confidence }) return True return False def rollback(self): if self.stack: return self.stack.pop() return None5.4 常见问题速查表问题现象可能原因排查方法解决思路Frida 附加失败应用有反调试检查frida-server是否运行尝试spawn模式用frida-gadget注入符号表为空二进制被剥离readelf -s查看符号表用字符串引用或函数签名定位反汇编乱码混淆或加密对比动态执行序列先动态后静态忽略虚假分支抓包无数据证书校验检查 SSL Pinning用 Frida hook 证书校验函数Agent 卡死数据量过大检查日志输出缩小分析范围增加过滤条件避坑技巧Frida 的版本要和frida-server的版本匹配否则会出现各种奇怪的错误。我一般用pip install frida-tools安装最新版然后去官方仓库下载对应版本的frida-server。6. 影响范围与扩展思路REA 这套思路不只适用于移动端逆向。任何需要“从高层行为追踪到低层实现”的场景都可以套用这个框架。比如Web 前端到后端从页面交互追踪到 API 实现再到数据库查询。IoT 设备分析从 App 控制指令追踪到固件中的处理逻辑。游戏逆向从游戏行为追踪到内存中的数据结构。核心逻辑是一样的用高层行为缩小范围用低层分析验证假设用 Agent 维护上下文。我实测下来这套流程能把分析效率提升至少三倍尤其是面对加固和混淆的应用时优势更明显。后续可以扩展的方向包括让 Agent 自动生成 Frida 脚本、自动识别加密算法、自动生成分析报告。这些我都还在摸索中等跑通了再分享。
阅读完成 · 觉得有帮助?
咨询建站