天天看點

CC_STACKPROTECTOR防核心堆棧溢出更新檔分析作者:王智通

cc_stackprotect更新檔是tejun heo在09年給主線kernel送出的一個用來防止核心堆棧溢出的更新檔。預設的config是将這個選項關閉的,可以在編譯核心的時候, 修改.config檔案為config_cc_stackprotector=y來啟用。未來飛天核心可以将這個選項開啟來防止利用核心stack溢出的0day攻擊。這個更新檔的防溢出原理是: 在程序啟動的時候, 在每個buffer的後面放置一個預先設定好的stack canary,你可以把它了解成一個哨兵, 當buffer發生緩沖區溢出的時候, 肯定會破壞stack canary的值, 當stack canary的值被破壞的時候, 核心就會直接當機。那麼是怎麼判斷stack canary被覆寫了呢? 其實這個事情是gcc來做的,核心在編譯的時候給gcc加了個-fstack-protector參數, 我們先來研究下這個參數是做什麼用的。
先寫個簡單的有溢出的程式:
段錯誤
反彙編看看:
沒什麼特别的,我們在加上-fstack-protector參數看看:
這次程式列印了一條堆棧被溢出的資訊,然後就自動退出了。
在反彙編看下:
使用-fstack-protector參數後, gcc在函數的開頭放置了幾條彙編代碼:
将代碼段gs偏移0×14記憶體處的值指派給了ebp-4, 也就是第一個變量值的後面。
在call完memeset後,有如下彙編代碼:
在memset後,gcc要檢查這個操作是否發生了堆棧溢出, 将儲存在ebp-4的這個值與原來的值對比一下,如果不相同, 說明堆棧發生了溢出,那麼就會執行stack_chk_fail這個函數, 這個函數是glibc實作的,列印出上面看到的資訊, 然後程序退出。
從這個例子中我們可以看出gcc使用了-fstack-protector參數後,會自動檢查堆棧是否發生了溢出, 但是有一個前提就是核心要給每個程序提前設定好一個檢測值放置在%gs:0×14位置處,這個值稱之為stack canary。是以我們可以看到防止堆棧溢出是由核心和gcc共同來完成的。
gcc的任務就是放置幾條彙編代碼, 然後和%gs:0×14位置處的值進行對比即可。 主要任務還是核心如何來設定stack canary, 也是cc_stackprotector更新檔要實作的目的, 下面我們仔細來看下這個更新檔是如何實作的。
既然gcc硬性規定了stack canary必須在%gs的某個偏移位置處, 那麼核心也必須按着這個規定來設定。
對于32位和64位核心, gs寄存器有着不同的功能。
64位核心gcc要求stack canary是放置在gs段的40偏移處, 并且gs寄存器在每cpu變量中是共享的,每cpu變量irq_stack_union的結構如下:
arch/x86/include/asm/processor.h
gs_base隻是一個40位元組的站位空間, stack_canary就緊挨其後。并且在應用程式進出核心的時候,核心會使用swapgs指令自動更換gs寄存器的内容。
32位下就稍微有點複雜了。由于某些處理器在加載不同的段寄存器時很慢, 是以核心使用fs段寄存器替換了gs寄存器。 但是gcc在使用-fstack-protector的時候, 還要用到gs段寄存器, 是以核心還要管理gs寄存器,我們要把config_x86_32_lazy_gs選項關閉, gs也隻在程序切換的時候才改變。 32位用每cpu變量stack_canary儲存stack canary。
核心是處于保護模式的, 是以gs寄存器就變成了保護模式下的段選子,在gdt表中也要有相應的設定:
gdt表中的第28個表項用來定為stack canary所在的段。
gdt_stack_canary_init在剛進入保護模式的時候被調用, 這個段描述符項被設定為基位址為0, 段大小設為24,因為隻在基位址為0, 偏移為0×14處放置一個4bytes的stack canary, 是以24位元組正好。不了解的同學可以看看intel保護模式的手冊, 對着段描述符結構一個個看就行了。
在進入保護模式後, start_kernel()會調用boot_init_stack_canary()來初始話一個stack canary。
随機出了一個值指派給每cpu變量, 32位是stack_canary, 64位是irq_stack_union。
核心在進一步初始化cpu的時候,會調用setup_stack_canary_segment()來設定每個cpu的gdt的stack canary描述符項:
start_kernel()->setup_per_cpu_areas()->setup_stack_canary_segment:
在核心剛進入保護模式的時候, stack canary描述符的基位址被初始化為0, 現在在cpu初始化的時候要重新設定為每cpu變量stack_canary的位址, 而不是變量儲存的值。通過這些設定當核心代碼在通路%gs:0×14的時候, 就會通路stack canry儲存的值。注意:setup_stack_canary_segment是針對32位核心做設定, 因為64位核心中的irq_stack_union是每cpu共享的, 不用針對每個cpu單獨設定。 然後就可以調用switch_to_new_gdt(cpu);來加載gdt表和加載gs寄存器。
經過上述初始化過程,在核心代碼裡通路%gs:0×14就可以定位stack canary的值了, 那麼每個程序的stack canary是什麼時候設定的呢?
在核心啟動一個程序的時候, 會把gs寄存器的值設為kernel_stack_canary
核心在fork一個程序的時候, 有如下操作:
随機初始化了一個stack_canary儲存在task_struct結構中的stack_canary變量中。當程序在切換的時候, 通過switch宏把新程序的stack canary儲存在每cpu變量stack_canary中, 目前程序的stack_canary也儲存在一個每cpu變量中,完成stack canary的切換。
前面講過當gcc檢測到堆棧溢出的時候, 會調用glibc的stack_chk_fail函數, 但是當核心堆棧發生溢出的時候,

不能調用glibc的函數,是以核心自己實作了一個stack_chk_fail函數:

kernel/panic.c
當核心堆棧發生溢出的時候,就會執行stack_chk_fail函數, 核心當機。 這就是這個更新檔的原理,不懂的同學請參考: ​http://git.kernel.org/?p=linux/kernel/git/next/linux-next.git;a=commitdiff;h=60a5317ff0f42dd313094b88f809f63041568b08

繼續閱讀