Stack Overflow

{{#include ../../banners/hacktricks-training.md}}

什么是栈溢出

一个 栈溢出 是一种漏洞,当程序向栈写入的数据超过其分配的容量时就会发生。这些多余的数据将 覆盖相邻的内存空间,导致有效数据的损坏、控制流的中断,并可能执行恶意代码。这个问题通常是由于使用不安全的函数而引起的,这些函数在输入时不进行边界检查。

这个覆盖的主要问题是 保存的指令指针 (EIP/RIP)保存的基指针 (EBP/RBP) 用于返回到上一个函数,它们是 存储在栈上的。因此,攻击者将能够覆盖这些并 控制程序的执行流

该漏洞通常是因为一个函数 在栈中复制的字节数超过了为其分配的数量,因此能够覆盖栈的其他部分。

一些常见的易受攻击的函数包括: strcpy, strcat, sprintf, gets... 此外,像 fgetsreadmemcpy 这样的函数,如果指定的长度大于分配的长度,可能会以脆弱的方式使用。

例如,以下函数可能是脆弱的:

void vulnerable() {
char buffer[128];
printf("Enter some text: ");
gets(buffer); // This is where the vulnerability lies
printf("You entered: %s\n", buffer);
}

寻找栈溢出偏移量

寻找栈溢出的最常见方法是输入非常大的 A(例如 python3 -c 'print("A"*1000)'),并期待出现 Segmentation Fault,这表明 尝试访问了地址 0x41414141

此外,一旦发现存在栈溢出漏洞,您需要找到偏移量,直到可以 覆盖返回地址,通常使用 De Bruijn 序列。对于给定大小为 k 的字母表和长度为 n 的子序列,这是一个 循环序列,其中每个可能的长度为 n 的子序列恰好出现一次,作为一个连续的子序列。

这样,您就不需要手动找出控制 EIP 所需的偏移量,可以使用这些序列作为填充,然后找到覆盖它的字节的偏移量。

可以使用 pwntools 来实现这一点:

from pwn import *

# Generate a De Bruijn sequence of length 1000 with an alphabet size of 256 (byte values)
pattern = cyclic(1000)

# This is an example value that you'd have found in the EIP/IP register upon crash
eip_value = p32(0x6161616c)
offset = cyclic_find(eip_value)  # Finds the offset of the sequence in the De Bruijn pattern
print(f"The offset is: {offset}")

GEF

#Patterns
pattern create 200 #Generate length 200 pattern
pattern search "avaaawaa" #Search for the offset of that substring
pattern search $rsp #Search the offset given the content of $rsp

利用栈溢出

在溢出期间(假设溢出大小足够大),您将能够覆盖栈内局部变量的值,直到达到保存的EBP/RBP 和 EIP/RIP甚至更多
滥用这种类型漏洞的最常见方法是修改返回地址,这样当函数结束时,控制流将被重定向到用户在此指针中指定的地方

然而,在其他场景中,仅仅覆盖栈中某些变量的值可能就足以进行利用(例如在简单的 CTF 挑战中)。

Ret2win

在这种类型的 CTF 挑战中,二进制文件中有一个函数从未被调用,而且您需要调用它才能获胜。对于这些挑战,您只需找到覆盖返回地址的偏移量找到要调用的函数的地址(通常ASLR会被禁用),这样当易受攻击的函数返回时,隐藏的函数将被调用:

{{#ref}} ret2win/ {{#endref}}

栈 Shellcode

在这种情况下,攻击者可以在栈中放置一个 shellcode并利用受控的 EIP/RIP 跳转到 shellcode 并执行任意代码:

{{#ref}} stack-shellcode/ {{#endref}}

ROP & Ret2... 技术

该技术是绕过前一种技术主要保护的基本框架:不可执行栈 (NX)。它允许执行其他几种技术ret2lib、ret2syscall...),通过滥用二进制中的现有指令来最终执行任意命令:

{{#ref}} ../rop-return-oriented-programing/ {{#endref}}

堆溢出

溢出不总是在栈中,它也可能发生在中,例如:

{{#ref}} ../libc-heap/heap-overflow.md {{#endref}}

保护类型

有几种保护措施试图防止漏洞的利用,请查看它们:

{{#ref}} ../common-binary-protections-and-bypasses/ {{#endref}}

现实世界示例CVE-2025-40596 (SonicWall SMA100)

一个很好的示例,说明**sscanf永远不应该被信任来解析不可信输入**出现在2025年SonicWall的SMA100 SSL-VPN设备中。
位于/usr/src/EasyAccess/bin/httpd中的易受攻击例程试图从任何以/__api__/开头的URI中提取版本和端点

char version[3];
char endpoint[0x800] = {0};
/* simplified proto-type */
sscanf(uri, "%*[^/]/%2s/%s", version, endpoint);
  1. 第一个转换 (%2s) 安全地将 两个 字节存储到 version 中(例如,"v1")。
  2. 第二个转换 (%s) 没有长度说明符,因此 sscanf 将继续复制 直到第一个 NUL 字节
  3. 因为 endpoint 位于 上并且长度为 0x800 字节,提供一个超过 0x800 字节的路径会破坏缓冲区后面的所有内容 包括 栈金丝雀保存的返回地址

一个单行的概念证明足以在 身份验证之前 触发崩溃:

import requests, warnings
warnings.filterwarnings('ignore')
url = "https://TARGET/__api__/v1/" + "A"*3000
requests.get(url, verify=False)

即使栈保护器会中止进程,攻击者仍然可以获得一个拒绝服务原语(并且,通过额外的信息泄露,可能实现代码执行)。教训很简单:

  • 始终提供最大字段宽度(例如%511s)。
  • 优先选择更安全的替代方案,如snprintf/strncpy_s

参考

{{#include ../../banners/hacktricks-training.md}}