C语言formatString漏洞如何产生与防范?

99ANYc3cd6
预计阅读时长 22 分钟
位置: 首页 C语言 正文

什么是格式化字符串?

在 C 语言中,格式化字符串是一个字符序列,它主要用在两个标准库函数中:

c语言formatString
(图片来源网络,侵删)
  1. printf 系列函数:用于将格式化后的数据输出到标准输出(如屏幕)。
  2. scanf 系列函数:用于从标准输入(如键盘)读取格式化数据。

格式化字符串的核心作用是作为模板或蓝图,告诉这些函数如何处理后续要输出的数据(对于 printf)或如何解释读取到的数据(对于 scanf)。

一个格式化字符串通常包含两种类型的字符:

  1. 普通字符:这些字符会原样输出或输入,在 printf("Hello, "); 中,"Hello, " 就是普通字符,会直接显示在屏幕上。
  2. 格式说明符:以百分号 () 开头,后面跟着一个或多个特定的字符,它是一个占位符,用于指定一个特定类型的数据应该如何被格式化。

printf 系列中的格式说明符

printf 的通用语法是: printf("格式化字符串", 变量1, 变量2, ...);

格式说明符的通用结构是:%[标志][宽度][.精度][长度]类型

c语言formatString
(图片来源网络,侵删)

下面我们分解来看各个部分,并提供最常用的示例。

核心类型

这是格式说明符中必不可少的部分,它定义了要显示的数据类型。

类型说明符 数据类型 示例
%d%i int (有符号十进制整数) int age = 25; printf("Age: %d", age);
%c char (单个字符) char grade = 'A'; printf("Grade: %c", grade);
%s char * (字符串,以 \0 char name[] = "Alice"; printf("Name: %s", name);
%f double (浮点数,默认6位小数) double pi = 3.14159; printf("Pi: %f", pi);
%lf double (用于 scanf 读取 double) double price; scanf("%lf", &price);
%p void * (指针地址) int num = 10; printf("Address: %p", &num);
%x%X unsigned int (十六进制数) int color = 0xFF00FF; printf("Color: %x", color);
%o unsigned int (八进制数) int permissions = 0755; printf("Permissions: %o", permissions);
%u unsigned int (无符号十进制整数) unsigned int count = 100; printf("Count: %u", count);
输出百分号字符本身 printf("100%%");

示例代码:

#include <stdio.h>
int main() {
    int integer = 42;
    float floating = 3.14f;
    double double_precision = 123.456789;
    char character = 'Z';
    char string[] = "Hello C";
    int hex_value = 255;
    printf("Integer: %d\n", integer);
    printf("Float: %f\n", floating);
    printf("Double: %lf\n", double_precision); // 注意:printf中%f和%lf对于double通常可以互换,但%lf是标准
    printf("Character: %c\n", character);
    printf("String: %s\n", string);
    printf("Hexadecimal: %x\n", hex_value);
    return 0;
}

修饰符

修饰符可以用来控制输出的格式。

c语言formatString
(图片来源网络,侵删)
  • 宽度:指定输出的最小字符数。

    • %5d:输出一个整数,如果不足5位,则在左边用空格补齐。
    • %-5d:输出一个整数,如果不足5位,则在右边用空格补齐(左对齐)。
    • %10.2f:输出一个浮点数,总宽度为10位,其中小数部分占2位。
  • 精度

    • 对于浮点数 (%f, %e, %g),.2 表示小数点后保留2位。
    • 对于字符串 (%s),.5 表示只输出前5个字符。
    • 对于整数 (%d),.5 表示输出至少5位,不足则在左边补0。

示例代码:

#include <stdio.h>
int main() {
    int num = 123;
    double pi = 3.1415926535;
    char text[] = "Format String";
    printf("Width 5: |%5d|\n", num);      // '|  123|'
    printf("Left align: |%-5d|\n", num);   // '|123  |'
    printf("Precision 2: |%.2f|\n", pi);   // '|3.14|'
    printf("Width 10, Precision 2: |%10.2f|\n", pi); // '|      3.14|'
    printf("String precision 5: |%.5s|\n", text);     // '|Forma|'
    return 0;
}

scanf 系列中的格式说明符

scanf 的通用语法是: scanf("格式化字符串", &变量1, &变量2, ...);

注意scanf 需要变量的地址& 运算符),因为它需要知道在哪里存储读取到的值。

scanf 的格式说明符比 printf 简单一些,但有一些非常重要的规则。

类型说明符 对应的变量类型 示例
%d int * (指向整数的指针) int age; scanf("%d", &age);
%c char * (指向字符的指针) char initial; scanf("%c", &initial);
%s char * (指向字符数组的指针,即字符串) char name[50]; scanf("%s", name);
%f float * (指向浮点数的指针) float price; scanf("%f", &price);
%lf double * (指向双精度浮点数的指针) double pi; scanf("%lf", &pi);

scanf 的关键注意事项

  1. 空白字符(空格、制表符、换行符)的跳过scanf 会自动跳过输入中的所有空白字符(包括空格、Tab、回车)。scanf("%d %d", &a, &b);,你输入 10 2010\n20 的效果是一样的。

  2. %s 的问题%s 会读取字符,直到遇到下一个空白字符为止,这意味着它不能读取包含空格的字符串(如 "Hello World")。

    • 解决方案:使用 fgets 函数来读取整行输入。
  3. 缓冲区溢出: 当使用 %s%c%[...] 时,如果用户输入的数据超过了你为变量分配的空间,就会发生缓冲区溢出,这是一个严重的安全漏洞。

    • 解决方案:指定最大读取宽度。char name[20]; scanf("%19s", name);,这里的 19 保证了最多读取19个字符,留1位给字符串结束符 \0
  4. %c 与空白字符: 如果你先使用 scanf("%d", ...) 读取一个数字,然后按回车,这个回车符会留在输入缓冲区中,如果你接着用 scanf("%c", ...) 读取一个字符,它会直接读取这个回车符,而不是等待你输入。

    • 解决方案:在读取 %c 之前,可以先清空输入缓冲区,一个简单的方法是 while (getchar() != '\n');,它会读取并丢弃所有字符,直到遇到换行符。

示例代码:

#include <stdio.h>
int main() {
    int id;
    char name[50];
    double salary;
    printf("Enter your ID: ");
    scanf("%d", &id);
    // 清除输入缓冲区中的换行符,防止它被 fgets 读取
    while (getchar() != '\n');
    printf("Enter your full name: ");
    // 使用 fgets 安全地读取一行
    fgets(name, sizeof(name), stdin);
    printf("Enter your salary: ");
    scanf("%lf", &salary);
    printf("\n--- Information ---\n");
    printf("ID: %d\n", id);
    printf("Name: %s", name); // fgets 会保留换行符,所以这里不用加\n
    printf("Salary: %.2f\n", salary);
    return 0;
}

高级和特殊格式说明符

  • %[] (扫描集): 这是 scanf 的一个强大功能,允许你指定一组要读取的字符。

    • %[a-z]:读取所有小写字母。
    • %[0-9]:读取所有数字。
    • %[^,]:读取所有字符,直到遇到逗号 () 为止。
  • : 在 printf 中,如果你想输出一个字面的 字符,你需要使用 。


安全性问题:格式化字符串漏洞

这是 C 语言中一个非常著名且危险的安全漏洞,它主要与 printf 系列函数有关。

漏洞原理

printf 函数的第一个参数是格式化字符串,如果这个字符串来自用户输入,而程序员又没有提供足够的其他参数来匹配格式说明符,就会导致未定义行为,甚至安全漏洞。

漏洞代码示例:

#include <stdio.h>
void vulnerable_function(char *user_input) {
    printf(user_input); // 危险!直接使用用户输入作为格式化字符串
}
int main() {
    char input[100];
    printf("Enter a format string: ");
    fgets(input, sizeof(input), stdin);
    vulnerable_function(input);
    return 0;
}

攻击方式

攻击者可以输入类似这样的字符串: %x %x %x %x

程序会尝试在栈上寻找四个整数参数并打印它们的十六进制值,这可以用来泄露内存信息。

更严重的是,攻击者可以输入: %n

%n 比较特殊,它不打印任何东西,而是将到目前为止已经打印的字符数量写入到一个对应的 int * 参数指向的内存地址中。

攻击示例: AAAA %x %x %x %x %n

如果攻击者能精确控制栈上的数据,他们可以将 AAAA 的地址作为 printf 的一个参数(通过将 AAAA 压入栈中),然后使用 %n 将一个特定的值(shellcode 的地址)写入到 AAAA 指向的内存,从而劫持程序执行流。

如何防御

  1. 永远不要将用户输入直接用作格式化字符串
  2. 始终为 printf 提供足够的参数
  3. 使用现代的安全函数
    • printf -> printf_s (MSVC特有)
    • sprintf -> sprintf_s
    • scanf -> scanf_s

这些安全版本的函数会检查参数数量是否匹配格式说明符,如果不匹配,它们会直接拒绝执行并报错,从而防止漏洞的发生。


特性 printf scanf
目的 输出格式化数据 输入并解析格式化数据
参数 变量的 变量的地址 (&var)
核心 格式说明符定义输出格式 格式说明符定义输入解析规则
安全性 漏洞风险较低 漏洞风险高(缓冲区溢出、格式化字符串漏洞)
关键函数 printf, fprintf, sprintf scanf, fscanf, sscanf

掌握格式化字符串是 C 语言编程的基础,理解它的语法、规则以及潜在的安全风险,是写出健壮、安全 C 代码的关键一步。

-- 展开阅读全文 --
头像
C语言与.NET Framework如何协同开发?
« 上一篇 01-03
C语言receivedata函数如何实现数据接收?
下一篇 » 01-03

相关文章

取消
微信二维码
支付宝二维码

目录[+]