未受控的格式化字符串(Uncontrolled Format Strings)是一种常见的安全问题,主要发生在使用格式化函数时,如 printf、sprintf、fprintf 等。这类问题通常导致格式化字符串未受到严格控制,从而可能引发严重的安全漏洞,例如缓冲区溢出、信息泄露或代码执行等。
问题说明
在 C++ 或 C 中,格式化字符串用于定义输出的格式,如 %s 用于插入字符串,%d 用于插入整数。然而,如果格式化字符串是由用户输入的或由不可信来源提供的,并且没有经过适当的验证或清理,就会造成未受控的格式化字符串漏洞。
示例代码
假设你有一个函数,它从用户输入中获取格式化字符串并直接传递给 printf 函数:
#include <cstdio>
#include <cstring>
void unsafe_function(const char* user_input) {
printf(user_input); // 潜在的安全问题
}
int main() {
char user_input[256];
// 假设用户输入了以下内容
strcpy(user_input, "%x %x %x %x");
unsafe_function(user_input);
return 0;
}
在上面的示例中,用户输入的格式化字符串 %x %x %x %x 会导致 printf 输出堆栈中的内容,这可能包含敏感信息,如程序的内部状态、地址等。
安全问题
信息泄露:攻击者可以利用未受控的格式化字符串查看程序内存中的敏感信息,例如堆栈内容。
代码执行:在某些情况下,攻击者可以利用格式化字符串漏洞执行任意代码,例如通过 %n 写入数据到任意内存位置。
缓冲区溢出:在一些老旧或不安全的库中,未受控的格式化字符串可能导致缓冲区溢出,从而导致程序崩溃或被攻击。
预防措施
- 使用固定格式化字符串:始终使用固定的格式化字符串,而不是直接使用用户输入。例如:
void safe_function(const char* user_input) {
printf("%s", user_input); // 安全地输出用户输入
}
这样做会将用户输入作为普通字符串输出,而不会被解释为格式化字符串。
验证和清理用户输入:如果必须使用用户输入来生成格式化字符串,请对输入进行验证和清理,以确保它不会包含恶意格式化指令。
使用安全函数:使用更安全的函数,如 snprintf 代替 sprintf,可以限制输出的长度,从而减少溢出风险:
char buffer[256];
snprintf(buffer, sizeof(buffer), "User input: %s", user_input);
printf("%s\n", buffer);
- 使用格式化字符串库:在 C++ 中,可以使用更安全的库,如 fmt 库,它在设计时考虑了安全性,并避免了未受控的格式化字符串问题。
#include <fmt/core.h>
void safe_function(const std::string& user_input) {
fmt::print("User input: {}\n", user_input);
}