漏洞分析实践_格式化字符串漏洞

1. 基础知识

1.1 常见的格式化输出函数

printf: 是一个用于向标准输出(stdout)按规定的格式输出信息的函数,其原型为 int printf(const char *format, [argument]…)。其中,format 是格式控制字符串,用于指定输出的格式,而其他参数则是输出项,用于提供与格式控制字符串对应的数据。

sprintf 是一个将格式化的数据写入指定字符串中的函数,其原型为 int sprintf(char *buffer, const char *format, [argument]…)。其中,buffer 是要写入数据的目标字符串缓冲区,format 是格式控制字符串,其他参数则是输出项。该函数将第三部分的数据根据第二部分格式控制字符串的要求进行格式化,然后将格式化后的数据存储到指定的字符串缓冲区中。

snprintf 是在 sprintf 基础上进行改进的函数,它限制了可写入字符的最大长度 n。其原型为 int snprintf(char *str, size_t size, const char *format, [argument]…)。当格式化后的字符串长度小于指定的 size 时,函数将整个字符串复制到 str 中,并在末尾添加字符串结束符 \0;而当格式化后的字符串长度大于或等于 size 时,函数将最多复制 size-1 个字符,并在末尾添加字符串结束符 \0。通过这种方式,snprintf 可以防止缓冲区溢出,确保字符串不会超出指定的缓冲区大小。

fprintf 用于将格式化的数据输出到指定的流或文件中,其原型为 int fprintf(FILE *stream, const char *format, [argument]…)。该函数根据指定的格式控制字符串 format,将数据写入到输出流 stream 中。当 streamstdout 时,fprintf 的功能与 printf 相同,都是将格式化的数据输出到标准输出。

vprintfvsprintfvsnprintfvfprintf 的功能分别对应于 printfsprintfsnprintffprintf,它们的区别在于将变长参数列表替换为一个 va_list 类型的参数。这使得这些函数可以接受可变参数,从而实现更加灵活的格式化输出。

1.2 格式化字符串

格式化字符串的定义:格式化字符串是由普通字符串和格式化规定字符构成的字符序列。普通字符会原封不动地复制到输出流中,而格式化规定字符以 % 开始,用于确定输出内容的格式。

格式化规定字符的格式:常见的格式化字符的格式为,%[parameter][flags][fieldwidth][.precision][length]type。

  • parameter 可以是省略的,也可以是 n$ 形式,其中 n 表示参数列表中的第 n 个参数,通过这种方式可以直接访问第 n 个参数。
  • flags 用于调整输出的格式,包括符号、空白、小数点、八进制和十六进制的前缀等。
  • fieldwidth 用于限制显示数值的最小宽度。当输出字符的个数不足指定宽度时,默认用空格填充,或者使用 flags 中指定的其他填充方式;如果输出字符超过限制宽度,则不会被截断,仍然正常显示。
  • precision 用于指定输出的最大长度。
  • length 指定浮点型或整型参数的长度。
  • type 是转换说明符,用于指定所应用的转换类型,它是格式字符串中唯一必须的部分。
#include <stdio.h>

int main() {
    int num = 12345;
    double pi = 3.141592653589793;

    // 示例 1: 输出整数,宽度为 10
    // 使用 %10d 来设置输出宽度为 10,不足时会用空格填充
    printf("%10d\n", num);                      // 输出 "     12345"

    // 示例 2: 输出浮点数,保留 4 位小数
    // 使用 %.4f 来保留 4 位小数
    printf("%.4f\n", pi);                       // 输出 "3.1416"

    // 示例 3: 输出浮点数,带符号,宽度为 10,保留 2 位小数
    // 使用 %+10.2f 来显示带符号的浮动数,宽度为 10,保留 2 位小数
    printf("%+10.2f\n", pi);                    // 输出 "     +3.14"

    // 示例 4: 输出字符串,宽度为 10,左对齐
    // 使用 %-10s 来设置字符串宽度为 10,并左对齐
    printf("%-10s\n", "Hello");                 // 输出 "Hello     "

    return 0;
}

转换说明符 %n 详解%n 是一种特殊的格式化字符,它并不向 printf() 传递格式化信息,而是令 printf() 将已经输出的字符总数写入对应的整型指针参数所指向的变量中。例如,printf("hello%n", &i); 使得 i 的值为 5,因为 "hello" 输出了 5 个字符。%n 可能会被恶意利用进行缓冲区溢出攻击,比如通过 printf("%n", a),程序会计算已输出的字符数量并将其写入到 a 指向的内存位置,从而改写栈中内存。具体来说,%n 写入的内存最大为 4 字节,%hn 最大为 2 字节,%hhn 最大为 1 字节。

#include <stdio.h>

int main() {
    int i;
    // 使用 %n 获取 printf 输出的字符数
    printf("hello%n", &i);
    
    // 输出已打印的字符数
    printf("The number of characters printed: %d\n", i); // 输出:The number of characters printed: 5
    return 0;
}

普通使用 %n 示例printf("hello%n", &i); 会输出字符串 "hello",并将输出的字符数(即 5)写入 i 变量中。然后,printf("The number of characters printed: %d\n", i); 打印 i 的值,输出结果为 The number of characters printed: 5

#include <stdio.h>

int main() {
    char buffer[20];
    int target;

    // 假设 buffer 的初始内容为空,target 变量位于栈上的某个位置
    printf("Hello%n World\n", &target);

    // 输出 target 的内容,攻击者可能修改它
    printf("Target value: %d\n", target); // 如果成功,target 的值将被修改
    return 0;
}

利用 %n 进行缓冲区溢出攻击%n 会将已经输出的字符数写入 target 变量中。在这个例子中,"Hello%n World\n" 会先输出 Hello(5个字符)然后将字符数 5 写入到 target 变量中。然而,在攻击者控制的环境中,%n 可以用来通过改变 target 的值来实现栈溢出,甚至改变程序的行为。

2. 漏洞原理

2.1 printf 运行原理

printf 的运行原理是通过接受可变参数来实现的。由于参数个数是动态的,调用者可以自由指定参数的数量和类型,而被调用者并不能知道函数调用时有多少参数被压入栈帧。为了应对这一问题,printf 会逐个解析 format 参数中的每个格式化规定字符,从中判断参数的个数和类型。然而,如果 format 中指定的参数数量超过了实际提供的变参列表中的参数数量,printf 由于无法判断这一点,可能会对未提供的内存数据进行读写操作,这可能导致内存错误或潜在的安全漏洞。

#include <stdio.h>

int main() {
    int a = 10;
    float b = 3.14;
    
    // 这里我们传入了两个参数 (int 和 float),但格式化字符串要求三个参数
    printf("Integer: %d, Float: %f, String: %s\n", a, b);

    return 0;
}


Integer: 10, Float: 3.140000, String: (some random memory content)

2.2 格式化字符串利用方法

泄漏内存数据:当 printf 只提供了 format 参数,却没有提供其他参数时,printf 会根据 format 的要求输出多个值,但这些值并不是实际输入的参数,而是栈中未初始化的数据。具体来说,打印出的这些数值实际上是位于 format 参数之后的栈数据。这种格式化字符串漏洞允许攻击者通过控制 format 字符串,从栈中泄露敏感信息,甚至可能利用该漏洞进一步破坏程序的安全性。

覆写内存:格式化输出函数不仅能够读取内存数据,还能通过转换说明符 %n 向指定地址写入一个整数值。这种向任意地址写入数据的能力,可以被攻击者利用来覆盖内存中的关键数据。比如,fs_write.c 演示了如何通过格式化字符串漏洞,修改 flag 变量的值,从而实现对程序行为的恶意控制。

复写内存的总体思路是通过构建特定的 printf(format, argument) 参数来实现的。首先,构造含有 %n 的特定 format 格式,将值 0xbeef 写入存放 flag 变量的地址(如 0x804a028 / 0x804a029)。具体步骤如下:

  • format 数组中放入 flag 变量的地址,以构建 printf()argument 参数(例如 0x804a028 / 0x804a029)。
  • 构造 printf()format 格式,使用 $ 参数指定 flag 变量的地址是第几个参数。例如,利用 sprintf() 输出格式 %231c%11$hhn%207c%12$hhn,通过 $ 来定位 flag 变量地址。
  • format 中加入 %n 和相关的输出字符长度,通过 %n (如 hhn)向对应的地址写入数据,将 flag 变量的值从 0xbabe 改为 0xbeef,实现内存的复写。
#include <stdio.h>

int flag = 0xbabe;  // 初始 flag 变量值为 0xbabe

int main() {
    printf("Flag address: %p\n", (void*)&flag);  // 打印 flag 的地址
    
    // 构造特定的格式化字符串进行内存覆盖
    // 假设我们通过漏洞输入:%231c%11$hhn%207c%12$hhn
    // 这将会修改 flag 变量的值为 0xbeef
    printf("Exploit: %231c%11$hhn%207c%12$hhn\n", 0, 0);

    printf("Modified flag: 0x%x\n", flag);  // 打印修改后的 flag 值
    return 0;
}

这里我们定义了一个 flag 变量,并初始化为 0xbabe。在程序运行时,我们想要通过格式化字符串漏洞将其值修改为 0xbeef

printf("Flag address: %p\n", (void*)&flag);

这行代码打印出 flag 变量的地址,假设输出的地址是 0x804a028(这个地址是虚构的,实际地址会根据程序的加载位置不同而有所变化)。

printf("Exploit: %231c%11$hhn%207c%12$hhn\n", 0, 0);

这行代码是构造格式化字符串漏洞的核心部分,详细分析如下:

  • %231c:这部分会输出 231 个字符的空格,填充输出缓冲区。
  • %11$hhn:这部分会将缓冲区中的字符数(231)写入第 11 个参数所指向的地址。假设第 11 个参数是 0x804a028(即 flag 变量的地址)。
  • %207c:这部分会输出 207 个字符的空格,再次填充缓冲区。
  • %12$hhn:这部分会将 207 写入第 12 个参数所指向的地址。假设第 12 个参数是 0x804a029flag 变量的下一个字节的地址)。

通过这些格式化字符串,我们可以控制内存的写入,将 flag 变量的两个字节覆盖为 0xbeef

### 关于蓝桥云课 Linux 基础入门课程的作业资料与教程 #### 文件打包与解压缩指南 针对蓝桥云课中的Linux基础入门课程,特别关注文件打包与解压缩部分的学习。由于Windows系统与Linux/Unix在文本文件格式上的差异,例如换行符的不同——前者采用CR+LF表示回车加换行,而后者仅使用LF作为换行标志,这可能导致跨平台传输时遇到显示异常的情况[^3]。 为了确保由Linux创建并打算在Windows环境下使用的ZIP档案能够正常工作,建议采取特定措施: ```bash zip -r -l -o shiyanlou.zip /home/shiyanlou/Desktop ``` 此命令通过`-l`选项实现了自动转换换行符的功能,从而解决了潜在的文字格式冲突问题;同时利用`-o`参数指定了输出文件名及其路径。 #### 文本处理工具简介 除了上述提到的操作外,了解一些基本但强大的文本处理工具有助于更高效地完成日常任务。例如AWK就是一个非常优秀的文本分析器,适用于多种场景下的字符串解析、模式匹配等工作流中[^4]。此外,还有诸如`tr`、`col`、`join`和`paste`这样的实用程序可以用来执行字符替换、列过滤以及多文件间的关联操作等复杂任务[^5]。 #### 实验环境搭建提示 考虑到初学者可能会面临配置开发环境方面的挑战,在准备阶段应当熟悉几个常用指令的基本语法及应用场景。尽管这部分内容看似简单,却构成了后续深入探索不可或缺的基础构件[^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值