KASAN實現原理【轉】

来源:https://www.cnblogs.com/linhaostudy/archive/2018/12/31/10203477.html
-Advertisement-
Play Games

1. 前言 KASAN是一個動態檢測記憶體錯誤的工具。KASAN可以檢測全局變數、棧、堆分配的記憶體發生越界訪問等問題。功能比SLUB DEBUG齊全並且支持實時檢測。越界訪問的嚴重性和危害性通過我之前的文章(SLUB DEBUG技術)應該有所瞭解。正是由於SLUB DEBUG缺陷,因此我們需要一種更加 ...


1. 前言

KASAN是一個動態檢測記憶體錯誤的工具。KASAN可以檢測全局變數、棧、堆分配的記憶體發生越界訪問等問題。功能比SLUB DEBUG齊全並且支持實時檢測。越界訪問的嚴重性和危害性通過我之前的文章(SLUB DEBUG技術)應該有所瞭解。正是由於SLUB DEBUG缺陷,因此我們需要一種更加強大的檢測工具。難道你不想嗎?KASAN就是其中一種。KASAN的使用真的很簡單。但是我是一個追求刨根問底的人。僅僅止步於使用的層面,我是不願意的,只有更清楚的瞭解實現原理才能更加熟練的使用工具。不止是KASAN,其他方面我也是這麼認為。但是,說實話,寫這篇文章是有點底氣不足的。因為從我查閱的資料來說,國內沒有一篇文章說KASAN的工作原理,國外也是沒有什麼文章關註KASAN的原理。大家好像都在說How to use。由於本人水平有限,就根據現有的資料以及自己閱讀代碼揣摩其中的意思。本文章作為拋準引玉,如果有不合理的地方還請指正。

註:文章代碼分析基於linux-4.15.0-rc3。

圖片顯示有點走形,請點開查看。

2. 簡介

KernelAddressSANitizer(KASAN)是一個動態檢測記憶體錯誤的工具。它為找到use-after-free和out-of-bounds問題提供了一個快速和全面的解決方案。KASAN使用編譯時檢測每個記憶體訪問,因此您需要GCC 4.9.2或更高版本。檢測堆棧或全局變數的越界訪問需要GCC 5.0或更高版本。目前KASAN僅支持x86_64和arm64架構(linux 4.4版本合入)。你使用ARM64架構,那麼就需要保證linux版本在4.4以上。當然了,如果你使用的linux也有可能打過KASAN的補丁。例如,使用高通平臺做手機的廠商使用linux 3.18同樣支持KASAN。

3. 如何使用

使用KASAN工具是比較簡單的,只需要添加kernel以下配置項。

CONFIG_SLUB_DEBUG=y
CONFIG_KASAN=y

為什麼這裡必須打開SLUB_DEBUG呢?是因為有段時間KASAN是依賴SLUBU_DEBUG的,什麼意思呢?就是在Kconfig中使用了depends on,明白了吧。不過最新的代碼已經不需要依賴了,可以看下提交。但是我建議你打開該選項,因為log可以輸出更多有用的信息。重新編譯kernel即可,編譯之後你會發現boot.img(Android環境)大小大了一倍左右。所以說,影響效率不是沒有道理的。不過我們可以作為產品發佈前的最後檢查,也可以排查越界訪問等問題。我們可以查看內核日誌內容是否包含KASAN檢查出的bugs信息。

4. KASAN是如何實現檢測的?

KASAN的原理是利用額外的記憶體標記可用記憶體的狀態。這部分額外的記憶體被稱作shadow memory(影子區)。KASAN將1/8的記憶體用作shadow memory。使用特殊的magic num填充shadow memory,在每一次load/store(load/store檢查指令由編譯器插入)記憶體的時候檢測對應的shadow memory確定操作是否valid。連續8 bytes記憶體(8 bytes align)使用1 byte shadow memory標記。如果8 bytes記憶體都可以訪問,則shadow memory的值為0;如果連續N(1 =< N <= 7) bytes可以訪問,則shadow memory的值為N;如果8 bytes記憶體訪問都是invalid,則shadow memory的值為負數。

image

在代碼運行時,每一次memory access都會檢測對應的shawdow memory的值是否valid。這就需要編譯器為我們做些工作。編譯的時候,在每一次memory access前編譯器會幫我們插入__asan_load##size()或者__asan_store##size()函數調用(size是訪問記憶體位元組的數量)。這也是要求更新版本gcc的原因,只有更新的版本才支持自動插入。

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地址寫5。在KASAN打開的情況下,編譯器會幫我們自動插入bl __asan_store1指令,__asan_store1函數就是檢測一個地址對應的shadow memory的值是否允許寫1 byte。藍色彙編指令就是真正的記憶體訪問。因此KASAN可以在out-of-bounds的時候及時檢測。__asan_load##size()和__asan_store##size()的代碼在mm/kasan/kasan.c文件實現。

4.1. 如何根據shadow memory的值判斷記憶體訪問操作是否valid?

shadow memory檢測原理的實現主要就是__asan_load##size()和__asan_store##size()函數的實現。那麼KASAN是如何根據訪問的address以及對應的shadow memory的狀態值來判斷訪問是否合法呢?首先看一種最簡單的情況。訪問8 bytes記憶體。

long *addr = (long *)0xffff800012345678;
*addr = 0;

以上代碼是訪問8 bytes情況,檢測原理如下:

long *addr = (long *)0xffff800012345678;
char *shadow = (char *)(((unsigned long)addr >> 3) + KASAN_SHADOW_OFFSE);
if (*shadow)
    report_bug();
*addr = 0;

紅色區域類似是編譯器插入的指令。既然是訪問8 bytes,必須要保證對應的shadow mempry的值必須是0,否則肯定是有問題。那麼如果訪問的是1,2 or 4 bytes該如何檢查呢?也很簡單,我們只需要修改一下if判斷條件即可。修改如下:

if (*shadow && *shadow < ((unsigned long)addr & 7) + N); //N = 1,2,4

如果*shadow的值為0代表8 bytes均可以訪問,自然就不需要report bug。addr & 7是計算訪問地址相對於8位元組對齊地址的偏移。還是使用下圖來說明關係吧。假設記憶體是從地址8~15一共8 bytes。對應的shadow memory值為5,現在訪問11地址。那麼這裡的N只要大於2就是invalid。

image

4.2. shadow memory記憶體如何分配?

在ARM64中,假設VA_BITS配置成48。那麼kernel space空間大小是256TB,因此shadow memory的記憶體需要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被分成幾個部分分別是KASAN、MODULE、VMALLOC、FIXMAP、PCI_IO、VMEMMAP以及linear mapping。其中KASAN的大小是32TB,正好是kernel space大小的1/8。不知道你註意到沒有,KERNEL的位置相對以前是不是有所不一樣。你的印象中,KERNEL是不是位於linear mapping區域,這裡怎麼變成了VMALLOC區域?這裡是Ard Biesheuvel提交的修改。主要是為了迎接ARM64世界的KASLR(which allows the kernel image to be located anywhere in the vmalloc area)的到來。

image

4.3. 如何建立shadow memory的映射關係?

當打開KASAN的時候,KASAN區域位於kernel space首地址處,從0xffff_0000_0000_0000地址開始,大小是32TB。shadow memory和kernel address轉換關係是:shadow_addr = (kaddr >> 3) + KASAN_SHADOW_OFFSE。為了將[0xffff_0000_0000_0000, 0xffff_ffff_ffff_ffff]和[0xffff_0000_0000_0000, 0xffff_1fff_ffff_ffff]對應起來,因此計算KASAN_SHADOW_OFFSE的值為0xdfff_2000_0000_0000。我們將KASAN區域放大,如下圖所示。

image

KASAN區域僅僅是分配的虛擬地址,在訪問的時候必須建立和物理地址的映射才可以訪問。上圖就是KASAN建立的映射佈局。左邊是系統啟動初期建立的映射。在kasan_early_init()函數中,將所有的KASAN區域映射到kasan_zero_page物理頁面。因此系統啟動初期,KASAN並不能工作。右側是在kasan_init()函數中建立的映射關係,kasan_init()函數執行結束就預示著KASAN的正常工作。我們將不需要address sanitizer功能的區域同樣還是映射到kasan_zero_page物理頁面,並且是readonly。我們主要是檢測kernel和物理記憶體是否存在UAF或者OOB問題。所以建立KERNEL和linear mapping(僅僅是所有的物理地址建立的映射區域)區域對應的shadow memory建立真實的映射關係。MOUDLE區域對應的shadow memory的映射關係也是需要創建的,但是映射關係建立是動態的,他在module載入的時候才會去創建映射關係。

4.4. 伙伴系統分配的記憶體的shadow memory值如何填充?

既然shadow memory已經建立映射,接下來的事情就是探究各種記憶體分配器向shadow memory填充什麼數據了。首先看一下伙伴系統allocate page(s)函數填充shadow memory情況。

image

假設我們從buddy system分配4 pages。系統首先從order=2的鏈表中摘下一塊記憶體,然後根據shadow memory address和memory address之間的對應的關係找對應的shadow memory。這裡shadow memory的大小將會是2KB,系統會全部填充0代表記憶體可以訪問。我們對分配的記憶體的任意地址記憶體進行訪問的時候,首先都會找到對應的shadow memory,然後根據shadow memory value判斷訪問記憶體操作是否valid。

如果釋放pages,情況又是如何呢?
image

同樣的,當釋放pages的時候,會填充shadow memory的值為0xFF。如果釋放之後,依然訪問記憶體的話,此時KASAN根據shadow memory的值是0xFF就可以斷,這是一個use-after-free問題。

4.5. SLUB分配對象的記憶體的shadow memory值如何填充?

當我們打開KASAN的時候,SLUB Allocator管理的object layout將會放生一定的變化。如下圖所示。
image

在打開SLUB_DEBUG的時候,object就增加很多記憶體,KASAN打開之後,在此基礎上又加了一截。為什麼這裡必須打開SLUB_DEBUG呢?是因為有段時間KASAN是依賴SLUBU_DEBUG的,什麼意思呢?就是在Kconfig中使用了depends on,明白了吧。不過最新的代碼已經不需要依賴了,可以看下提交。

當我們第一次創建slab緩存池的時候,系統會調用kasan_poison_slab()函數初始化shadow memory為下圖的模樣。整個slab對應的shadow memory都填充0xFC。

image

上述步驟雖然填充了0xFC,但是接下來初始化object的時候,會改變一些shadow memory的值。我們先看一下kmalloc(20)的情況。我們知道kmalloc()就是基於SLUB Allocator實現的,所以會從kmalloc-32的kmem_cache中分配一個32 bytes object。

image

首先調用kmalloc(20)函數會匹配到kmalloc-32的kmem_cache,因此實際分配的object大小是32 bytes。KASAN同樣會標記剩下的12 bytes的shadow memory為不可訪問狀態。根據object的地址,計算shadow memory的地址,並開始填充數值。由於kmalloc()返回的object的size是32 bytes,由於kmalloc(20)只申請了20 bytes,剩下的12 bytes不能使用。KASAN必須標記shadow memory這種情況。object對應的4 bytes shadow memory分別填充00 00 04 FC。00代表8個連續的位元組可以訪問。04代表前4個位元組可以訪問。作為越界訪問的檢測的方法。總共加在一起是正好是20 bytes可訪問。0xFC是Redzone標記。如果訪問了Redzone區域KASAN就會檢測out-of-bounds的發生。

當申請使用之後,現在調用kfree()釋放之後的shadow memory情況是怎樣的呢?看下圖。

image

根據object首地址找到對應的shadow memory,32 bytes object對應4 bytes的shadow memory,現在填充0xFB標記記憶體是釋放的狀態。此時如果繼續訪問object,那麼根據shadow memory的狀態值既可以確定是use-after-free問題。

4.6. 全局變數的shadow memory值如何填充?

前面的分析都是基於記憶體分配器的,Redzone都會隨著記憶體分配器一起分配。那麼global variables如何檢測呢?global variable的Redzone在哪裡呢?這就需要編譯器下手了。編譯器會幫我們填充Redzone區域。例如我們定義一個全局變數a,編譯器會幫我們填充成下麵的樣子。

char a[4];
struct {
    char original[4];
    char redzone[60];
} a; //32 bytes aligned

如果這裡你問我為什麼填充60 bytes。其實我也不知道。這個轉換例子也是從KASAN作者的PPT中拿過來的。估計要涉及編譯器相關的知識,我無能為力了,但是下麵做實驗來猜吧。當然了,PPT的內容也需要驗證才具有說服力。盡信書則不如無書。我特地寫三個全局變數來驗證。發現System.map分配地址之間的差值正好是0x40。因此這裡的確是填充60 bytes。

另外從我的測試發現,如果上述的數組a的大小是33的時候,填充的redzone就是63 bytes。所以我推測,填充的原理是這樣的。全局變數實際占用記憶體總數S(以byte為單位)按照每塊32 bytes平均分成N塊。假設最後一塊記憶體距離目標32 bytes還差y bytes(if S%32 == 0,y = 0),那麼redzone填充的大小就是(y + 32) bytes。畫圖示意如下(S%32 != 0)。因此總結的規律是:redzone = 63 – (S - 1) % 32。

image

全局變數redzone區域對應的shadow memory是在什麼填充的呢?又是如何調用的呢?這部分是由編譯器幫我們完成的。編譯器會為每一個全局變數創建一個函數,函數名稱是:_GLOBAL__sub_I_65535_1_##global_variable_name。這個函數中通過調用__asan_register_globals()函數完成shadow memory標記。並且將自動生成的這個函數的首地址放在.init_array段。在kernel啟動階段,通過以下代調用關係最終調用所有全局變數的構造函數。kernel_init_freeable()->do_basic_setup() ->do_ctors()。do_ctors()代碼實現如下:

這裡的代碼意思對於輕車熟路的你再熟悉不過了吧。因為內核中這麼搞的太多了。便利__ctors_start和__ctors_end之間的所有數據,作為函數地址進行調用,即完成了所有的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) = .;

上面說了這麼多,不知道你是否產生了疑心?怎麼都是猜啊!猜的能準確嗎?是的,我也這麼覺得。是騾子是馬,拉出來溜溜唄!現在用事實說話。首先我創建一個c文件drivers/input/smc.c。在smc.c文件中創建3個全局變數如下:

image

然後就隨便使用吧!編譯kernel,我們先看看System.map文件中,3個全局變數分配的地址。

ffff200009f540e0 B smc_num1
ffff200009f54120 B smc_num2
ffff200009f54160 B smc_num3

還記得上面說會有一個形如_GLOBAL_sub_I_65535_1##global_variable_name的函數嗎?在System.map文件文件中,我看到了_GLOBAL__sub_I_65535_1_smc_num1符號。但是沒有smc_num2和smc_num3的構造函數。你是不是很奇怪,不是每一個全局變數都會創建一個類似的構造函數嗎?馬上為你揭曉。我們先執行aarch64-linux-gnu-objdump –s –x –d vmlinux > vmlinux.txt命令得到反編譯文件。現在好多重要的信息在vmlinux.txt。現在主要就是查看vmlinux.txt文件。先看一下_GLOBAL__sub_I_65535_1_smc_num1函數的實現。

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語言傳遞參數在ARM64平臺使用的是x0~x7。通過上面的彙編計算一下,x0=0xffff200009682c50,x1=3。然後調用__asan_register_globals()函數,x0和x1就是傳遞的參數。我們看一下__asan_register_globals()函數實現。

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就是要初始化全局變數的個數,所以這裡只需要一個構造函數即可。一次性將3個全局變數全部搞定。這裡再說一點猜測吧!我猜測是以文件為單位編譯器創建一個構造函數即可,將本文件全局變數一次性全部打包初始化。第一個參數globals是0xffff200009682c50,繼續從vmlinux.txt中查看該地址處的數據。struct kasan_global是編譯器幫我們自動創建的結構體,每一個全局變數對應一個struct kasan_global結構體。struct kasan_global結構體存放的位置是.data段,因此我們可以從.data段查找當前地址對應的數據。數據如下:

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

首先ffff200009682c50對應的第一個數據6041f509 0020ffff,這是個啥?其實是一個地址數據,你是不是又疑問了,ARM64的kernel space地址不是ffff開頭嗎?這個怎麼60開頭?其實這個地址數據是反過來的,你應該從右向左看。這個地址其實是ffff200009f54160。這不正是smc_num3的地址嘛!解析這段數據之前需要瞭解一下struct kasan_global結構體。

/* 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
};

第一個成員beg就是全局變數的首地址。跟上面的分析一致。第二個成員size從上面數據看出是7,正好對應我們定義的smc_num3[7],正好7 bytes。size_with_redzone的值是0x40,正好是64。根據上面猜測redzone=63-(7-1)%32=57。加上size正好是64,說明之前猜測的redzone計算方法沒錯。name成員對應的地址是ffff2000092bd6d0。看下ffff2000092bd6d0存儲的是什麼。

ffff2000092bd6d0 736d635f 6e756d33 00000000 00000000 smc_num3........
所以name就是全局變數的名稱轉換成字元串。同樣的方式得到module_name的地址是ffff2000092bd6b8。繼續看看這段地址存儲的數據。
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,繼續追蹤該地址的數據。

ffff200009682c20 b8d62b09 0020ffff 0e000000 0f000000

解析這段數據之前要先瞭解struct kasan_source_location結構體。

/* The layout of struct dictated by compiler */
struct kasan_source_location {
    const char *filename;
    int line_no;
    int column_no;
};

第一個成員filename地址是ffff2000092bd6b8和module_name一樣的數據。剩下兩個數據分別是14和15,分別代表全局變數定義地方的行號和列號。現在回到上面我定義變數的截圖,仔細數數列號是不是15,行號截圖中也有哦!特地截出來給你看的。剩下的struct kasan_global數據就是smc_num1和smc_num2的數據。分析就不說了。前面說_GLOBAL__sub_I_65535_1_smc_num1函數會被自動調用,該地址數據填充在__ctors_start和__ctors_end之間。現在也證明一下觀點。先從System.map得到符號的地址數據。

ffff2000093ac5d8 T __ctors_start
ffff2000093ae860 T __ctors_end

然後搜索一下_GLOBAL__sub_I_65535_1_smc_num1的地址ffff200009381df0被存儲在什麼位置,記得搜索的關鍵字是f01d3809 0020ffff。

ffff2000093ae0c0 f01d3809 0020ffff 181e3809 0020ffff

可以看出ffff2000093ae0c0地址處存儲著_GLOBAL__sub_I_65535_1_smc_num1函數地址。這個地址不是正好位於__ctors_start和__ctors_end之間嘛!

現在就剩下__asan_register_globals()函數到底是是怎麼初始化shadow memory的呢?以char a[4]為例,如下圖所示。

image

a[4]只有4 bytes可以訪問,所以對應的shadow memory的第一個byte值是4,後面的redzone就填充0xFA作為越界檢測。a[4]只有4 bytes可以訪問,所以對應的shadow memory的第一個byte值是4,後面的redzone就填充0xFA作為越界檢測。因為這裡是全局變數,因此分配的記憶體區域位於kernel區域。

4.7. 棧分配變數的readzone是如何分配的?

從棧中分配的變數同樣和全局變數一樣需要填充一些記憶體作為redzone區域。下麵繼續舉個例子說明編譯器怎麼填充。首先來一段正常的代碼,沒有編譯器的插手。

void foo()
{
    char a[328];
}

再來看看編譯器插了哪些東西進去。

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;
}

紅色部分是編譯器填充記憶體,rz2是56,可以根據上一節全局變數的公式套用計算得到。但是這裡在變數前面竟然還有32 bytes的rz1。這個是和全局變數的不同,我猜測這裡是為了檢測棧變數左邊界越界問題。藍色部分代碼也是編譯器填充,初始化shadow memory。棧的填充就沒有探究那麼深入了,有興趣的讀者可以自己探究。

5. Error log信息包含哪些信息?

從kernel的Documentation文檔找份典型的KASAN bug輸出的log信息如下。

==================================================================
BUG: AddressSanitizer: out of bounds access in kmalloc_oob_right+0x65/0x75 [test_kasan] at addr ffff8800693bc5d3
Write of size 1 by task modprobe/1689
=============================================================================
BUG kmalloc-128 (Not tainted): kasan error
-----------------------------------------------------------------------------

Disabling lock debugging due to kernel taint
INFO: Allocated in kmalloc_oob_right+0x3d/0x75 [test_kasan] age=0 cpu=0 pid=1689
 __slab_alloc+0x4b4/0x4f0
 kmem_cache_alloc_trace+0x10b/0x190
 kmalloc_oob_right+0x3d/0x75 [test_kasan]
 init_module+0x9/0x47 [test_kasan]
 do_one_initcall+0x99/0x200
 load_module+0x2cb3/0x3b20
 SyS_finit_module+0x76/0x80
 system_call_fastpath+0x12/0x17
INFO: Slab 0xffffea0001a4ef00 objects=17 used=7 fp=0xffff8800693bd728 flags=0x100000000004080
INFO: Object 0xffff8800693bc558 @offset=1368 fp=0xffff8800693bc720
Bytes b4 ffff8800693bc548: 00 00 00 00 00 00 00 00 5a 5a 5a 5a 5a 5a 5a 5a  ........ZZZZZZZZ
Object ffff8800693bc558: 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b  kkkkkkkkkkkkkkkk
Object ffff8800693bc568: 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b  kkkkkkkkkkkkkkkk
Object ffff8800693bc578: 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b  kkkkkkkkkkkkkkkk
Object ffff8800693bc588: 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b  kkkkkkkkkkkkkkkk
Object ffff8800693bc598: 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b  kkkkkkkkkkkkkkkk
Object ffff8800693bc5a8: 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b  kkkkkkkkkkkkkkkk
Object ffff8800693bc5b8: 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b  kkkkkkkkkkkkkkkk
Object ffff8800693bc5c8: 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b a5  kkkkkkkkkkkkkkk.
Redzone ffff8800693bc5d8: cc cc cc cc cc cc cc cc                          ........
Padding ffff8800693bc718: 5a 5a 5a 5a 5a 5a 5a 5a                          ZZZZZZZZ
CPU: 0 PID: 1689 Comm: modprobe Tainted: G    B          3.18.0-rc1-mm1+ #98
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS rel-1.7.5-0-ge51488c-20140602_164612-nilsson.home.kraxel.org 04/01/2014
 ffff8800693bc000 0000000000000000 ffff8800693bc558 ffff88006923bb78
 ffffffff81cc68ae 00000000000000f3 ffff88006d407600 ffff88006923bba8
 ffffffff811fd848 ffff88006d407600 ffffea0001a4ef00 ffff8800693bc558
Call Trace:
 [<ffffffff81cc68ae>] dump_stack+0x46/0x58
 [<ffffffff811fd848>] print_trailer+0xf8/0x160
 [<ffffffffa00026a7>] ? kmem_cache_oob+0xc3/0xc3 [test_kasan]
 [<ffffffff811ff0f5>] object_err+0x35/0x40
 [<ffffffffa0002065>] ? kmalloc_oob_right+0x65/0x75 [test_kasan]
 [<ffffffff8120b9fa>] kasan_report_error+0x38a/0x3f0
 [<ffffffff8120a79f>] ? kasan_poison_shadow+0x2f/0x40
 [<ffffffff8120b344>] ? kasan_unpoison_shadow+0x14/0x40
 [<ffffffff8120a79f>] ? kasan_poison_shadow+0x2f/0x40
 [<ffffffffa00026a7>] ? kmem_cache_oob+0xc3/0xc3 [test_kasan]
 [<ffffffff8120a995>] __asan_store1+0x75/0xb0
 [<ffffffffa0002601>] ? kmem_cache_oob+0x1d/0xc3 [test_kasan]
 [<ffffffffa0002065>] ? kmalloc_oob_right+0x65/0x75 [test_kasan]
 [<ffffffffa0002065>] kmalloc_oob_right+0x65/0x75 [test_kasan]
 [<ffffffffa00026b0>] init_module+0x9/0x47 [test_kasan]
 [<ffffffff810002d9>] do_one_initcall+0x99/0x200
 [<ffffffff811e4e5c>] ? __vunmap+0xec/0x160
 [<ffffffff81114f63>] load_module+0x2cb3/0x3b20
 [<ffffffff8110fd70>] ? m_show+0x240/0x240
 [<ffffffff81115f06>] SyS_finit_module+0x76/0x80
 [<ffffffff81cd3129>] system_call_fastpath+0x12/0x17
Memory state around the buggy address:
 ffff8800693bc300: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
 ffff8800693bc380: fc fc 00 00 00 00 00 00 00 00 00 00 00 00 00 fc
 ffff8800693bc400: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
 ffff8800693bc480: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
 ffff8800693bc500: fc fc fc fc fc fc fc fc fc fc fc 00 00 00 00 00
>ffff8800693bc580: 00 00 00 00 00 00 00 00 00 00 03 fc fc fc fc fc
                                                                           ^
 ffff8800693bc600: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
 ffff8800693bc680: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
 ffff8800693bc700: fc fc fc fc fb fb fb fb fb fb fb fb fb fb fb fb
 ffff8800693bc780: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
 ffff8800693bc800: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
==================================================================

輸出的信息很豐富,包含了bug發生的類型、SLUB輸出的object記憶體信息、Call Trace以及shadow memory的狀態值。其中紅色信息都是比較重要的信息。我沒有寫demo歷程,而是找了一份log信息,不是我想偷懶,而是鍛煉自己。怎麼鍛煉呢?我想問的是,從這份log中你可以推測代碼應該是怎麼樣的?我可以得到一下信息:

  1. 程式是通過kmalloc介面申請記憶體的;
  2. 申請的記憶體大小是123 bytes,即p = kamlloc(123);
  3. 代碼中類似往p[123]中寫1 bytes導致越界訪問的bug;
  4. 在3)步驟發生前沒有任何的對該記憶體的寫操作;

如果你也能得到以上4點猜測,我覺的我寫的這幾篇文章你是真的看明白了。首先輸出信息是有SLUB的信息,所以應該是通過kmalloc()介面;在列印的shadow memory的值中,我們看到連續的15個0和一個3,所以申請的記憶體size就是15x8+3=123;由於是往ffff8800693bc5d3地址寫1個位元組,並且object首地址是ffff8800693bc558,所以推測是往p[123]寫1 byte出問題;由於log中將object中所有的128 bytes數據全部列印出來,一共是127個0x6b和一個0xa5(SLUB DEBUG文章介紹的內容)。所以我推測在3)步驟發生前沒有任何的對該記憶體的寫操作。

6. 補充

我看了linux-4.18的代碼,KASAN的log輸出已經發生了部分變化。例如:上面舉例的SLUB的object的內容就不會列印了。我們用一下的程式展示這些變化(實際上就是上面舉例用的程式)。

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);
}

針對以上代碼,KASAN檢測到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:
[<ffffff90cf88d9f8>] dump_backtrace+0x0/0x320
[<ffffff90cf88dd2c>] show_stack+0x14/0x20
[<ffffff90cfdd1148>] dump_stack+0xa8/0xd0
[<ffffff90cfabf298>] print_address_description+0x60/0x250
[<ffffff90cfabf6a0>] kasan_report.part.2+0x218/0x2f0
[<ffffff90cfabfac0>] kasan_report+0x20/0x28
[<ffffff90cfabdc64>] __asan_store1+0x4c/0x58
[<ffffff90d1a4f760>] kmalloc_oob_right+0x6c/0x8c
[<ffffff90d1a50448>] kmalloc_tests_init+0xc/0x68
[<ffffff90cf8845dc>] do_one_initcall+0xa4/0x1f0
[<ffffff90d1a011ac>] kernel_init_freeable+0x244/0x300
[<ffffff90d0d6da70>] kernel_init+0x10/0x110
[<ffffff90cf8842a0>] ret_from_fork+0x10/0x30
 
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可以分析如下數據:

  • line2:發生越界訪問位置。
  • line3:越界寫1個位元組,寫的地址是0xffffffc0cb114d7b。當前進程是comm是swapper/0,pid是1。
  • line7:Call trace,方便定位出問題的函數調用關係。
  • line22:該object分配的調用棧,並指出分配記憶體的進程pid是1。
  • line32:釋放該object的調用棧(上次釋放),並指出釋放記憶體的進程pid是1。
  • line49:指出slub相關的信息,從“kmalloc-28”的kmem_cache分配的object。object起始地址是0xffffffc0cb114d00。
  • line51:訪問出問題的地址位於object起始地址偏移123 bytes的位置。object的地址範圍是[0xffffffc0cb114d00, 0xffffffc0cb114d80)。object實際大小是128 bytes。
  • line61:出問題地址對應的shadow memory的值,可以確定申請記憶體的實際大小是123 bytes。

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 卡拉茲(Callatz)猜想已經在1001中給出了描述。在這個題目里,情況稍微有些複雜。 當我們驗證卡拉茲猜想的時候,為了避免重覆計算,可以記錄下遞推過程中遇到的每一個數。例如對 n=3 進行驗證的時候,我們需要計算 3、5、8、4、2、1,則當我們對 n=5、8、4、2 進行驗證的時候,就可以直接 ...
  • 2018-12-31 更新聲明:切片系列文章本是分三篇寫成,現已合併成一篇。合併後,修正了一些嚴重的錯誤(如自定義序列切片的部分),還對行文結構與章節銜接做了大量改動。原系列的單篇就不刪除了,畢竟也是有單獨成篇的作用。特此聲明,請閱讀改進版—— Python進階:全面解讀高級特性之切片!https: ...
  • 原題傳送門:https://www.luogu.org/problemnew/show/P1209 首先,這是一道貪心題。 我們先來分析它的貪心策略。 例如,樣例: 4 50 18 3 4 6 8 1415 16 17 2125 26 27 30 31 40 41 42 43 它們之間的差是: 1 ...
  • 溫度轉換問題 一、溫度轉換 目前有兩種表示溫度的方法一種是攝氏度另一種是華氏度,攝氏度的結冰點為0度,沸點為100度將溫度等分刻畫,華氏度的結冰點為32度,沸點為212度將溫度進行等刻度劃分。 現需要將按格式輸入的攝氏度轉換為華氏度,將輸入的華氏度轉換為攝氏度。 二、問題分析 根據IPO的分析方法可 ...
  • 任何使用 async/await 進行修飾的方法,都會被認為是一個非同步方法;實際上,這些非同步方法都是基於隊列的線程任務,從你開始使用 Task 去運行一段代碼的時候,實際上就相當於開啟了一個線程,預設情況下,這個線程數由線程池 ThreadPool 進行管理的。 ...
  • 載入超過100M的xml文件時(可能不是很常見),XmlDocument這種全部載入到記憶體里的模式就有點不友好了,耗時長、記憶體高。 這時用xmlreader就會有自行車換超跑的感覺,但其間遇到幾個坑,記錄一下。 先看源碼,包括dom和sax兩種模式的讀取和寫入 DOM模式: SAX(simple A ...
  • 概述 Microsoft.AspNetCore.NodeServices庫 實例 新建aspnet core站點 添加nuget包 建立node環境,此處示例用於掃描wifi環境 建立nodejs的程式文件 index.js 設置js文件為始終複製 註入配置 在控制器-Action處調用 返回情況 ...
  • 前言 2018年還有幾天就結束了,回顧一下今年的博客blog-posts, 簡單整理一下行業與軟體過程(Software Industry & process improvement) 關註軟體過程改進到效能改進,除了軟體開發,還是軟體測試。全球Scrum在應用中,全球軟體測試行業演化;不必多說,研... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...