4. Target 详解:Target 的概念与分类

好,咱们进入正题。上一章我们把 Avatar2 的框架搭起来了,也跑了个简单的例子。但说实话,那只是「能用」。真正要上手干活,你得把 Target 搞明白。

Target 是什么?说白了,它就是你要分析的那个「目标」在 Avatar2 里的抽象。你想想看,我们做固件逆向,目标可能是跑在 ARM 开发板上的路由器固件,也可能是某个 MIPS 架构的 IoT 设备。这些东西物理形态千差万别,但 Avatar2 通过 Target 这个统一接口,把它们都管起来了。

我个人习惯把 Target 理解成一个「虚拟的调试探针」。它不关心你背后是 QEMU 模拟器,还是真实的 GDB Server,它只负责一件事:让你能读写内存、设置断点、单步执行。

核心概念:Target 是 Avatar2 中所有分析操作的入口。没有 Target,你连内存都读不了。

4.1 Target 的分类

Avatar2 支持三种 Target,我按使用频率排个序:

Target 类型 后端引擎 典型场景 我的评价
QEMU Target QEMU(系统级/用户级) 全系统模拟,跑完整固件 最常用,功能最全
GDB Target GDB Server(真实硬件或模拟器) 连接真实开发板或已有调试环境 灵活,但依赖外部环境
Unicorn Target Unicorn Engine 轻量级代码片段执行,指令级分析 快,但功能有限

4.1.1 QEMU Target

这是我最常用的 Target。为什么?因为 QEMU 能模拟完整的硬件环境。你拿一个路由器固件,直接丢给 QEMU Target,它就能把整个系统跑起来。

我在项目中遇到过这样的情况:一个 VxWorks 的固件,死活找不到硬件调试接口。后来我用 QEMU Target 模拟了它的 CPU 和外设,直接在模拟环境里把漏洞点给定位了。嗯,这招救过我不少次。

from avatar2 import *

avatar = Avatar(arch=ARM_CORTEX_M3)
qemu_target = avatar.add_target(
    QemuTarget,
    name="my_firmware",
    cpu="cortex-m3",
    executable="/usr/bin/qemu-system-arm"
)

这里要注意,QEMU Target 有两种模式:系统级(system)和用户级(user)。系统级模拟整个板子,用户级只模拟一个进程。做固件逆向,99% 的情况用系统级。

4.1.2 GDB Target

GDB Target 是个好东西。它不自己模拟任何东西,而是通过 GDB 远程协议连接到一个已经运行的环境。

你想想看,如果你手里有一块真实的开发板,上面跑着 GDB Server,那 Avatar2 就能通过 GDB Target 直接操作它。我去年帮一个团队调试某款安防摄像头,就是用 GDB Target 连到他们的 JTAG 调试器上,直接在硬件上做动态分析。

gdb_target = avatar.add_target(
    GDBTarget,
    name="real_device",
    gdb_executable="/usr/bin/gdb-multiarch",
    host="192.168.1.100",
    port=2333
)

小技巧:GDB Target 也可以连到 QEMU 的 GDB stub 上。这样你既有 QEMU 的模拟能力,又能用 GDB 的调试接口。我经常这么干。

4.1.3 Unicorn Target

Unicorn Target 是最轻量的。它基于 Unicorn Engine,只做 CPU 指令级的模拟。没有外设,没有操作系统,就是一个裸 CPU。

什么时候用它?当你只需要执行一小段代码,比如解密某个函数、验证一个算法的时候。我曾经用它来跑一个加密算法的关键循环,速度比 QEMU 快了一个数量级。

unicorn_target = avatar.add_target(
    UnicornTarget,
    name="code_snippet",
    arch=ARM
)

注意:Unicorn Target 没有内存映射的概念。你需要手动设置内存区域。而且它不支持系统调用,所以别指望它能跑完整的 Linux 内核。

4.2 Target 的生命周期

每个 Target 都有明确的四个阶段:创建、配置、启动、停止。我刚开始用 Avatar2 的时候,经常搞混顺序,结果程序跑着跑着就崩了。后来我总结了一个口诀:「先创建,再配置,启动后才能干活,用完记得停」。

4.2.1 创建

创建就是告诉 Avatar2:「我要一个某某类型的 Target」。这一步很简单,调用 add_target() 就行。

target = avatar.add_target(
    QemuTarget,
    name="my_target"
)

创建的时候,你可以指定名字。这个名字后面会用到,比如在日志里区分不同的 Target。我习惯用固件的名字或者芯片型号来命名。

4.2.2 配置

配置是重头戏。不同的 Target 有不同的配置项,但有几个是通用的:

  • 架构:ARM、MIPS、x86 等
  • 内存映射:哪些地址范围是可读写的
  • 外设模型:对于 QEMU Target,需要指定外设
  • 调试接口:GDB Target 需要指定 host 和 port

我个人习惯在配置阶段就把内存映射写好。比如一个典型的 ARM 固件:

target.set_memory_range(0x00000000, 0x00100000, "flash")
target.set_memory_range(0x20000000, 0x20010000, "sram")
target.set_memory_range(0x40000000, 0x40001000, "peripherals")

嗯,这里要注意,内存映射的顺序会影响模拟器的行为。我建议把 Flash 放在最前面,因为大多数固件都是从 Flash 启动的。

4.2.3 启动

配置完了,就该启动了。调用 target.init() 或者 avatar.load_target() 都可以。

avatar.load_target(target)
# 或者
target.init()

启动之后,Target 就进入「运行」状态。这时候你可以读写内存、设置断点、单步执行。我曾经犯过一个错误:在启动之前就去读内存,结果返回的全是 0。后来才意识到,Target 没启动,内存还没映射好。

关键点:启动之后,Target 的 CPU 会停在复位向量处。你需要手动设置 PC 寄存器,或者让它跑起来。

4.2.4 停止

分析完了,记得停止 Target。这一步很多人会忘,尤其是调试到半夜的时候。

target.shutdown()
# 或者
avatar.shutdown()

停止之后,Avatar2 会释放所有资源。如果你用的是 QEMU Target,QEMU 进程会被杀掉。如果是 GDB Target,GDB 连接会被断开。

避坑指南:我曾经在循环里反复创建和销毁 Target,结果内存泄漏了。后来我改成复用同一个 Target,只在需要的时候重新配置。记住:创建 Target 开销很大,能复用就复用。

4.3 知识体系总览

说了这么多,我画了一张图,把 Target 的核心逻辑串起来。你看一眼就能明白整个体系:

Avatar2 Target 知识体系 Target 核心 QEMU Target GDB Target Unicorn Target 全系统模拟 跑完整固件 连接真实硬件 或已有调试环境 轻量级指令模拟 代码片段执行 生命周期:创建 → 配置 → 启动 → 停止 add_target() set_memory_range() 等 init() / shutdown() 图:Avatar2 Target 分类与生命周期总览

这张图把 Target 的分类和生命周期都串起来了。你从上往下看,先选类型,再走生命周期。每一步都有对应的 API 调用。

好了,Target 这块就讲这么多。记住:选对类型,走对流程,你的动态分析就成功了一半。剩下的,就是多练。

我的建议:刚开始学的时候,先死磕 QEMU Target。它最通用,资料也最多。等 QEMU Target 玩熟了,GDB Target 和 Unicorn Target 自然就上手了。


专注资料整理