本文作者:杉木@涂鸦智能安全实验室
前置知识点
栈
栈(Stack)是计算机中的一种数据结构,用于存储临时数据。它的特点是后入先出(LIFO),只能在栈顶添加或删除数据。在程序中,栈被用于存储函数调用时的临时变量,以及用于控制程序执行流程的返回地址。
堆
堆(Heap)是计算机中的一种数据结构,它是程序在运行时动态分配的内存。与栈相比,堆的结构并无特定的规则,可以随时在堆上分配或释放内存。在程序中,堆通常被用于存储需要在函数调用之间持久存在的数据。
寄存器
寄存器的类型
寄存器的类型和用途各不相同,它们包括但不限于:
- 通用寄存器:可以用来存储数值或内存地址,并在各种操作中使用。
- 专用寄存器:对操作系统或CPU操作有特殊影响的寄存器。
- 数据寄存器:存储数值数据,用于算术、逻辑等操作。
- 地址寄存器:存储内存地址,允许CPU访问和操作内存中的数据。
- 程序计数器(PC):存储即将执行的下一条指令的地址。
- 基址寄存器(Base Register):与索引寄存器一起用于数组和字符串操作。
- 堆栈指针(Stack Pointer):指向当前顶部的堆栈的地址。
- 指令寄存器(Instruction Register):存储当前被执行的指令。
- 状态寄存器/标志寄存器(Status Register / Flags Register):反映了CPU最近操作的各种数学和逻辑条件。
ESP
ESP(Stack Pointer)是堆栈指针寄存器,存放执行函数对应栈帧的栈顶地址(也是系统栈的顶部),且始终指向栈顶;
EBP
EBP(Base Pointer)是栈帧基址指针寄存器,存放执行函数对应栈帧的栈底地址,用于C运行库访问栈中的局部变量和参数。
EIP
EIP(Instruction Pointer)是指令寄存器,指向处理器下条等待执行的指令地址(代码段内的偏移量),每次执行完相应汇编指令EIP值就会增加。
不同架构之间的差别
x86
• 函数参数在函数返回地址的上方
x64
- System V AMD64 ABI (Linux、FreeBSD、macOS 等采用) 中前六个整型或指针参数依次保存在 RDI, RSI, RDX, RCX, R8 和 R9 寄存器中,如果还有更多的参数的话才会保存在栈上。
- 内存地址不能大于 0x00007FFFFFFFFFFF,6 个字节长度,否则会抛出异常。
堆栈操作
缓冲区溢出常见的几种操作
压栈push
出栈pop
调用cal
离开leave
返回ret
其他
-
Top/Peek: 查看栈顶元素而不移除它。
-
IsEmpty: 检查栈是否为空。
-
Size: 获取栈中元素的数量。
-
Insert (添加): 向堆中插入一个新元素。
-
Maximum/Minimum (取最大/最小值): 返回最大堆中的最大值或最小堆中的最小值(通常是根节点)。
-
Extract-Max/Extract-Min (提取最大/最小值): 移除并返回最大堆中的最大值或最小堆中的最小值。
-
Increase-Key (增加节点的值): 将某个节点的值增加到一个较大的值。(最大堆中使用)
-
Decrease-Key (减小节点的值): 将某个节点的值减小到一个较小的值。(最小堆中使用)
-
Heapify (堆化): 将一个不满足堆属性的二叉树调整为一个堆。
-
Build-Heap (建堆): 从无序的输入数组构建一个新的堆。
栈溢出
栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。这种问题是一种特定的缓冲区溢出漏洞,类似的还有堆溢出,bss 段溢出等溢出方式。栈溢出漏洞轻则可以使程序崩溃,重则可以使攻击者控制程序执行流程。此外,我们也不难发现,发生栈溢出的基本前提是
- 程序必须向栈上写入数据。
- 写入的数据大小没有被良好地控制。
想要覆盖程序返回的地址为可控地址,需要确保这个地址所在的段具有可执行权限。如何判断呢?这里可以先了解一下gcc编译器以及PIE(Position Independent Executable),地址空间随机化(ASLR)机制,也是编译器自带的能力,但是版本不同默认配置不同;
关于gcc编译参数的相关了解参考:
elf文件分析–checksec–检查gcc安全编译配置
现在新版的很多编译器都是默认带上安全配置,除了上面的PIE,还有NX保护等,随着这些保护的开启对应以往直接向栈或者堆上直接注入代码的方式就不能实现了,后面主要思路就是ROP(Return Oriented Programming)。
寻找危险函数
通过寻找危险函数,我们快速确定程序是否可能有栈溢出,以及有的话,栈溢出的位置在哪里。常见的危险函数如下
- 输入
- gets,直接读取一行,忽略’\x00’
- scanf
- vscanf
- 输出
- sprintf
- 字符串
- strcpy,字符串复制,遇到’\x00’停止
- strcat,字符串拼接,遇到’\x00’停止
- bcopy
确定填充长度
这一部分主要是计算我们所要操作的地址与我们所要覆盖的地址的距离。常见的操作方法就是打开 IDA,根据其给定的地址计算偏移。一般变量会有以下几种索引模式
- 相对于栈基地址的的索引,可以直接通过查看 EBP 相对偏移获得
- 相对应栈顶指针的索引,一般需要进行调试,之后还是会转换到第一种类型。
- 直接地址索引,就相当于直接给定了地址。
一般来说,我们会有如下的覆盖需求
- 覆盖函数返回地址,这时候就是直接看 EBP 即可。
- 覆盖栈上某个变量的内容,这时候就需要更加精细的计算了。
- 覆盖 bss 段某个变量的内容。
- 根据现实执行情况,覆盖特定的变量或地址的内容。
参考
C语言函数调用栈(一) - clover_toeic - 博客园 (cnblogs.com)
C语言函数调用栈(二) - clover_toeic - 博客园 (cnblogs.com)
栈溢出原理 - CTF Wiki (ctf-wiki.org)
漏洞悬赏计划:涂鸦智能安全响应中心(https://src.tuya.com)欢迎白帽子来探索。