1. 前言
KASAN是一個(gè)動(dòng)態(tài)檢測(cè)內(nèi)存錯(cuò)誤的工具。KASAN可以檢測(cè)全局變量、棧、堆分配的內(nèi)存發(fā)生越界訪問(wèn)等問(wèn)題。功能比SLUB DEBUG齊全并且支持實(shí)時(shí)檢測(cè)。越界訪問(wèn)的嚴(yán)重性和危害性通過(guò)我之前的文章(SLUB DEBUG技術(shù))應(yīng)該有所了解。正是由于SLUB DEBUG缺陷,因此我們需要一種更加強(qiáng)大的檢測(cè)工具。難道你不想嗎?KASAN就是其中一種。KASAN的使用真的很簡(jiǎn)單。但是我是一個(gè)追求刨根問(wèn)底的人。僅僅止步于使用的層面,我是不愿意的,只有更清楚的了解實(shí)現(xiàn)原理才能更加熟練的使用工具。不止是KASAN,其他方面我也是這么認(rèn)為。但是,說(shuō)實(shí)話,寫(xiě)這篇文章是有點(diǎn)底氣不足的。因?yàn)閺奈也殚喌?a href="http://www.asorrir.com/soft/special/" target="_blank">資料來(lái)說(shuō),國(guó)內(nèi)沒(méi)有一篇文章說(shuō)KASAN的工作原理,國(guó)外也是沒(méi)有什么文章關(guān)注KASAN的原理。大家好像都在說(shuō)How to use。由于本人水平有限,就根據(jù)現(xiàn)有的資料以及自己閱讀代碼揣摩其中的意思。本文章作為拋準(zhǔn)引玉,如果有不合理的地方還請(qǐng)指正。
注:文章代碼分析基于linux-4.15.0-rc3。
2. 簡(jiǎn)介
KernelAddressSANitizer(KASAN)是一個(gè)動(dòng)態(tài)檢測(cè)內(nèi)存錯(cuò)誤的工具。它為找到use-after-free和out-of-bounds問(wèn)題提供了一個(gè)快速和全面的解決方案。KASAN使用編譯時(shí)檢測(cè)每個(gè)內(nèi)存訪問(wèn),因此您需要GCC 4.9.2或更高版本。檢測(cè)堆棧或全局變量的越界訪問(wèn)需要GCC 5.0或更高版本。目前KASAN僅支持x86_64和arm64架構(gòu)(linux 4.4版本合入)。你使用ARM64架構(gòu),那么就需要保證linux版本在4.4以上。當(dāng)然了,如果你使用的linux也有可能打過(guò)KASAN的補(bǔ)丁。例如,使用高通平臺(tái)做手機(jī)的廠商使用linux 3.18同樣支持KASAN。
3. 如何使用
使用KASAN工具是比較簡(jiǎn)單的,只需要添加kernel以下配置項(xiàng)。
CONFIG_SLUB_DEBUG=y
CONFIG_KASAN=y
為什么這里必須打開(kāi)SLUB_DEBUG呢?是因?yàn)橛卸螘r(shí)間KASAN是依賴SLUBU_DEBUG的,什么意思呢?就是在Kconfig中使用了depends on,明白了吧。不過(guò)最新的代碼已經(jīng)不需要依賴了,可以看下提交。但是我建議你打開(kāi)該選項(xiàng),因?yàn)閘og可以輸出更多有用的信息。重新編譯kernel即可,編譯之后你會(huì)發(fā)現(xiàn)boot.img(Android環(huán)境)大小大了一倍左右。所以說(shuō),影響效率不是沒(méi)有道理的。不過(guò)我們可以作為產(chǎn)品發(fā)布前的最后檢查,也可以排查越界訪問(wèn)等問(wèn)題。我們可以查看內(nèi)核日志內(nèi)容是否包含KASAN檢查出的bugs信息。
4. KASAN是如何實(shí)現(xiàn)檢測(cè)的?
KASAN的原理是利用額外的內(nèi)存標(biāo)記可用內(nèi)存的狀態(tài)。這部分額外的內(nèi)存被稱作shadow memory(影子區(qū))。KASAN將1/8的內(nèi)存用作shadow memory。使用特殊的magic num填充shadow memory,在每一次load/store(load/store檢查指令由編譯器插入)內(nèi)存的時(shí)候檢測(cè)對(duì)應(yīng)的shadow memory確定操作是否valid。連續(xù)8 bytes內(nèi)存(8 bytes align)使用1 byte shadow memory標(biāo)記。如果8 bytes內(nèi)存都可以訪問(wèn),則shadow memory的值為0;如果連續(xù)N(1 =< N <= 7) bytes可以訪問(wèn),則shadow memory的值為N;如果8 bytes內(nèi)存訪問(wèn)都是invalid,則shadow memory的值為負(fù)數(shù)。
?
在代碼運(yùn)行時(shí),每一次memory access都會(huì)檢測(cè)對(duì)應(yīng)的shawdow memory的值是否valid。這就需要編譯器為我們做些工作。編譯的時(shí)候,在每一次memory access前編譯器會(huì)幫我們插入__asan_load##size()或者_(dá)_asan_store##size()函數(shù)調(diào)用(size是訪問(wèn)內(nèi)存字節(jié)的數(shù)量)。這也是要求更新版本gcc的原因,只有更新的版本才支持自動(dòng)插入。
mov x0, #0x5678
movk x0, #0x1234, lsl #16
movk x0, #0x8000, lsl #32
movk x0, #0xffff, lsl #48
mov w1, #0x5
bl __asan_store1
strb w1, [x0]
上面一段匯編指令是往0xffff800012345678地址寫(xiě)5。在KASAN打開(kāi)的情況下,編譯器會(huì)幫我們自動(dòng)插入bl __asan_store1指令,__asan_store1函數(shù)就是檢測(cè)一個(gè)地址對(duì)應(yīng)的shadow memory的值是否允許寫(xiě)1 byte。藍(lán)色匯編指令就是真正的內(nèi)存訪問(wèn)。因此KASAN可以在out-of-bounds的時(shí)候及時(shí)檢測(cè)。__asan_load##size()和__asan_store##size()的代碼在mm/kasan/kasan.c文件實(shí)現(xiàn)。
4.1. 如何根據(jù)shadow memory的值判斷內(nèi)存訪問(wèn)操作是否valid?
shadow memory檢測(cè)原理的實(shí)現(xiàn)主要就是__asan_load##size()和__asan_store##size()函數(shù)的實(shí)現(xiàn)。那么KASAN是如何根據(jù)訪問(wèn)的address以及對(duì)應(yīng)的shadow memory的狀態(tài)值來(lái)判斷訪問(wèn)是否合法呢?首先看一種最簡(jiǎn)單的情況。訪問(wèn)8 bytes內(nèi)存。
long *addr = (long *)0xffff800012345678;
*addr = 0;
以上代碼是訪問(wèn)8 bytes情況,檢測(cè)原理如下:
long *addr = (long *)0xffff800012345678;
char *shadow = (char *)(((unsigned long)addr >> 3) + KASAN_SHADOW_OFFSE);
if (*shadow)
??? report_bug();
*addr = 0;
紅色區(qū)域類似是編譯器插入的指令。既然是訪問(wèn)8 bytes,必須要保證對(duì)應(yīng)的shadow mempry的值必須是0,否則肯定是有問(wèn)題。那么如果訪問(wèn)的是1,2 or 4 bytes該如何檢查呢?也很簡(jiǎn)單,我們只需要修改一下if判斷條件即可。修改如下:
if (*shadow && *shadow < ((unsigned long)addr & 7) + N); //N = 1,2,4
如果*shadow的值為0代表8 bytes均可以訪問(wèn),自然就不需要report bug。addr & 7是計(jì)算訪問(wèn)地址相對(duì)于8字節(jié)對(duì)齊地址的偏移。還是使用下圖來(lái)說(shuō)明關(guān)系吧。假設(shè)內(nèi)存是從地址8~15一共8 bytes。對(duì)應(yīng)的shadow memory值為5,現(xiàn)在訪問(wèn)11地址。那么這里的N只要大于2就是invalid。
4.2. shadow memory內(nèi)存如何分配?
在ARM64中,假設(shè)VA_BITS配置成48。那么kernel space空間大小是256TB,因此shadow memory的內(nèi)存需要32TB。我們需要在虛擬地址空間為KASAN shadow memory分配地址空間。所以我們有必要了解一下ARM64 memory layout。
基于linux-4.15.0-rc3的代碼分析,我繪制了如下memory layout(VA_BITS = 48)。kernel space起始虛擬地址是0xffff_0000_0000_0000,kernel space被分成幾個(gè)部分分別是KASAN、MODULE、VMALLOC、FIXMAP、PCI_IO、VMEMMAP以及linear mapping。其中KASAN的大小是32TB,正好是kernel space大小的1/8。不知道你注意到?jīng)]有,KERNEL的位置相對(duì)以前是不是有所不一樣。你的印象中,KERNEL是不是位于linear mapping區(qū)域,這里怎么變成了VMALLOC區(qū)域?這里是Ard Biesheuvel提交的修改。主要是為了迎接ARM64世界的KASLR(which allows the kernel image to be located anywhere in the vmalloc area)的到來(lái)。
?
4.3. 如何建立shadow memory的映射關(guān)系?
當(dāng)打開(kāi)KASAN的時(shí)候,KASAN區(qū)域位于kernel space首地址處,從0xffff_0000_0000_0000地址開(kāi)始,大小是32TB。shadow memory和kernel address轉(zhuǎn)換關(guān)系是:shadow_addr = (kaddr >> 3)? + KASAN_SHADOW_OFFSE。為了將[0xffff_0000_0000_0000, 0xffff_ffff_ffff_ffff]和[0xffff_0000_0000_0000, 0xffff_1fff_ffff_ffff]對(duì)應(yīng)起來(lái),因此計(jì)算KASAN_SHADOW_OFFSE的值為0xdfff_2000_0000_0000。我們將KASAN區(qū)域放大,如下圖所示。
?
KASAN區(qū)域僅僅是分配的虛擬地址,在訪問(wèn)的時(shí)候必須建立和物理地址的映射才可以訪問(wèn)。上圖就是KASAN建立的映射布局。左邊是系統(tǒng)啟動(dòng)初期建立的映射。在kasan_early_init()函數(shù)中,將所有的KASAN區(qū)域映射到kasan_zero_page物理頁(yè)面。因此系統(tǒng)啟動(dòng)初期,KASAN并不能工作。右側(cè)是在kasan_init()函數(shù)中建立的映射關(guān)系,kasan_init()函數(shù)執(zhí)行結(jié)束就預(yù)示著KASAN的正常工作。我們將不需要address sanitizer功能的區(qū)域同樣還是映射到kasan_zero_page物理頁(yè)面,并且是readonly。我們主要是檢測(cè)kernel和物理內(nèi)存是否存在UAF或者OOB問(wèn)題。所以建立KERNEL和linear mapping(僅僅是所有的物理地址建立的映射區(qū)域)區(qū)域?qū)?yīng)的shadow memory建立真實(shí)的映射關(guān)系。MOUDLE區(qū)域?qū)?yīng)的shadow memory的映射關(guān)系也是需要?jiǎng)?chuàng)建的,但是映射關(guān)系建立是動(dòng)態(tài)的,他在module加載的時(shí)候才會(huì)去創(chuàng)建映射關(guān)系。
4.4. 伙伴系統(tǒng)分配的內(nèi)存的shadow memory值如何填充?
既然shadow memory已經(jīng)建立映射,接下來(lái)的事情就是探究各種內(nèi)存分配器向shadow memory填充什么數(shù)據(jù)了。首先看一下伙伴系統(tǒng)allocate page(s)函數(shù)填充shadow memory情況。
?
假設(shè)我們從buddy system分配4 pages。系統(tǒng)首先從order=2的鏈表中摘下一塊內(nèi)存,然后根據(jù)shadow memory address和memory address之間的對(duì)應(yīng)的關(guān)系找對(duì)應(yīng)的shadow memory。這里shadow memory的大小將會(huì)是2KB,系統(tǒng)會(huì)全部填充0代表內(nèi)存可以訪問(wèn)。我們對(duì)分配的內(nèi)存的任意地址內(nèi)存進(jìn)行訪問(wèn)的時(shí)候,首先都會(huì)找到對(duì)應(yīng)的shadow memory,然后根據(jù)shadow memory value判斷訪問(wèn)內(nèi)存操作是否valid。
如果釋放pages,情況又是如何呢?
?
同樣的,當(dāng)釋放pages的時(shí)候,會(huì)填充shadow memory的值為0xFF。如果釋放之后,依然訪問(wèn)內(nèi)存的話,此時(shí)KASAN根據(jù)shadow memory的值是0xFF就可以斷,這是一個(gè)use-after-free問(wèn)題。
4.5. SLUB分配對(duì)象的內(nèi)存的shadow memory值如何填充?
當(dāng)我們打開(kāi)KASAN的時(shí)候,SLUB Allocator管理的object layout將會(huì)放生一定的變化。如下圖所示。
?
在打開(kāi)SLUB_DEBUG的時(shí)候,object就增加很多內(nèi)存,KASAN打開(kāi)之后,在此基礎(chǔ)上又加了一截。為什么這里必須打開(kāi)SLUB_DEBUG呢?是因?yàn)橛卸螘r(shí)間KASAN是依賴SLUBU_DEBUG的,什么意思呢?就是在Kconfig中使用了depends on,明白了吧。不過(guò)最新的代碼已經(jīng)不需要依賴了,可以看下提交。
當(dāng)我們第一次創(chuàng)建slab緩存池的時(shí)候,系統(tǒng)會(huì)調(diào)用kasan_poison_slab()函數(shù)初始化shadow memory為下圖的模樣。整個(gè)slab對(duì)應(yīng)的shadow memory都填充0xFC。
?
上述步驟雖然填充了0xFC,但是接下來(lái)初始化object的時(shí)候,會(huì)改變一些shadow memory的值。我們先看一下kmalloc(20)的情況。我們知道kmalloc()就是基于SLUB Allocator實(shí)現(xiàn)的,所以會(huì)從kmalloc-32的kmem_cache中分配一個(gè)32 bytes object。
?
首先調(diào)用kmalloc(20)函數(shù)會(huì)匹配到kmalloc-32的kmem_cache,因此實(shí)際分配的object大小是32 bytes。KASAN同樣會(huì)標(biāo)記剩下的12 bytes的shadow memory為不可訪問(wèn)狀態(tài)。根據(jù)object的地址,計(jì)算shadow memory的地址,并開(kāi)始填充數(shù)值。由于kmalloc()返回的object的size是32 bytes,由于kmalloc(20)只申請(qǐng)了20 bytes,剩下的12 bytes不能使用。KASAN必須標(biāo)記shadow memory這種情況。object對(duì)應(yīng)的4 bytes shadow memory分別填充00 00 04 FC。00代表8個(gè)連續(xù)的字節(jié)可以訪問(wèn)。04代表前4個(gè)字節(jié)可以訪問(wèn)。作為越界訪問(wèn)的檢測(cè)的方法。總共加在一起是正好是20 bytes可訪問(wèn)。0xFC是Redzone標(biāo)記。如果訪問(wèn)了Redzone區(qū)域KASAN就會(huì)檢測(cè)out-of-bounds的發(fā)生。
當(dāng)申請(qǐng)使用之后,現(xiàn)在調(diào)用kfree()釋放之后的shadow memory情況是怎樣的呢?看下圖。
?
根據(jù)object首地址找到對(duì)應(yīng)的shadow memory,32 bytes object對(duì)應(yīng)4 bytes的shadow memory,現(xiàn)在填充0xFB標(biāo)記內(nèi)存是釋放的狀態(tài)。此時(shí)如果繼續(xù)訪問(wèn)object,那么根據(jù)shadow memory的狀態(tài)值既可以確定是use-after-free問(wèn)題。
4.6. 全局變量的shadow memory值如何填充?
前面的分析都是基于內(nèi)存分配器的,Redzone都會(huì)隨著內(nèi)存分配器一起分配。那么global variables如何檢測(cè)呢?global variable的Redzone在哪里呢?這就需要編譯器下手了。編譯器會(huì)幫我們填充Redzone區(qū)域。例如我們定義一個(gè)全局變量a,編譯器會(huì)幫我們填充成下面的樣子。
char a[4];
轉(zhuǎn)換
struct {
char original[4];
char redzone[60];
} a; //32 bytes aligned
如果這里你問(wèn)我為什么填充60 bytes。其實(shí)我也不知道。這個(gè)轉(zhuǎn)換例子也是從KASAN作者的PPT中拿過(guò)來(lái)的。估計(jì)要涉及編譯器相關(guān)的知識(shí),我無(wú)能為力了,但是下面做實(shí)驗(yàn)來(lái)猜吧。當(dāng)然了,PPT的內(nèi)容也需要驗(yàn)證才具有說(shuō)服力。盡信書(shū)則不如無(wú)書(shū)。我特地寫(xiě)三個(gè)全局變量來(lái)驗(yàn)證。發(fā)現(xiàn)System.map分配地址之間的差值正好是0x40。因此這里的確是填充60 bytes。 另外從我的測(cè)試發(fā)現(xiàn),如果上述的數(shù)組a的大小是33的時(shí)候,填充的redzone就是63 bytes。所以我推測(cè),填充的原理是這樣的。全局變量實(shí)際占用內(nèi)存總數(shù)S(以byte為單位)按照每塊32 bytes平均分成N塊。假設(shè)最后一塊內(nèi)存距離目標(biāo)32 bytes還差y bytes(if S%32 == 0,y = 0),那么redzone填充的大小就是(y + 32) bytes。畫(huà)圖示意如下(S%32 != 0)。因此總結(jié)的規(guī)律是:redzone = 63 – (S - 1) % 32。 ?
全局變量redzone區(qū)域?qū)?yīng)的shadow memory是在什么填充的呢?又是如何調(diào)用的呢?這部分是由編譯器幫我們完成的。編譯器會(huì)為每一個(gè)全局變量創(chuàng)建一個(gè)函數(shù),函數(shù)名稱是:_GLOBAL__sub_I_65535_1_##global_variable_name。這個(gè)函數(shù)中通過(guò)調(diào)用__asan_register_globals()函數(shù)完成shadow memory標(biāo)記。并且將自動(dòng)生成的這個(gè)函數(shù)的首地址放在.init_array段。在kernel啟動(dòng)階段,通過(guò)以下代調(diào)用關(guān)系最終調(diào)用所有全局變量的構(gòu)造函數(shù)。kernel_init_freeable()->do_basic_setup() ->do_ctors()。do_ctors()代碼實(shí)現(xiàn)如下:
static void __init do_ctors(void)
{
ctor_fn_t *fn = (ctor_fn_t *) __ctors_start;
for (; fn < (ctor_fn_t *) __ctors_end; fn++)
(*fn)();
}
這里的代碼意思對(duì)于輕車熟路的你再熟悉不過(guò)了吧。因?yàn)閮?nèi)核中這么搞的太多了。便利__ctors_start和__ctors_end之間的所有數(shù)據(jù),作為函數(shù)地址進(jìn)行調(diào)用,即完成了所有的global variables的shadow memory初始化。我們可以從鏈接腳本中知道__ctors_start和__ctors_end的意思。 #define KERNEL_CTORS()? . = ALIGN(8);????????????? \ ??????????? VMLINUX_SYMBOL(__ctors_start) = .; \ ??????????? KEEP(*(.ctors))??????????? \ ??????????? KEEP(*(SORT(.init_array.*)))?????? \ ??????????? KEEP(*(.init_array))?????????? \ ??????????? VMLINUX_SYMBOL(__ctors_end) = .; 上面說(shuō)了這么多,不知道你是否產(chǎn)生了疑心?怎么都是猜啊!猜的能準(zhǔn)確嗎?是的,我也這么覺(jué)得。是騾子是馬,拉出來(lái)溜溜唄!現(xiàn)在用事實(shí)說(shuō)話。首先我創(chuàng)建一個(gè)c文件drivers/input/smc.c。在smc.c文件中創(chuàng)建3個(gè)全局變量如下: ?
然后就隨便使用吧!編譯kernel,我們先看看System.map文件中,3個(gè)全局變量分配的地址。 ffff200009f540e0 B smc_num1 ffff200009f54120 B smc_num2 ffff200009f54160 B smc_num3 還記得上面說(shuō)會(huì)有一個(gè)形如_GLOBAL__sub_I_65535_1_##global_variable_name的函數(shù)嗎?在System.map文件文件中,我看到了_GLOBAL__sub_I_65535_1_smc_num1符號(hào)。但是沒(méi)有smc_num2和smc_num3的構(gòu)造函數(shù)。你是不是很奇怪,不是每一個(gè)全局變量都會(huì)創(chuàng)建一個(gè)類似的構(gòu)造函數(shù)嗎?馬上為你揭曉。我們先執(zhí)行aarch64-linux-gnu-objdump –s –x –d vmlinux > vmlinux.txt命令得到反編譯文件。現(xiàn)在好多重要的信息在vmlinux.txt。現(xiàn)在主要就是查看vmlinux.txt文件。先看一下_GLOBAL__sub_I_65535_1_smc_num1函數(shù)的實(shí)現(xiàn)。 ffff200009381df0 <_GLOBAL__sub_I_65535_1_smc_num1>: ffff200009381df0:?? a9bf7bfd??? stp x29, x30, [sp,#-16]! ffff200009381df4:?? b0001800??? adrp??? x0, ffff200009682000 ffff200009381df8:?? 91308000??? add x0, x0, #0xc20 ffff200009381dfc:?? d2800061??? mov x1, #0x3??????????????????? // #3 ffff200009381e00:?? 910003fd??? mov x29, sp ffff200009381e04:?? 9100c000??? add x0, x0, #0x30 ffff200009381e08:?? 97c09fb8??? bl? ffff2000083a9ce8 <__asan_register_globals> ffff200009381e0c:?? a8c17bfd??? ldp x29, x30, [sp],#16 ffff200009381e10:?? d65f03c0??? ret 匯編和C語(yǔ)言傳遞參數(shù)在ARM64平臺(tái)使用的是x0~x7。通過(guò)上面的匯編計(jì)算一下,x0=0xffff200009682c50,x1=3。然后調(diào)用__asan_register_globals()函數(shù),x0和x1就是傳遞的參數(shù)。我們看一下__asan_register_globals()函數(shù)實(shí)現(xiàn)。
void __asan_register_globals(struct kasan_global *globals, size_t size)
{
int i;
for (i = 0; i < size; i++)
register_global(&globals[i]);
}
size是3就是要初始化全局變量的個(gè)數(shù),所以這里只需要一個(gè)構(gòu)造函數(shù)即可。一次性將3個(gè)全局變量全部搞定。這里再說(shuō)一點(diǎn)猜測(cè)吧!我猜測(cè)是以文件為單位編譯器創(chuàng)建一個(gè)構(gòu)造函數(shù)即可,將本文件全局變量一次性全部打包初始化。第一個(gè)參數(shù)globals是0xffff200009682c50,繼續(xù)從vmlinux.txt中查看該地址處的數(shù)據(jù)。struct kasan_global是編譯器幫我們自動(dòng)創(chuàng)建的結(jié)構(gòu)體,每一個(gè)全局變量對(duì)應(yīng)一個(gè)struct kasan_global結(jié)構(gòu)體。struct kasan_global結(jié)構(gòu)體存放的位置是.data段,因此我們可以從.data段查找當(dāng)前地址對(duì)應(yīng)的數(shù)據(jù)。數(shù)據(jù)如下: ffff200009682c50 6041f509 0020ffff 07000000 00000000 ffff200009682c60 40000000 00000000 d0d62b09 0020ffff ffff200009682c70 b8d62b09 0020ffff 00000000 00000000 ffff200009682c80 202c6809 0020ffff 2041f509 0020ffff ffff200009682c90 1f000000 00000000 40000000 00000000 ffff200009682ca0 e0d62b09 0020ffff b8d62b09 0020ffff ffff200009682cb0 00000000 00000000 302c6809 0020ffff ffff200009682cc0 e040f509 0020ffff 04000000 00000000 ffff200009682cd0 40000000 00000000 f0d62b09 0020ffff ffff200009682ce0 b8d62b09 0020ffff 00000000 00000000 首先f(wàn)fff200009682c50對(duì)應(yīng)的第一個(gè)數(shù)據(jù)6041f509 0020ffff,這是個(gè)啥?其實(shí)是一個(gè)地址數(shù)據(jù),你是不是又疑問(wèn)了,ARM64的kernel space地址不是ffff開(kāi)頭嗎?這個(gè)怎么60開(kāi)頭?其實(shí)這個(gè)地址數(shù)據(jù)是反過(guò)來(lái)的,你應(yīng)該從右向左看。這個(gè)地址其實(shí)是ffff200009f54160。這不正是smc_num3的地址嘛!解析這段數(shù)據(jù)之前需要了解一下struct kasan_global結(jié)構(gòu)體。
/* The layout of struct dictated by compiler */
struct kasan_global {
const void *beg; /* Address of the beginning of the global variable. */
size_t size; /* Size of the global variable. */
size_t size_with_redzone; /* Size of the variable + size of the red zone. 32 bytes aligned */
const void *name;
const void *module_name; /* Name of the module where the global variable is declared. */
unsigned long has_dynamic_init; /* This needed for C++ */
#if KASAN_ABI_VERSION >= 4
struct kasan_source_location *location;
#endif
};
第一個(gè)成員beg就是全局變量的首地址。跟上面的分析一致。第二個(gè)成員size從上面數(shù)據(jù)看出是7,正好對(duì)應(yīng)我們定義的smc_num3[7],正好7 bytes。size_with_redzone的值是0x40,正好是64。根據(jù)上面猜測(cè)redzone=63-(7-1)%32=57。加上size正好是64,說(shuō)明之前猜測(cè)的redzone計(jì)算方法沒(méi)錯(cuò)。name成員對(duì)應(yīng)的地址是ffff2000092bd6d0。看下ffff2000092bd6d0存儲(chǔ)的是什么。 ffff2000092bd6d0 736d635f 6e756d33 00000000 00000000? smc_num3........ 所以name就是全局變量的名稱轉(zhuǎn)換成字符串。同樣的方式得到module_name的地址是ffff2000092bd6b8。繼續(xù)看看這段地址存儲(chǔ)的數(shù)據(jù)。 ffff2000092bd6b0 65000000 00000000 64726976 6572732f? e.......drivers/ ffff2000092bd6c0 696e7075 742f736d 632e6300 00000000? input/smc.c..... 一目了然,module_name是文件的路徑。has_dynamic_init的值就是0,這是C++需要的。我用的GCC版本是5.0左右,所以這里的KASAN_ABI_VERSION=4。這里location成員的地址是ffff200009682c20,繼續(xù)追蹤該地址的數(shù)據(jù)。 ffff200009682c20 b8d62b09 0020ffff 0e000000 0f000000 解析這段數(shù)據(jù)之前要先了解struct kasan_source_location結(jié)構(gòu)體。
/* The layout of struct dictated by compiler */
struct kasan_source_location {
const char *filename;
int line_no;
int column_no;
};
第一個(gè)成員filename地址是ffff2000092bd6b8和module_name一樣的數(shù)據(jù)。剩下兩個(gè)數(shù)據(jù)分別是14和15,分別代表全局變量定義地方的行號(hào)和列號(hào)。現(xiàn)在回到上面我定義變量的截圖,仔細(xì)數(shù)數(shù)列號(hào)是不是15,行號(hào)截圖中也有哦!特地截出來(lái)給你看的。剩下的struct kasan_global數(shù)據(jù)就是smc_num1和smc_num2的數(shù)據(jù)。分析就不說(shuō)了。前面說(shuō)_GLOBAL__sub_I_65535_1_smc_num1函數(shù)會(huì)被自動(dòng)調(diào)用,該地址數(shù)據(jù)填充在__ctors_start和__ctors_end之間。現(xiàn)在也證明一下觀點(diǎn)。先從System.map得到符號(hào)的地址數(shù)據(jù)。 ffff2000093ac5d8 T __ctors_start ffff2000093ae860 T __ctors_end 然后搜索一下_GLOBAL__sub_I_65535_1_smc_num1的地址ffff200009381df0被存儲(chǔ)在什么位置,記得搜索的關(guān)鍵字是f01d3809 0020ffff。 ffff2000093ae0c0 f01d3809 0020ffff 181e3809 0020ffff 可以看出ffff2000093ae0c0地址處存儲(chǔ)著_GLOBAL__sub_I_65535_1_smc_num1函數(shù)地址。這個(gè)地址不是正好位于__ctors_start和__ctors_end之間嘛! 現(xiàn)在就剩下__asan_register_globals()函數(shù)到底是是怎么初始化shadow memory的呢?以char a[4]為例,如下圖所示。 ?
a[4]只有4 bytes可以訪問(wèn),所以對(duì)應(yīng)的shadow memory的第一個(gè)byte值是4,后面的redzone就填充0xFA作為越界檢測(cè)。a[4]只有4 bytes可以訪問(wèn),所以對(duì)應(yīng)的shadow memory的第一個(gè)byte值是4,后面的redzone就填充0xFA作為越界檢測(cè)。因?yàn)檫@里是全局變量,因此分配的內(nèi)存區(qū)域位于kernel區(qū)域。
4.7. 棧分配變量的readzone是如何分配的?
從棧中分配的變量同樣和全局變量一樣需要填充一些內(nèi)存作為redzone區(qū)域。下面繼續(xù)舉個(gè)例子說(shuō)明編譯器怎么填充。首先來(lái)一段正常的代碼,沒(méi)有編譯器的插手。
void foo()
{
char a[328];
}
再來(lái)看看編譯器插了哪些東西進(jìn)去。
void foo() { ????char rz1[32]; ??? char a[328]; ??? char rz2[56]; ????int *shadow = (&rz1 >> 3)+ KASAN_SHADOW_OFFSE; ??? shadow[0] = 0xffffffff; ??? shadow[11] = 0xffffff00; ??? shadow[12] = 0xffffffff; ?------------------------使用完畢----------------------------------------? ??? shadow[0] = shadow[11] = shadow[12] = 0; }
紅色部分是編譯器填充內(nèi)存,rz2是56,可以根據(jù)上一節(jié)全局變量的公式套用計(jì)算得到。但是這里在變量前面竟然還有32 bytes的rz1。這個(gè)是和全局變量的不同,我猜測(cè)這里是為了檢測(cè)棧變量左邊界越界問(wèn)題。藍(lán)色部分代碼也是編譯器填充,初始化shadow memory。棧的填充就沒(méi)有探究那么深入了,有興趣的讀者可以自己探究。
和一個(gè)0xa5(SLUB DEBUG文章介紹的內(nèi)容)。所以我推測(cè)在3)步驟發(fā)生前沒(méi)有任何的對(duì)該內(nèi)存的寫(xiě)操作。?
補(bǔ)充
我看了linux-4.18的代碼,KASAN的log輸出已經(jīng)發(fā)生了部分變化。例如:上面舉例的SLUB的object的內(nèi)容就不會(huì)打印了。我們用一下的程序展示這些變化(實(shí)際上就是上面舉例用的程序)。
static noinline void __init kmalloc_oob_right(void)
{
char *ptr;
size_t size = 123;
ptr = kmalloc(size, GFP_KERNEL);
if (!ptr) {
pr_err("Allocation failed\n");
return;
}
ptr[size] = 'x';
kfree(ptr);
}
針對(duì)以上代碼,KASAN檢測(cè)到bug后的輸出log如下:
==================================================================
BUG: KASAN: slab-out-of-bounds in kmalloc_oob_right+0x6c/0x8c
Write of size 1 at addr ffffffc0cb114d7b by task swapper/0/1
CPU: 4 PID: 1 Comm: swapper/0 Tainted: G S W 4.9.82-perf+ #310
Hardware name: Qualcomm Technologies, Inc. SDM632 PMI632
Call trace:
[
[
[
[
[
[
[
[
[
[
[
[
[
Allocated by task 1:
kasan_kmalloc+0xd8/0x188
kmem_cache_alloc_trace+0x130/0x248
kmalloc_oob_right+0x4c/0x8c
kmalloc_tests_init+0xc/0x68
do_one_initcall+0xa4/0x1f0
kernel_init_freeable+0x244/0x300
kernel_init+0x10/0x110
ret_from_fork+0x10/0x30
Freed by task 1:
kasan_slab_free+0x88/0x178
kfree+0x84/0x298
kobject_uevent_env+0x144/0x620
kobject_uevent+0x10/0x18
device_add+0x5f8/0x860
amba_device_try_add+0x22c/0x2f8
amba_device_add+0x20/0x128
of_platform_bus_create+0x390/0x478
of_platform_bus_create+0x21c/0x478
of_platform_populate+0x4c/0xb8
of_platform_default_populate_init+0x78/0x8c
do_one_initcall+0xa4/0x1f0
kernel_init_freeable+0x244/0x300
kernel_init+0x10/0x110
ret_from_fork+0x10/0x30
The buggy address belongs to the object at ffffffc0cb114d00
which belongs to the cache kmalloc-128 of size 128
The buggy address is located 123 bytes inside of
128-byte region [ffffffc0cb114d00, ffffffc0cb114d80)
The buggy address belongs to the page:
page:ffffffbf032c4500 count:1 mapcount:0 mapping: (null) index:0xffffffc0cb115200 compound_mapcount: 0
flags: 0x4080(slab|head)
page dumped because: kasan: bad access detected
Memory state around the buggy address:
ffffffc0cb114c00: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
ffffffc0cb114c80: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
>ffffffc0cb114d00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03
^
ffffffc0cb114d80: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
ffffffc0cb114e00: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
==================================================================
我們從上面的log可以分析如下數(shù)據(jù):
line2:發(fā)生越界訪問(wèn)位置。
line3:越界寫(xiě)1個(gè)字節(jié),寫(xiě)的地址是0xffffffc0cb114d7b。當(dāng)前進(jìn)程是comm是swapper/0,pid是1。
line7:Call trace,方便定位出問(wèn)題的函數(shù)調(diào)用關(guān)系。
line22:該object分配的調(diào)用棧,并指出分配內(nèi)存的進(jìn)程pid是1。
line32:釋放該object的調(diào)用棧(上次釋放),并指出釋放內(nèi)存的進(jìn)程pid是1。
line49:指出slub相關(guān)的信息,從“kmalloc-28”的kmem_cache分配的object。object起始地址是0xffffffc0cb114d00。
line51:訪問(wèn)出問(wèn)題的地址位于object起始地址偏移123 bytes的位置。object的地址范圍是[0xffffffc0cb114d00, 0xffffffc0cb114d80)。object實(shí)際大小是128 bytes。
line61:出問(wèn)題地址對(duì)應(yīng)的shadow memory的值,可以確定申請(qǐng)內(nèi)存的實(shí)際大小是123 bytes。
參考文獻(xiàn): 1.How to use KASAN to debug memory corruption in OpenStack environment.pdf
2.KernelAddressSanitizer (KASan) a fast memory error detector for the Linux kernel.pdf
評(píng)論