从 5000 个 A 看不到溢出,到一行命令点破栈写入真相

栈溢出
2025-11-13 14:38
1759

安全声明:本文仅用于教育与防御研究,所有实验应在隔离、授权的环境中进行。禁止在未经授权的系统上进行漏洞利用。本文不提供可被滥用的利用链或远程攻击步骤。

一、为什么5000个A看不到溢出?

灵感来源于GPT给我写的一段C溢出代码,包括大部分AI都会出现这个问题。

// s2_strcpy.c
#include <stdio.h>
#include <string.h>

void secret() { puts("secret() called"); }

void vuln() {
    char buf[32];
    char src[128];
    puts("enter source:");
    fgets(src, sizeof(src), stdin);
    // remove newline
    src[strcspn(src, "\n")] = 0;
    strcpy(buf, src); // will overflow if src too long
    puts("done");
}

int main(){ vuln(); return 0; }

1、C程序代码分析

(1)、secret函数

我们先来简单分析一下这段C代码,这里可以看到有一个secret()函数,这是我们想要做到的,也就是说我们只需要再IDA中找到secret()函数入口就有机会跳转过去。

(2)、vuln函数

再往下声明了一个vuln()函数,然后定义了一个32字节空间的buf[],一个128字节空间的src[],下面就是fgets(src, sizeof(src), stdin),这一句我们按常规思路是可以构造溢出。
最后调用strcpy函数,将src里面内容复制到buf里面,这里都看似完美,我们实操一遍。
注意:
fgets(dst, n, stdin) 最多写入 n-1 字节为可见字符,并以 '\0' 结束。若输入更长,多余字符留在 stdin 等待下一次读取。

2、尝试溢出

(1)、编译成mips架构C程序

image.png
这就是我们的C程序,我们先编译一下。

mips-linux-gnu-gcc -fno-stack-protector -z execstack -no-pie -o stackbow test.c

这里我们使用mips交叉编译,生成一个可执行文件文件保存为stackbow,原文件名为test.c
注:这里第一个无法溢出文件为stackbow,可溢出文件为stackbow2

(2)、检查是否关闭了栈保护和NX保护

image.png

这里我们使用checksec( 检测 ELF 二进制安全特性的工具 )工具来检查我们文件保护状态。
确定关闭了文件包含就开始溢出。
PIE(Position-Independent Executable):可执行文件编译为位置无关,使代码段在运行时可被随机化。
ASLR(Address Space Layout Randomization):内核级地址空间随机化机制,随机化栈/堆/共享库基址。
NX(Non-Executable stack/DEP):阻止在栈上执行代码(防止 shellcode)。
Stack canary(栈金丝雀):编译器在返回地址前放置随机值以检测栈溢出。

(3)、IDA分析断点

image.png
拿到手后我们就不过一遍C代码了,我们先看main函数里面要执行的vlun函数

image.png
看着就是一个简单的栈溢出,没什么特别的,我们记一下这两个函数地址

image.png

image.png

text:004007D8                 la      $v0, puts

text:0040089C                 jr      $ra

(4)、pwndbg、quem-mips搭配干活

image.png
这里就是用qemu-mips启动这个二进制程序,-L是我们要指定的动态链接库

image.png
我们使用pwndbg连接一下,设置好断点

image.png
我们按下c跳转到断点位置,其实我们想做的就是再puts位置输入足够多的字节,让他溢出,我们尝试输入5000个A。

image.png
然后我们输入下看看回显,正常情况下我们调用strcpy函数后就会溢出,我们继续。

image.png
这里出了些问题,返回的居然是done?这是为什么,我们不是调用完strcpy就该溢出了吗。我们看下$ra的值。
image.png
我们发现RA据然还是main+24,为什么不出现A呢?,我们继续模拟居然是正常运行完C程序?

image.png
我们从这个点引出下文的思考,为此我总结了一句话:“溢出不是你输入多少,而是你写到哪里。”

二、栈的真相

1、点睛之笔(写入规则)

我们要调用的是strcpy函数,明明src[]大于buf[],为啥就写不了?我们来看看栈长什么样。

image.png

至此我们是否有了些灵感?我们输入的字节存入了src[]调用strcpy函数不就应该向高地址覆盖,直到返回地址$ra吗?但是这篇文章的点睛之笔就在这里,稍微为你揭开栈的原理。
读取位置单调递增(src[0], src[1], ...),写入位置单调递增(dest[0], dest[1], ...)。

image.png
这里我们用一张栈的图来示意,strcpy 从 buf[0] 开始,逐字节向高地址写,写入内容来自 src[0]、src[1] … 注意,栈增长方向与 strcpy 写入方向无关。
这里面栈是向高地址写的,就是说我们调用的strcpy函数的确是把buf[]填满了32个A,但是剩下的字符会进入到src[]里面,这会导致几种情况我来分开表达。

(1)、第一种情况

当我们输入的字节小于32字节,并不会造成溢出,他会将字节写入到buf[]也就是我们正常的程序运行。

(2)、第二种情况高地址(栈顶)

这种情况下,数据不会溢出到 $ra 返回地址,因为 buf 和 src 的内存布局决定了溢出的数据流向。

即它不会覆盖 src,而是可能影响栈上其他部分。
因此,在调用 strcpy 时,buf 不足的部分不会直接影响 src,而是会覆盖栈上 buf 后面的内存区域——通常是 栈上的其它局部变量或栈帧结构,而不会是返回地址 $ra,除非栈布局特别不幸。

image.png

(3)、第三种情况

我们上面说过无法溢出,或许有师傅会想到我们不能输入超过128字节的数据吗?直接覆盖到返回地址$ra不可以吗,直接覆盖中途所有函数不就可以了?这是个很棒的思路,但是前面我曾提到这点,因为我们调用的函数是fgets(src)函数,就导致我们输入5000个A,fgets 最多读 127 字节(留 1 字节给 \0)。

image.png
所以就是文章标题,我们输入了5000个字节但无法溢出的原因所在。

(4)、结论

至此,我们把所有方法都想到,这段C代码是无法溢出的,也就是一个死局,但是我一开始给GPT提出的诉求就是给我一段可以溢出的C代码,所以拿出来单独说这个点。
因此也注意一个点strcpy 复制的方向 ≠ 栈的增长方向!
strcpy(bu'f, src) 永远是从 buf 开始,逐字节向高地址写,不管 src 在哪。
如果 buf 在栈上较低的地址,并且 src 的长度超过了 buf 的缓冲区大小,溢出数据会覆盖栈上更高地址的内容,例如返回地址 $ra。这时,栈溢出会导致栈上较高地址的数据被覆盖,而栈本身是向低地址增长的。

三、死局与活局

1、解决灵感

我们想解决一个问题的关键在于我们能否懂问题产生的原因,一下有几个想法我们会一一实现。

(1)、交换src、buf位置

上文我们提到了,src在下面时调用strcpy后会出现从buf开始不断向上写,所以我们改一下他们两个的位置即可解决。

char src[128];
char buf[32];
strcpy(buf, src);  // 向上溢出 → 覆盖 $ra!

(2)、将char src[32]修改为char src[5000] 炸穿一切

如果我们将 char src[32] 修改为 char src[5000],栈的布局就可能被破坏,导致栈溢出。
交换后,buf 在低地址,src 在高地址,当执行 strcpy(buf, src) 时,它会从 src 向 buf 写入数据,并且由于 src 较大,可能会覆盖栈上更高地址的内存,进而覆盖返回地址 $ra。
这种情况可以通过 printf 输出栈中变量的地址来验证内存布局,确认是否发生了溢出。

2、灵魂复现

(1)、编译可溢出的C程序

image.png
这里我们就不一一尝试了,直接两种方案一起使用

mips-linux-gnu-gcc -fno-stack-protector -z execstack -no-pie -o stackbow2 test.c

依然编译一个mips架构c程序,然后关闭栈保护,我们另存为stackbow2

(2)、IDA分析

我们打开IDA找下断点

image.png

image.png
这就是我们要找的两个断点了,我们记录一下地址
`text:00400804 la $v0, fgets

text:0040089C jr $ra`

(3)、开始溢出

image.png
这里我们开一个7777的端口,然后用pwndbg去连接一下

qemu-mips -L /usr/mips-linux-gnu/ -g 7777 stackbow2

启动pwndbg

image.png

image.png

这里我们看到了gets函数,我们到这个点的时候就开始输入,我们还是使用cyclic生成字节,生成5000字符个先尝试输入判断偏移量。

image.png
这里我们看到结果了,这里是jaaa,我们计算一下偏移量,这里的偏移量是36,也就是说37往后就是返回地址了,我们使用python脚本发个包。
image.png
这里说明一点,就是我们用pwndbg设置断点是按照地址顺序执行的,和输入顺序无关。
image.png
我们启动脚本尝试一下。

image.png
这里要改一下python脚本,因为这里secret()函数被调用就立刻退出了,我们加上p.recvuntil("done\n")即可。

image.png

四、空间即控制流

这篇文章写的意义就是解决一个师傅们可能会遇到的问题,虽然不起眼,没有多高深,但我尽量拓展,尽量把栈逻辑写出来,让师傅们理解更深刻一点,下次看到这种代码就能一眼发现问题所在。
最后附上Payload:

from pwn import *

context(arch='mips', os='linux', endian='big')

addr = 0x00400760
payload = b'a' * 36 + p32(addr)

p = process(['qemu-mips', '-L', '/usr/mips-linux-gnu/', './stackbow2'])

p.sendline(payload)
p.recvuntil("done\n")   # 等待 done 打印
print("[+] secret() 正在执行...")
p.interactive()         # 保持进程不退出
分享到

参与评论

0 / 200

全部评论 0

暂无人评论
投稿
签到
联系我们
关于我们