栈溢出的概念
栈溢出:向栈中某个变量写入的字节数超过了变量本身所申请的空间,与其相邻的空间变量的值被改变。
栈溢出是一种缓冲区溢出。类似的还有堆溢出、bss段溢出等溢出方式。
栈溢出是一种可以使程序崩溃的漏洞,我们可以利用漏洞控制程序流程甚至拿下系统shell权限。
发生栈溢出的条件:
- 程序向栈上写数据
- 写入的数据大小没有被正常控制
基本例子
最典型的栈溢出是用攻击者所能控制的地址覆盖程序函数返回地址。
需要确保这个地址所在的段具有可执行权限。
下面,列举一个简单的例子:
1 | #include <stdio.h> |
这个程序的作用是读取字符串并打印字符串。
我们希望利用漏洞可以控制程序执行success函数。
栈溢出
查看PIE保护
PIE保护会打乱加载基址,为了避免被打乱,我们执行下列操作。
首先,我们使用gcc -v查看pie保护是否开启:
如图所示,–enable-default-pie 代表已经开启pie保护。
关闭ASLR
开启PIE保护并开启ASLR,加载基址才会被打乱。
单纯开启PIE并不会打乱基址,但和no PIE时的基址不同。
为了使后续操作简便,我们关闭ASLR保护。
我们可以通过修改 /proc/sys/kernel/randomize_va_space 来控制ASLR:
1 | echo 0 > /proc/sys/kernel/randomize_va_space |
0代表关闭ASLR,1代表开启ASLR,2代表增强ASLR。
编译程序
然后,我们编译上述测试程序:
1 | gcc -m32 -fno-stack-protector -no-pie example.c -o example |
如上图所示,编译成功,提示我们gets是一个危险的函数。
因为gets读取一行字符串,直至遇到换行符截止,并不检查字符串长度。
checksec检查程序
我们使用checksec检查程序信息:
1 | checksec example |
IDA逆向分析
拖入IDA进行分析,首先在Exports中找到success函数的地址(08049172):
然后左侧找到vulnerable函数,按下F5进行反编译:
得到下面结果:
相应的栈结构:
ReturnAddress |
---|
ebp |
… |
ebp - 14 |
我们如果想覆盖返回地址,需要输入 14个字节的字符串 + 覆盖ebp(4字节)+ 覆盖的内容:
1 | 14*'a'+'bbbb'+success_addr |
编写exploit脚本
为什么需要编写脚本?
如果我们直接输入的话,14 * ‘a’ 和 ‘bbbb’ 没问题,但是地址如何输入呢?
每个值是按字节存放的,且一般是小端存储(低位字节序内容放在低字节、高位字节序内容放在高字节)。
所以08049172表示形式:
1 | \x72\x91\x04\x08 |
如果我们直接输入的话,\和x都会被算作字符,所以我们通过编写脚本进行漏洞注入。
编写脚本
这里利用pwntools进行编写:
1 | ##coding=utf8 |
漏洞注入
执行exploit脚本,成功利用漏洞:
总结
栈溢出的流程:
- 寻找危险函数
- 确定填充长度
- 构造注入内容
危险函数如下:
- 输入函数:gets、scanf、vscanf
- 输出函数:sprintf
- 字符串:strcpy、strcat、bcopy