---
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](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)