https://bbs.kanxue.com/thread-250858.htm 大神的格式化字符串文章

printf 格式化字符串漏洞是一类较为常见的安全漏洞,主要出现在程序通过不安全的方式处理用户提供的输入,并将其直接传递给 printf 系列函数(例如 printfsprintffprintf 等)时。由于格式化字符串的灵活性,如果没有正确过滤或限制用户输入,攻击者可以利用格式化字符串功能执行一些未预期的操作,例如泄露内存内容、覆盖程序内存等,最终可能导致远程代码执行或程序崩溃。

漏洞成因

printf 系列函数允许使用格式化字符串(如 %d%s%x 等)来格式化输出。当程序直接使用用户输入作为格式化字符串,而没有进行适当的验证或限制时,攻击者可以精心构造恶意的格式化字符串来利用漏洞。

漏洞示例

考虑以下简单的 C 代码:

#include <stdio.h>

int main() {
    char input[100];
    printf("Please enter your input: ");
    scanf("%s", input);
    printf(input);  // 潜在的格式化字符串漏洞
    return 0;
}

在这个代码中,printf 直接使用用户提供的 input 作为格式化字符串,没有进行任何过滤。如果用户输入类似于 %xprintf 会将栈上的数据以十六进制的形式打印出来,而不是简单地输出用户输入的字符串。这种行为可以用于窥探栈上的敏感信息,比如函数返回地址、局部变量等。

漏洞利用

  1. 信息泄露:使用格式化符号如 %x%p,攻击者可以逐个打印栈上的内容,从而泄露敏感信息。例如,如果用户输入 "%x %x %x %x",攻击者可以看到程序栈上的四个值。

  2. 内存写入:通过 %n 格式化符号,攻击者可以控制 printf 函数将打印的字符数写入指定的内存地址。如果攻击者能控制写入的地址和内容,则可以改变程序的行为,甚至执行任意代码。例如,用户输入 "AAAA%n" 将会把字符数 4 写入栈上的一个地址。

printf("AAAA%n", &i);  // 会将 4 写入变量 i 中

漏洞的潜在危害

  • 信息泄露:可以通过读取栈上的数据泄露敏感信息,如密码、函数地址、返回地址等,甚至是程序的源代码地址。

  • 远程代码执行:攻击者可以通过修改程序的返回地址、函数指针或其他控制流来执行任意代码。

  • 拒绝服务(DoS):利用 %s 格式符可以让程序尝试打印某个无法访问的地址,导致程序崩溃。

典型的攻击步骤

  1. 信息收集:使用 %x%p 等格式符来收集栈信息,确定关键的内存地址。

  2. 覆盖数据:通过 %n 格式符将攻击者选择的值写入目标内存地址,从而改变程序执行流程,达到覆盖返回地址等目的。

  3. 利用:通过覆盖内存地址或控制关键数据,实现任意代码执行或崩溃攻击。

修复与预防

  1. 使用格式化字符串:应避免直接将用户输入作为格式化字符串传递给 printf 系列函数。应明确指定格式化字符串,例如:

printf("%s", input);  // 安全:指定了格式化符为 %s
  1. 输入验证:在接受用户输入时,应该对输入进行严格验证和过滤,防止恶意构造的字符串进入到 printf 函数。

  2. 使用安全函数:尽量使用更安全的函数替代,如 snprintf,以防止缓冲区溢出和格式化字符串漏洞。

  3. 编译器保护:现代编译器提供了一些保护机制,如格式化字符串检查(-Wformat-Wformat-security),可以在编译时警告潜在的格式化字符串漏洞。

泄露canary值

通过格式化字符串漏洞可以泄露堆栈上的canary值,从而绕过栈保护机制(Stack Canary)。栈保护机制(如 GCC 的 -fstack-protector 选项)通过在函数栈帧中插入一个称为canary的特殊值,在函数返回时检查其是否被修改,以防止栈溢出攻击。通常,canary 值是一个随机数,在程序的启动过程中生成,且与攻击者不可预知。

泄露canary的步骤

格式化字符串漏洞允许攻击者访问堆栈上的数据,通过精心构造的输入,可以读取栈上的 canary 值。一旦 canary 值泄露,攻击者就可以使用这个值来绕过栈保护机制并进行进一步的攻击。

典型的攻击思路如下:

  1. 探测canary位置:利用格式化字符串漏洞,攻击者通过像 %x%p 这样的格式符,不断读取栈上的内容,试图找到 canary 值的位置。因为 canary 通常位于返回地址和局部变量之间,它会被放置在栈中靠近函数栈帧的尾部。

  2. 泄露canary值:一旦找到 canary 的位置,攻击者可以通过使用适当数量的 %x%p 格式符,将该位置的值打印出来。例如:

printf(user_input);  // 存在格式化字符串漏洞

如果 user_input"%12$x",且 canary 在第12个栈位置,那么 printf 会输出 canary 的值。

  1. 利用泄露的canary值进行缓冲区溢出攻击:泄露到 canary 值后,攻击者可以构造精确的攻击载荷,确保 canary 的值在栈溢出过程中不会被改变,以避免触发栈保护机制。同时,攻击者可以修改返回地址或其他栈上的数据,从而执行任意代码。

示例

假设程序中存在以下代码:

#include <stdio.h>
#include <string.h>

void vulnerable_function(char *input) {
    char buffer[64];
    printf(input);  // 潜在格式化字符串漏洞
    strcpy(buffer, input);  // 潜在缓冲区溢出
}

int main() {
    char input[100];
    printf("Please enter input: ");
    scanf("%s", input);
    vulnerable_function(input);
    return 0;
}

攻击者可以先通过格式化字符串漏洞泄露 canary 值:

  • 如果输入 "AAA %12$x"printf 可能输出 canary 值(假设在第12个位置)。

接着,攻击者可以利用这个 canary 值构造精确的溢出载荷,将返回地址覆盖为恶意代码的地址,同时将正确的 canary 值放回栈上,绕过栈保护机制。

如何防止canary泄露

  1. 避免格式化字符串漏洞:关键点在于避免直接将不可信的用户输入传递给 printf 等函数,始终指定格式化字符串,如:

printf("%s", input);  // 避免将用户输入直接作为格式化字符串
  1. 启用更多保护机制

  • 地址空间布局随机化(ASLR):通过随机化堆栈、堆和代码段的地址,使攻击者更难确定 canary 和其他内存地址的位置。

  • 加强编译器保护:除了 -fstack-protector 外,还可以使用 -fstack-protector-all 进行更严格的栈保护。