--- tags: LINUX KERNEL, LKI --- # Linux 核心模組運作原理 > 資料整理: [jserv](http://wiki.csie.ncku.edu.tw/User/jserv) 本文從分析 [Hello World](https://en.wikipedia.org/wiki/%22Hello,_World!%22_program) 等級的 Linux 核心模組出發,探究 Linux 核心掛載和卸載核心模組背後的運作機制,理解這些概念後,再實作[可自我隱藏蹤跡的 Linux 核心模組](https://github.com/sysprog21/lkm-hidden) 作為驗證。 :::success 「幹壞事是進步最大的原動力」 -- [gslin](https://blog.gslin.org/) ::: ## 基本概念 [鳥哥私房菜: Linux 核心編譯與管理](http://linux.vbird.org/linux_basic/centos7/0540kernel.php) ### 從 Hello World 開始 不免俗,從 Hello world 開始。 ![](https://i.imgur.com/qnSJ9CI.png) ### 前期準備 自從 Linux 核心 4.4 版以來,Ubuntu Linux 預設開啟 `EFI_SECURE_BOOT_SIG_ENFORCE`,這使得核心模組需要適度的簽章才可掛載進入 Linux 核心,為了後續測試的便利,我們需要將 UEFI Secure Boot 的功能**關閉**,請見 [Why do I get “Required key not available” when install 3rd party kernel modules or after a kernel upgrade?](https://askubuntu.com/questions/762254/why-do-i-get-required-key-not-available-when-install-3rd-party-kernel-modules) * 檢查 Linux 核心版本 ```shell $ uname -r ``` 預期是大於等於 `5.4.0` 的版本,例如 `5.4.0-66-generic`。若在你的開發環境中,核心版本低於 `5.4` 的話,需要更新 Linux 核心,請自行參照相關文件 * 安裝 `linux-headers` 套件 (注意寫法裡頭有 `s`),以 [Ubuntu Linux 20.04 LTS](https://packages.ubuntu.com/focal/linux-headers-generic) 為例: ```shell $ sudo apt install linux-headers-`uname -r` ``` * 確認 `linux-headers` 套件已正確安裝於開發環境 ```shell $ dpkg -L linux-headers-`uname -r` | grep "/lib/modules/.*/build" ``` 預期得到以下輸出: ``` /lib/modules/5.4.0-66-generic/build ``` * 檢驗目前的使用者身份 ```shell $ whoami ``` 預期為「不是 root 的使用者名稱」,例如 `jserv` (或者你安裝 Ubuntu Linux 指定的登入帳號名稱)。由於測試過程需要用到 sudo,請一併查驗: ```shell $ sudo whoami ``` 預期輸出是 `root` :notes: 在下列操作中,請==避免用 root 帳號輸入命令==,而該善用 `sudo` :::danger 之後的實驗中,若濫用 root 權限,可能會破壞 GNU/Linux 開發環境 (當然,你還是可重新安裝),現在開始養成好習慣 ::: ### 程式碼準備 建立 `hello` 目錄並在其中建立 `Makefile` 及 `hello.c` - [ ] `hello.c` ```cpp #include <linux/init.h> #include <linux/module.h> MODULE_LICENSE("Dual BSD/GPL"); static int hello_init(void) { printk(KERN_INFO "Hello, world\n"); return 0; } static void hello_exit(void) { printk(KERN_INFO "Goodbye, cruel world\n"); } module_init(hello_init); module_exit(hello_exit); ``` - [ ] `Makefile` ``` obj-m := hello.o clean: rm -rf *.o *.ko *.mod.* *.symvers *.order *.mod.cmd *.mod ``` > 注意 `clean:` 下一行 `rm -rf` 之前要用 tab 分隔,而非空白 ### 建構核心模組 編譯核心模組的命令: ```shell $ make -C /lib/modules/`uname -r`/build M=`pwd` modules ``` 成功後會產出許多檔案,這邊我們會用到的只有 `hello.ko` 值得注意的是你可能在編譯過程結果會看到下面的敘述: ``` warning: the compiler differs from the one used to build the kernel The kernel was built by: gcc (Ubuntu 11.3.0-1ubuntu1~22.04.1) 11.3.0 You are using: gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0 ``` 表示你的編譯器版本與kernel開發套件所用的編譯器版本不一致,建議安裝對應的版本避免後續開發出問題。 ### 掛載核心模組 產生 `hello.ko` 之後,可將其掛載: ```shell $ sudo insmod hello.ko ``` `dmesg` 命令可顯示核心訊息 ``` [ 3824.676183] Hello, world ``` ### 卸載核心模組 其卸載稍早載入的 `hello` 核心模組: ``` $ sudo rmmod hello ``` `dmesg` 顯示核心訊息 ``` [ 3824.676183] Hello, world [ 3921.232076] Goodbye, cruel world ``` 以上為第一個 Linux 核心模組的撰寫、掛載及卸載。 繼續參閱以下教材: * ==[The Linux Kernel Module Programming Guide](https://github.com/sysprog21/lkmpg)== * [Introduction to Linux kernel driver programming](https://events.linuxfoundation.org/wp-content/uploads/2017/12/Introduction-to-Linux-Kernel-Driver-Programming-Michael-Opdenacker-Bootlin-.pdf) * [Part 1: Introduction](http://derekmolloy.ie/writing-a-linux-kernel-module-part-1-introduction/) * [Part 2: A Character Device](http://derekmolloy.ie/writing-a-linux-kernel-module-part-2-a-character-device/) --- ## `fibdrv`: 可輸出 Fibonacci 數列的 Linux 核心模組 取得原始程式碼 ```shell $ git clone https://github.com/sysprog21/fibdrv $ cd fibdrv ``` 編譯並測試 ```shell $ make check ``` 預期會看到綠色的 ` Passed [-]` 字樣,隨後是 ``` f(93) fail input: 7540113804746346429 expected: 12200160415121876738 ``` 這符合預期,因為給定的 `fibdrv` 存在==缺陷==。 :::info 就因世界不完美,才有我們工程師存在的空間。 ::: 觀察產生的 `fibdrv.ko` 核心模組 ```shell $ modinfo fibdrv.ko ``` 預期可得以下輸出: ``` description: Fibonacci engine driver author: National Cheng Kung University, Taiwan license: Dual MIT/GPL name: fibdrv vermagic: 5.4.0-45-generic SMP mod_unload ``` 觀察 `fibdrv.ko` 核心模組在 Linux 核心掛載後的行為(要先透過 `insmod` 將模組載入核心後才會有下面的裝置檔案 `/dev/fibonacci`) ```shell $ ls -l /dev/fibonacci $ cat /sys/class/fibonacci/fibonacci/dev ``` 新建立的裝置檔案 `/dev/fibonacci`,注意到 `236` 這個數字,在你的電腦也許會有出入。試著對照 [fibdev.c](https://github.com/sysprog21/fibdrv/blob/master/fibdrv.c),找尋彼此的關聯。 ```shell $ cat /sys/module/fibdrv/version ``` 預期輸出是 `0.1`,這和 [fibdev.c](https://github.com/sysprog21/fibdrv/blob/master/fibdrv.c) 透過 `MODULE_VERSION` 所指定的版本號碼相同。 ```shell $ lsmod | grep fibdrv $ cat /sys/module/fibdrv/refcnt ``` 這兩道命令的輸出都是 `0`,意味著目前的 [reference counting](https://en.wikipedia.org/wiki/Reference_counting)。 延伸閱讀: * [Fibonacci Driver 作業說明](https://hackmd.io/@sysprog/linux2022-fibdrv) --- ## Linux 核心模組掛載機制 - [ ] 檔案 [fibdrv.c](https://github.com/sysprog21/fibdrv/blob/master/fibdrv.c) 裡頭的 `MODULE_LICENSE`, `MODULE_AUTHOR`, `MODULE_DESCRIPTION`, `MODULE_VERSION` 等巨集做了什麼事,可以讓核心知曉呢? `insmod` 這命令背後,對應 Linux 核心內部有什麼操作呢? 這些巨集本質上就是在編譯過後在 `.ko` 檔 (`ko` 即 kernel object 之意,對比使用者層級的 shared object [相當於 Microsoft Windows 的 DLL],可對照 [你所不知道的 C 語言:動態連結器篇](https://hackmd.io/@sysprog/c-dynamic-linkage)) 中提供相對應的資訊,由於性質相同,這邊就先專注於 `MODULE_AUTHOR`。在範例程式中我們指定 module 的作者 ```cpp MODULE_AUTHOR("National Cheng Kung University, Taiwan"); ``` 以下摘自 **[include/linux/module.h](https://elixir.bootlin.com/linux/v4.18/source/include/linux/module.h#L205)**: ```cpp /* * Author(s), use "Name <email>" or just "Name", for multiple * authors use multiple MODULE_AUTHOR() statements/lines. */ #define MODULE_AUTHOR(_author) MODULE_INFO(author, _author) ``` 上述註解說明 `_author` 的格式和若有多個 author 則應該呼叫多次 `MODULE_AUTHOR`。 若將巨集展開應得 `MODULE_INFO(author, "National Cheng Kung University, Taiwan")` ```cpp /* Generic info of form tag = "info" */ #define MODULE_INFO(tag, info) __MODULE_INFO(tag, tag, info) ``` 繼續將上述展開得 `__MODULE_INFO(author, author, "National Cheng Kung University, Taiwan")` 以下摘自 **[include/linux/moduleparam.h](https://elixir.bootlin.com/linux/v4.18/source/include/linux/moduleparam.h)**: ```cpp #ifdef MODULE #define __MODULE_INFO(tag, name, info) \ static const char __UNIQUE_ID(name)[] \ __used __attribute__((section(".modinfo"), unused, aligned(1))) \ = __stringify(tag) "=" info #else /* !MODULE */ /* This struct is here for syntactic coherency, it is not used */ #define __MODULE_INFO(tag, name, info) \ struct __UNIQUE_ID(name) {} #endif ``` 上述巨集的定義根據 `MODULE` 是否有被定義,`MODULE` 是在此核心模組被編譯時期所定義,若此模組編譯時已內建於核心則不會被定義。繼續將上述巨集展開 ```cpp static const char __UNIQUE_ID(author)[] \ __used __attribute__((section(".modinfo"), unused, aligned(1))) \ = __stringify(author) "=" "National Cheng Kung University, Taiwan" ``` 以下摘自 **[include/linux/compiler-gcc.h](https://elixir.bootlin.com/linux/v4.18/source/include/linux/compiler-gcc.h#L208)**: ```cpp #define __UNIQUE_ID(prefix) __PASTE(__PASTE(__UNIQUE_ID_, prefix), __COUNTER__) ``` 繼續將 \__UNIQUE_ID 展開,`__COUNTER__` 這個巨集由 GCC 自動更新,每當遇到使用到 `__COUNTER__` 就會將其值加一。 ```cpp static const char __PASTE(__PASTE(__UNIQUE_ID_, author), __COUNTER__)[] \ __used __attribute__((section(".modinfo"), unused, aligned(1))) \ = __stringify(author) "=" "National Cheng Kung University, Taiwan" ``` 1. `__UNIQUE_ID` 會根據參數產生一個不重複的名字(參考[linux/compiler.h](https://elixir.bootlin.com/linux/v4.15/source/include/linux/compiler.h#L161)),其中使用到的技術是利用巨集中的 `##` 來將兩個引數合併成一個新的字串。 2. 透過 `__attribute__` 關鍵字告訴編譯器,這段訊息 1. 要被放在 `.modinfo` 段 2. 應該不會被程式使用到,所以不要產生警告訊息 3. 最小的對齊格式需要是 1 bit > 延伸閱讀: [Specifying Attributes of Variables](https://gcc.gnu.org/onlinedocs/gcc-3.2/gcc/Variable-Attributes.html) 3. 在 [linux/stringfy.h](https://elixir.bootlin.com/linux/v4.15/source/include/linux/stringify.h) 裡頭,我們可以看到 `__stringify` 的目的是為了把引數轉換成字串形式。以 `MODULE_LICENSE("Dual MIT/GPL")` 為例,被展開後的 `__stringify(tag) "=" info` 會是 `"license = Dual MIT/GPL"` 字串。 總結這部份,`MODULE_XXX` 系列的巨集在最後都會被轉變成 ```cpp static const char 獨一無二的變數[] = "操作 = 參數" ``` 再放到 `.modinfo` 區段中。這裡對應到 C99/C11 規格書中的 6.4.5 String Literals: > In translation phase 6, the multibyte character sequences specified by any sequence of adjacent character and wide string literal tokens are concatenated into a single multibytecharacter sequence. 大致的意思是把 string literal 並排,等同於一個合併起來的字串。 以下摘自 **[include/linux/compiler_types.h](https://elixir.bootlin.com/linux/v4.18/source/include/linux/compiler_types.h#L53)**: ```cpp /* Indirect macros required for expanded argument pasting, eg. __LINE__. */ #define ___PASTE(a,b) a##b #define __PASTE(a,b) ___PASTE(a,b) ``` 繼續展開: ```cpp static const char __UNIQUE_ID_author0[] \ __used __attribute__((section(".modinfo"), unused, aligned(1))) \ = __stringify(author) "=" "National Cheng Kung University, Taiwan" ``` 摘自 **[include/linux/stringify.h](https://elixir.bootlin.com/linux/v4.18/source/include/linux/stringify.h#L10)**: ```cpp #define __stringify_1(x...) #x #define __stringify(x...) __stringify_1(x) ``` 注意到 `#` 和 `##` 這兩個都是 preprocessor 語法,請參照 [你所不知道的 C 語言:前置處理器應用篇](https://hackmd.io/@sysprog/c-preprocessor) 以得知詳細用法。 做最後的展開能夠得到以下的結果 ```cpp static const char __UNIQUE_ID_author0[] \ __used __attribute__((section(".modinfo"), unused, aligned(1))) \ = "author=National Cheng Kung University, Taiwan" ``` 根據 GNU GCC 文件說明對於 Variable attribute 的解說,`section` 會特別將此 variable 放到指定的 ELF section 中,這邊為 `.modinfo`。關於 ELF 的資訊,請參照 [你所不知道的 C 語言:連結器和執行檔資訊](https://hackmd.io/@sysprog/c-linker-loader)。 ``` $ objdump -s fibdrv.ko ... Contents of section .modinfo: 0000 76657273 696f6e3d 302e3100 64657363 version=0.1.desc 0010 72697074 696f6e3d 4669626f 6e616363 ription=Fibonacc 0020 6920656e 67696e65 20647269 76657200 i engine driver. 0030 61757468 6f723d4e 6174696f 6e616c20 author=National 0040 4368656e 67204b75 6e672055 6e697665 Cheng Kung Unive 0050 72736974 792c2054 61697761 6e006c69 rsity, Taiwan.li 0060 63656e73 653d4475 616c204d 49542f47 cense=Dual MIT/G 0070 504c0000 00000000 73726376 65727369 PL......srcversi 0080 6f6e3d34 42373436 37453631 43414238 on=4B7467E61CAB8 0090 32354539 35364446 38330000 00000000 25E956DF83...... 00a0 64657065 6e64733d 00726574 706f6c69 depends=.retpoli 00b0 6e653d59 006e616d 653d6669 62647276 ne=Y.name=fibdrv 00c0 00766572 6d616769 633d342e 31382e30 .vermagic=4.18.0 00d0 2d31362d 67656e65 72696320 534d5020 -16-generic SMP 00e0 6d6f645f 756e6c6f 61642000 mod_unload . ... ``` 上述可以看到 author 的資訊被寫入到 `.modinfo` section 中。 更進一步,用 vim 打開 `fibdrv.ko`,並且使用 16 進位模式閱讀,可以在 [readelf](https://sourceware.org/binutils/docs/binutils/readelf.html) 輸出的 `modinfo` 區段的 `offset` (560) 中找到下面內容: ```hex 00000530: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000540: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000550: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000560: 7665 7273 696f 6e3d 302e 3100 6465 7363 version=0.1.desc 00000570: 7269 7074 696f 6e3d 4669 626f 6e61 6363 ription=Fibonacc 00000580: 6920 656e 6769 6e65 2064 7269 7665 7200 i engine driver. 00000590: 6175 7468 6f72 3d4e 6174 696f 6e61 6c20 author=National 000005a0: 4368 656e 6720 4b75 6e67 2055 6e69 7665 Cheng Kung Unive 000005b0: 7273 6974 792c 2054 6169 7761 6e00 6c69 rsity, Taiwan.li 000005c0: 6365 6e73 653d 4475 616c 204d 4954 2f47 cense=Dual MIT/G 000005d0: 504c 0000 0000 0000 7372 6376 6572 7369 PL......srcversi 000005e0: 6f6e 3d32 3444 4335 4642 3745 3736 3038 on=24DC5FB7E7608 000005f0: 4146 3136 4230 4343 3146 0000 0000 0000 AF16B0CC1F...... 00000600: 6465 7065 6e64 733d 0072 6574 706f 6c69 depends=.retpoli 00000610: 6e65 3d59 006e 616d 653d 6669 6264 7276 ne=Y.name=fibdrv 00000620: 0076 6572 6d61 6769 633d 342e 3138 2e30 .vermagic=4.18.0 00000630: 2d31 352d 6765 6e65 7269 6320 534d 5020 -15-generic SMP 00000640: 6d6f 645f 756e 6c6f 6164 2000 0000 0000 mod_unload ..... 00000650: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000660: 0000 0000 0000 0000 0000 0000 0000 0000 ................ ``` 再來看執行 `insmod` 時 Linux 核心做了什麼。 摘自 [Linux Device Driver 3/e](https://lwn.net/Kernel/LDD3/) 第 2 章的資訊: > ...The use of module_init is mandatory. This macro adds a special section to the module’s object code stating where the module’s initialization function is to be found. Without this definition, your initialization function is never called. 可知 `module_init` 巨集在編譯出來的 object 中,加入初始化模組函數的起始位置。類似地,`module_exit` 的相關敘述: > Once again, the module_exit declaration is necessary to enable to kernel to find your cleanup function. ![image](https://hackmd.io/_uploads/r127fL5lR.png) 這邊利用 [strace](https://linux.die.net/man/1/strace) 追蹤執行 `insmod fibdrv.ko` 的過程有哪些系統呼叫被執行: ```shell= $ sudo strace insmod fibdrv.ko execve("/sbin/insmod", ["insmod", "fibdrv.ko"], 0x7ffeab43f308 /* 25 vars */) = 0 brk(NULL) = 0x561084511000 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 fstat(3, {st_mode=S_IFREG|0644, st_size=83948, ...}) = 0 mmap(NULL, 83948, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f0621290000 close(3) = 0 ... close(3) = 0 getcwd("/tmp/fibdrv", 4096) = 24 stat("/tmp/fibdrv/fibdrv.ko", {st_mode=S_IFREG|0644, st_size=8288, ...}) = 0 openat(AT_FDCWD, "/tmp/fibdrv/fibdrv.ko", O_RDONLY|O_CLOEXEC) = 3 fstat(3, {st_mode=S_IFREG|0644, st_size=8288, ...}) = 0 mmap(NULL, 8288, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f06212a2000 finit_module(3, "", 0) = 0 munmap(0x7f06212a2000, 8288) = 0 close(3) = 0 exit_group(0) = ? +++ exited with 0 +++m ``` 自上述第 18 行可以發現呼叫到 [finit_module](https://linux.die.net/man/2/finit_module)。去查看 linux 核心中如何宣告和實作 `finit_module`。 **[kernel/module.c](https://elixir.bootlin.com/linux/v4.18/source/kernel/module.c)** ```clike= SYSCALL_DEFINE3(finit_module, int, fd, const char __user *, uargs, int, flags) { // ... return load_module(&info, uargs, flags); } ``` 在第 4 行可以發現執行 `load_module` 這個函式: ```cpp /* Allocate and load the module: note that size of section 0 is always zero, and we rely on this for optional sections. */ static int load_module(struct load_info *info, const char __user *uargs, int flags) { ... } ``` 在 Linux kernel v6.8.5 中, `finit_module` 系統呼叫的實作方式已變更,呼叫 `idempotent_init_module` 後在該函式當中還會呼叫 `init_module_from_file` ,在 `init_module_from_file` 才真正呼叫到 `load_module`。舊版核心原始程式碼的 [kernel/module.c](https://elixir.bootlin.com/linux/v4.18/source/kernel/module.c) 已在新版拆解成若干檔案,前述程式碼如今在 [kernel/module/main.c](https://elixir.bootlin.com/linux/latest/source/kernel/module/main.c)。 而在註解的部分可以看到 `load_module` 大致就是 Linux 核心為模組配置記憶體和載入模組相關資料的地方。 - [ ] 當我們透過 `insmod` 去載入一個核心模組時,為何 `module_init` 所設定的函式得以執行呢?Linux 核心做了什麼事呢? 首先,先看看原始碼 ```cpp static int __init init_fib_dev(void) { // ... } static void __exit exit_fib_dev(void) { // ... } module_init(init_fib_dev); module_exit(exit_fib_dev); ``` 以下摘自 **[include/linux/module.h](https://elixir.bootlin.com/linux/v4.18/source/include/linux/module.h#L129)**: ```cpp= /* Each module must use one module_init(). */ #define module_init(initfn) \ static inline initcall_t __maybe_unused __inittest(void) \ { return initfn; } \ int init_module(void) __attribute__((alias(#initfn))); ``` 在第 5 行,可以看到 gcc 會在編譯過後將 `initfn` 設為 `int init_module(void)` 的別名。 :::info 1. 請參閱 ==[GCC 手冊](https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html)==,得知 `__attribute__((alias( ..)))` 的用法。 2. 可透過 `gcc -E fibdrv.c -I$TREE/include -I$TREE/arch/x86/include -I$TREE/include/uapi` 看到經過前置處理後的程式碼,`$TREE` 指的是核心原始程式碼的路徑,即 `/usr/src/linux-headers-$(uname -r)` 參考 [Stack Overflow - Kernel module source file after preprocessing](https://stackoverflow.com/questions/21177935/kernel-module-source-file-after-preprocessing) ::: 在核心 v.6.5.0 ,輸入上述第二點的 gcc 命令會導致編譯錯誤 (找不到 [asm/rwonce.h](https://stackoverflow.com/questions/77661674/fatal-error-asm-rwonce-h-no-such-file-or-directory-symlink-not-enough)),改成上述命令可以產出一段結果 (但與教材的預期結果不一樣) ```bash gcc -E fibdrv.c -I$TREE/include -I$TREE/arch/x86/include -I$TREE/arch/x86/include/generated ``` 在 `linux/module.h` 裏面,有兩處定義 `module_init`,分別是 還沒定義 `MODULE` 的 ```cpp #ifndef MODULE #define module_init(x) __initcall(x); ``` 還有已定義 `MODULE` 的 ```cpp #else /* MODULE */ ... /* Each module must use one module_init(). */ #define module_init(initfn) \ static inline initcall_t __maybe_unused __inittest(void) \ { return initfn; } \ int init_module(void) __copy(initfn) __attribute__((alias(#initfn))); ``` 從前置處理過後的結果可知,這裡選用沒定義 `MODULE` 的 `module_init(x)` ,也就是之後會繼續展開 `__initcall(x)`。 `__initcall(x)` 巨集被定義在 [linux/init.h](https://elixir.bootlin.com/linux/v4.15/source/include/linux/init.h) ```clike #define __initcall(fn) device_initcall(fn) ``` `device_initcall` 展開成 [`__define_initcall(fn, 6)`](https://0xax.gitbooks.io/linux-insides/content/Concepts/linux-cpu-3.html#:~:text=__define_initcall(fn%2C%206)),最後會變成我們預處理看到的結果。最後展開的巨集 ```cpp static inline __attribute__((unused)) __attribute__((no_instrument_function)) initcall_t __attribute__((unused)) __inittest(void) { return init_fib_dev; } int init_module(void) __attribute__((alias("init_fib_dev")));; ``` 再來解讀: 首先是第一段 `static inline ....`,這邊用了一個小技巧讓我們可在編譯時期就知道傳入的 function pointer 是不是合法的。我們回傳的 `return init_fib_dev`,他的資料型態必須要和 `initcall_t` 相同,否則編譯器會報錯。 > 這種作法和 [BUG_ON](https://kernelnewbies.org/FAQ/BUG) 和 C++ 的 [static assertion](https://en.cppreference.com/w/cpp/language/static_assert) 相似。 再來是 [init_module](http://man7.org/linux/man-pages/man2/init_module.2.html),我們知道這個**系統呼叫**讓我們把一個 ELF image 載入到 kernel space,而在最後一行 `int init_module(void) __attribute__((alias("init_fib_dev")))` 的目的是為了替 `init_module` 取一個別名。 之所以要這樣做,是因前面的地方有寫到 ```cpp /* These are either module local, or the kernel's dummy ones. */ extern int init_module(void); ``` 這行告訴我們說,有 `init_module` 可以使用,但是不在這個地方實作。那實作在什麼地方呢?就是我們寫的 `init_fib_dev`,因為我們把 `init_module` 取了一個別名叫作 `init_fib_dev`。 總結一下,`module_init` 巨集幫我們做 2 件事 1. 檢查傳入的函式,回傳值是否正確 2. 把 `init_module` 和傳入的函式關聯起來,因為 `insmod` 指令實作內部會呼叫 `init_module`。如此一來呼叫 `init_module` 就等同於呼叫我們自己寫的函式。 透過以下實驗可確認否達到別名的效果: ```cpp #include <stdio.h> int __func() { printf("In __func()\n"); return 0; } int func() __attribute__((alias("__func"))); /* no function body */ int main() { func(); return 0; } ``` 編譯並執行: ```shell $ gcc -o test test.c $ ./test In __func() ``` 因此執行 `init_module()` 就相當於執行使用者自定義的函式 `initfn`。 再來繼續回到為什麼 init_module 會被執行? 就要回想系統會呼叫 `finit_module` 再來 `load_module`。 摘自 **[kernel/module.c](https://elixir.bootlin.com/linux/v4.18/source/kernel/module.c#L3785)**: ```cpp= /* Allocate and load the module: note that size of section 0 is always zero, and we rely on this for optional sections. */ static int load_module(struct load_info *info, const char __user *uargs, int flags) { struct module *mod; //... /* Figure out module layout, and allocate all the memory. */ mod = layout_and_allocate(info, flags); if (IS_ERR(mod)) { err = PTR_ERR(mod); goto free_copy; } // ... return do_init_module(mod); // ... } ``` 自第 11 行可發現,在 `do_init_module` 之前,核心先做 `layout_and_allocate` 為載入的 module 進行記憶體配置。最後在第 19 行對 module 做初始化。 摘自 **[kernel/module.c](https://elixir.bootlin.com/linux/v4.18/source/kernel/module.c#L3456)**: ```cpp /* * This is where the real work happens. * * Keep it uninlined to provide a reliable breakpoint target, e.g. for the gdb * helper command 'lx-symbols'. */ static noinline int do_init_module(struct module *mod) { // ... /* Start the module */ if (mod->init != NULL) ret = do_one_initcall(mod->init); // ... } ``` 摘自 **[init/main.c](https://elixir.bootlin.com/linux/v4.18/source/init/main.c#L874)**: ```cpp= int __init_or_module do_one_initcall(initcall_t fn) { // ... do_trace_initcall_start(fn); ret = fn(); do_trace_initcall_finish(fn, ret); // ... } ``` 可見到 `fn` 亦即傳入的 `mod->init`,核心模組的 init_function 在上述程式碼第 6 行被執行,為核心模組進行真正的初始化的工作。 - [ ] 試著執行 `$ readelf -a fibdrv.ko`, 觀察裡頭的資訊和原始程式碼及 `modinfo` 的關聯,搭配上述提問,解釋像 `fibdrv.ko` 這樣的 ELF 執行檔案是如何「植入」到 Linux 核心 Executable and Linking Format 簡稱為 ELF,可以表示一個 executable binary file 或是 object file。由於這次實驗,`fibdrv.ko` 並非可執行檔,因此這邊專注於解釋 ELF 檔案以 object file 的觀點。ELF 可大致分為 3 個部分: 1. **ELF header** 存放了有關於此 object file 的訊息 ```shell $ readelf -h fibdrv.ko ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: REL (Relocatable file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x0 Start of program headers: 0 (bytes into file) Start of section headers: 6688 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 0 (bytes) Number of program headers: 0 Size of section headers: 64 (bytes) Number of section headers: 25 Section header string table index: 24 ``` 2. **Section(s)** 有系統預定義的 section,如 `.text`, `.data`, `.bss` 等等,但也有使用者定義的 section,在本例中就有 `.modinfo`. 3. **Section Header(s)** 有對應的關於每個 section 的 metadata。例如某 Section 的 size。 ```shell $ readelf -S fibdrv.ko There are 25 section headers, starting at offset 0x1a20: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .note.gnu.build-i NOTE 0000000000000000 00000040 0000000000000024 0000000000000000 A 0 0 4 [ 2] .text PROGBITS 0000000000000000 00000070 000000000000015c 0000000000000000 AX 0 0 16 [ 3] .rela.text RELA 0000000000000000 00001218 0000000000000120 0000000000000018 I 22 2 8 [ 4] .init.text PROGBITS 0000000000000000 000001cc 0000000000000153 0000000000000000 AX 0 0 1 [ 5] .rela.init.text RELA 0000000000000000 00001338 00000000000003a8 0000000000000018 I 22 4 8 [ 6] .exit.text PROGBITS 0000000000000000 0000031f 0000000000000040 0000000000000000 AX 0 0 1 [ 7] .rela.exit.text RELA 0000000000000000 000016e0 00000000000000d8 0000000000000018 I 22 6 8 [ 8] __mcount_loc PROGBITS 0000000000000000 0000035f 0000000000000030 0000000000000000 A 0 0 1 [ 9] .rela__mcount_loc RELA 0000000000000000 000017b8 0000000000000090 0000000000000018 I 22 8 8 [10] .rodata.str1.1 PROGBITS 0000000000000000 0000038f :q 000000000000006e 0000000000000001 AMS 0 0 1 [11] .rodata.str1.8 PROGBITS 0000000000000000 00000400 0000000000000058 0000000000000001 AMS 0 0 8 [12] .rodata PROGBITS 0000000000000000 00000460 0000000000000100 0000000000000000 A 0 0 32 [13] .rela.rodata RELA 0000000000000000 00001848 0000000000000090 0000000000000018 I 22 12 8 [14] .modinfo PROGBITS 0000000000000000 00000560 00000000000000ec 0000000000000000 A 0 0 8 [15] .data PROGBITS 0000000000000000 00000660 0000000000000020 0000000000000000 WA 0 0 32 [16] .rela.data RELA 0000000000000000 000018d8 ... ``` 再來看 `modinfo` 這個程式和 `fibdrv.ko` 的關聯。由稍早推斷,`MODULE_XXX` 等巨集會將 module 的額外資訊放入 `fibdrv.ko` 中 `.modinfo` 中,`modinfo` 這個程式應該就是到 `fibdrv.ko` 中的 `.modinfo` 區段讀取資料並做顯示。以下是 `man modinfo` 中關於 `modinfo` 的描述。 > DESCRIPTION > ==modinfo extracts information from the Linux Kernel modules== given on the command line. If the module name is not a filename, then the /lib/modules/version directory is > searched, as is also done by modprobe(8) when loading kernel modules. > ==modinfo by default lists each attribute of the module in form fieldname : value, for > easy reading.== The filename is listed the same way (although it's not really an > attribute). - [ ] 解釋像 `fibdrv.ko` 這樣的 ELF 執行檔案是如何「植入」到 Linux 核心 `fibdrv.ko` 不是能在 shell 呼叫並執行的執行檔,它只是 ELF 格式的 object file。如果 `fibdrv.ko` 是執行檔,那麼其內容應該會包含了 Program headers 這些訊息,但是查看 ELF header 可以發現並沒有 Program header。 ```shell $ readelf -h fibdrv.ko ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: REL (Relocatable file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x0 Start of program headers: 0 (bytes into file) Start of section headers: 6688 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 0 (bytes) Number of program headers: 0 Size of section headers: 64 (bytes) Number of section headers: 25 Section header string table index: 24 ``` 因此我們需要透過 `insmod` 這個程式(可執行檔)來將 `fibdrv.ko` 植入核心中。kernel module 是執行在 kernel space 中,但是 `insmod fibdrv.ko` 是一個在 user space 的程序,因此在 `insmod` 中應該需要呼叫相關管理記憶體的 system call,將在 user space 中 kernel module 的資料複製到 kernel space 中。 回頭看之前說 `insmod` 會使核心執行 `finit_module` 摘自 **[linux/kernel/module.c](https://elixir.bootlin.com/linux/v4.18/source/kernel/module.c)**: ```cpp= SYSCALL_DEFINE3(finit_module, int, fd, const char __user *, uargs, int, flags) { struct load_info info = { }; loff_t size; void *hdr; int err; // ... 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); } ``` 在第 10 行核心會讀取一個檔案,在本例中,就是 `fibdrv.ko`: ```shell= $ sudo insmod fibdrv.ko ... getcwd("/tmp/fibdrv", 4096) = 24 stat("/tmp/fibdrv/fibdrv.ko", {st_mode=S_IFREG|0644, st_size=8288, ...}) = 0 openat(AT_FDCWD, "/tmp/fibdrv/fibdrv.ko", O_RDONLY|O_CLOEXEC) = 3 fstat(3, {st_mode=S_IFREG|0644, st_size=8288, ...}) = 0 mmap(NULL, 8288, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f06212a2000 finit_module(3, "", 0) = 0 ... ``` 可見上述執行 `strace insmod fibdrv.ko` 後,在第 5 行開啟 `fibdrv.ko` 這個檔案並得到其 file descriptor 為 `3`。並在第 8 行傳入 `finit_module` 中。 - [ ] sysfs 的原理和實作 Patrick Mochel 撰寫的報告 [The sysfs Filesystem](https://mirrors.edge.kernel.org/pub/linux/kernel/people/mochel/doc/papers/ols-2005/mochel.pdf) 第 1 頁提到: > sysfs is a mechanism for representing kernel objects, their attributes, and their relationships with each other. 並在第 5 頁的 module 中提到: > The module directory contains subdirectories for each module that is loaded into the kernel.The name of each directory is the name of the module -- both the name of the module object file and the internal name of the module. `/sys/module` 這個目錄下會有以已載入 module 名稱命名的子目錄;在 [sysfs(5) -man page](http://man7.org/linux/man-pages/man5/sysfs.5.html) 提到,在 `/sys/module/`"module-name" 目錄中會有一些相關檔案,這些檔案分別紀錄了此 module 的一些資料,例如,傳入的參數值;另外,在 [/kernel/module.c - 1703 行](https://elixir.bootlin.com/linux/latest/source/kernel/module.c#L1721) 中, `module_add_modinfo_attrs`() 中,第 19行有 sysfs 相關的函式 `sysfs_create_file`: ```cpp= static int module_add_modinfo_attrs(struct module *mod) { struct module_attribute *attr; struct module_attribute *temp_attr; int error = 0; int i; mod->modinfo_attrs = kzalloc((sizeof(struct module_attribute) * (ARRAY_SIZE(modinfo_attrs) + 1)), GFP_KERNEL); if (!mod->modinfo_attrs) return -ENOMEM; temp_attr = mod->modinfo_attrs; for (i = 0; (attr = modinfo_attrs[i]) && !error; i++) { if (!attr->test || attr->test(mod)) { memcpy(temp_attr, attr, sizeof(*temp_attr)); sysfs_attr_init(&temp_attr->attr); error = sysfs_create_file(&mod->mkobj.kobj, &temp_attr->attr); ++temp_attr; } } return error; } ``` `sysfs_create_file` 函式的第二個參數為 `&temp_attr->attr`,而 `temp_attr` 是個指向 struct module_attribute [struct module_attribute](https://elixir.bootlin.com/linux/latest/source/include/linux/module.h#L52) 的指標: ```cpp struct module_attribute { struct attribute attr; ssize_t (*show)(struct module_attribute *, struct module_kobject *, char *); ssize_t (*store)(struct module_attribute *, struct module_kobject *, const char *, size_t count); void (*setup)(struct module *, const char *); int (*test)(struct module *); void (*free)(struct module *); }; ``` 第 1 行宣告 `attr`,其型態為 [struct attribute](https://elixir.bootlin.com/linux/latest/source/include/linux/sysfs.h#L30) ( 定義於 /include/linux/sysfs.h ) : ```cpp struct attribute { const char *name; umode_t mode; #ifdef CONFIG_DEBUG_LOCK_ALLOC bool ignore_lockdep:1; struct lock_class_key *key; struct lock_class_key skey; #endif }; ``` 延伸閱讀: 《[The Linux Kernel Module Programming Guide](https://sysprog21.github.io/lkmpg/)》 ## 可自我隱藏的 Linux 核心模組 所謂「可自我隱藏」的 Linux 核心模組就是指,一旦掛載後,難以利用工具 (如 `lsmod` 命令) 得知其蹤跡,許多 [rootkit](https://en.wikipedia.org/wiki/Rootkit) 會採取這個策略,以欺瞞 Linux 系統管理者,從而為所欲為。本文並非探討 [rootkit](https://en.wikipedia.org/wiki/Rootkit),但解析為何一個 Linux 核心模組得以匿蹤,不啻是理解 Linux 核心的切入點 —— 我們要充分考慮到 Linux 核心的運作機制,才能有效隱藏 Linux 核心模組。 [lkm-hidden](https://github.com/sysprog21/lkm-hidden) 是個 Linux 核心模組,有限度地展現上述隱藏自己的存在。在 [main.c](https://github.com/sysprog21/lkm-hidden/blob/master/main.c) 使用巨集宣告: ```cpp MODULE_DESCRIPTION("Catch Me If You Can"); ``` 這則 [Catch Me If You Can](https://en.wikipedia.org/wiki/Catch_Me_If_You_Can) 訊息,暗示這個核心模組掛載後,難以找到其蹤跡。 主要程式碼在 [main.c](https://github.com/sysprog21/lkm-hidden/blob/master/main.c) 的 `hide_myself` 函式: ```cpp static void __init hide_myself(void) { struct vmap_area *va, *vtmp; struct module_use *use, *tmp; struct list_head *_vmap_area_list = (struct list_head *) kallsyms_lookup_name("vmap_area_list"); struct rb_root *_vmap_area_root = (struct rb_root *) kallsyms_lookup_name("vmap_area_root"); /* hidden from /proc/vmallocinfo */ list_for_each_entry_safe (va, vtmp, _vmap_area_list, list) { if ((unsigned long) THIS_MODULE > va->va_start && (unsigned long) THIS_MODULE < va->va_end) { list_del(&va->list); /* remove from red-black tree */ rb_erase(&va->rb_node, _vmap_area_root); } } /* hidden from /proc/modules */ list_del_init(&THIS_MODULE->list); /* hidden from /sys/modules */ kobject_del(&THIS_MODULE->mkobj.kobj); /* decouple the dependency */ list_for_each_entry_safe (use, tmp, &THIS_MODULE->target_list, target_list) { list_del(&use->source_list); list_del(&use->target_list); sysfs_remove_link(use->target->holders_dir, THIS_MODULE->name); kfree(use); } } ``` 前述提及 `THIS_MODULE` 巨集的作用,我們利用鏈結串列的行為,予以「脫鉤」,這樣才不會在 `/proc/module` 被發現。此外,當我們執行以下命令: ```shell sudo cat /proc/vmallocinfo | less ``` 清楚可見 Linux 核心個別函式對應的記憶體地址範圍,這樣會洩漏 Linux 核心模組的執行,於是也該迴避。這裡利用取得 `vmap_area_list` 和 `vmap_area_root` 符號,並變更內容。 延伸閱讀: * [Hiding with a Linux Rootkit](https://0x00sec.org/t/hiding-with-a-linux-rootkit/4532) * [Hiding Kernel Modules from Userspace](https://xcellerator.github.io/posts/linux_rootkits_05/) * [rootkit-kernel-module](https://github.com/croemheld/lkm-rootkit)