网络攻防-软件漏洞1-缓冲区溢出,涉及实验:SEED Labs – Buffer Overflow Attack Lab,代码见:https://github.com/Seanxz401/seed-labs
理论
软件漏洞
漏洞:可能被攻击者利用的系统弱点
典型漏洞类型:栈溢出、堆溢出、格式化串、整型溢出、释放再使用
漏洞攻击的步骤:1.漏洞发现;2.漏洞分析;3.漏洞利用
可能导致:以匿名身份直接获取系统最高权限;2.从普通用户提升为管理员;实施远程dos攻击
栈溢出
当程序运行时,计算机会在内存区域中开辟一段连续的内存块, 包括代码段(.text)、数据段(.bss+.data)和堆栈段三部分。
程序在内存中的存放形式:
- 对代码段进行写操作会导致段错误(Segmentation Fault)
- 数据段在编译时分配
- 堆中的变量由malloc、new这样的内存分配函数实现,用free、delete释放内存
- 栈用来存储函数调用时的临时信息:参数、返回地址、函数内局部变量……,不需要时自动清除
- 栈的特性:先入后出FILO
- 函数栈帧由高地址到低地址(数组由低地址到高地址)
- 栈顶指针ESP,基地址指针EBP
函数fun()被调用时,压栈情况:
高地址 | 传递的参数 |
---|---|
退出fun函数后的返回地址RET 即调用fun的下一条指令的地址 |
|
调用fun函数之前的EBP 也就是上一个函数栈帧的EBP |
|
低地址 (后入栈) |
函数中的局部变量: buf[10] …… buf[0] |
当局部变量buf发生溢出,可能会覆盖掉EBP和RET
tips:这一块儿文字描述不好,建议找视频观看,还需要一点点寄存器的相关知识(EIP,ESP,EBP)
栈溢出实例gets
1 | void fun(){ |
调用fun函数时:
- 将返回地址压入栈,也就是fun()的下一条指令printf(“EIP”)的地址
- 将EBP压入栈(此处是main函数栈帧的栈底地址),并将EBP修改为当前ESP(此处EBP内容变更为fun函数栈帧的栈底地址)
- ESP减16,即向低地址扩展16个字节,用来存放name数组
- 执行gets获取输入,假设输入”hello world”,然后for循环打印,直至0x00截止符
- 从fun返回到main
- 令ESP=EBP,回收name数组空间,并使ESP指向当前EBP。当前EBP中的内容为main函数栈帧的栈底地址。
- 程序将这个值弹出并赋给EBP,使EBP重新指向main()函数栈帧的栈底
- 再弹出现在位于栈顶的返回地址RET,赋给EIP,CPU继续执行EIP所指向的命令,也就是printf(“EIP”)。
栈溢出情况:当输入的字符串超过16字节后,多出的内容会继续向高地址蔓延,导致覆盖掉EBP和RET,那么返回时会把新的内容视作EBP和RET,CPU会执行新的RET中地址指向的内容。
溢出漏洞利用
攻击流程:1.注入恶意数据;2.溢出缓冲区;3.控制流重定向;4.执行有效载荷
- 溢出点定位
- 探测法:构造数据,根据出错情况来判断
- 反汇编分析
- 覆盖执行控制地址:返回地址、函数指针变量、异常处理结构
- 覆盖异常处理结构
- 跳转地址的确定:用户空间的任意地址、系统dll、进程代码段、PEB、 TEB
- Shellcode的定位和跳转:具体见shellcode实验
实验
SEED Labs – Buffer Overflow Attack Lab (Server Version)
实验环境:本实验将提供四台不同的服务器,每台服务器运行一个带有缓冲区溢出漏洞的程序。
实验目的:开发一个利用漏洞的程序,并最终获得这些服务器上的root权限。除了进行这些攻击实验之外,还将尝试几种对抗缓冲区溢出攻击的对策,然后需要评估这些办法是否有效,并解释原因。
实验准备
首先关闭实验环境(SEED虚拟机)中的随机化地址策略,此步骤是为了保证程序每次执行时的初始地址不变,以便于进行多次实验和调试:
1 | sudo /sbin/sysctl -w kernel.randomize_va_space=0 |
原理
首先要清楚函数栈帧的结构,包括参数压栈、返回地址、ebp/rbp等。
server
server.c中接收来自客户端的TCP连接并把TCP连接重定向至服务器的标准输入
1 | dup2(socket_fd, STDIN_FILENO); |
然后执行stack(PROGRAM)程序
1 | execle(PROGRAM, PROGRAM, (char *)NULL, generate_random_env(random_n)); |
stack
stack从server的标准输入获取数据存入str[517],而server的标准输入现在是来自TCP连接,相当于str存入的是客户端传来的指令。
在bof函数中执行strcpy:
1 | strcpy(buffer, str); |
strcpy不检查缓冲区边界,由于源字符串str[517]大于目的字符串buffer[200]时会发生缓冲区溢出。因此可以在分配给buffer大小以外的部分构造攻击代码,溢出部分拥有当前用户权限来执行指令。
服务端代码编译
1 | gcc -DBUF_SIZE=$(L1) -o stack -z execstack -fno-stack-protector stack.c |
编译时需要加上两个选项-DBUF_SIZE
和-fno-stack-protector
来关闭堆栈保护器和不可执行的堆栈保护。然后通过Makefile安装容器环境。
1 | make |
docker配置
1 | # 在docker-compose.yaml所属目录下编译 |
可以看到4个容器已经在运行了
Task1
修改shellcode脚本中的命令为删除文件(先创建好这个文件),注意命令末尾*
符号的位置要保持不变。(就是保持shellcode长度不变)
运行脚本生成codefile→make
→运行.out可执行文件结果如图,在执行命令行test.txt文件被删除了:
Task2
向容器10.9.0.5
发送消息:echo hello | nc 10.9.0.5 9090
,观察容器端打印出的信息:
- ebp:
0xffffd5a8
- buffer开始的位置:
0xffffd538
2.1 shellcode编写
需要修改的地方:
- shellcode:可以参照Task1的shellcode(32和64不一样,对应的后面步长不一样,以下内容参考32位的shellcode);修改如下:
- start:
517-len(shellcode)
,把content(badfile)中的后面部分替换为攻击代码 - ret:覆盖返回地址,
ebp的地址+大于等于8的数量
(ps.填充若干长度的\x90
这个机器码对应的指令是 NOP (No Operation),也就是告诉 CPU 什么也不做,然后跳到下一条指令。有了这一段 NOP 的填充,只要返回地址能够命中这一段中的任意位置,最后都可以跳转到 shellcode 的起始处。); - offset:
ebp-buffer+4
执行脚本生成badfile,并将badfile发送给服务器(容器):
1 | cat badfile | nc 10.9.0.5 9090 |
观察容器的输出,可以看到badfile中嵌入的代码已经被执行:
2.2 reverse shell
2.1只能实现让服务器执行shellcode中既定的指令,因此我们要将shellcode改为reverse shell,使攻击者能远程连接上被攻击的服务器,拿到root权限。
修改shellcode如下:
10.9.0.1
是攻击端的IP(从容器的打印输出可以看到);0<&1
,0表示标准输入stdout,1表示标准输出stdin,即将stdout重定向到stdin,由于服务器的stdout重定向到了tcp连接,因此最终效果是将tcp连接中攻击者的输入定向到stdin2>&1
,2表示标准错误输出stderr
重新生成badfile,先在攻击端1打开端口等待连接nc -lnv 9090
,再新建终端2上传badfile。在终端1可以看到已经拿到shell的root权限(命令提示符为#),用ifconfig测试可以看到确实是10.9.0.5
。
Task3
向容器10.9.0.6
发送消息:echo hello | nc 10.9.0.6 9090
,观察容器端打印出的信息,此次没有提示ebp的位置:
题目中给出了buffer_size的限制在:[100, 300]。需要修改的:
- ret:
buffer+大于等于(300+8)的值
- offset:由于不确定,因此将这个范围内[100+4,300+4]都填充为返回地址,以4为步长
生成badfile上传,拿到root shell:
Task4
前两节都是32bit,现在切换到64bit。向10.9.0.7
发送消息可以看到rbp
和buffer
的地址信息,是64bit。
ps.Task的难点在于64位计算机中的地址范围为0x00
~`0x00007FFFFFFFFFFF,因此所有地址最高位的两个字节都是
0x00`。而strcpy函数遇到0会停止,如果和前面的方法一样,则shellcode不会被copy到缓冲区。因此解决办法是把shellcode移到badfile的前面部分,ret的值指向前面部分。由于是小端存储,在截止前ret的非零部分已经被copy到了缓冲区。需要如下:
- shellcode:参考shellcode_64
- start:很小的值(0),使shellcode位于缓冲区的前一部分
- ret:
buffer+start
的位置,这样覆盖后的返回地址就是shellcode的位置 - offset:
rbp-buffer+8
,后面将ret转为字节码的部分改为以8为步长
生成badfile并上传到10.9.0.7
,nc连接,顺利拿到root shell。
Task5
向10.9.0.8
发送消息可得:
rbp与buffer之间的距离只有96bytes,修改如下:
- 将shellcode放在高位。
- offset=rbp-buffer+8;
- ret:取一个较大值,在 1184到 1424之间。由于
\x00
截断了strcpy函数,因此需要触发的shellcode并没有被拷贝到缓冲区,因此ret指向的位置需是主函数中str数组中shellcode的位置。- 调试L4级别的stack.c,可以获取到str数组的地址,我们需要跳转到str数组中的ret和shellcode中间的NOP中。
str的地址+offset+8-rbp=1184
;str的地址+517-165-rbp=1424
;shellcode是165个字节。
生成badfile上传,nc连接。
Task6
关闭地址随机化:
1 | sudo /sbin/sysctl -w kernel.randomize_va_space=2 |
向容器多次发送消息可以看到每次得到的地址都在变化。因此使用爆破的办法,选定一个作为返回地址,一旦命中就停止。自动化sh脚本在实验环境中有。
Task7
7.1栈溢出保护
修改stack.c,使badfile作为fread的输入:
1 | FILE *file=fopen("badfile","rb"); |
编译stack.c时不使用-fno-stack-protector
1 | gcc -DBUF_SIZE=80 -o stack -z execstack stack.c |
可以看到错误提示:stack smashing
7.2栈不可执行
编译call_shellcode.c时不使用-z execstack
1 | gcc -m32 -o a32.out call_shellcode.c |
运行a32.out可以看到segmentation fault
结果分析
实验中共提到了三种栈溢出攻击的防御措施:
- 开启地址随机化:开启后较难猜中想要跳转的地址,但是我们在Task6中通过爆破还是能攻击成功;
- 栈保护措施:开启后能检测到程序有栈溢出的风险,不允许执行。不保证能百分百检测出有栈溢出的点;
- 栈不可执行措施:将数据所在内存页标识为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令。可以尝试ROP攻击。
参考
strcpy栈溢出:https://www.33ip.com/support/16.html
函数栈帧及栈溢出攻击原理:https://paper.seebug.org/271/,https://paper.seebug.org/272/
linux程序保护机制&gcc编译选项:https://www.jianshu.com/p/91fae054f922