--- tags: linux2019 --- # 2019q1 Homework2 (fibdrv) contributed by < `HexRabbit` > ## Environment 下方參考的 kernel source 皆出自 linux kernel v5.0.2 ## 自我檢查事項 ### 1. 檔案 `fibdrv.c` 裡頭的 `MODULE_LICENSE`, `MODULE_AUTHOR`, `MODULE_DESCRIPTION`, `MODULE_VERSION` 等巨集做了什麼事,可以讓核心知曉呢? `insmod` 這命令背後,對應 Linux 核心內部有什麼操作呢?請舉出相關 Linux 核心原始碼並解讀 macro 的展開相對繁瑣,我們可以先利用 `nm` 指令來觀察展開後的模樣 ``` 0000000000000030 r __UNIQUE_ID_author24 000000000000000c r __UNIQUE_ID_description25 000000000000005e r __UNIQUE_ID_license23 0000000000000000 r __UNIQUE_ID_version26 ``` :::info 這邊應該要用 `gcc -E` 直接觀察 preprocessor 展開後的輸出比較好,可是因為這是 kernel module,不太清楚要怎麼做 ::: 有關 kernel module 的 macro 被放在 `/include/linux/module.h` 以及 `/include/linux/moduleparam.h` 中,下面以 `MODULE_LICENSE` 為例列出相關的 macro - #### `MODULE_LICENSE` ```cpp #define MODULE_LICENSE(_license) MODULE_INFO(license, _license) #define MODULE_INFO(tag, info) __MODULE_INFO(tag, tag, info) #define __MODULE_INFO(tag, name, info) \ static const char __UNIQUE_ID(name)[] \ __used __attribute__((section(".modinfo"), unused, aligned(1))) \ = __stringify(tag) "=" info #define __UNIQUE_ID(prefix) __PASTE(__PASTE(__UNIQUE_ID_, prefix), __COUNTER__) #define __PASTE(a, b) a##b #define __stringify(x...) #x ``` 光是展開一個 macro 就跑出一堆相關的 code,我們從最下方開始一步步組裝 - **`__PASTE(a, b)`** `a##b` 是一個 gcc preprocessor feature: **Token pasting**,會將 token a 和 token b 黏合,例如: `__PASTE(Hello, world) == Helloworld`,要注意的是這裡的輸出並不是一個字串而是一個新的 token - **`__COUNTER__`** 這個就好玩了,這是一個 gcc predefined macro,可以參考 [Common-Predefined-Macros](https://gcc.gnu.org/onlinedocs/cpp/Common-Predefined-Macros.html),第一次展開時 `__COUNTER__` 會是 `0`,再來每一次展開該 macro 其值就會遞增,是個方便使用者搭配 `##` 製造變數的好工具 - **`__UNIQUE_ID_`** 沒什麼,就是個 token,用來作為 `##` 黏合之後形成的變數的前綴 - **`__used`** 這是 gcc 提供的一個 extension,根據 [gcc 文件](https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html) 可以知道,這是為了讓一些只有在 inline assembly 中被使用到的變數不會被 optimizer 優化而被刪除 > This attribute, attached to a function, **means that code must be emitted for the function even if it appears that the function is not referenced**. This is useful, for example, when the function is referenced only in inline assembly. :::warning 不知道為甚麼可以不寫在 `__attributes__` 裡面 ::: - **`__stringify(x...)`** 這裡的 `x...` 是利用 [Variadic-Macros](https://gcc.gnu.org/onlinedocs/cpp/Variadic-Macros.html) 的技巧,`#` 則是會把參數變成字串, 所以 `#x` 會將所有參數變成字串(包含分隔他們的那些逗號),建議可以用 `gcc -E` 試驗一遍比較清楚 - **`__attribute__ ((attribute-list))`** 這是個 gcc 擴展 [Function-Attributes](https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#Common-Function-Attributes),可以在編譯時為變數指定一些 attribute 進而能夠直接讓 compiler 針對設定的 attribute 進行優化,而不是在進行一系列分析後才做出優化,不過在使用時也必須要小心,因為 compiler 會根據設定的 attribute 做優化不會另作分析,所以如果沒有設定好可能出現危險的 bug (例如設定 pure 但讓 function 進行 non-pure 的指標操作) - **`section("section-name")`** 指定變數在編譯時該被放入哪個 section - **`unused`** 用於消除 compile 時產生出該變數未被使用的 warning 注意這與前面提到的 `used` 是完全不同的概念 - **`aligned`** 告知編譯器該變數在記憶體中的位置需要向多少 byte 對齊 最後以 `MODULE_LICENSE("GPL");` 為例把它們組起來,就會形成這樣一個宣告: ```clike static const char __UNIQUE_ID_license23[] \ __used __attribute__((section(".modinfo"), unused, aligned(1))) \ = license="GPL"; ``` ### 2. 當我們透過 `insmod` 去載入一個核心模組時,為何 `module_init` 所設定的函式得以執行呢?Linux 核心做了什麼事呢? 先了解 macro `module_init` 做了甚麼吧,以 `module_init(init_fn);` 為例,最終會被展開為: ```clike static inline initcall_t __maybe_unused __inittest(void) \ { return init_fn; } \ int init_module(void) __attribute__((alias("init_fn"))) ``` `initcall_t` 為 function pointer 型別,由 `typedef int (*initcall_t)(void);` 定義, 接著注意到 `__inittest` 是個 function,猜測在這裡用於在編譯時期檢查 `init_fn` 的型別是否跟 `initcall_t` 相同 (回傳值必須要是`initcall_t`) 所以這個 macro 基本上只是將給定的 `init_fn` 設定為 `init_module` 這個 function 的 alias。 再來看到編譯中額外產生的檔案 `fibdrv.mod.c`,透過查閱一些[文件](https://www.ibm.com/developerworks/cn/linux/l-cn-kernelmodules/index.html)可以得知這是用於將一些必要的 metadata 和 version 資訊一併在編譯時附加進去,在其中可以發現 init_module 被放入 `__this_module` 這個結構體變數中,看來會在載入時使用 ```cpp __visible struct module __this_module __attribute__((section(".gnu.linkonce.this_module"))) = { .name = KBUILD_MODNAME, .init = init_module, #ifdef CONFIG_MODULE_UNLOAD .exit = cleanup_module, #endif .arch = MODULE_ARCH_INIT, }; ``` 接著來看看我們到底編出了甚麼東西 ``` fibdrv.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV) ``` 看起來 kernel module 似乎就像是個 dynamically linked library,會被 kernel 在 runtime 載入。不知道該從哪裡開始看起,總之先 strace 一次看看到底用了些甚麼 syscall ```clike execve("/sbin/insmod", ["insmod", "fibdrv.ko"], [/* 19 vars */]) : : open("/lib/modules/4.15.0-46-generic/modules.softdep", O_RDONLY|O_CLOEXEC) = 3 : : stat("/home/user/fibdrv/fibdrv.ko", {st_mode=S_IFREG|0664, st_size=8416, ...}) = 0 open("/home/user/fibdrv/fibdrv.ko", O_RDONLY|O_CLOEXEC) = 3 fstat(3, {st_mode=S_IFREG|0664, st_size=8416, ...}) = 0 mmap(NULL, 8416, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7ff23ed79000 finit_module(3, "", 0) = 0 ``` 可以發現一個不常見(對我來說啦)的 syscall `finit_module`,透過查詢 man [finit_module](https://linux.die.net/man/2/finit_module) 得知`finit_module`當中的 "f" 是 file descriptor 的意思,而這是自從 linux kernel 3.8 後才新加入的 syscall,在這之前使用的是 `init_module` syscall 來載入 kernel modules 既然找到了對應的 syscall 那就直接看 [kernel source](https://elixir.bootlin.com/linux/latest/source/kernel/module.c#L3878) 來理解吧 ```cpp=3878 SYSCALL_DEFINE3(finit_module, int, fd, const char __user *, uargs, int, flags) { struct load_info info = { }; loff_t size; void *hdr; int err; err = may_init_module(); if (err) return err; pr_debug("finit_module: fd=%d, uargs=%p, flags=%i\n", fd, uargs, flags); if (flags & ~(MODULE_INIT_IGNORE_MODVERSIONS |MODULE_INIT_IGNORE_VERMAGIC)) return -EINVAL; err = kernel_read_file_from_fd(fd, &hdr, &size, INT_MAX, READING_MODULE); if (err) return err; info.hdr = hdr; info.len = size; return load_module(&info, uargs, flags); } ``` 有關 `SYSCALL_DEFINE` 的解釋可以參考 [Linux系统调用之SYSCALL_DEFINE](https://blog.csdn.net/hxmhyp/article/details/22699669) 這裡在做的事情就是透過`kernel_read_file_from_fd` 把整個編譯好的 kernel module 從 userspace 讀到一個 buffer `*hdr` 然後放進 `info` 中。 接著會呼叫`load_module` 來解析 kernel module 中的資訊,並將其放入 `struct module mod` 變數中對應的欄位上,在 ` setup_load_info(info, flags)` 中會將先前提到用於存放 metadata 的 `__this_module` 指標複製到 `info->mod` ```cpp=2981 info->index.mod = find_sec(info, ".gnu.linkonce.this_module"); if (!info->index.mod) { pr_warn("%s: No module found in object\n", info->name ?: "(missing .modinfo name field)"); return -ENOEXEC; } /* This is temporary: point mod into copy of data. */ info->mod = (void *)info->hdr + info->sechdrs[info->index.mod].sh_offset; ``` 接著在 `layout_and_allocate(info, flags)` 時將所有 `info` 中的資訊讀取到 `mod` 裡 在所有資訊都讀取完成後,函式會呼叫 `do_init_module(mod)` 進行 module 的初始化。 ```cpp=3458 /* Start the module */ if (mod->init != NULL) ret = do_one_initcall(mod->init); ``` 而在其中若是 `mod->init != NULL` 則我們定義的 init function 就會在這裡被呼叫到。 ### 3.解釋像 fibdrv.ko 這樣的 ELF 執行檔案是如何「植入」到 Linux 核心 ![](https://i.imgur.com/ebEzOMW.png =x400) 要瞭解 ELF 是如何被載入到 kernel 的,首先必須對它的格式有所瞭解,所以接著就來閱讀 glibc/kernel 中 ELF 的相關代碼吧,以下代碼取自 [elf/elf.h](https://code.woboq.org/userspace/glibc/elf/elf.h.html), [include/uapi/linux/elf.h](https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/elf.h),詳細的解釋也可以透過 [man elf](http://man7.org/linux/man-pages/man5/elf.5.html) 找到 ### ELF header ```cpp typedef struct { unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */ Elf64_Half e_type; /* Object file type */ Elf64_Half e_machine; /* Architecture */ Elf64_Word e_version; /* Object file version */ Elf64_Addr e_entry; /* Entry point virtual address */ Elf64_Off e_phoff; /* Program header table file offset */ Elf64_Off e_shoff; /* Section header table file offset */ Elf64_Word e_flags; /* Processor-specific flags */ Elf64_Half e_ehsize; /* ELF header size in bytes */ Elf64_Half e_phentsize; /* Program header table entry size */ Elf64_Half e_phnum; /* Program header table entry count */ Elf64_Half e_shentsize; /* Section header table entry size */ Elf64_Half e_shnum; /* Section header table entry count */ Elf64_Half e_shstrndx; /* Section header string table index */ } Elf64_Ehdr; ``` ELF header 放置了一些與程式相關的 metadata,以及接下來的各個 table 跟 ELF header 之間的 offest,這裡比較有趣的是 `e_entry` 會在之後 kernel 載入 ELF 時決定運行時的 entry point address,對一般的程式來說就是 _start 的位置,可以用 `readelf -a elf` 觀察看看。 不過 kernel module 比較特別,在這個欄位的數值是零,我想這有很大部分是跟他的實作方式有關,畢竟在 insmod 時,kernel 已經將不少前置作業完成並「插入」到 kernel 中,也就不需要 userspace 中程式入口點的設計了 ### Program header ```cpp typedef struct { Elf64_Word p_type; /* Segment type */ Elf64_Word p_flags; /* Segment flags */ Elf64_Off p_offset; /* Segment file offset */ Elf64_Addr p_vaddr; /* Segment virtual address */ Elf64_Addr p_paddr; /* Segment physical address */ Elf64_Xword p_filesz; /* Segment size in file */ Elf64_Xword p_memsz; /* Segment size in memory */ Elf64_Xword p_align; /* Segment alignment */ } Elf64_Phdr; ``` program header 的資訊多用於建立 runtime 的相關資料,是用來告訴 kernel 該 ELF 中的各個 segment 在載入記憶體時該被放置到甚麼位置、讀寫權限、以及類型相關的資訊,要注意的是 segment 和 section 的關係,一個 segment 可以包含多個 section。 :::warning 有關於讀寫權限,實際上 kernel 似乎不太一定會照做 ? (之前有一次奇怪的經驗) ::: ### Section header ```cpp typedef struct { Elf64_Word sh_name; /* Section name (string tbl index) */ Elf64_Word sh_type; /* Section type */ Elf64_Xword sh_flags; /* Section flags */ Elf64_Addr sh_addr; /* Section virtual addr at execution */ Elf64_Off sh_offset; /* Section file offset */ Elf64_Xword sh_size; /* Section size in bytes */ Elf64_Word sh_link; /* Link to another section */ Elf64_Word sh_info; /* Additional section information */ Elf64_Xword sh_addralign; /* Section alignment */ Elf64_Xword sh_entsize; /* Entry size if section holds table */ } Elf64_Shdr; ``` section header 中的資訊可以協助 kernel 辨識/載入整個 ELF 中的資料,在程式剛被讀進 kernel 時常會看到它的身影 有了這些概念後,就可以回頭檢視 `load_module()` 中各個 function 是如何一步步的將 kernel module 載入的 ### elf_header_check 檢查 ELF header 中各變數是否有被妥善的設定,從資安觀點來看這相當重要,若是沒有進行檢查則極有可能受攻擊者利用,進而越界讀取洩漏 kernel stack 上的資訊 ```cpp=2805 static int elf_header_check(struct load_info *info) { // 檢查讀入的大小是否大於 Elf64_Ehdr 的大小 if (info->len < sizeof(*(info->hdr))) return -ENOEXEC; // 注意這邊 info->hdr 雖然讀進了整個 binary,但是 elf header 的結構是相同的 // 所以下方可以直接存取結構中相關的變數 // 檢查各個 offset 以及 size if (memcmp(info->hdr->e_ident, ELFMAG, SELFMAG) != 0 || info->hdr->e_type != ET_REL || !elf_check_arch(info->hdr) || info->hdr->e_shentsize != sizeof(Elf_Shdr)) return -ENOEXEC; if (info->hdr->e_shoff >= info->len || (info->hdr->e_shnum * sizeof(Elf_Shdr) > info->len - info->hdr->e_shoff)) return -ENOEXEC; return 0; } ``` ### setup_load_info ~~可以利用 `objdump -s --section .strtab elf` (錯了它不能用,因為它太廢ㄌ...~~ [參考資料](https://stackoverflow.com/questions/22160621/why-does-objdump-not-show-bss-shstratab-symtab-and-strtab-sections) 可以利用 `readelf -p "section-name" elf` 以字串方式印出該 section 中的資料 ```cpp static int setup_load_info(struct load_info *info, int flags) { unsigned int i; /* Set up the convenience variables */ info->sechdrs = (void *)info->hdr + info->hdr->e_shoff; info->secstrings = (void *)info->hdr + info->sechdrs[info->hdr->e_shstrndx].sh_offset; /* Try to find a name early so we can log errors with a module name */ // find_sec 利用了上方 setup 好的資訊來搜尋,return 目標於 section table 中的 index info->index.info = find_sec(info, ".modinfo"); if (!info->index.info) info->name = "(missing .modinfo section)"; else info->name = get_modinfo(info, "name"); /* Find internal symbols and strings. */ for (i = 1; i < info->hdr->e_shnum; i++) { if (info->sechdrs[i].sh_type == SHT_SYMTAB) { info->index.sym = i; info->index.str = info->sechdrs[i].sh_link; info->strtab = (char *)info->hdr + info->sechdrs[info->index.str].sh_offset; break; } } if (info->index.sym == 0) { pr_warn("%s: module has no symbols (stripped?)\n", info->name); return -ENOEXEC; } info->index.mod = find_sec(info, ".gnu.linkonce.this_module"); if (!info->index.mod) { pr_warn("%s: No module found in object\n", info->name ?: "(missing .modinfo name field)"); return -ENOEXEC; } /* This is temporary: point mod into copy of data. */ info->mod = (void *)info->hdr + info->sechdrs[info->index.mod].sh_offset; /* * If we didn't load the .modinfo 'name' field earlier, fall back to * on-disk struct mod 'name' field. */ if (!info->name) info->name = info->mod->name; if (flags & MODULE_INIT_IGNORE_MODVERSIONS) info->index.vers = 0; /* Pretend no __versions section! */ else info->index.vers = find_sec(info, "__versions"); info->index.pcpu = find_pcpusec(info); return 0; } ``` 先到這邊,之後繼續更新 ## fibdrv kernel module 在看 fibdrv kernel module 的實作之前總之先來觀察一下要怎麼跟 fibdrv 互動吧 ```cpp fd = open(FIB_DEV, O_RDWR); if (fd < 0) { perror("Failed to open character device"); exit(1); } for (i = 0; i <= offset; i++) { sz = write(fd, write_buf, strlen(write_buf)); printf("Writing to " FIB_DEV ", returned the sequence %lld\n", sz); } for (i = 0; i <= offset; i++) { lseek(fd, i, SEEK_SET); sz = read(fd, buf, 1); printf("Reading from " FIB_DEV " at offset %d, returned the sequence " "%lld.\n", i, sz); } ``` 看起來 fibdrv kernel module 被設計成一個 [character device](https://linux-kernel-labs.github.io/master/labs/device_drivers.html),粗淺的看可以理解成像是一個能夠循序存取的文件,透過定義相關的函數,可以利用存取檔案的 system call 去存取他 (e.g. open, read, write, mmap...等)。 不過等等,這似乎跟我想像中的 fibdrv 的實作不太一樣,原以為應該會是一種類似 function 的互動模式,送一個參數 x 給 fibdrv 然後他會透過 socket 或類似的管道回傳 fib(x) 給我,沒有想到要透過 "read" 他來得到輸出,實在是相當新奇。接著就來看看這到底是怎麼實現的 ```cpp /* calculate the fibonacci number at given offset */ static ssize_t fib_read(struct file *file, char *buf, size_t size, loff_t *offset) { return (ssize_t) fib_sequence(*offset); } const struct file_operations fib_fops = { .owner = THIS_MODULE, .read = fib_read, .write = fib_write, .open = fib_open, .release = fib_release, .llseek = fib_device_lseek, }; ``` 發現其實是透過 `fib_fops` 中的自定義的 read 來實作讀取操作,而 fib_read 最終會 return `fib_sequence(*offset)`,所以其實就是透過讓使用者指定不同的 offest 作為費伯納契數列的 $X$ 然後透過 read 的回傳值輸出給使用者。呃,這種做法看起來很 hack XD ### 測量時間 接著利用 ktime 提供的方便計時工具來測量時間,我們可以發現到 write 在這個 module 中毫無用武之地,可以拿來做點小 hack 改造成輸出上一次 fib 的執行時間。 ```cpp static ktime_t kt; static long long fib_time_proxy(long long k) { kt = ktime_get(); long long result = fib_sequence(k); kt = ktime_sub(ktime_get(), kt); return result; } static ssize_t fib_read(struct file *file, char *buf, size_t size, loff_t *offset) { return (ssize_t) fib_time_proxy(*offset); } static ssize_t fib_write(struct file *file, const char *buf, size_t size, loff_t *offset) { return ktime_to_ns(kt); } ```