ctf常见模式就是拿到flag,而在做pwn题时,flag放在当前目录或者根目录,那么我们在与程序交互时,一般需要执行system("/bin/sh")来拿到shell,也就是启动一个交互式的 Shell 进程,来找到flag。 execve(‘/bin/sh’,NULL,NULL)
一、 核心数据处理工具
在 Pwn 中,我们主要是在和字节流(bytes)打交道,利用pwntools
1. 模板与上下文设置 (Setup)
context.log_level = 'debug' # 开启调试日志,看到发送接收的每个字节
context.arch = 'amd64' # 或者 'i386'
# 或者直接自动识别:
context.binary = './vuln'
#常见
context(os='linux', arch='amd64', log_level='debug')
2. 统一交互接口 (Interface)
binary = './vuln'
elf = ELF(binary)
# 如果题目给了 libc,也要加载,方便计算基址
# libc = ELF('./libc.so.6')
p = remote('1.2.3.4', 12345) #远程
p = process(binary) #本地
3. ROP 链构造 (可用)
# 这是一个典型的栈溢出 Payload
payload = flat([
b'A' * 0x28, # Padding
pop_rdi, # pop rdi; ret
elf.got['puts'], # 参数:puts 的 GOT 表项
elf.plt['puts'], # 调用 puts 函数 (泄露地址)
elf.symbols['main'] # 返回 main 函数,重新利用漏洞
])
io.sendline(payload)
4. 接收数据 (Receive)
recv(n): 接收n个字节。严格读取,不够会阻塞。recvline(): 接收一行数据,以\n结尾。recvuntil(): 最常用。接收数据直到遇到某个字符串(如b"Input:")。- Tips: 建议总是使用
b"..."前缀来确保是 bytes 类型。
- Tips: 建议总是使用
5. 发送数据
send(n):写入数据不附带'\n'sendline(n):写入数据附带'\n'sendafter(n):在b' '后面发送数据,写入数据不附带'\n'sendlineafter(n)
6. 打包与解包 (Packing & Unpacking)
计算机内存中,数据通常是小端序 (Little-Endian) 存储的。我们需要在“整数 (Integer)”和“字节流 (Bytes)”之间转换。
p64(num): Pack 64-bit. 将 Python 的整数转换为 8 字节的小端序 bytes。- 例:
p64(0xdeadbeef)\rightarrowb'\xef\xbe\xad\xde\x00\x00\x00\x00'(补齐到8字节) - 用于: 构造 Payload,比如把返回地址覆盖为
system的地址。
- 例:
u64(data): Unpack 64-bit. 将 8 字节的小端序 bytes 转换回 Python 整数。- 关键点:
u64必须接收长度正好为 8 的 bytes。如果不够,需要用ljust补齐。 - 用于: 接收 Leak 的地址(Libc基址、Canary等)。
- 关键点:
p32/u32: 对应 32 位系统的 4 字节转换。
7. 自动化地址获取 (Dynamic Addressing)
- 获取 PLT/GOT 表地址:
elf.plt['puts'](调用 puts 的地址)elf.got['puts'](存储 puts 真实地址的内存单元)
- 获取符号地址:
elf.symbols['magic_function'](程序中某个后门函数的地址)elf.bss()(BSS 段地址,常用于写入/bin/sh字符串)
8. 交互 (Interactive)
p.interactive(): 拿到 shell 后,将控制权交给用户,让你能手动输入ls,cat flag等命令
二、checksec
1. Arch (架构)
显示内容: i386-32-little 或 amd64-64-little
- 含义:
- 32位 (x86):函数参数通过栈传递,指针长 4 字节。
- 64位 (x64):前 6 个参数通过寄存器 (RDI, RSI, RDX, RCX, R8, R9) 传递,指针长 8 字节。
- 决定了 ROP 链的构造方式(32位直接传栈上,64位需要
pop rdi; ret等 gadget)。 - 在 pwntools 中设置
context.arch即可自动适配。
2. NX (No-Execute / DEP)
显示内容: NX enabled 或 NX disabled ,多数是开启
- 含义: 数据不可执行。
- Enabled:栈 (Stack) 和 堆 (Heap) 上的数据只能读写,不能作为代码执行。
- Disabled:栈上的数据可以直接被 CPU 执行。
- NX enabled (常态):写入栈的 Shellcode 无法运行。必须利用程序里已有的代码片段(Gadgets)或者 libc 函数(如
system) -> ROP (Return Oriented Programming) 攻击。
3. Stack (Canary / SSP)
显示内容: Canary found 或 No canary found
- 含义: 栈溢出哨兵。 在函数开始时,在栈上的
rbp之前插入一个随机值(Canary,通常以\x00结尾)。在函数结束ret之前,检查这个值是否被修改。 - 原理: 如果你试图通过溢出覆盖返回地址,必然会先覆盖掉中间的 Canary。程序检测到 Canary 变了,就会直接报错退出 (
*** stack smashing detected ***),不会执行ret。 - 攻击路径决策:
- No canary:直接溢出覆盖返回地址。
- Canary found:
- Leak Canary:利用
printf格式化字符串漏洞或read/puts配合,先读出 Canary 的值,在构造 Payload 时把原本的 Canary 填回去。 - 劫持
__stack_chk_fail:修改报错函数的 GOT 表(高阶技巧)。
- Leak Canary:利用
4. PIE (Position Independent Executable)
显示内容: PIE enabled 或 No PIE
- 含义: 地址随机化。
- No PIE:代码段地址固定。例如
main函数永远在0x400000这种地方。 - PIE enabled:程序每次运行,加载的基地址 (Base Address) 都是随机的。
- No PIE:代码段地址固定。例如
- 对 Pwn 的影响:
- No PIE:你可以直接在 IDA 里看地址,硬编码在脚本里(如
0x401234)。 - PIE enabled:你在 IDA 里看到的只是偏移(Offset)。你必须先泄露一个程序内的地址,减去它在 IDA 里的偏移,算出程序的基地址。
- No PIE:你可以直接在 IDA 里看地址,硬编码在脚本里(如
- 应对 :
# 1. 泄露某个函数地址 (比如 main 的真实地址) leak_main = u64(io.recv(6).ljust(8, b'\x00')) # 2. 计算基地址 elf.address = leak_main - elf.symbols['main'] print(f"Base Address: {hex(elf.address)}") # 3. 之后所有地址都会自动加上基地址 system_addr = elf.plt['system'] # pwntools 会自动计算
5. RELRO (Relocation Read-Only)
显示内容: No RELRO, Partial RELRO, Full RELRO
- 含义: 重定位表只读。主要保护 GOT 表 (Global Offset Table)。
- 详解:
- No RELRO / Partial RELRO (常见):
- GOT 表可写。
- 攻击手法:GOT Hijacking (GOT 劫持)。你可以把
puts的 GOT 表项修改为system的地址。这样下次程序调用puts("/bin/sh")时,实际上执行的是system("/bin/sh")。
- Full RELRO (高难):
- GOT 表只读。程序启动时就解析完所有函数地址,然后把 GOT 表设为 Read-Only。
- 攻击手法:无法修改 GOT 表。必须寻找其他内存钩子(Hook),如
__free_hook(libc中的钩子) 或__malloc_hook,或者直接攻击栈上的返回地址。
- No RELRO / Partial RELRO (常见):
三、ida与gdb
更多内容可见该文章 https://bbs.kanxue.com/thread-266021-1.htm
# ===断点===
pwndbg>b main # 断在指定函数位置
pwndbg>b *main+5 # 断点在main+5的位置
pwndbg>b *0x401186 # 在指定地址断点
# ===查看===
pwndbg>info b # 查看所有断点
pwndbg>info functions # 查看所有函数
pwndbg>info sharedlibrary # 查看共享链接库
pwndbg>info sharedlibrary hiredi* # 查看指定共享链接库,可以用前缀 + * 匹配
pwndbg>vmmap # 打印虚拟内存映射页
pwndbg>stack 30 # 查看栈,数字表示查看栈帧数
pwndbg>heap # 查看堆
pwndbg>p fun_name # 打印函数地址
pwndbg>p 0x10-0x08 # 计算0x10-0x08的结果
pwndbg>p *(0x123456789) # 查看指定地址指向的值
pwndbg>p $rdi # 查看寄存器存放的地址
pwndbg>p *($rdi) # 查看rdi存放的地址指向的值
# ===运行===
pwndbg>r # 运行程序
pwndbg>n # c语言的执行下一步指令
pwndbg>ni # 汇编指令的执行下一步指令
pwndbg>c # 执行到下一个断点处
pwndbg>si # 调试过程中进入当前使用的函数
pwndbg>q # 退出调试程序
pwndbg>start # 从程序入口开始执行函数
telescope
提供更友好的内存查看方式,自动解析指针和字符串。- 用法: tele ,tel
telescope ADDRESS:从指定地址开始显示内存内容。
- 用法: tele ,tel
vmmap
显示当前进程的内存映射信息。search
在内存中搜索特定的字节序列。
1. x 命令
x 命令用于查看内存中的数据。它可以显示指定地址或变量所占内存的内容。其基本语法如下:
x/[n][f][u] addr
"参数说明:"
n:可选,表示要显示的单元数量,默认为1。f:可选,表示显示的格式,常见格式包括:d:十进制整数x:十六进制c:字符
u:可选,表示数据单位,常见单位包括:b:字节h:半字(2 字节)w:字(4 字节)g:巨字(8 字节)
示例:
- 查看内存地址的内容:
x/10gx 0x7fffffffe000
以上命令将以十六进制格式显示从 0x7fffffffe000 开始的10个字节的内容。
2. 查看变量的内容:
x/4d my_array
该命令将以十进制格式查看 `my_array` 数组的前4个元素。
2. p 命令
p 命令用于打印变量的值,通常用于查看变量的当前状态。
四、栈溢出与rop
栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。这种问题是一种特定的缓冲区溢出漏洞,类似的还有堆溢出,bss 段溢出等溢出方式。栈溢出漏洞轻则可以使程序崩溃,重则可以使攻击者控制程序执行流程。此外,我们也不难发现,发生栈溢出的基本前提是:
- 程序必须向栈上写入数据。
- 写入的数据大小没有被良好地控制。
ROP (Return-Oriented Programming):面向返回编程
ROP 是 Pwn 选手中期必须掌握的核心技能。如果说栈溢出是“暴力破门”,那 ROP 就是“借力打力”。
命令行工具:**`ROPgadget --binary ./pwn --only "pop|ret"
1. 为什么需要 ROP?
还记得 checksec 里的 NX (No-Execute) 保护吗?
- 没有 ROP 之前:我们把 shellcode 写在栈上,把返回地址改到栈上执行。
- 开启 NX 之后:栈变成了“只读/只写,不可执行”。你写的 shellcode 就算填进去了,CPU 也不认。
ROP 的核心思想:
既然不能自己写代码执行,那我们就利用程序里(或 Libc 里)原本就有的代码片段。
这就好比绑匪写的勒索信:他不会自己手写字(怕被认出笔迹/被 NX 拦截),而是从报纸上剪下一个个单词(Gadgets),拼凑成一句完整的话(ROP Chain)。
2. ROP 的基本单位:Gadget
Gadget 是指以 ret 指令结尾的一小段汇编指令。
最典型的 Gadget:pop rdi; ret
为什么是 ret?
ret指令本质上等于pop rip。它会把栈顶的一个数值弹出来,赋值给指令指针寄存器 (RIP),告诉 CPU 下一步去哪。- 只要我们把栈布局好,一个 Gadget 执行完
ret后,就会跳到下一个 Gadget 的地址。这样就能把无数个小片段串起来执行。
3. 构造 ROP 链的逻辑 (以 x64 为例)
在 64 位系统中,函数的前 6 个参数依次保存在寄存器 RDI, RSI, RDX, RCX, R8, R9 中。
我们的终极目标通常是执行 system("/bin/sh")。
任务分解:
- 调用函数:我们需要调用
system。 - 传递参数:
system需要一个参数(字符串指针),这个参数必须放在 RDI 寄存器里。 - 我们要做的:找到一个能修改
RDI的 Gadget。
完美 Gadget:pop rdi; ret
- 执行过程:
pop rdi:把栈顶的数据(我们填好的"/bin/sh"的地址)弹入RDI。ret:把栈顶的下一个数据(我们填好的system的地址)弹入RIP,跳转执行。
4. 手把手构造一条 ROP 链
假设我们已经有了以下地址(通过 pwntools 或调试获得):
pop_rdi_ret_addr=0x401123(Gadget 地址)bin_sh_addr=0x404050(字符串 "/bin/sh" 的地址)system_addr=0x401060(system 函数地址)
栈的布局(Payload)应该是这样的:
| 栈内容 (从低地址到高地址) | 作用 |
|---|---|
| Padding (比如 'A' * 40) | 填满缓冲区,直到覆盖到 Return Address 之前 |
0x401123 (pop rdi; ret) | [Step 1] 覆盖原本的 Ret Addr,程序跳到这里执行 |
0x404050 (&"/bin/sh") | [Step 2] 被 pop rdi 消耗掉,存入 RDI 寄存器 |
0x401060 (&system) | [Step 3] 上一步 ret 后跳到这里,执行 system(RDI) |
0xdeadbeef (任意地址) | [Step 4] system 返回后的地址 (通常不重要,除非想优雅退出) |
5. 坑:Stack Alignment (栈对齐)
这是新手在 x64 ROP 中遇到最崩溃的问题:"明明逻辑都对,本地通了,远程一打就 Crash,报错 SIGSEGV (Segmentation Fault)。"
原因:
在 Ubuntu 18.04 及更高版本的 GLIBC 中,调用 system 时,有些指令(如 movaps)要求栈顶指针 (RSP) 必须是 16 字节对齐的(即地址以此结尾:...0 或 ...8 的某种对齐关系)。
Pasted image 20260306193314.png
简单解法:
如果在调用 system 前程序崩了,加一个 ret gadget。
ret 指令相当于“什么都不做,只是跳一次”,但这会让 RSP 移动 8 个字节,从而让栈重新对齐。
修正后的 Payload:
ret_addr = 0x000000000040101a # 找一个单纯的 ret 指令地址
payload = flat([
b'A' * 40,
ret_addr, # <--- 这里的 ret 纯粹为了垫高 8 字节,解决对齐问题
pop_rdi_ret,
bin_sh,
system_plt
])
730

被折叠的 条评论
为什么被折叠?



