news 2026/9/29 2:41:53

Windows 用户态栈溢出原理:从最后一字节溢出到劫持程序流程(ctf-wiki 实战解析)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Windows 用户态栈溢出原理:从最后一字节溢出到劫持程序流程(ctf-wiki 实战解析)
  • 文档
  • 网络安全
  • 教程

【免费下载链接】ctf-wiki

Come and join us, we need you!

项目地址:https://gitcode.com/gh_mirrors/ct/ctf-wiki
点击查看免费下载

导读

本文以 ctf-wiki 仓库中 Windows 用户态 Pwn 实战文档为基础,通过一个经典的密码校验程序,完整演示 x86 Windows 下栈溢出的发生机理:局部变量声明顺序与 buffer 大小如何诱发"最后一字节溢出",攻击者又是如何利用strcpy复制的字符串结束符覆盖strcmp的返回值,从而在GS(栈金丝雀)开启的情况下改变程序执行流程。读完本文,你将掌握用 VC6.0 编译 Windows 用户态目标、用 Winchecksec 检查防护机制、用 OllyDbg 下断点观察栈布局,以及从栈内存转储中推导溢出点的完整技能。

一、背景:Windows 用户态漏洞利用的入门环节

在 ctf-wiki 的 Windows 用户态章节中,栈溢出是后续一切利用手法的地基。与 Linux 用户态 Pwn 不同,Windows 用户态的利用需要考虑 PE 文件格式、导入导出表、GS 栈金丝雀、SafeSEH、DEP/NX 等机制的约束,调试工具与栈布局也略有差异。本仓库中的 Windows 栈介绍 承接了 Linux 侧的 x86 栈介绍——那里详细讲述了栈的后进先出特性、压栈/出栈操作,以及程序栈从进程地址空间的高地址向低地址增长这一关键事实。Windows 下的函数调用栈同样遵循这一模型:函数参数、返回地址、保存的 EBP、局部变量在栈上依次排列,局部变量越靠近栈顶(低地址),就越容易被不安全的字符串拷贝从低地址向高地址(即返回地址方向)覆盖。

本仓库将 Windows 用户态栈溢出按难度拆解为三篇递进的文档:

文档核心内容
stackoverflow-basic.md本文主体:最后一字节溢出覆盖返回值
shellcode-in-stack.md通过 padding 控制返回地址,跳转到栈上布置的 shellcode
ret2dll.md类似 Linux ret2libc,泄漏 DLL 基址后调用 system 执行命令

本文讲解第一层:不需要覆盖返回地址、仅仅覆盖一个局部返回值,就能让程序逻辑"短路"。

二、示例程序与漏洞根因分析

下面给出本节的典型例子(完整源码位于 stackoverflow-basic.md):

#include <stdio.h> #include <string.h> #define PASSWORD "666666" int verify_password(char *password) { int authenticated; char buffer[8]; authenticated = strcmp(password,PASSWORD); strcpy(buffer,password); return authenticated; } void main() { int valid_flag =0; char password[128]; while(1) { printf("please input password: "); scanf("%s",password); valid_flag = verify_password(password); if (valid_flag !=0) { printf("incorrect password!\n"); } else { printf("Congratulation! You have passed the verification!\n"); break; } } }

这是一个简单的密码校验程序,会判断输入的字符串是否与666666相等。程序的逻辑本身没有问题——verify_password先调用strcmp得到比较结果存入局部变量authenticated,再用strcpy把用户输入复制进局部缓冲区buffer,最后返回authenticated;主函数根据返回值决定是输出incorrect password!还是Congratulation! You have passed the verification!。

但漏洞恰恰隐藏在verify_password函数的局部变量布局里,结合 ctf-wiki 的栈知识可以逐条拆解:

  1. 局部变量声明顺序:源码中先声明int authenticated;,后声明char buffer[8];。在 VC6.0(x86)的默认栈帧布局下,后声明的局部变量位于更低地址,先声明的位于更高地址。因此内存中buffer紧邻authenticated下方,authenticated恰好位于buffer的上方(更高地址处)。
  2. buffer 的大小:char buffer[8]只有 8 字节,而strcpy(buffer, password)是一个典型的不检查目标缓冲区大小的危险函数,会一直复制直到遇到源字符串的\0结束符。
  3. 最后一字节溢出:如果输入恰好是 8 个字节(如 8 个a),strcpy会把这 8 个字符填入buffer[0..7],随后把字符串结束符\0(ASCII 0x00)写入buffer的下一字节——这一字节正是authenticated这个局部变量的最低字节所在位置。由于strcmp返回值为非零(输入不等于666666,返回 1)时,其最低字节为 1,被\0覆盖后整个返回值变成 0,主函数的if (valid_flag != 0)判断随即被绕过。

需要强调的是:这并不需要覆盖返回地址,也不需要利用 EIP。它改写的是函数内部一个局部变量在栈上的存储,属于"最后一字节溢出"(off-by-one 的一种形态),因此即便目标开启了 GS(栈金丝雀)也不妨碍此次利用——金丝雀保护的是返回地址附近的区域,而这里被覆盖的是局部变量区。

三、编译与防护机制检查

使用VC6.0编译上述程序(示例生成demo1.exe),然后在命令行下用Winchecksec查看目标程序的防护机制:

C:\Users\CarlStar\Desktop>winchecksec.exe demo1.exe Dynamic Base : false ASLR : true High Entropy VA : false Force Integrity : false Isolation : true NX : true SEH : true CFG : false RFG : false SafeSEH : true GS : true Authenticode : false .NET : true

Winchecksec 是 Trail of Bits 出品的 Windows 二进制安全特性检查工具,上面这份输出逐项说明了 demo1.exe 的防护状态:

检查项值含义
Dynamic Basefalse未设置动态基址标志
ASLRtrue启用地址空间布局随机化(由系统级策略体现)
High Entropy VAfalse未启用高熵虚拟地址随机化(64 位特性)
Force Integrityfalse未强制签名完整性
Isolationtrue启用进程隔离相关机制
NXtrue数据执行保护开启,栈上代码不可执行
SEHtrue启用结构化异常处理
CFGfalse未启用控制流防护
RFGfalse未启用返回流防护
SafeSEHtrue启用 SafeSEH,异常处理函数需在合法表内
GStrue栈金丝雀(缓冲区溢出检测)开启
Authenticodefalse未进行 Authenticode 签名
.NETtrue程序与 .NET 运行时相关

注意到最关键的一行:GS 是开启的。许多初学者会认为开了 GS 就无法做栈溢出,但本例证明:GS 金丝雀只保护"从局部变量区到返回地址之间"的栈区域——当溢出只触及局部变量authenticated、尚未越过金丝雀触及返回地址时,GS 的检测逻辑根本不会被触发。这正说明了**理解溢出边界(可以覆盖到什么位置、不能覆盖到什么位置)**在 Windows 栈利用中的核心价值。

四、OllyDbg 动态调试:观察正常执行流程

接下来使用OllyDbg动态调试这个程序。先输入aaaaaa(6 个a)观察程序正常的执行流程。为了方便理解整个过程,在strcmp函数和strcpy执行完后各下一个断点(对应反汇编中00401041处的strcmp调用与00401059处的strcpy调用),如下图所示:

让程序运行并输入aaaaaa后,程序执行到第一个断点(strcmp调用处)。进入strcmp函数观察它的返回值:因为a的 ASCII 码值(0x61)大于6的 ASCII 码值(0x36),不出意外函数会返回1。在 x86 下,函数返回值保存在EAX 寄存器中。函数正常返回后,由于程序完成其余功能还会继续使用这些寄存器,所以这个返回值会先被保存到栈上——也就是地址ss:[0012FEA0]这个地方。

当执行到第二个断点(strcpy之后)时,查看栈结构。此时输入 6 个a时的栈如下:

0012FE90 CCCCCCCC 0012FE94 CCCCCCCC 0012FE98 61616161 0012FE9C CC006161 0012FEA0 00000001

逐字节解读这份栈转储:

  • 0012FE98处的61616161是输入前 4 个a的 ASCII 码(0x61);
  • 0012FE9C处的CC006161中,6161是第 5、6 个a,00是字符串结束符,CC是 VC6.0 调试版填充的栈初始化字节(0xCC 同时也是 INT 3 指令的操作码,用于捕捉非法执行);
  • 0012FEA0处的00000001正是之前被保存下来的strcmp 返回值,即authenticated = 1。

注意观察:buffer从0012FE98开始(8 字节,到0012FE9F结束),而authenticated恰好位于0012FEA0——正是buffer结束位置的下一个 4 字节单元。这印证了前面分析的局部变量布局。

五、最后一字节溢出:用\0覆盖返回值

如果输入8 个a呢?strcpy会把 8 个字符填满buffer(0012FE98~0012FE9F),随后把字符串结束符\0写入0012FEA0这一字节——也就是authenticated的最低字节。原值是0x00000001(strcmp 返回值 1),最低字节被覆盖为0x00后,整个变量变成0x00000000。

下图展示了输入长度逼近溢出边界时的临界状态(输入 7 个a,栈窗口仍显示ss:[0012FEA0]=CCCCCCCC,说明 7 字节尚未触及返回值;再增加一个字符即发生覆盖):

首先让程序正常运行(输入 6 个a),确认结果是incorrect password!:

接着输入 8 个a验证我们的推断:字符串的结束符会溢出到 strcmp 的返回值。程序运行到strcpy之后的第二个断点时,strcmp的返回值已经成功由 1 溢出为 0,栈转储如下:

0012FE90 CCCCCCCC 0012FE94 CCCCCCCC 0012FE98 61616161 0012FE9C 61616161 0012FEA0 00000000

与 6 个a时的栈对比:

  • 0012FE98与0012FE9C现在都是完整的61616161,8 个a填满了整个 8 字节buffer;
  • 0012FEA0由00000001变成了00000000——strcmp 的返回值 1 被字符串结束符\0成功覆盖为 0。

此时让程序继续运行,verify_password返回 0,主函数的if (valid_flag != 0)判断为假,程序输出预想中的成功提示:

至此,一次典型的"最后一字节栈溢出"利用完整闭环:用不安全的 strcpy 把多余的一个字节\0写进紧邻的局部变量,篡改校验结果,改变程序控制流。整个过程没有越过 GS 金丝雀、没有改写返回地址,因此 GS、SafeSEH 等防护全部形同虚设。

六、从返回值覆盖到真正劫持 EIP

理解了"最后一字节溢出"的机理后,可以将同样的思路延伸出本仓库中接下来的两个进阶主题:

  1. 执行 Shellcode(见 shellcode-in-stack.md):当需要真正劫持执行流时,思路从"覆盖局部变量"升级为"覆盖返回地址"。示例程序使用memcpy(buffer, password, strlen(password))把输入原样布置到栈上,通过 OllyDbg 动态调试确定padding 长度(示例中先试探 50 BYTES,最终确定返回地址距缓冲区起点为 60 BYTES),把返回地址精确覆盖为栈中 shellcode 的起始地址(如0012FAE4),函数返回时EIP被置为该地址,CPU 便按机器码执行栈中的指令(示例演示了弹出内容为hackhack的消息框)。前提是编译时关闭 ASLR、NX、CANARY,并借助LoadLibrary("user32.dll")预先装载所需模块。

  2. ret2dll 利用(见 ret2dll.md):与 Linux 下的 ret2libc 原理类似,但 Windows 侧pwntools目前无法直接提取 exe 文件中的地址,需要手动提取关键地址。示例使用 MinGW(gcc "-Wl,--disable-reloc" -g -o ret2dll.exe ret2dll.c)编译,通过ncat -l 8080 --keep-open --exec ".\ret2dll.exe"或win_server ./ret2dll.exe 8080搭建临时交互环境,利用puts@got泄漏 DLL 基址,再计算出system与cmd的实际地址完成调用链。

七、参考阅读

  • 本仓库 Windows 用户态栈溢出系列:stack-introduction.md、shellcode-in-stack.md、ret2dll.md
  • Linux 侧栈基础:x86 stack-intro.md(栈结构、x86/x64 调用约定的系统讲解)
  • 书籍《0day安全:软件漏洞分析技术》(上述示例的思路来源之一)
  • Winchecksec:Trail of Bits 出品的 Windows 二进制安全特性检测工具(文中防护输出即来自该工具)
  • 文档
  • 网络安全
  • 教程

【免费下载链接】ctf-wiki

Come and join us, we need you!

项目地址:https://gitcode.com/gh_mirrors/ct/ctf-wiki
点击查看免费下载
上一篇:QuantsPlaybook:重塑量化投资研究范式的下一代开源平台
下一篇:iOS性能优化终极指南:如何使用Aspects实现应用后台刷新监控

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 2:37:54

FPGA工程创建全流程:从Verilog到Vivado比特流下载实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华