从实验到实战:BUPT软件安全之格式化字符串漏洞深度利用
1. 格式化字符串漏洞从“打印”到“掌控”的惊险一跃大家好我是老张在软件安全这个行当里摸爬滚打了十几年从早期的栈溢出玩到现在的各种高级利用格式化字符串漏洞一直是我觉得特别“优雅”也特别危险的一个。很多刚入门安全的朋友可能对缓冲区溢出耳熟能详觉得往数组里塞满数据就能搞定一切。但格式化字符串漏洞它更像一个“内鬼”利用的是程序对你毫无保留的信任。你只是想让程序帮你打印一条信息比如printf(user_input)但如果你能控制这个user_input事情就完全不一样了。简单来说格式化字符串漏洞的核心就是程序把用户输入的数据直接当成了格式化字符串的指令来执行而不是单纯的数据。比如printf、sprintf、fprintf这些函数它们第一个参数是格式化字符串像Hello, %s。后面的参数会按照%s、%d这些“指令”被处理。但如果这个格式化字符串本身是用户可控的攻击者就可以塞入一堆%x、%s甚至%n让程序去执行一些原本不该做的操作偷看内存、篡改数据最终夺取程序的控制权。这听起来可能有点抽象我打个比方。这就像你去银行柜台本来你只需要说“取钱”柜员就会按流程操作。但如果你能控制柜员手里的操作手册格式化字符串你在手册里写上“取钱并把旁边客户保险箱的密码也念出来最后在经理的授权书上改成我的名字”而柜员又毫不怀疑地照做了这就是格式化字符串漏洞的威力。在BUPT的软件安全实验里我们就是要亲手实践这种“掌控”从最简单的栈内存侦察开始一步步构建起完整的攻击链。别担心我会带你像侦探一样用调试器作为我们的“显微镜”把每一步内存的变化都看得清清楚楚。2. 实战环境搭建与漏洞程序初探工欲善其事必先利其器。在开始我们的深度利用之旅前得先把场子搭好。我强烈建议你在一个隔离的环境里做这些实验比如一台虚拟机。我常用的搭配是 Windows XP 或 Windows 7 虚拟机配上经典的OllyDbg调试器。为什么用老系统因为现代操作系统如Win10/11和编译器如VS默认开启了非常多的安全机制像地址空间布局随机化ASLR、数据执行保护DEP等它们会给我们的实验学习增加很多不必要的复杂度。在老环境里我们可以更纯粹地理解漏洞的本质。2.1 准备漏洞温床关闭安全编译选项我们的“靶场”程序需要故意留下漏洞。用 Visual Studio 的话记得关闭 GS缓冲区安全检查、SDL安全开发生命周期检查等选项。编译时最好选择Release模式但关闭优化这样代码更清晰方便调试。一个典型的漏洞代码片段就像下面这样#include stdio.h #include string.h void vulnerable_function(char *user_input) { char buffer[100]; // 致命的错误用户输入直接被当作格式化字符串使用 sprintf(buffer, user_input); // 或者 printf(user_input); } int main(int argc, char **argv) { if (argc 2) { printf(Usage: %s input_string\n, argv[0]); return 1; } vulnerable_function(argv[1]); return 0; }把这段代码编译成一个可执行文件比如vuln.exe。我们的攻击都将通过命令行参数传递给这个程序。准备好 OllyDbg用它将vuln.exe载入但先别急着运行。调试器的魅力在于能让时间暂停让我们有机会仔细端详内存的每一个角落。2.2 理解栈帧攻击者的地图在利用格式化字符串漏洞前你必须对函数调用时的栈布局有个清晰的印象。当vulnerable_function被调用时栈上大概会依次压入函数的返回地址、旧的栈帧指针EBP、然后是局部变量比如我们的buffer[100]再往下可能还有调用者的参数等。printf或sprintf函数的工作方式是它从格式化字符串指针开始遇到%开头的格式符就按照顺序去栈上找对应的参数。关键来了如果我们提供的格式符如%x数量超过了程序实际传递给格式化函数的参数数量会发生什么函数并不会报错它会继续忠实地按照格式符的指示把栈上本不属于参数区域的内存内容给“打印”出来。这就为我们打开了一扇窥视内存的窗口。在OllyDbg里你可以在调用sprintf或printf的CALL指令处下断点。运行程序传入一个简单的%x作为参数当断点命中时观察栈窗口通常右下角。你会发现除了你提供的格式化字符串地址后面跟着的就是栈上的其他数据。第一个%x可能会打印出某个寄存器的值或返回地址的偏移第二个、第三个%x会一步步深入栈的腹地。这个过程就像在黑暗中用手电筒一层层照亮栈内存我们称之为栈侦察。3. 攻击第一步栈内存侦察与信息泄露侦察是攻击的前奏目的是绘制内存地图找到对我们有价值的“地标”。3.1 使用 %x 进行栈内容遍历让我们来点实际的。在命令行运行我们的漏洞程序vuln.exe “AAAA%x.%x.%x.%x.%x”。你可能会看到类似AAAA12ff80.401234.0.0.41414141的输出。AAAA是我们输入的字符串前缀在内存中就是0x41414141‘A’的ASCII码。后面的12ff80、401234等就是sprintf在栈上找到的“参数”。注意看最后一个值41414141它很可能就是我们输入的AAAA本身在栈上的存储位置这说明我们通过控制格式符的数量成功让函数把我们输入的数据地址当成了一个参数来解析。在OllyDbg里验证这一点非常直观。单步执行到sprintf内部观察格式化字符串的处理过程。你会看到函数用一个内部指针依次读取格式符每读到一个%x就从栈上取4个字节32位系统打印出来。通过输入大量的%x比如%x.重复几十次我们几乎可以把当前函数栈帧上方的一大片内存内容都“dump”出来。这些数据里可能藏着函数的返回地址这是我们的首要目标控制了它就能控制程序流。栈上的其他敏感数据比如之前函数调用留下的密码、密钥的残留。库函数地址可以用来推算系统库的基址绕过ASLR在本次实验的简单环境中ASLR是关闭的。3.2 使用 %s 进行任意地址读取%x只能看不能细读。如果我们发现栈上某个位置存放着一个指针而这个指针指向了我们感兴趣的数据比如一个字符串%s就派上用场了。%s格式符会把对应参数当作一个指针去读取该地址开始的字符串直到遇到空字符\x00。攻击的关键在于我们如何让栈上的某个位置恰好存放着我们想读取的目标地址呢答案是利用我们可控的输入缓冲区。比如我们在输入字符串的开头直接写入一个目标地址的字节序列例如\x44\x10\x40\x00注意小端序后面跟上大量的%x进行“垫步”直到某个%x输出的内容正好是我们写入的地址0x00401044。那么在这个%x对应的参数位置上我们把它替换成%s程序就会去读取0x00401044地址处的字符串并打印出来在OllyDbg中你需要精确计算偏移。我常用的方法是先输入一串AAAA%x.%x.%x...数一数第几个%x输出了41414141。假设是第8个那么说明我们输入字符串的地址位于栈上第8个参数的位置。那么我们把AAAA换成真正的目标地址把第8个%x换成%s命令就变成了\x44\x10\x40\x00%x%x%x%x%x%x%x%s。在调试器中单步跟进你会清晰地看到当处理到%s时函数从我们指定的地址0x00401044开始读取内存并将内容输出。这相当于从进程的内存空间里“偷”出了一段数据。4. 攻击第二步关键的%n写操作与程序流劫持读内存只是开胃菜写内存才是主菜而%n就是这个魔法开关。%n是一个极其特殊的格式符它不输出任何内容而是将截至目前已成功输出的字符总数写入到对应参数所指向的地址。这就给了我们向任意地址写入一个数值的能力。4.1 %n 写操作原理剖析理解%n是理解整个攻击链的枢纽。举个例子printf(“ABCD%n”, counter);执行后counter变量的值会被设置为4因为“ABCD”输出了4个字符。 在漏洞利用中我们通过栈侦察已经知道了我们输入的字符串其中包含我们想写入的地址位于栈上的哪个参数位置。我们在这个位置放置一个%n。那么printf或sprintf在处理到%n时就会把已经输出的字符总数写入到我们输入的那个地址所指向的内存单元。写入的值字符总数是我们可以通过格式化字符串精确控制的怎么控制通过调整%d、%x等输出字段的宽度。例如%100d会输出一个至少100字符宽的十进制数。如果我们先输出100个字符再用%n写入的值就是100。如果我们想写入一个很大的地址值比如0x080491a2十进制是134,517,666我们可以用%134517666d这样的格式来输出海量空格和数字从而控制写入的数值。4.2 劫持控制流调用隐藏函数现在我们来看BUPT实验思考题中的foo.exe。目标是调用一个隐藏的foo()函数。分析源码发现程序有一个函数指针fErrFunc正常情况下它指向某个错误处理函数。我们的攻击思路就是利用%n将fErrFunc指针的值修改为foo函数的地址。步骤拆解定位地址首先我们需要知道两件事一是fErrFunc指针本身存放在内存的哪个地址假设为0x0012FF18二是foo函数的地址是多少假设为0x00401014。构造输入我们的输入需要让程序把0x00401014这个值写入0x0012FF18这个地址。所以我们输入的字符串开头部分应该是地址0x0012FF18的字节序列\x18\xff\x12\x00小端序。这个地址会被保存在栈上。计算偏移通过输入%x%x%x...确定我们的输入字符串地址是栈上的第几个“参数”。假设是第12个。构造写入那么我们在第12个参数的位置使用%n。但在这之前我们需要让已输出的字符总数等于foo的地址值0x00401014十进制4198420。直接输出四百多万个字符不现实而且会破坏栈结构。这里有个技巧使用%hn写入2个字节短整型。我们可以把0x00401014拆成高2字节0x0040十进制64和低2字节0x1014十进制4116。然后分两次写入分别指向0x0012FF18和0x0012FF1A。精确控制输出长度通过组合%d、%x的字段宽度以及直接输出的普通字符如点号.来微调已输出的字符数使其先达到4116用%hn写入低地址再增加到64注意是累计值需要计算第二个写入时的增量用第二个%hn写入高地址。在OllyDbg中动态调试这个过程非常精彩。你可以在fErrFunc指针的地址0x0012FF18处设置内存写入断点。然后运行构造好的攻击字符串。你会看到当程序执行到fprintf处理%hn时断点触发内存窗口显示0x0012FF18处的值从原来的错误处理函数地址瞬间变成了foo函数的地址0x00401014继续运行程序流就会神奇地跳转到foo函数执行。这就是一次完美的控制流劫持。5. 攻击第三步结合缓冲区溢出执行Shellcode格式化字符串漏洞的威力还不止于此。当它与缓冲区溢出结合时能产生更强大的攻击效果比如直接执行我们注入的机器指令Shellcode。5.1 利用sprintf构造溢出回顾实验中的关键代码sprintf(outbuf, buffer);其中buffer的内容完全用户可控。这里存在两个漏洞一是buffer被直接当作格式化字符串格式化字符串漏洞二是outbuf的大小是固定的512字节但格式化输出的结果可能远超这个大小缓冲区溢出。攻击者的构造非常巧妙在buffer中先放入一个常规的格式化字符串前缀如ERR Wrong command:。接着放入一个超宽的格式符%497d。这意味着程序会尝试将一个整数输出为至少497字符宽。由于没有提供对应的整数参数程序会从栈上取一个值比如某个地址0x12FF80十进制是1245056来填充这497个字符。最终“ERR Wrong command:” (19字节) 497字节的填充 516字节超过了outbuf的512字节。最关键的一步在%497d之后紧接着放入 Shellcode 的起始地址例如\x39\x4a\x42\x00。由于发生了溢出多出的4个字节就会覆盖掉函数返回地址所在的内存位置。5.2 Shellcode的布局与执行那么 Shellcode 本身放在哪里呢它被放置在了用户输入的更早部分也就是user数组里。在实验代码中user数组里包含了大量的空操作指令\x90NOP滑板和真正的弹窗Shellcode\x33...\xD0。\x39\x4a\x42\x00这个地址正好指向了user数组中 Shellcode 开始的位置。在OllyDbg中跟踪整个过程第一次sprintf将user的内容格式化到buffer中此时buffer里已经包含了%497d和返回地址。第二次sprintf(outbuf, buffer)执行时由于%497d产生了巨大的输出导致outbuf溢出。单步执行到函数结尾的RET指令观察栈窗口你会发现原本的返回地址已经被替换成了0x00424A39即\x39\x4a\x42\x00的小端序解读。按下F9继续执行程序并没有返回到原来的调用者而是跳转到了user数组中的地址开始执行 NOP 指令并最终执行 Shellcode弹出一个消息框。这意味着我们成功地将一段任意代码注入并执行了。这个过程将格式化字符串的“任意读/写”能力与缓冲区溢出的“控制流劫持”能力完美结合。格式化字符串在这里起到了两个作用一是通过%497d精确控制输出的长度触发精确的溢出二是在构造输入时其本身就可以携带 Shellcode 和地址。6. 防御之道从开发到编译的立体防护经历了这么刺激的攻击实践我们更应该思考如何防御。防御格式化字符串漏洞需要从多个层面入手。首先对开发者而言最根本的黄金法则就是永远不要将用户输入直接作为格式化字符串的参数。对于printf、sprintf、fprintf等函数第一个参数格式化字符串必须是明确的字符串常量或者经过严格检查的、完全可信的字符串。如果确实需要动态构造格式应该使用安全的函数比如snprintf来限制输出长度并且确保格式符与参数类型、数量严格匹配。其次使用现代编译器的安全特性。虽然我们在实验里关闭了它们但在真实开发中必须开启。GCC 的-Wformat-security和-Wformat2警告选项这些选项能在编译时检测到可疑的格式化字符串用法比如printf(user_input)这种形式会给出强烈警告。Visual Studio 的/GS缓冲区安全检查和/SDL选项它们虽然主要针对栈溢出但能增加攻击者利用漏洞的难度。地址空间布局随机化ASLR和数据执行保护DEP这是操作系统的防护。ASLR 让每次程序运行时栈、堆、库的地址都随机变化让攻击者难以准确定位地址。DEP 将数据内存页如栈标记为不可执行即使攻击者注入了 Shellcode也无法运行。我们的实验在关闭这些保护的环境下才能成功这反证了它们的重要性。最后进行代码审计和模糊测试。在代码审查中要特别关注所有使用格式化字符串函数的调用点。使用自动化工具进行模糊测试向程序输入大量包含%符号的畸形数据可以有效地发现这类漏洞。我见过太多因为一个简单的printf(user_input)而导致的安全事件。防御其实并不复杂难的是将安全编码的意识变成一种肌肉记忆。每次敲下printf的时候都下意识地想一想这个格式字符串我还能控制吗多这一秒的思考可能就堵住了一个巨大的安全缺口。安全攻防就像一场永不停歇的猫鼠游戏我们了解攻击是为了更好地防御。希望这次从实验到实战的深度旅程能让你不仅掌握攻击的技巧更深刻理解守护的意义。下次当你写代码时或许会对那些小小的%符号多一份敬畏。

相关新闻

相机标定与3D重建(1)ChArUco标定板生成与检测(上)

相机标定与3D重建(1)ChArUco标定板生成与检测(上)

1. 为什么我们需要ChArUco标定板? 如果你玩过机器人、无人机,或者尝试过用摄像头做三维测量,那你肯定对“相机标定”这个词不陌生。简单来说,相机标定就是给相机做一次“体检”,搞清楚它的“视力”到底怎么样——比如镜…

2026/6/26 10:57:44 阅读更多 →
Qwen3-TTS-12Hz-1.7B-VoiceDesign保姆级教程:音色迁移与跨语种音色一致性控制

Qwen3-TTS-12Hz-1.7B-VoiceDesign保姆级教程:音色迁移与跨语种音色一致性控制

Qwen3-TTS-12Hz-1.7B-VoiceDesign保姆级教程:音色迁移与跨语种音色一致性控制 本文约3800字,预计阅读时间10分钟,包含完整操作步骤和实用技巧 1. 认识Qwen3-TTS语音设计模型 1.1 模型核心能力概览 Qwen3-TTS-12Hz-1.7B-VoiceDesign是一个功…

2026/7/3 2:36:20 阅读更多 →
IDEA高效Debug:一键掌握所有断点与调试技巧

IDEA高效Debug:一键掌握所有断点与调试技巧

1. 从“乱点”到“精控”:你的断点管理真的高效吗? 不知道你有没有过这样的经历:在调试一个复杂的业务逻辑时,为了追踪不同分支的变量变化,你在代码里噼里啪啦打了一堆断点。调试到一半,你突然想看看自己到…

2026/6/30 13:24:41 阅读更多 →

最新新闻

VisualCppRedist AIO:一站式解决Windows软件兼容性问题的终极工具

VisualCppRedist AIO:一站式解决Windows软件兼容性问题的终极工具

VisualCppRedist AIO:一站式解决Windows软件兼容性问题的终极工具 【免费下载链接】vcredist AIO Repack for latest Microsoft Visual C Redistributable Runtimes 项目地址: https://gitcode.com/gh_mirrors/vc/vcredist 你是否曾经遇到过软件无法启动、游…

2026/7/4 1:41:21 阅读更多 →
UE5多线程编程与FQueuedThreadPool实战指南

UE5多线程编程与FQueuedThreadPool实战指南

1. UE5多线程编程基础与FQueuedThreadPool概述在UE5游戏开发中,多线程编程是提升性能的关键技术之一。虚幻引擎提供了完善的多线程框架,其中FQueuedThreadPool作为核心线程池实现,为开发者管理并发任务提供了便利。与直接创建线程相比&#x…

2026/7/4 1:39:20 阅读更多 →
Unity Addressables内存管理优化实战指南

Unity Addressables内存管理优化实战指南

1. 内存管理在Addressables中的核心地位在Unity项目中使用Addressables资源管理系统时,内存管理是决定项目性能和稳定性的关键因素。不同于传统的Resources加载方式,Addressables采用异步加载和引用计数机制,这给内存管理带来了新的挑战和优化…

2026/7/4 1:37:19 阅读更多 →
FBX导入Unreal缺失平滑组问题的解决方案

FBX导入Unreal缺失平滑组问题的解决方案

1. 问题背景与现象解析最近在将FBX格式的3D模型导入Unreal Engine时,遇到了一个典型警告:"[ue SkeletalMesh] 在FBX文件中未找到这个网格体Mesh_001的平滑组信息"。这个看似简单的提示背后,实际上涉及到3D建模流程中几个关键的技术…

2026/7/4 1:37:19 阅读更多 →
Ubuntu下UE5与AirSim集成开发指南

Ubuntu下UE5与AirSim集成开发指南

1. 项目概述:Ubuntu系统下的UE5与Project AirSim集成方案在Linux生态中部署虚幻引擎5(UE5)与微软开源仿真平台Project AirSim的组合,为自动驾驶、无人机开发等领域提供了高性能的仿真测试环境。不同于Windows平台的"开箱即用…

2026/7/4 1:35:19 阅读更多 →
libgdx游戏UI元素定位与调试实战技巧

libgdx游戏UI元素定位与调试实战技巧

1. libgdx界面元素定位调试实战指南在libgdx游戏开发中,UI元素的精确定位是个看似简单却容易踩坑的环节。我刚接触libgdx时,曾花了两天时间就为了把一个按钮摆到理想位置。经过多个项目实战,我总结出三种不同维度的调试方案,从依赖…

2026/7/4 1:35:19 阅读更多 →

日新闻

Memcached 1.6.43 发布:关键安全修复版本,多项问题得到解决

Memcached 1.6.43 发布:关键安全修复版本,多项问题得到解决

Memcached 1.6.43 正式发布,这是一个关键的安全修复版本,修复了多个方面的问题,还对部分功能进行了优化。 安全修复亮点 此次发布在安全修复上表现突出。binprot 避免了项目引用计数溢出,mcmc 因安全问题提升了上游版本号&#xf…

2026/7/4 0:04:29 阅读更多 →
终极指南:使用HMCL启动器跨平台畅玩Minecraft的完整解决方案

终极指南:使用HMCL启动器跨平台畅玩Minecraft的完整解决方案

终极指南:使用HMCL启动器跨平台畅玩Minecraft的完整解决方案 【免费下载链接】HMCL A Minecraft Launcher which is multi-functional, cross-platform and popular 项目地址: https://gitcode.com/gh_mirrors/hm/HMCL HMCL(Hello Minecraft! Lau…

2026/7/4 0:06:29 阅读更多 →
KMX63与PIC18F66K40在嵌入式HMI中的硬件协同与低功耗设计

KMX63与PIC18F66K40在嵌入式HMI中的硬件协同与低功耗设计

1. KMX63与PIC18F66K40的硬件协同架构解析KMX63作为一款三轴加速度计和磁力计组合传感器,与PIC18F66K40微控制器的搭配堪称嵌入式HMI开发的黄金组合。这套硬件组合的核心优势在于KMX63提供的高精度运动感知能力与PIC18F66K40强大的信号处理能力形成了完美互补。KMX6…

2026/7/4 0:06:29 阅读更多 →

周新闻

月新闻