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: 逆向工具链速查

完整工具箱

工具分类用途安装一句命令
GhidraGUI反汇编NSA开源,对标IDApacman -S ghidraghidraRun
radare2CLI反汇编终端原生逆向框架pacman -S radare2r2 -A file
rizinCLI反汇编r2的社区fork,Cutter的引擎pacman -S rizinrizin -A file
CutterGUI反汇编r2的Qt图形前端pacman -S cuttercutter file
Binary NinjaGUI反汇编商业软件,反编译器质量极高官网下载binaryninja file
GDB调试器GNU调试器pacman -S gdbgdb ./file
GDB-GEFGDB插件美化GDB,增加pwn功能pacman -S gefGDB自动加载
GDB-PEDAGDB插件Python Exploit Development Assistancegit clonesource peda.py
pwntools漏洞利用Python漏洞利用库pip install pwntoolsfrom pwn import *
pwndbgGDB插件现代化的GDB pwn插件pacman -S pwndbgGDB自动加载
checksec安全检查检查二进制保护机制pwntools自带checksec ./file
ropperROP搜索搜索ROP gadgetpip install ropperropper -f file
ROPgadgetROP搜索搜索ROP gadgetpip install ROPgadgetROPgadget --binary file
one_gadgetROP搜索找libc中的one-shot gadgetpacman -S one_gadgetone_gadget libc.so
objdump反汇编ELF文件分析和反汇编系统自带(binutils)objdump -d file
readelfELF分析读取ELF结构和段信息系统自带(binutils)readelf -a file
nm符号表列出符号表系统自带(binutils)nm -C file
strings字符串提取可打印字符串系统自带(binutils)strings file
ltrace库追踪追踪动态库函数调用pacman -S ltraceltrace ./file
strace系统调用追踪系统调用pacman -S stracestrace ./file
objcopy段操作提取/转换段数据系统自带(binutils)objcopy -O binary -j .rodata file out
xxd十六进制十六进制dumppacman -S xxdxxd file
hexdump十六进制十六进制查看pacman -S util-linuxhexdump -C file
file文件类型识别文件类型系统自带(coreutils)file binary
patchelfELF修改修改ELF属性pacman -S patchelfpatchelf --set-interpreter ld.so binary
die查壳检测可执行文件壳和编译器pacman -S diediec file
upx加壳/脱壳UPX压缩/解压pacman -S upxupx -d file
binwalk固件分析固件解包和文件提取pacman -S binwalkbinwalk -e firmware.bin
qemu-user模拟执行不同架构下运行ELFpacman -S qemu-userqemu-x86_64 ./mips_binary
volatility3内存取证内存dump分析pip install volatility3vol -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 \
#     -deleteProject

Part 7: 实战靶机列表

以下是学习逆向与pwn的优质靶机和CTF平台:

靶机/平台类型说明
pwnable.kr在线经典pwn教学,从fd开始由浅入深
pwn.college在线ASU出品,从零开始的交互式教学,带视频讲解
ROP Emporium本地专注ROP链训练的8道题,每道都有详细writeup
Exploit Education本地VMPhoenix/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 里。每逆向一个二进制,你的攻击面就拓宽一寸。下一章我们进入硬件层面——用逻辑分析仪和芯片拆解来攻防嵌入式设备。