River5tone

栈溢出漏洞及其利用

字数统计: 2.7k阅读时长: 10 min
2018/10/23 Share

//感谢wyx学姐

0x00 栈帧复习

栈帧建立过程:

压参(从右向左)—>将紧接着CALL后面的那条指令的内存地址即返回地址压入栈中—>压入当前栈帧EBP—>EBP指向ESP指向的栈顶即刚压入的当前栈帧EBP—>ESP上移设置新的栈顶

栈帧销毁过程:

ESP下移指向EBP销毁栈空间—>弹出上一个栈帧的EBP(ESP指向返回地址)—>弹出返回地址

0x01 栈溢出

栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致栈中与其相邻的变量的值被改变。

栈溢出前提

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

ebp-x = esp+y

system("/bin/sh")调用shell。

实例

level1

如标红部分所示。在ebp上方88hbuf存储位置,而读进buf的长度

面向题解写脚本,pwntools了解一波。

关于pwntools

使用from pwn import *将所有的模块导入到当前namespace,这条语句还会帮你把os,sys等常用的系统库导入。

常用模块如下:

  • asm : 汇编与反汇编,支持x86/x64/arm/mips/powerpc等基本上所有的主流平台
  • dynelf : 用于远程符号泄漏,需要提供leak方法
  • elf : 对elf文件进行操作
  • gdb : 配合gdb进行调试
  • memleak : 用于内存泄漏
  • shellcraft : shellcode的生成器
  • tubes : 包括tubes.sock, tubes.process, tubes.ssh, tubes.serialtube,分别适用于不同场景的PIPE
  • utils : 一些实用的小功能,例如CRC计算,cyclic pattern等

1.连接

1
2
3
本地 :sh = porcess("./level0")
远程:sh = remote("127.0.0.1",10001) #用来建立一个远程连接,url或者ip作为地址,然后指明端口
关闭连接:sh.close()

2.IO模块

1
2
3
4
5
6
7
8
sh.send(data)  发送数据
sh.sendline(data) 发送一行数据,相当于在数据后面加\n
sh.recv(numb = 2048, timeout = default) 接受数据,numb指定接收的字节,timeout指定超时
sh.recvline(keepends=True) 接受一行数据,keepends为是否保留行尾的\n
sh.recvuntil("Hello,World\n",drop=fasle) 接受数据直到我们设置的标志出现
sh.recvall() 一直接收直到EOF
sh.recvrepeat(timeout = default) 持续接受直到EOF或timeout
sh.interactive() 直接进行交互,相当于回到shell的模式,在取得shell之后使用

3.Shellcode生成器

结合asm可以可以得到最终的pyaload。

其中的子模块声明架构,比如shellcraft.arm 是ARM架构的,shellcraft.amd64是AMD64架构,shellcraft.i386Intel 80386架构的,以及有一个shellcraft.common是所有架构通用的。而这里的shellcraft.sh()则是执行/bin/sh的shellcode。

contextpwntools用来设置环境的功能。在很多时候,由于二进制文件的情况不同,我们可能需要进行一些环境设置才能够正常运行exp,比如有一些需要进行汇编,但是32的汇编和64的汇编不同,如果不设置context会导致一些问题。

log_level设置日志输出的等级为debug,这句话在调试的时候一般会设置,这样pwntools会将完整的io过程都打印下来,使得调试更加方便,可以避免在完成CTF题目时出现一些和IO相关的错误。

1
2
3
4
5
6
7
8
9
from pwn import *
context(os='linux',arch='amd64') #用来导入pwntools模块
shellcode = asm(shellcraft.sh())

或者

from pwn import *
shellcode = asm(shellcraft.amd64.linux.sh())
#asm()函数接收一个字符串作为参数,得到汇编码的机器代码

4.ELF文件操作

1
2
3
4
5
6
7
8
9
10
>>> e = ELF('/bin/cat')
>>> print hex(e.address) # 文件装载的基地址
0x400000
>>> print hex(e.symbols['write']) # 函数地址
0x401680
>>> print hex(e.got['write']) # GOT表的地址
0x60b070
>>> print hex(e.plt['write']) # PLT的地址
0x401680
>>> print hex(e.search('/bin/sh').next())# 字符串/bin/sh的地址

5.数据打包

数据打包,即将整数值转换为32位或者64位地址一样的表示方式,比如0x400010表示为\x10\x00\x40一样,这使得我们构造payload变得很方便。

用法:

  • p32/p64: 打包一个整数,分别打包为32或64位
  • u32/u64: 解包一个字符串,得到整数

p对应pack,打包,u对应unpack,解包,简单好记。

0x02 安全机制

1.Canary探测

要检测对函数栈的破坏,需要修改函数栈的组织,在缓冲区和控制信息(如 EBP 等)间插入一个 canary word。这样,当缓冲区被溢出时,在返回地址被覆盖之前 canary word 会首先被覆盖。通过检查 canary word 的值是否被修改,就可以判断是否发生了溢出攻击。

  • GS编译选项(金丝雀)

    微软在编译程序时使用的安全编译选项,在VS中默认启动这个编译选项。在所有函数调用发生时,向栈中压入一个额外的随机DWORD,被称作“Security cookie”。

  • SSP保护

    作为GCC的一部分来提供堆栈保护,取fs寄存器0x28处的值作为canary word,且有意将局部变量中的数组放在函数栈的高地址。

2.DEP

基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常而不是去执行恶意指令。

3.ASLR

内存地址随机化,通过加载程序的时候不在使用固定的基地址,从而干扰shellcode定位的一种保护机制。

(操作系统分页)

0x03 ROP

什么是ROP?

ROP的全称为Return-oriented programming(返回导向编程),这是一种高级的内存攻击技术可以用来绕过现代操作系统的各种通用防御。

ROP主要思想

其主要思想是在栈缓冲区溢出的基础上,利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程。所谓 gadgets 就是以 ret 结尾的指令序列,通过这些指令序列,我们可以修改某些地址的内容,方便控制程序的执行流程。

ROP攻击前提

  1. 程序存在溢出,并且可以控制返回地址。
  2. 可以找到满足条件的 gadgets 以及相应 gadgets 的地址。(一个漏洞可以利用很多次)

基本ROP分类

ret2text

即控制程序执行程序本身已有的的代码 (.text)。其实,这种攻击方法是一种笼统的描述。我们控制执行程序已有的代码的时候也可以控制程序执行好几段不相邻的程序已有的代码。

ret2shellcode

在栈溢出的基础上,要想执行shellcode,需要对应的 binary 在运行时,shellcode所在的区域具有可执行权限。

ret2syscall

控制程序执行系统调用,获取shell。一般情况下,利用execve(“/bin/sh”,NULL,NULL)这个系统调用。

ret2libc

控制函数的执行 libc 中的函数,通常是返回至某个函数的 plt 处或者函数的具体位置 (即函数对应的 got 表项的内容)。一般情况下,我们会选择执行 system(“/bin/sh”)

0x04 .plt/.got表

GOT(Global Offset Table)和PLT(Procedure Linkage Table)是Linux系统下面ELF格式的可执行文件中,用于定位全局变量和过程的数据信息。

共享库机制

在二进制文件(比如object file)中会有一段叫relocations的部分,这部分的内容在链接时候(link time)再进行敲定确切的值,注意链接可以发生在运行前(称为静态链接,toolchain linker),也可发生在运行时(称为动态链接,dynamic linker)。具体relocations部分中的内容就是在讲:“确定X这个符号(symbol)的值,然后把这个值写到二进制文件的Y偏移处”。每一条relocation都有确定的类型(定义与ABI文档中),从而确切地说明每个类型的值到底该如何敲定。

1.Global Offset Table (.got)

共享库(.so文件)
  1. 可以加载到任意一段物理内存上就用。

  2. 代码重用(code sharing)。

virtual memory的神奇机制实现,对于每一个进程,要找到其特有的data段时,当前地址+已知的相对位置=索引的data段。

如果一个so文件想去索引其他so文件中的data怎么办?

我们可以patch这个so文件的代码部分,直接把那个data的位置为patch上,但这样就破坏了so文件的 code-sharability。而计算机中有一个原理就是:所有问题都可以通过加一个间接层来解决。这里,这个间接层就叫global offset table or GOT.

2.procedure linkage table(.plt)

如果是外部函数调用?

此处,我们使用的“间接层”叫做procedure linkage table or PLT。code只会通过PLT stub(PLT桩代码,其实就是一小段代码),实现外部函数调用。

延迟绑定

第一次调用foo函数,通过PLT stub(PLT桩代码)进行动态链接(地址解析),找到foo函数真实地址并patch GOT表,第二次直接通过PLT桩代码跳到foo函数真实地址。

通过PLT表跳转到GOT表,而在第一次运行某一个函数之前,这个函数PLT表对应的GOT表中的数据为@plt函数中第二行指令的地址。0x540为PLT[0]的地址,该处的指令会进入动态链接器入口,执行一个函数将真正的函数地址覆盖到GOT表中。

0x05 附件:PWN向环境配置(DJ)

改架构

第一步: 确认你有一个64位架构的内核 你可以打开终端然后输入:

1
$dpkg --print-architecture

你将会看到像下面这样的内容 amd64 这说明着你已经拥有了64位架构内核。

第二步: 确认你打开了多架构支持功能(多架构支持可以让你在有64位库的情况 下使用32位库。) 输入:

1
$dpkg --print-foreign-architectures

输出是: i386 如果你还没有多架构支持你需要打开它。 打开多架构支持: 输入

1
2
$sudo dpkg --add-architecture i386  
$sudo apt-get update

然后就会开始下载更新然后运行。 之后你需要输入:

1
$sudo apt-get -f dist-upgrade

它会检查你已经拥有的库文件是否有更新的版本。 然后可能需要安装32位 库,在终端里输入以下命令

1
2
$sudo apt-get install lib32c-dev   
$sudo apt-get install lib32stdc++6

安装pwntools

1
2
3
4
$sudo apt-get update 
$sudo apt-get install python2.7 python-pip python-dev  git libssl-dev libffi-dev build-essential
$sudo pip install --upgrade pip
$sudo pip install pwntools
CATALOG
  1. 1. //感谢wyx学姐
  • 0x00 栈帧复习
    1. 1. 栈帧建立过程:
    2. 2. 栈帧销毁过程:
  • 0x01 栈溢出
    1. 1. 实例
      1. 1.1. level1
        1. 1.1.1. 关于pwntools
  • 0x02 安全机制
    1. 1. 1.Canary探测
    2. 2. 2.DEP
    3. 3. 3.ASLR
  • 0x03 ROP
    1. 1. 什么是ROP?
    2. 2. ROP主要思想
    3. 3. ROP攻击前提
    4. 4. 基本ROP分类
      1. 4.1. ret2text
      2. 4.2. ret2shellcode
      3. 4.3. ret2syscall
      4. 4.4. ret2libc
  • 0x04 .plt/.got表
    1. 1. 共享库机制
      1. 1.1. 共享库(.so文件)
    2. 2. 2.procedure linkage table(.plt)
  • 0x05 附件:PWN向环境配置(DJ)
    1. 1. 改架构
    2. 2. 安装pwntools