缓冲区溢出
缓冲区溢出
复习定位
缓冲区溢出是计算机安全史上最经典也最危险的一类漏洞。当程序在栈上写入超过数组容量的数据时——覆盖了局部存储区以外的内容(保存的ebp、返回地址、局部变量)。攻击者精心设计溢出的内容——改写函数的返回地址——让CPU跳到攻击者设计的恶意代码或者复用已有代码片段执行的ROP链上运行。防止——Stack Canary在栈上放置一个随机值——溢出篡改它时会触发栈不可用检测——NX使栈不可执行从而shellcode无法运行——ASLR增加攻击者猜测地址的难度。
栈溢出基本原理
调用函数时——栈上按地址从高到低依次存放:调用者的栈帧信息(高地址)、返回地址(ret地址——函数通过ret指令跳回)、旧的rbp(基址指针——恢复栈帧用)、局部变量(低地址)。局部数组从低地址向高地址生长。
高地址
调用者的栈帧
[返回地址] ← ret指令将弹回的值(攻击目标)
[保存的rbp] ← 旧栈基址
[arr[63]] ← 局部数组——向上(高地址)越界写入
[arr[0]]
低地址调用gets(buf)或strcpy(dst,src)且不检查长度时——攻击者传入64字节以上的数据——就会覆盖到return address——攻击者可以填充任意的值作为"返回地址"——使函数返回时跳到攻击者指定的位置。
NX位与DEP
现代操作系统禁止栈和堆执行代码。页表中的NX位(No eXecute)标记栈页为不可执行。当CPU试图从栈上取指令时——触发页异常——进程被终止。这意味着攻击者无法将shellcode放在栈上直接执行——传统的"写shellcode到栈+覆盖返回地址=跳转到shellcode"失效了。
ROP攻击
攻击者不会放弃——他们从程序已有的可执行段(.text)中寻找以ret指令结尾的指令序列(称为gadgets,小工具)——这些小工具在内存中的地址是已知的——将它们串联起来(一个接一个的返回地址链)——形成ROP链(Return-Oriented Programming)。每个返回地址指向一个gadget——执行该gadget——执行完以ret结束——跳转到下一个gadget。ROP链可以用来在禁止栈执行的情况下凑齐任意逻辑——调用系统函数或虚拟内存权限设置。
Stack Canary
编译器(如GCC)在编译时在栈上局部变量和返回地址之间插入一个随机的"金丝雀值(canary)"——函数在返回之前检查canary是否被改——如果被改(局部变量的溢出可能覆盖了它)——触发__stack_chk_fail——进程中止而不是跳转到恶意地址。攻击者必须读取或猜测canary值才能绕过。
ASLR
地址空间布局随机化(ASLR)在每次程序启动时随机化栈地址、堆地址、共享库加载地址等。这使得攻击者难以猜测gadgets的确切地址——增加了构造ROP攻击的难度。
复习检查
栈溢出时——为什么覆盖返回地址能让CPU跳转到shellcode——函数结束时通过
ret指令从栈上弹出返回地址到RIP——如果栈上对应位置被覆盖—RIP就被设置成攻击者指定的值——开始执行那个地址处的代码。在NX开启的情况下——为什么不能直接执行shellcode(栈不可执行——页表NX位)?那攻击者通过ROP需要如何达成攻击目标——逐个调用现有的函数(gadgets)来组合攻击逻辑。
Stack Canary是在编译时由编译器插入额外的函数调用——检查canary值是否在被调用时被修改——如果溢出覆盖了canary——函数退出前的确定检查步骤检测到canary被篡改——程序被主动终止——但攻击者可以通过泄露信息(如格式化字符串)读取canary值——从而在构造payload时保留其值——绕过检测。
ASLR的随机化范围有限——32位系统的栈随机化有效熵只有约8-12位——攻击者可暴力尝试;64位系统的随机空间大得多——但攻击者仍可通过信息泄露(如通过某种漏洞读一个指针值)得知关键地址信息。
为什么C语言的
strcpy/strcat/gets这些函数危险——它们不检查目标缓冲区的长度——盲目地将源数据复制到目标缓冲区直到遇到NUL字节——在源数据长于缓冲区时必然溢出——所以所有新代码都应使用strncpy/strncat在指定长度上复制。