☰
REA:AI Agent 驱动的从应用表象到二进制深处的全链路逆向追踪
2026/10/20 22:21:00 网站建设 项目流程

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 Service,iOS 上用 XCUITest 的层级描述。目的是建立“用户操作 → 界面响应”的映射。
  • 网络维度:请求 URL、参数、响应体、加密方式。抓包工具很多,但关键是把请求和触发它的界面操作关联起来。我习惯在抓包工具里给每个请求打时间戳,然后和界面操作日志对齐。
  • 系统调用维度:文件读写、进程创建、动态库加载。Android 上看/proc/[pid]/maps和strace,iOS 上看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 是 ELF,iOS 是 Mach-O,Windows 是 PE。Agent 需要根据平台选择对应的解析器。

我一般用lief这个库来做跨平台解析,它支持 ELF、Mach-O、PE,API 也比较统一。下面是一个用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(f"Found: {symbol.name} at 0x{symbol.value:x}") # 进一步获取函数所在的节区 section = binary.section_from_virtual_address(symbol.value) if section: print(f"Section: {section.name}")

如果符号被剥离了,就需要用函数签名或者字符串引用来定位。Agent 可以结合 IDA 的批量反汇编结果,或者用radare2的aaa分析后导出函数列表。我比较喜欢用radare2的命令行模式,因为它的脚本化能力很强:

# 用 radare2 分析并导出函数列表 r2 -A -q -c "afl" target.so > functions.txt

afl命令会列出所有识别到的函数,包括地址、大小、名称。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(仅 Android)
radare2静态分析命令行友好,适合自动化IDA Pro(商业)、Ghidra
lief二进制解析跨平台,Python API 简洁pyelftools、macholib
mitmproxy网络抓包可编程,支持脚本扩展Charles、Fiddler
Python 3.10+胶水语言生态丰富,适合做 Agent 逻辑-

Agent 本身可以用任何语言写,我选 Python 是因为上面这些工具都有 Python 绑定。如果你习惯用 JavaScript,也可以用 Node.js 调 Frida 的 API。

4.2 第一步:采集应用行为并生成行为图

假设我们要分析一个模拟的 App,叫TargetApp。先启动抓包和界面采集:

# 启动 mitmproxy,把流量保存到文件 mitmproxy -w traffic.mitm --set save_stream_file=traffic.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(f"Offset 0x{target_offset:x} is in section: {section.name}") # 用 radare2 反汇编目标函数 # 先找到包含目标偏移的函数 result = subprocess.run( ['r2', '-A', '-q', '-c', f'af @ 0x{target_offset}; pdf', so_path], capture_output=True, text=True ) 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 None

5.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 脚本、自动识别加密算法、自动生成分析报告。这些我都还在摸索中,等跑通了再分享。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询