09 — 逆向工程与二进制攻防
哲学:逆向不是”学完理论再动手”——你读完这句话的时候,下面第一个实战的
file命令就已经可以跑了。二进制攻防的本质是:用工具把机器码翻译成人能看的逻辑,再用漏洞把人能看的逻辑变成机器能执行的 shell。从strings到 ROP 链,从 Ghidra 到 pwntools,每一步都有完整的命令,复制粘贴就能出结果。环境:Arch Linux,工具全部来自 pacman/AUR。
目标:看到
file命令输出 ELF header 时,你已经开始了;看到 shell 弹出来时,本章结束。
Part 1: 快速入手——命令行快速分析
1.0 准备实验靶机
在你跑任何逆向命令之前,先造一个目标二进制:
# 创建一个有密码保护的小程序
cat > /tmp/target.c << 'EOF'
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
char secret_flag[] = "FLAG{strings_reveals_everything}";
int check_password(char *input) {
char *password = "sup3r_s3cr3t_p@ss";
if (strcmp(input, password) == 0) {
printf("[+] Access granted! Flag: %s\n", secret_flag);
return 1;
}
printf("[-] Wrong password!\n");
return 0;
}
int main(int argc, char **argv) {
if (argc < 2) {
printf("Usage: %s <password>\n", argv[0]);
return 1;
}
// 模拟一些HTTP网络连接字符串(混淆视线)
char *c2 = "http://evil-c2.attacker.com:8080/beacon";
char *api = "https://api.malware.local/v2/exfil";
printf("Connecting to server...\n");
check_password(argv[1]);
return 0;
}
void hidden_backdoor() {
printf("[!] Backdoor triggered!\n");
system("/bin/sh");
}
EOF
gcc -o /tmp/target.bin /tmp/target.c
echo "=== 编译完成 ==="
file /tmp/target.bin跑完上面这段,你就有了一个带密码保护、隐藏后门、伪装C2字符串的二进制。下面的实战全部围着它打。
实战1:file + strings + objdump
# ========== 1.1 file:识别二进制类型 ==========
file /tmp/target.bin
# 输出: /tmp/target.bin: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=..., for GNU/Linux 4.4.0, not stripped
#
# 关键信息解读:
# ELF 64-bit → 64位可执行文件
# LSB → 小端序
# pie → 地址无关可执行文件(ALSR保护)
# dynamically linked → 动态链接(依赖libc)
# not stripped → 符号表没去掉(幸福!函数名都在)
# 如果是 stripped → objdump -d 里只有地址没有函数名
file $(which ls)
# /usr/bin/ls: ELF 64-bit LSB pie executable, x86-64, stripped
# ↑ 系统自带的工具通常都是 stripped 的
# 批量识别目录下所有文件
file /tmp/target.bin /usr/bin/* 2>/dev/null | head -20
# ========== 1.2 strings:提取可打印字符串 ==========
strings /tmp/target.bin | head -30
# 你会直接看到:
# sup3r_s3cr3t_p@ss
# FLAG{strings_reveals_everything}
# http://evil-c2.attacker.com:8080/beacon
# https://api.malware.local/v2/exfil
# 关键词过滤——红队最常用的几条 strings 管道:
strings /tmp/target.bin | grep -i "flag\|password\|key\|secret\|http"
# 输出:
# http://evil-c2.attacker.com:8080/beacon
# https://api.malware.local/v2/exfil
# FLAG{strings_reveals_everything}
# sup3r_s3cr3t_p@ss
# 四行字符串全暴露了——这就是为什么生产环境代码中绝对不能硬编码密钥和URL
# 进阶过滤:找IP地址
strings /tmp/target.bin | grep -Eo '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}'
# 找URL
strings /tmp/target.bin | grep -Eo 'https?://[a-zA-Z0-9./?=_%:-]*'
# 找路径
strings /tmp/target.bin | grep -E '^/' | head -20
# strings 的高级参数:
strings -n 6 /tmp/target.bin # 只显示长度>=6的字符串(过滤噪音)
strings -e l /tmp/target.bin # 指定编码为16位小端
strings -e b /tmp/target.bin # 指定编码为16位大端
strings -t x /tmp/target.bin # 显示字符串在文件中的偏移(十六进制)
strings -t x /tmp/target.bin | grep flag
# 输出: 201c FLAG{strings_reveals_everything}
# ^^^^ 文件偏移地址
# 实战: 快速分析一个陌生ELF
# 一条命令看全局:
strings /tmp/target.bin | head -50 && echo "===== divider =====" && strings /tmp/target.bin | tail -50
# 字符串通常在文件头和文件尾各有一部分
# ========== 1.3 objdump:反汇编 ==========
# 反汇编 .text 段(代码段)的前50行
objdump -d /tmp/target.bin | head -50
# 输出示例:
# 0000000000001149 <main>:
# 1149: push %rbp
# 114a: mov %rsp,%rbp
# ...
# 这就是main函数的汇编代码——Ghidra做的事情的底层版本
# 显示符号表
objdump -t /tmp/target.bin
# 输出:
# 0000000000001149 g F .text 000000a9 main
# 00000000000011f2 g F .text 0000005e check_password
# 0000000000001250 g F .text 00000024 hidden_backdoor
# ^ 所有函数名、地址、大小一览无余
# 只显示 .text 段中的符号
objdump -t /tmp/target.bin | grep "\.text"
# 0000000000001000 l d .text 0000000000000000 .text
# 0000000000001050 g F .text 0000000000000039 _start
# 0000000000001149 g F .text 00000000000000a9 main
# 00000000000011f2 g F .text 000000000000005e check_password
# 0000000000001250 g F .text 0000000000000024 hidden_backdoor
# 反汇编单个函数
objdump -d /tmp/target.bin --disassemble=check_password
# 只输出 check_password 函数的汇编代码
# 反汇编并混合源代码(如果编译时加了 -g)
# gcc -g -o /tmp/target_debug.bin /tmp/target.c
# objdump -S /tmp/target_debug.bin
# ↑ -S 参数会穿插显示C源码行
# 显示所有段信息
objdump -h /tmp/target.bin
# 输出每个段的名称、大小、VMA、LMA、文件偏移
# 显示动态符号表
objdump -T /tmp/target.bin
# 输出依赖的外部函数: printf, strcmp, system, puts...
# 分析调用图
objdump -d /tmp/target.bin | grep -E "<.*>:" | head -20
# 提取所有函数标签实战2:ltrace + strace 动态观察
# ========== 2.1 ltrace:追踪库函数调用 ==========
# ltrace = 用LD_PRELOAD拦截所有动态库函数调用
# 这是找密码最快的办法,没有之一
ltrace /tmp/target.bin wrongpassword 2>&1
# 关键输出:
# strcmp("wrongpassword", "sup3r_s3cr3t_p@ss") = -17
# ^^^^^^^^^^^^^^^^^^ 密码就在这里!
# strcmp(a,b): 返回值 = a[0]-b[0], 'w' - 's' = 4, 实际返回-17是因为比了整个字符串
# 试试正确密码
ltrace /tmp/target.bin "sup3r_s3cr3t_p@ss" 2>&1
# strcmp("sup3r_s3cr3t_p@ss", "sup3r_s3cr3t_p@ss") = 0
# puts("[+] Access granted! Flag: FLAG{str"...) = 49
# 验证密码正确
# 实战:用 ltrace 分析一切带密码的程序
# 如果你在CTF中拿到一个未知二进制:
ltrace ./crackme $(python3 -c "print('A'*20)") 2>&1 | grep -E "strcmp|memcmp|strncmp|strstr"
# 任何字符串比较函数都会把参数暴露出来
# ltrace 更多参数:
ltrace -e strcmp ./target.bin test # 只追踪strcmp
ltrace -e 'strcmp+strncmp+memcmp' ./target.bin test # 追踪多个函数
ltrace -t ./target.bin test # 显示时间戳
ltrace -c ./target.bin test # 统计每个函数调用次数和耗时
ltrace -S ./target.bin test # 同时显示系统调用
ltrace -o /tmp/ltrace.log ./target.bin test # 输出到文件
# 实战:分析恶意程序的文件操作
ltrace -e 'fopen+fread+fwrite' ./malware.bin 2>&1
# 看它读了什么文件、写了什么文件
# ========== 2.2 strace:追踪系统调用 ==========
# strace = 用 ptrace 拦截所有 syscall
# 看程序对操作系统做了什么:打开文件、读写、网络连接
strace /tmp/target.bin "sup3r_s3cr3t_p@ss" 2>&1
# 你会看到全流程:
# execve("./target.bin", ...) ← 程序启动
# brk(NULL) ← 堆初始化
# mmap(NULL, ...) ← 内存映射
# openat(AT_FDCWD, "/etc/ld.so.cache") ← 动态链接器
# openat(AT_FDCWD, "/usr/lib/libc.so.6")← 加载libc
# write(1, "Connecting to server...\n", 24) ← 输出到stdout
# write(1, "[+] Access granted...", ...) ← 输出flag
# exit_group(0) ← 程序退出
# 只看网络相关系统调用
strace -e trace=network /tmp/target.bin test 2>&1
# 能抓到 connect, sendto, recvfrom, socket 等
# 只看文件操作
strace -e trace=file /tmp/target.bin test 2>&1
# open, openat, read, write, stat, lseek...
# 比 ltrace 更底层的信息: strace -k 显示内核堆栈
strace -k /tmp/target.bin test 2>&1 | head -40
# 实战:分析一个未知程序有没有偷偷连网
strace -f -e trace=network ./suspicious.bin 2>&1
# -f: 追踪子进程
# 实战:查看程序读了哪些文件
strace -e trace=open,openat,read ./crackme 2>&1 | grep -v ENOENT
# ========== 2.3 组合拳:一行命令同时跑 strace 和 ltrace ==========
# strace 和 ltrace 不能同时 attach 同一个进程(都用了ptrace)
# 但可以做差时对比:
ltrace -o /tmp/ltrace_output.txt /tmp/target.bin test
strace -o /tmp/strace_output.txt /tmp/target.bin test
diff <(grep strcmp /tmp/ltrace_output.txt) <(grep write /tmp/strace_output.txt)
# 实战技巧:pid attach
# 如果程序已经跑起来了:
# ps aux | grep target
# strace -p <PID> # attach 到运行中的进程
# ltrace -p <PID>实战3:readelf 分析 ELF 结构
# ========== 3.1 ELF 头部 ==========
readelf -h /tmp/target.bin
# 输出:
# Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
# Class: ELF64
# Data: 2's complement, little endian
# OS/ABI: UNIX - System V
# Type: DYN (Position-Independent Executable file)
# Machine: Advanced Micro Devices X86-64
# Entry point address: 0x1050
# Start of program headers: 64 (bytes into file)
# Start of section headers: 13808 (bytes into file)
# Number of program headers: 13
# Number of section headers: 30
# Size of section headers: 64 (bytes)
# Section header string table index: 29
#
# 关键字段:
# Entry point address → 程序入口,调试时的初始断点
# Type: DYN → PIE二进制,地址在加载时随机化
# Type: EXEC → 非PIE,固定地址
# 对比系统二进制
readelf -h $(which ls)
# Type: DYN (Position-Independent Executable file) ← 现代Linux几乎都是PIE
# ========== 3.2 段表 (Section Headers) ==========
readelf -S /tmp/target.bin
# 输出所有段的详细信息:
# [Nr] Name Type Address Offset
# Size EntSize Flags Link Info Align
# [ 0] NULL 0000000000000000 00000000
# 0000000000000000 0000000000000000 0 0 0
# [ 1] .interp PROGBITS 0000000000000318 00000318
# 000000000000001c 0000000000000000 A 0 0 1
# [14] .text PROGBITS 0000000000001050 00001050
# 0000000000000225 0000000000000000 AX 0 0 16
# ^^^^^^^^^^^^^^^^^
# .text 段大小 = 0x225 = 549 字节(你的所有代码)
# [24] .data PROGBITS 0000000000004000 00003000
# 0000000000000010 0000000000000000 WA 0 0 8
# [25] .bss NOBITS 0000000000004010 00003010
# 0000000000000008 0000000000000000 WA 0 0 1
#
# 关键段说明:
# .text → 代码段 (Flags: AX = Alloc + Execute)
# .rodata → 只读数据 (你的密码、FLAG、URL 都在这里)
# .data → 已初始化全局变量
# .bss → 未初始化全局变量 (NOBITS = 文件中不占空间)
# .got.plt → 全局偏移表 (PIE/动态链接关键)
# .dynamic → 动态链接信息
# 只显示 .rodata (找密码最快的方式之一)
readelf -S /tmp/target.bin | grep -A1 rodata
# ========== 3.3 程序头 (Program Headers) ==========
# 程序头告诉加载器(ld.so)如何将文件映射到内存
readelf -l /tmp/target.bin
# 输出:
# Type Offset VirtAddr PhysAddr
# FileSiz MemSiz Flags Align
# PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
# 0x00000000000002d8 0x00000000000002d8 R 0x8
# LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
# 0x0000000000000678 0x0000000000000678 R 0x1000
# LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000
# 0x0000000000000275 0x0000000000000275 R E 0x1000
# ^^^^^^^^^^^^^^^^^^
# 代码段: Offset 0x1000, 大小 0x275, Flags = R+E
# 用 readelf -l 快速判断保护机制:
# 如果有 GNU_STACK 段且 Flags 包含 E → 栈可执行 (execstack)
# 如果没有 GNU_RELRO → 没有 RELRO 保护
readelf -l /tmp/target.bin | grep -A1 "GNU_STACK\|GNU_RELRO"
# ========== 3.4 动态段 (共享库依赖) ==========
readelf -d /tmp/target.bin
# 输出:
# Tag Type Name/Value
# 0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
# ^ 你的程序依赖 libc,如果这个列表里有 libssl、libcurl 就值得深入看
# 看看程序需要哪些共享库
readelf -d /tmp/target.bin | grep NEEDED
# 0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
# 查看运行时搜索路径
readelf -d /tmp/target.bin | grep -E "RUNPATH|RPATH"
# RPATH/RUNPATH 可以被用来注入恶意so文件(如果路径可写)
# ========== 3.5 重定位表 ==========
readelf -r /tmp/target.bin
# 输出所有重定位条目:
# Offset Info Type Symbol's Value Symbol's Name + Addend
# 0000000000003f98 0000000100000007 R_X86_64_JUMP_SLOT 0000000000000000 printf@GLIBC_2.2.5 + 0
# 0000000000003fa0 0000000200000007 R_X86_64_JUMP_SLOT 0000000000000000 strcmp@GLIBC_2.2.5 + 0
# 0000000000003fa8 0000000300000007 R_X86_64_JUMP_SLOT 0000000000000000 puts@GLIBC_2.2.5 + 0
# 0000000000003fb0 0000000400000007 R_X86_64_JUMP_SLOT 0000000000000000 system@GLIBC_2.2.5 + 0
# ^^^^^^ ^^^^^^
# 危险函数! 危险函数!
# 看到 system@GLIBC → 程序里可能调用了 system()
# 这可以从重定位表直接识别后门
# 实战:用 readelf 做一个快速安全审计
# 一行命令查看这个二进制用了哪些危险函数
readelf -r /tmp/target.bin | grep -E "system|exec|popen|fork|socket|connect|dlopen"
# 0x3fb0 system@GLIBC_2.2.5
# ↑ 确认有 system 调用,值得在 Ghidra 里追踪
# ========== 3.6 提取只读数据(.rodata) ==========
# 用 objcopy 提取 rodata
objcopy -O binary -j .rodata /tmp/target.bin /tmp/rodata.bin
strings /tmp/rodata.bin
# 输出:
# Usage: %s <password>
# Connecting to server...
# [-] Wrong password!
# FLAG{strings_reveals_everything}
# sup3r_s3cr3t_p@ss
# http://evil-c2.attacker.com:8080/beacon
# https://api.malware.local/v2/exfil
# [+] Access granted! Flag: %s
# /bin/sh
# 所有常量字符串赤裸裸地摆在这里
# 也可以直接 hexdump rodata
readelf -x .rodata /tmp/target.bin
# 以十六进制+ASCII对照显示
# ========== 3.7 检查二进制保护机制 ==========
# 安装 checksec (pwntools自带)
pip install pwntools 2>/dev/null
# 或者: sudo pacman -S python-pwntools
python3 -c "from pwn import *; print(ELF('/tmp/target.bin'))"
# 输出:
# ELF('/tmp/target.bin')
# Arch: amd64-64-little
# RELRO: Partial RELRO ← GOT可写,可被覆盖
# Stack: No canary found ← 无栈保护,溢出可覆写返回地址
# NX: NX enabled ← 栈不可执行,shellcode 不能直接放栈上
# PIE: PIE enabled ← 地址随机化
# 用 checksec 脚本(如果装了pwntools):
python3 -c "
from pwn import *
elf = ELF('/tmp/target.bin')
print(f'Canary: {elf.canary}')
print(f'NX: {elf.nx}')
print(f'PIE: {elf.pie}')
print(f'RELRO: {elf.relro}')
print(f'RWX: {elf.rwx_segments}')
"Part 3: radare2 命令行逆向
radare2 是终端原生的逆向工程框架。不需要 GUI,SSH 到目标上就能用。它的学习曲线陡峭但回报极高——当你只有一台被黑的跳板机上的 shell 时,r2 是你唯一的逆向武器。
实战11:radare2 基本用法
# 安装
sudo pacman -S radare2
# ========== 11.1 打开文件并自动分析 ==========
r2 -A /tmp/target.bin
# -A = 打开后立即执行 aaa (自动分析)
# 等效于: r2 /tmp/target.bin ; aaa
# r2 启动后你会看到一个命令提示符:
# [0x00001050]>
# 0x1050 是入口点地址 (_start)
# ========== 11.2 核心分析命令 ==========
aaaa # 全量分析 (比 aaa 更深入,包含更多启发式分析)
# 输出会显示分析结果:
# [x] Analyze all flags starting with sym. and entry0 (aa)
# [x] Analyze function calls
# [x] Analyze len bytes of instructions for references
# ...
afl # List all functions (列出所有函数)
# 0x00001050 35 entry0
# 0x00001080 32 sym._init
# 0x00001090 16 sym.imp.puts
# 0x000010a0 16 sym.imp.printf
# 0x000010b0 16 sym.imp.strcmp
# 0x00001149 169 main
# 0x000011f2 94 sym.check_password
# 0x00001250 36 sym.hidden_backdoor
afl~main # ~ 是 r2 的 grep 操作符,过滤包含 "main" 的行
afl~check # 过滤包含 "check" 的函数
# ========== 11.3 定位并反汇编函数 ==========
s main # Seek to main (定位到main函数地址)
# 提示符变成 [0x00001149]> ← main 的地址
pdf # Print Disassembly of Function (反汇编当前函数)
# 输出:
# 169: main (int argc, char **argv, char **envp);
# | ; var int64_t var_8h @ rbp-0x8
# | 0x00001149 push rbp
# | 0x0000114a mov rbp, rsp
# | 0x0000114d sub rsp, 0x10
# | 0x00001150 mov dword [var_4h], edi ; argc
# | 0x00001153 mov qword [var_10h], rsi ; argv
# | 0x00001157 cmp dword [var_4h], 1
# | < 0x0000115b jg 0x116a
# | | 0x0000115d mov rax, qword [var_10h]
# | | ...
# | 0x000011f0 ret
pdf @main # 等价于 s main ; pdf (定位到main然后反汇编)
pdf @sym.check_password # 反汇编 check_password 函数
pdf @sym.hidden_backdoor # 反汇编后门函数
# ========== 11.4 可视化模式 ==========
VV # 进入可视化模式 (Visual mode)
# 你会看到一个 ASCII 控制流图:
#
# | [0x00001149] main |
#
# |
# | push rbp
# | ...
# | cmp dword [var_4h], 1
# |
# jg 0x116a
# | → 0x115d:
# | mov rax, qword [var_10h]
# | ...
# → 0x116a:
# lea rdi, str.Connecting_to_server
# call sym.imp.puts
# ...
# 在 VV 模式下:
# h/j/k/l → 导航
# q → 退出
# Tab → 切换到伪代码视图
# p/P → 切换不同视图模式
# o → 切换汇编语法 (intel/att)
# : → 进入命令模式
# ========== 11.5 搜索字符串 ==========
# 退出 VV 模式 (q) 回到命令模式,然后:
izz # 列出所有字符串 (Iz = 二进制内, izz = 段内)
# [0x00000000] 00000000 596 "Usage: %s <password>"
# [0x00000001] 0000001c 595 "Connecting to server..."
# ...
# [0x00000004] 00000072 595 "sup3r_s3cr3t_p@ss"
# [0x00000005] 00000050 595 "FLAG{strings_reveals_everything}"
izz~flag # 过滤包含 "flag" 的行
izz~http # 过滤包含 "http" 的行
izz~pass # 过滤包含 "pass" 的行
# ========== 11.6 交叉引用 (Xrefs) ==========
axt @0x00002072 # Find data/code references to this address
# 谁引用了地址 0x2072 (密码字符串)?
# 输出: sym.check_password+0x12 0x1204 [DATA] lea rax, str.sup3r_s3cr3t_p_ss
axf @main # 显示 main 调用了哪些函数
# 0x00001149 main
# 0x116d puts
# 0x117c sym.check_password
# ========== 11.7 十六进制查看 ==========
px 64 @0x00002070 # Print hex, 64 bytes at address 0x2070
# -r-x- 0x00002070 7300 7375 7033 725f 7333 6372 3374 5f70 s.sup3r_s3cr3t_p
# -r-x- 0x00002080 4073 7300 6874 7470 3a2f 2f65 7669 6c2d @ss.http://evil-
pxQ @0x00002070 # Print hex as 8-byte words (QWORDs)
# ========== 11.8 导出分析结果 ==========
pdc @main # Pseudocode (pdc = pseudo-decompiler, 类似 Ghidra 的伪代码)
# int main (int32_t arg1, char **arg2)
# {
# if (arg1 <= 1) {
# printf ("Usage: %s <password>", *arg2);
# eax = 1;
# }
# else {
# puts ("Connecting to server...");
# sym.check_password (arg2[1]);
# eax = 0;
# }
# return eax;
# }
# ↑ r2 内置的轻量级反编译器
pdc @sym.hidden_backdoor
# void sym.hidden_backdoor ()
# {
# puts ("[!] Backdoor triggered!");
# system ("/bin/sh");
# }实战12:radare2 找密码
# ========== 方法1:直接搜字符串 + 交叉引用 ==========
r2 -A /tmp/target.bin
# 搜所有字符串,过滤可疑的
izz~password
izz~flag
izz~secret
izz~key
# 看到 "sup3r_s3cr3t_p@ss" 在地址 0x2072
# 追踪谁引用了它:
axt @0x00002072
# sym.check_password 0x1204 [DATA] lea rax, str.sup3r_s3cr3t_p_ss
# 查看这个引用处的上下文:
s 0x1204
pd -5 # 打印前5条指令
pd 10 # 打印后10条指令
# ========== 方法2: 反汇编 main 追踪执行流 ==========
pdf @main
# 看到 main 调用了 sym.check_password
# 追踪进去:
pdf @sym.check_password
# 你会在 cmp 指令前后看到两个字符串的加载:
# lea rax, <密码字符串地址> ← 这个地址就是密码
# lea rdi, <用户输入>
# call sym.imp.strcmp
# test eax, eax
# je <成功分支>
# 查看密码字符串的内容:
ps @0x00002072 # Print String at address
# sup3r_s3cr3t_p@ss
# ========== 方法3: 通过十六进制搜索特征字节 ==========
# 假设你不知道具体偏移,但知道 strcmp 的字节特征
# 搜索 "call strcmp" 附近的模式
e search.from = 0x1000
e search.to = 0x2000
/w strcmp # 在数据中搜索 "strcmp" 字符串
# ========== 方法4: 用 r2 管道命令一步提取 ==========
r2 -A -q -c 'izz~pass' /tmp/target.bin
# -A: 自动分析
# -q: 安静模式
# -c: 执行命令后退出
# 输出: sup3r_s3cr3t_p@ss
r2 -A -q -c 'izz~flag\|key\|pass\|http' /tmp/target.bin
# 一条命令提取所有敏感字符串
# ========== 高级:r2 管道脚本化分析 ==========
cat > /tmp/analyze_binary.r2 << 'EOF'
aaaa
echo "=== Functions ==="
afl
echo "=== Sensitive Strings ==="
izz~flag|pass|key|secret|http|https|\.com|\.net
echo "=== Dangerous Imports ==="
ii~system|exec|popen|socket|connect
echo "=== Entry Points ==="
ie
echo "=== Security Check ==="
i~nx|canary|pic|relro
quit
EOF
r2 -q -i /tmp/analyze_binary.r2 /tmp/target.bin实战13:radare2 动态调试
# ========== 13.1 在 r2 中调试 ==========
r2 -d /tmp/target.bin
# -d: 调试模式 (Debug)
# r2 会启动一个子进程用于调试,类似 gdb
# 或者 attach 到运行中的进程:
# r2 -d -p <PID>
# ========== 13.2 设置断点 ==========
db main # 在 main 函数设置断点
db @0x00001149 # 在指定地址设置断点
db sym.check_password # 在 check_password 设置断点
# 查看所有断点:
dbi # Debug Breakpoint Info
# ========== 13.3 运行和单步执行 ==========
dc # Debug Continue (运行直到断点)
# 执行到 main 的断点处
ds # Debug Step (单步执行,跳过函数调用)
dso # Debug Step Over (单步跳过)
dss # Debug Step Over (跳过函数调用)
# ========== 13.4 查看寄存器和内存 ==========
dr # Debug Registers (显示所有寄存器)
# rax = 0x00000002
# rbx = 0x00000000
# rcx = 0x00000000
# rdx = 0x00000000
# rsi = 0x7ffc12345678
# rdi = 0x00000002 ← argc
# rbp = 0x7ffc12345640
# rsp = 0x7ffc12345630
# rip = 0x00001149 ← 当前指令地址
dr rax # 只查看 rax 的值
dr rip # 当前指令指针
# ========== 13.5 查看栈 ==========
px 64 @rsp # 打印栈顶64字节
px 128 @rbp # 打印基址指针处的128字节
# 查看栈上的字符串:
ps @rsp # Print String at rsp
# ========== 13.6 实操: 在 strcmp 处断点看密码 ==========
r2 -d /tmp/target.bin
# 在 strcmp 调用处设断点 (从源码看 strcmp 在 check_password+0x1f)
db @sym.check_password+0x1f
# 或者直接:
db sym.imp.strcmp
# 运行程序(需要传参数)
dcr "sup3r_s3cr3t_p@ss" # dcr = continue with argument
# 或者:
# ood testpassword # ood = reopen in debug mode with args
# dc
# 断点命中后,查看 rdi 和 rsi (strcmp 的两个参数在 x86_64 调用约定中)
dr rdi # 第1个参数 = 用户输入
dr rsi # 第2个参数 = 密码字符串
# 查看 rdi 指向的字符串:
ps @rdi
# sup3r_s3cr3t_p@ss
# 查看 rsi 指向的字符串:
ps @rsi
# sup3r_s3cr3t_p@ss
# ↑ 这就是找到密码的过程: strcmp 比较的两个参数一目了然
# ========== 13.7 修改内存值绕过检查 ==========
# 在 strcmp 返回后, test eax,eax 之前设断点:
db @sym.check_password+0x24 # test eax,eax 的地址
# 继续执行:
dc
# 在断点处修改 eax = 0 (模拟 strcmp 返回相等):
dr eax=0
# 继续执行:
dc
# [+] Access granted! ← 绕过了密码检查!
# ========== 13.8 调试技巧总结 ==========
# dcu main → 执行直到到达 main 函数
# dcu 0xdeadbeef → 执行直到到达指定地址
# dbt → 显示当前调用栈 (backtrace)
# dm → 显示内存映射 (类似 /proc/<pid>/maps)
# dmp → 修改内存权限
# drr → 显示所有寄存器的值(紧凑格式)
# drr~rax → 获取 rax 的值
# ?v $r rax → 计算并显示 rax 的值Part 5: 逆向工具链速查
完整工具箱
| 工具 | 分类 | 用途 | 安装 | 一句命令 |
|---|---|---|---|---|
| Ghidra | GUI反汇编 | NSA开源,对标IDA | pacman -S ghidra | ghidraRun |
| radare2 | CLI反汇编 | 终端原生逆向框架 | pacman -S radare2 | r2 -A file |
| rizin | CLI反汇编 | r2的社区fork,Cutter的引擎 | pacman -S rizin | rizin -A file |
| Cutter | GUI反汇编 | r2的Qt图形前端 | pacman -S cutter | cutter file |
| Binary Ninja | GUI反汇编 | 商业软件,反编译器质量极高 | 官网下载 | binaryninja file |
| GDB | 调试器 | GNU调试器 | pacman -S gdb | gdb ./file |
| GDB-GEF | GDB插件 | 美化GDB,增加pwn功能 | pacman -S gef | GDB自动加载 |
| GDB-PEDA | GDB插件 | Python Exploit Development Assistance | git clone | source peda.py |
| pwntools | 漏洞利用 | Python漏洞利用库 | pip install pwntools | from pwn import * |
| pwndbg | GDB插件 | 现代化的GDB pwn插件 | pacman -S pwndbg | GDB自动加载 |
| checksec | 安全检查 | 检查二进制保护机制 | pwntools自带 | checksec ./file |
| ropper | ROP搜索 | 搜索ROP gadget | pip install ropper | ropper -f file |
| ROPgadget | ROP搜索 | 搜索ROP gadget | pip install ROPgadget | ROPgadget --binary file |
| one_gadget | ROP搜索 | 找libc中的one-shot gadget | pacman -S one_gadget | one_gadget libc.so |
| objdump | 反汇编 | ELF文件分析和反汇编 | 系统自带(binutils) | objdump -d file |
| readelf | ELF分析 | 读取ELF结构和段信息 | 系统自带(binutils) | readelf -a file |
| nm | 符号表 | 列出符号表 | 系统自带(binutils) | nm -C file |
| strings | 字符串 | 提取可打印字符串 | 系统自带(binutils) | strings file |
| ltrace | 库追踪 | 追踪动态库函数调用 | pacman -S ltrace | ltrace ./file |
| strace | 系统调用 | 追踪系统调用 | pacman -S strace | strace ./file |
| objcopy | 段操作 | 提取/转换段数据 | 系统自带(binutils) | objcopy -O binary -j .rodata file out |
| xxd | 十六进制 | 十六进制dump | pacman -S xxd | xxd file |
| hexdump | 十六进制 | 十六进制查看 | pacman -S util-linux | hexdump -C file |
| file | 文件类型 | 识别文件类型 | 系统自带(coreutils) | file binary |
| patchelf | ELF修改 | 修改ELF属性 | pacman -S patchelf | patchelf --set-interpreter ld.so binary |
| die | 查壳 | 检测可执行文件壳和编译器 | pacman -S die | diec file |
| upx | 加壳/脱壳 | UPX压缩/解压 | pacman -S upx | upx -d file |
| binwalk | 固件分析 | 固件解包和文件提取 | pacman -S binwalk | binwalk -e firmware.bin |
| qemu-user | 模拟执行 | 不同架构下运行ELF | pacman -S qemu-user | qemu-x86_64 ./mips_binary |
| volatility3 | 内存取证 | 内存dump分析 | pip install volatility3 | vol -f mem.dmp windows.pslist |
常用命令组合
# ========== 一键分析脚本 ==========
# 保存到 ~/bin/quick_recon.sh
cat > /tmp/quick_recon.sh << 'SCRIPTEOF'
#!/bin/bash
# quick_recon.sh - 红队二进制快速侦察
TARGET=$1
if [ -z "$TARGET" ]; then
echo "Usage: $0 <binary>"
exit 1
fi
echo "=========================================="
echo " Quick Binary Recon: $TARGET"
echo "=========================================="
echo ""
echo "=== File Type ==="
file "$TARGET"
echo ""
echo "=== Security Protections ==="
python3 -c "
from pwn import *
e = ELF('$TARGET')
print(f'Arch: {e.arch}')
print(f'Bits: {e.bits}')
print(f'NX: {e.nx}')
print(f'Canary: {e.canary}')
print(f'PIE: {e.pie}')
print(f'RELRO: {e.relro}')
print(f'Stripped:{e.stripped}')
" 2>/dev/null || echo "pwntools not installed"
echo ""
echo "=== Sensitive Strings ==="
strings "$TARGET" | grep -iE 'flag|password|key|secret|token|http|https|\.com|\.net|\.org|admin|root|/bin/sh|/bin/bash' | sort -u
echo ""
echo "=== Dangerous Functions ==="
readelf -r "$TARGET" 2>/dev/null | grep -iE 'system|exec|popen|fork|socket|connect|dlopen|ptrace'
echo ""
echo "=== Library Dependencies ==="
readelf -d "$TARGET" 2>/dev/null | grep NEEDED
echo ""
echo "=== Entry Points ==="
readelf -h "$TARGET" 2>/dev/null | grep Entry
echo ""
echo "=========================================="
echo " Recon Complete"
echo "=========================================="
SCRIPTEOF
chmod +x /tmp/quick_recon.sh
# 使用:
# /tmp/quick_recon.sh /tmp/target.bin
# ========== 批量分析目录下所有二进制 ==========
for f in /tmp/*.bin; do
echo "=== $f ==="
/tmp/quick_recon.sh "$f"
echo ""
done
# ========== libc 数据库查询 (确定 libc 版本) ==========
# 当你泄露了某个函数地址后,可以用在线数据库确定 libc 版本:
# https://libc.blukat.me/
# https://libc.rip/
# 命令行查询:
# 已知: puts 地址 = 0x7f1234568750
# 最后12位: 750 (页内偏移不变)
# curl -s "https://libc.rip/api/find" -H "Content-Type: application/json" \
# -d '{"symbols": {"puts": "750"}}' | python3 -m json.tool无头模式(Headless)下的 Ghidra 分析
# Ghidra 支持无头模式——在服务器上跑,没有 GUI
# 路径通常在: /opt/ghidra/support/analyzeHeadless
# Arch Linux 上:
GHIDRA_HOME=$(dirname $(dirname $(readlink -f $(which ghidraRun))))
# 通常是 /opt/ghidra
# 无头分析一个二进制:
$GHIDRA_HOME/support/analyzeHeadless /tmp ghidra_tmp_project \
-import /tmp/target.bin \
-postScript GhidraScript.py \
-deleteProject
# /tmp: 项目目录
# ghidra_tmp_project: 项目名
# -import: 要分析的文件
# -postScript: 分析完成后执行的脚本
# -deleteProject: 分析完成后删除项目
# 使用 Python 脚本做无头分析:
cat > /tmp/headless_extract.py << 'PYEOF'
# Ghidra Headless Extract Script
# 在 analyzeHeadless 的 -postScript 中使用
from ghidra.program.model.symbol import SourceType
def extract_info():
fm = currentProgram.getFunctionManager()
listing = currentProgram.getListing()
# 输出所有函数
functions = fm.getFunctions(True)
with open("/tmp/ghidra_functions.txt", "w") as f:
for func in functions:
f.write("{} @ {}\n".format(func.getName(), func.getEntryPoint()))
# 输出所有字符串
data_iter = listing.getDefinedData(True)
with open("/tmp/ghidra_strings.txt", "w") as f:
while data_iter.hasNext():
data = data_iter.next()
try:
val = data.getValue()
if val and len(str(val)) > 2:
f.write("{} : {}\n".format(data.getAddress(), repr(val)))
except:
pass
extract_info()
PYEOF
# 运行无头分析:
# $GHIDRA_HOME/support/analyzeHeadless /tmp ghidra_project \
# -import /tmp/target.bin \
# -postScript /tmp/headless_extract.py \
# -deleteProjectPart 7: 实战靶机列表
以下是学习逆向与pwn的优质靶机和CTF平台:
| 靶机/平台 | 类型 | 说明 |
|---|---|---|
| pwnable.kr | 在线 | 经典pwn教学,从fd开始由浅入深 |
| pwn.college | 在线 | ASU出品,从零开始的交互式教学,带视频讲解 |
| ROP Emporium | 本地 | 专注ROP链训练的8道题,每道都有详细writeup |
| Exploit Education | 本地VM | Phoenix/Fusion系列,VirtualBox镜像 |
| Exploit Exercises | 本地 | Nebula/Protostar系列,入门级的漏洞利用 |
| Microcorruption | 在线 | TI MSP430微控制器的汇编级漏洞利用 |
| Crackmes.one | 在线 | 社区驱动的逆向题目库 |
| TryHackMe: Buffer Overflow Prep | 在线 | 栈溢出专项训练房 |
| HackTheBox: Jeeves | 在线 | 含二进制逆向的完整渗透链 |
| Nightmare | 在线 | 按难度分类的pwn题writeup合集 |
本章到此结束。逆向工程的能力不是”看完学会的”——是你在 Ghidra 里对着伪代码发呆两小时后突然理解了那个循环的意图,是你在 gdb 里反复
stepi几百次后终于在寄存器里看到了你控制的地址,是你在凌晨三点发现pop rdi; ret的 gadget 刚好就在 libc 里。每逆向一个二进制,你的攻击面就拓宽一寸。下一章我们进入硬件层面——用逻辑分析仪和芯片拆解来攻防嵌入式设备。