owned this note changed a year ago
Published Linked with GitHub

Linux 核心模組運作原理

資料整理: jserv

本文從分析 Hello World 等級的 Linux 核心模組出發,探究 Linux 核心掛載和卸載核心模組背後的運作機制,理解這些概念後,再實作可自我隱藏蹤跡的 Linux 核心模組 作為驗證。

「幹壞事是進步最大的原動力」 gslin

基本概念

鳥哥私房菜: Linux 核心編譯與管理

從 Hello World 開始

不免俗,從 Hello world 開始。

前期準備

自從 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?

  • 檢查 Linux 核心版本
    ​​​​$ uname -r
    
    預期是大於等於 5.4.0 的版本,例如 5.4.0-66-generic。若在你的開發環境中,核心版本低於 5.4 的話,需要更新 Linux 核心,請自行參照相關文件
  • 安裝 linux-headers 套件 (注意寫法裡頭有 s),以 Ubuntu Linux 20.04 LTS 為例:
    ​​​​$ sudo apt install linux-headers-`uname -r`
    
  • 確認 linux-headers 套件已正確安裝於開發環境
    ​​​​$ dpkg -L linux-headers-`uname -r` | grep "/lib/modules/.*/build"
    
    預期得到以下輸出:
    ​​​​/lib/modules/5.4.0-66-generic/build
    
  • 檢驗目前的使用者身份
    ​​​​$ whoami
    
    預期為「不是 root 的使用者名稱」,例如 jserv (或者你安裝 Ubuntu Linux 指定的登入帳號名稱)。由於測試過程需要用到 sudo,請一併查驗:
    ​​​​$ sudo whoami
    
    預期輸出是 root
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
    在下列操作中,請避免用 root 帳號輸入命令,而該善用 sudo

    之後的實驗中,若濫用 root 權限,可能會破壞 GNU/Linux 開發環境 (當然,你還是可重新安裝),現在開始養成好習慣

程式碼準備

建立 hello 目錄並在其中建立 Makefilehello.c

  • hello.c
#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 分隔,而非空白

建構核心模組

編譯核心模組的命令:

$ 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 之後,可將其掛載:

$ sudo insmod hello.ko

dmesg 命令可顯示核心訊息

[ 3824.676183] Hello, world

卸載核心模組

其卸載稍早載入的 hello 核心模組:

$ sudo rmmod hello

dmesg 顯示核心訊息

[ 3824.676183] Hello, world
[ 3921.232076] Goodbye, cruel world

以上為第一個 Linux 核心模組的撰寫、掛載及卸載。

繼續參閱以下教材:


fibdrv: 可輸出 Fibonacci 數列的 Linux 核心模組

取得原始程式碼

$ git clone https://github.com/sysprog21/fibdrv
$ cd fibdrv

編譯並測試

$ make check

預期會看到綠色的 Passed [-] 字樣,隨後是

f(93) fail
input: 7540113804746346429
expected: 12200160415121876738

這符合預期,因為給定的 fibdrv 存在缺陷

就因世界不完美,才有我們工程師存在的空間。

觀察產生的 fibdrv.ko 核心模組

$ 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

$ ls -l /dev/fibonacci
$ cat /sys/class/fibonacci/fibonacci/dev

新建立的裝置檔案 /dev/fibonacci,注意到 236 這個數字,在你的電腦也許會有出入。試著對照 fibdev.c,找尋彼此的關聯。

$ cat /sys/module/fibdrv/version 

預期輸出是 0.1,這和 fibdev.c 透過 MODULE_VERSION 所指定的版本號碼相同。

$ lsmod | grep fibdrv
$ cat /sys/module/fibdrv/refcnt

這兩道命令的輸出都是 0,意味著目前的 reference counting

延伸閱讀:


Linux 核心模組掛載機制

  • 檔案 fibdrv.c 裡頭的 MODULE_LICENSE, MODULE_AUTHOR, MODULE_DESCRIPTION, MODULE_VERSION 等巨集做了什麼事,可以讓核心知曉呢? insmod 這命令背後,對應 Linux 核心內部有什麼操作呢?

這些巨集本質上就是在編譯過後在 .ko 檔 (ko 即 kernel object 之意,對比使用者層級的 shared object [相當於 Microsoft Windows 的 DLL],可對照 你所不知道的 C 語言:動態連結器篇) 中提供相對應的資訊,由於性質相同,這邊就先專注於 MODULE_AUTHOR。在範例程式中我們指定 module 的作者

MODULE_AUTHOR("National Cheng Kung University, Taiwan");

以下摘自 include/linux/module.h:

/*
 * 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")

/* 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:

#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 是在此核心模組被編譯時期所定義,若此模組編譯時已內建於核心則不會被定義。繼續將上述巨集展開

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:

#define __UNIQUE_ID(prefix) __PASTE(__PASTE(__UNIQUE_ID_, prefix), __COUNTER__)

繼續將 __UNIQUE_ID 展開,__COUNTER__ 這個巨集由 GCC 自動更新,每當遇到使用到 __COUNTER__ 就會將其值加一。

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),其中使用到的技術是利用巨集中的 ## 來將兩個引數合併成一個新的字串。
  2. 透過 __attribute__ 關鍵字告訴編譯器,這段訊息
    1. 要被放在 .modinfo
    2. 應該不會被程式使用到,所以不要產生警告訊息
    3. 最小的對齊格式需要是 1 bit

    延伸閱讀: Specifying Attributes of Variables

  3. linux/stringfy.h 裡頭,我們可以看到 __stringify 的目的是為了把引數轉換成字串形式。以 MODULE_LICENSE("Dual MIT/GPL") 為例,被展開後的 __stringify(tag) "=" info 會是 "license = Dual MIT/GPL" 字串。

總結這部份,MODULE_XXX 系列的巨集在最後都會被轉變成

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:

/* Indirect macros required for expanded argument pasting, eg. __LINE__. */
#define ___PASTE(a,b) a##b
#define __PASTE(a,b) ___PASTE(a,b)

繼續展開:

static const char __UNIQUE_ID_author0[]			    \
  __used __attribute__((section(".modinfo"), unused, aligned(1)))	  \
  = __stringify(author) "=" "National Cheng Kung University, Taiwan"

摘自 include/linux/stringify.h:

#define __stringify_1(x...)	#x
#define __stringify(x...)	__stringify_1(x)

注意到 ### 這兩個都是 preprocessor 語法,請參照 你所不知道的 C 語言:前置處理器應用篇 以得知詳細用法。

做最後的展開能夠得到以下的結果

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 語言:連結器和執行檔資訊

$ 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 輸出的 modinfo 區段的 offset (560) 中找到下面內容:

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 第 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

這邊利用 strace 追蹤執行 insmod fibdrv.ko 的過程有哪些系統呼叫被執行:

$ 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。去查看 linux 核心中如何宣告和實作 finit_module

kernel/module.c

SYSCALL_DEFINE3(finit_module, int, fd, const char __user *, uargs, int, flags) { // ... return load_module(&info, uargs, flags); }

在第 4 行可以發現執行 load_module 這個函式:

/* 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 已在新版拆解成若干檔案,前述程式碼如今在 kernel/module/main.c

而在註解的部分可以看到 load_module 大致就是 Linux 核心為模組配置記憶體和載入模組相關資料的地方。

  • 當我們透過 insmod 去載入一個核心模組時,為何 module_init 所設定的函式得以執行呢?Linux 核心做了什麼事呢?

首先,先看看原始碼

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:

/* 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) 的別名。

  1. 請參閱 GCC 手冊,得知 __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

在核心 v.6.5.0 ,輸入上述第二點的 gcc 命令會導致編譯錯誤 (找不到 asm/rwonce.h),改成上述命令可以產出一段結果 (但與教材的預期結果不一樣)

gcc -E fibdrv.c -I$TREE/include -I$TREE/arch/x86/include 
                                   -I$TREE/arch/x86/include/generated

linux/module.h 裏面,有兩處定義 module_init,分別是
還沒定義 MODULE

#ifndef MODULE
#define module_init(x)	__initcall(x);

還有已定義 MODULE

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

從前置處理過後的結果可知,這裡選用沒定義 MODULEmodule_init(x) ,也就是之後會繼續展開 __initcall(x)

__initcall(x) 巨集被定義在 linux/init.h

#define __initcall(fn) device_initcall(fn)

device_initcall 展開成 __define_initcall(fn, 6),最後會變成我們預處理看到的結果。最後展開的巨集

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 和 C++ 的 static assertion 相似。

再來是 init_module,我們知道這個系統呼叫讓我們把一個 ELF image 載入到 kernel space,而在最後一行 int init_module(void) __attribute__((alias("init_fib_dev"))) 的目的是為了替 init_module 取一個別名。

之所以要這樣做,是因前面的地方有寫到

/* 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 就等同於呼叫我們自己寫的函式。

透過以下實驗可確認否達到別名的效果:

#include <stdio.h>
int __func() {
    printf("In __func()\n");
    return 0;
}
int func() __attribute__((alias("__func"))); /* no function body */
int main() {
    func();
    return 0;
}

編譯並執行:

$ gcc -o test test.c
$ ./test
In __func()

因此執行 init_module() 就相當於執行使用者自定義的函式 initfn

再來繼續回到為什麼 init_module 會被執行?
就要回想系統會呼叫 finit_module 再來 load_module

摘自 kernel/module.c:

/* 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:

/*
 * 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:

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 的訊息
$ 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
  1. Section(s)
    有系統預定義的 section,如 .text, .data, .bss 等等,但也有使用者定義的 section,在本例中就有 .modinfo.

  2. Section Header(s)
    有對應的關於每個 section 的 metadata。例如某 Section 的 size。

$ 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。

$ 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:

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:

$ 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 第 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 提到,在 /sys/module/"module-name" 目錄中會有一些相關檔案,這些檔案分別紀錄了此 module 的一些資料,例如,傳入的參數值;另外,在 /kernel/module.c - 1703 行 中, module_add_modinfo_attrs() 中,第 19行有 sysfs 相關的函式 sysfs_create_file:

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 {
	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 ( 定義於 /include/linux/sysfs.h ) :

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

可自我隱藏的 Linux 核心模組

所謂「可自我隱藏」的 Linux 核心模組就是指,一旦掛載後,難以利用工具 (如 lsmod 命令) 得知其蹤跡,許多 rootkit 會採取這個策略,以欺瞞 Linux 系統管理者,從而為所欲為。本文並非探討 rootkit,但解析為何一個 Linux 核心模組得以匿蹤,不啻是理解 Linux 核心的切入點 —— 我們要充分考慮到 Linux 核心的運作機制,才能有效隱藏 Linux 核心模組。

lkm-hidden 是個 Linux 核心模組,有限度地展現上述隱藏自己的存在。在 main.c 使用巨集宣告:

MODULE_DESCRIPTION("Catch Me If You Can");

這則 Catch Me If You Can 訊息,暗示這個核心模組掛載後,難以找到其蹤跡。

主要程式碼在 main.chide_myself 函式:

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 被發現。此外,當我們執行以下命令:

sudo cat /proc/vmallocinfo | less

清楚可見 Linux 核心個別函式對應的記憶體地址範圍,這樣會洩漏 Linux 核心模組的執行,於是也該迴避。這裡利用取得 vmap_area_listvmap_area_root 符號,並變更內容。

延伸閱讀:

Select a repo