二进制逆向动态分析(Phase 4)
目的
观察实际的运行时行为。验证静态分析的假设。捕获仅在执行期间可见的数据。
人工介入要求
CRITICAL: 所有执行都需要人工批准。
在运行任何二进制文件之前:
- 确认沙箱配置是否可接受
- 如有需要,验证网络隔离
- 记录将尝试执行的内容
- 获取明确的批准
平台支持矩阵
| 主机平台 | 目标架构 | 方法 | 复杂度 |
|---|---|---|---|
| Linux x86_64 | ARM32/64, MIPS | 原生 qemu-user |
低 |
| Linux x86_64 | x86-32 | 原生或 linux32 |
低 |
| macOS (任何) | ARM32/64 | Docker + binfmt | 中等 |
| macOS (任何) | x86-32 | Docker --platform linux/i386 |
中等 |
| Windows | 任何 | WSL2 → Linux 方法 | 中等 |
macOS Docker 设置(一次性)
# 启动 Docker 运行时(Colima, Docker Desktop 等)
colima start
# 注册 ARM 仿真处理器(需要特权模式)
docker run --rm --privileged --platform linux/arm64 \
tonistiigi/binfmt --install arm
Docker 挂载最佳实践
CRITICAL: 在 Colima 上,/tmp 挂载经常默默失败。始终使用家目录路径:
# ✅ 好 - 使用家目录
docker run -v ~/code/samples:/work:ro ...
# ❌ 坏 - /tmp 挂载在 Colima 上可能失败
docker run -v /tmp/samples:/work:ro ...
分析选项
| 方法 | 隔离 | 粒度 | 最适合 |
|---|---|---|---|
| QEMU -strace | 高 | 系统调用级别 | 初始行为映射 |
| QEMU + GDB | 高 | 指令级别 | 详细调试 |
| Docker | 高 | 进程级别 | 在 macOS 上跨架构 |
| Frida | 中等 | 函数级别 | 无需重新编译即可钩子 |
| 设备上 | 低 | 全系统 | 当仿真失败时 |
选项 A: QEMU 用户模式与系统调用跟踪
最安全的方法 - 在隔离中运行并记录系统调用日志。
设置
# 验证 sysroot 存在
ls /usr/arm-linux-gnueabihf/lib/libc.so*
# ARM 32位执行
qemu-arm -L /usr/arm-linux-gnueabihf -strace -- ./binary
# ARM 64位执行
qemu-aarch64 -L /usr/aarch64-linux-gnu -strace -- ./binary
Sysroot 选择
| 二进制 ABI | Sysroot 路径 | QEMU 标志 |
|---|---|---|
| ARM glibc 硬浮点 | /usr/arm-linux-gnueabihf |
-L |
| ARM glibc 软浮点 | /usr/arm-linux-gnueabi |
-L |
| ARM64 glibc | /usr/aarch64-linux-gnu |
-L |
| ARM musl | 需要自定义提取 | -L |
环境控制
# 设置环境变量
qemu-arm -L /sysroot \
-E HOME=/tmp \
-E USER=nobody \
-E LD_DEBUG=bindings \
-- ./binary
# 取消危险变量
qemu-arm -L /sysroot \
-U LD_PRELOAD \
-- ./binary
系统调用分析
Strace 输出模式要关注:
# 网络活动
openat.*socket
connect(.*AF_INET
sendto|send|write.*socket
recvfrom|recv|read.*socket
# 文件访问
openat.*O_RDONLY.*"/etc
openat.*O_WRONLY
stat|lstat.*"/
# 进程操作
execve
fork|clone
选项 B: QEMU + GDB 进行深度调试
附加调试器以进行指令级控制。
在 GDB 下启动二进制文件
# 以 GDB 服务器启动 QEMU
qemu-arm -g 1234 -L /usr/arm-linux-gnueabihf ./binary &
# 使用 gdb-multiarch 连接
gdb-multiarch -q \
-ex "set architecture arm" \
-ex "target remote :1234" \
-ex "source ~/.gdbinit-gef.py" \
./binary
GDB 逆向工程命令
# 断点
break *0x8400 # 地址
break main # 符号
break *0x8400 if $r0 == 5 # 条件
# 执行控制
continue # 运行直到断点
stepi # 单步指令
nexti # 跳过调用
finish # 运行直到返回
# 检查
info registers # 所有寄存器
x/20i $pc # 从 PC 反汇编
x/10wx $sp # 栈内容
x/s 0x12345 # 地址处的字符串
# 内存
find 0x8000, 0x10000, "pattern" # 搜索内存
dump memory /tmp/mem.bin 0x8000 0x9000 # 提取区域
GEF 增强功能
使用 GEF 加载后,附加命令:
gef> vmmap # 内存布局
gef> checksec # 安全特性
gef> context # 全状态显示
gef> hexdump qword $sp 10 # 更好的十六进制转储
gef> pcustom # 结构定义
批量调试脚本
# 创建 GDB 脚本
cat > analyze.gdb << 'EOF'
set architecture arm
target remote :1234
break main
continue
info registers
x/20i $pc
continue
quit
EOF
# 运行批量
gdb-multiarch -batch -x analyze.gdb ./binary
选项 C: Frida 用于函数钩子
无需修改二进制文件即可拦截函数调用。
⚠️ 架构限制: Frida 需要原生架构执行。它 不能 附加到 QEMU-user 目标。
| 场景 | 可行? | 替代方案 |
|---|---|---|
| 本机二进制(x86_64 上的 x86_64) | ✅ | - |
| QEMU-user 下的跨架构 | ❌ | 使用设备上的 frida-server |
| 本机架构容器下的 Docker | ✅ | - |
| 模拟的跨架构 Docker | ❌ | 使用设备上的 frida-server |
对于跨架构 Frida,将 frida-server 部署到目标设备:
# 在目标设备上:
./frida-server &
# 在主机上:
frida -H device:27042 -f ./binary -l hook.js --no-pause
基本钩子
// hook_connect.js
Interceptor.attach(Module.findExportByName(null, "connect"), {
onEnter: function(args) {
console.log("[connect] Called");
var sockaddr = args[1];
var family = sockaddr.readU16();
if (family == 2) { // AF_INET
var port = sockaddr.add(2).readU16();
var ip = sockaddr.add(4).readByteArray(4);
console.log(" Port: " + ((port >> 8) | ((port & 0xff) << 8)));
console.log(" IP: " + new Uint8Array(ip).join("."));
}
},
onLeave: function(retval) {
console.log(" Return: " + retval);
}
});
# 使用 Frida 运行
frida -f ./binary -l hook_connect.js --no-pause
跟踪所有库调用
// trace_libcurl.js
var libcurl = Process.findModuleByName("libcurl.so.4");
if (libcurl) {
libcurl.enumerateExports().forEach(function(exp) {
if (exp.type === "function") {
Interceptor.attach(exp.address, {
onEnter: function(args) {
console.log("[" + exp.name + "] called");
}
});
}
});
}
内存检查
// dump_memory.js
var base = Module.findBaseAddress("binary");
console.log("Base: " + base);
// 转储区域
var data = base.add(0x1000).readByteArray(256);
console.log(hexdump(data, { offset: 0, length: 256 }));
选项 D: 基于 Docker 的跨架构(macOS)
当原生 QEMU 不可用时,使用 Docker 进行跨架构执行。
macOS 上的 ARM32 二进制文件
docker run --rm --platform linux/arm/v7 \
-v ~/code/samples:/work:ro \
arm32v7/debian:bullseye-slim \
sh -c '
# 修复链接器路径不匹配(常见问题)
ln -sf /lib/ld-linux-armhf.so.3 /lib/ld-linux.so.3 2>/dev/null || true
# 如有需要,安装依赖项(检查 rabin2 -l 输出)
apt-get update -qq && apt-get install -qq -y libcap2 libacl1 2>/dev/null
# 使用库调试输出运行(strace 替代)
LD_DEBUG=libs /work/binary args
'
macOS 上的 ARM64 二进制文件
docker run --rm --platform linux/arm64 \
-v ~/code/samples:/work:ro \
arm64v8/debian:bullseye-slim \
sh -c 'LD_DEBUG=libs /work/binary args'
macOS 上的 x86 32位二进制文件
docker run --rm --platform linux/i386 \
-v ~/code/samples:/work:ro \
i386/debian:bullseye-slim \
sh -c '/work/binary args'
Docker/QEMU 用户模式中的跟踪限制
| 方法 | 可行? | 替代方案 |
|---|---|---|
| strace | ❌ (ptrace 未实现) | LD_DEBUG=files,libs |
| ltrace | ❌ (同样的原因) | 直接观察或 Frida |
| gdb | ✓ (带 QEMU -g 标志) |
N/A |
LD_DEBUG 选项(strace 替代)
LD_DEBUG=libs # 库搜索和加载
LD_DEBUG=files # 加载期间的文件操作
LD_DEBUG=symbols # 符号解析
LD_DEBUG=bindings # 符号绑定详情
LD_DEBUG=all # 一切(详细)
选项 E: 设备上的分析
当仿真失败或需要设备特定行为时。
通过 gdbserver 进行远程 GDB
# 在目标设备上(通过 SSH/ADB)
gdbserver :1234 ./binary
# 在主机上(带有端口转发)
ssh -L 1234:localhost:1234 user@device &
gdb-multiarch -q \
-ex "target remote localhost:1234" \
./binary
远程 strace(如果可用)
# 在目标设备上
strace -f -o /tmp/trace.log ./binary
# 拉取日志
scp user@device:/tmp/trace.log .
沙箱配置
最小沙箱(nsjail)
nsjail \
--mode o \
--chroot /sysroot \
--user 65534 \
--group 65534 \
--disable_clone_newnet \
--rlimit_as 512 \
--time_limit 60 \
-- /binary
QEMU 与资源限制
# CPU 时间限制
timeout 60 qemu-arm -L /sysroot -strace ./binary
# 通过 cgroup 限制内存(需要设置)
cgexec -g memory:qemu_sandbox qemu-arm -L /sysroot ./binary
反分析检测
在动态分析之前,检查常见的反调试/反分析模式:
静态检测(执行前)
# 检查反调试字符串/导入
strings -a binary | grep -Ei 'ptrace|anti|debugger|seccomp|LD_PRELOAD|/proc/self'
# r2: 查找 ptrace/prctl/seccomp 导入
r2 -q -c 'iij' binary | jq '.[].name' | grep -Ei 'ptrace|prctl|seccomp'
# 常见反分析指标:
# - ptrace(PTRACE_TRACEME) - 防止调试器附加
# - prctl(PR_SET_DUMPABLE, 0) - 防止核心转储
# - seccomp - 系统调用过滤
# - /proc/self/status 检查 - 检测 TracerPid
运行时检测
# 如果可能进行本地执行:
strace -f ./binary 2>&1 | grep -E 'ptrace|prctl|seccomp|/proc/self'
缓解策略
| 模式 | 检测 | 绕过 |
|---|---|---|
ptrace(TRACEME) |
如果附加了调试器则返回 EPERM | 修补调用为 NOP,使用 QEMU |
/proc/self/status 检查 |
读取 TracerPid 字段 | 使用 QEMU(无 /proc 模拟) |
| 计时检查 | gettimeofday/rdtsc 循环 |
使用 GDB 单步,修补检查 |
| 自我校验和 | 读取自己的二进制/内存 | 计算预期校验和,修补 |
当检测到反分析时: 优先选择 QEMU-strace 而不是 GDB(检测向量更少),或在执行前在 r2 中修补检查。
错误恢复
| 错误 | 原因 | 解决方案 |
|---|---|---|
Unsupported syscall |
QEMU 限制 | 尝试 Qiling 或设备上 |
Invalid ELF image |
错误的架构/ sysroot | 验证 file 输出 |
Segfault at 0x0 |
缺少库 | 检查 ldd 等效物 |
QEMU hangs |
阻塞在 I/O 上 | 添加超时,检查 strace |
Anti-debugging |
检测代码 | 使用 Frida stalker 模式 |
exec format error 在 Docker 中 |
binfmt 未注册 | 运行 tonistiigi/binfmt --install arm |
ld-linux.so.3 not found |
链接器路径不匹配 | 在容器中创建符号链接 |
libXXX.so not found |
缺少依赖 | 容器中的 apt install |
| Docker 中的空挂载 | Colima /tmp 问题 | 使用 ~/ 路径而不是 /tmp/ |
ptrace: Operation not permitted |
QEMU 中的 strace | 使用 LD_DEBUG 代替 |
输出格式
将观察结果记录为结构化数据:
{
"experiment": {
"id": "exp_001",
"method": "qemu_strace",
"command": "qemu-arm -L /usr/arm-linux-gnueabihf -strace ./binary",
"duration_secs": 12,
"exit_code": 0
},
"syscall_summary": {
"network": {
"socket": 2,
"connect": 1,
"send": 5,
"recv": 3
},
"file": {
"openat": 4,
"read": 12,
"close": 4
}
},
"network_connections": [
{
"family": "AF_INET",
"address": "192.168.1.100",
"port": 8443,
"protocol": "tcp"
}
],
"files_accessed": [
{"path": "/etc/config.json", "mode": "read"},
{"path": "/var/log/app.log", "mode": "write"}
],
"hypotheses_tested": [
{
"hypothesis_id": "hyp_001",
"result": "confirmed",
"evidence": "connect() to 192.168.1.100:8443 observed"
}
]
}
知识日志记录
动态分析后,记录发现以便后续记忆:
[BINARY-RE:dynamic] {filename} (sha256: {hash})
执行方法:{qemu-strace|qemu-gdb|frida|on-device}
决策:批准执行 {sandbox_config}(理由:{why_safe})
运行时观察:
FACT: 二进制文件在启动时读取 {path}(来源:strace openat)
FACT: 二进制文件尝试连接到 {ip}:{port}(来源:strace connect)
FACT: 二进制文件写入到 {path}(来源:strace openat O_WRONLY)
FACT: 在运行时,函数 {addr} 接收参数 {values}(来源:gdb)
系统调用摘要:
网络:{socket|connect|send|recv 计数}
文件:{open|read|write|close 计数}
进程:{fork|exec|clone 计数}
假设更新:{确认或细化的理论}(信心:{new_value})
确认由:{运行时观察}
相矛盾:{如果有}
新问题:
QUESTION: {运行时发现的未知}
已回答问题:
RESOLVED: {问题} → {运行时证据}
示例日志条目
[BINARY-RE:dynamic] thermostat_daemon (sha256: a1b2c3d4...)
执行方法:qemu-strace
决策:批准执行网络阻塞沙箱(理由:静态分析显示仅出站,无服务器)
运行时观察:
FACT: 二进制文件在启动时读取 /etc/thermostat.conf(来源:strace openat)
FACT: 二进制文件尝试连接到 93.184.216.34:443(来源:strace connect)
FACT: 二进制文件写入到 /var/log/thermostat.log(来源:strace openat O_WRONLY)
FACT: 在运行时,sleep(30) 在网络尝试之间被调用(来源:strace nanosleep)
系统调用摘要:
网络:socket(2), connect(1-blocked), send(0), recv(0)
文件:openat(4), read(12), write(8), close(4)
进程:无
假设更新:遥测客户端确认 - 读取配置,每 30s 尝试 HTTPS 到 thermco 服务器(信心:0.95)
确认由:预期 IP 的 connect(),sleep(30) 定时,配置文件读取
相矛盾:无
已回答问题:
RESOLVED: "它真的打电话回家吗?" → 是的,观察到 connect() 到 93.184.216.34:443
RESOLVED: "它访问了哪些文件?" → /etc/thermostat.conf(读取),/var/log/thermostat.log(写入)
下一步
→ binary-re-synthesis 将发现编译成报告
→ 如果识别出新功能,则进行额外的静态分析
→ 如果行为变化,则用不同输入重复