資料整理: jserv
本文從分析 Hello World 等級的 Linux 核心模組出發,探究 Linux 核心掛載和卸載核心模組背後的運作機制,理解這些概念後,再實作可自我隱藏蹤跡的 Linux 核心模組 作為驗證。
「幹壞事是進步最大的原動力」 – gslin
不免俗,從 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?
5.4.0
的版本,例如 5.4.0-66-generic
。若在你的開發環境中,核心版本低於 5.4
的話,需要更新 Linux 核心,請自行參照相關文件linux-headers
套件 (注意寫法裡頭有 s
),以 Ubuntu Linux 20.04 LTS 為例:
linux-headers
套件已正確安裝於開發環境
預期得到以下輸出:
jserv
(或者你安裝 Ubuntu Linux 指定的登入帳號名稱)。由於測試過程需要用到 sudo,請一併查驗:
預期輸出是 root
sudo
之後的實驗中,若濫用 root 權限,可能會破壞 GNU/Linux 開發環境 (當然,你還是可重新安裝),現在開始養成好習慣
建立 hello
目錄並在其中建立 Makefile
及 hello.c
hello.c
Makefile
注意
clean:
下一行rm -rf
之前要用 tab 分隔,而非空白
編譯核心模組的命令:
成功後會產出許多檔案,這邊我們會用到的只有 hello.ko
值得注意的是你可能在編譯過程結果會看到下面的敘述:
表示你的編譯器版本與kernel開發套件所用的編譯器版本不一致,建議安裝對應的版本避免後續開發出問題。
產生 hello.ko
之後,可將其掛載:
dmesg
命令可顯示核心訊息
其卸載稍早載入的 hello
核心模組:
dmesg
顯示核心訊息
以上為第一個 Linux 核心模組的撰寫、掛載及卸載。
繼續參閱以下教材:
fibdrv
: 可輸出 Fibonacci 數列的 Linux 核心模組取得原始程式碼
編譯並測試
預期會看到綠色的 Passed [-]
字樣,隨後是
這符合預期,因為給定的 fibdrv
存在缺陷。
就因世界不完美,才有我們工程師存在的空間。
觀察產生的 fibdrv.ko
核心模組
預期可得以下輸出:
觀察 fibdrv.ko
核心模組在 Linux 核心掛載後的行為(要先透過 insmod
將模組載入核心後才會有下面的裝置檔案 /dev/fibonacci
)
新建立的裝置檔案 /dev/fibonacci
,注意到 236
這個數字,在你的電腦也許會有出入。試著對照 fibdev.c,找尋彼此的關聯。
預期輸出是 0.1
,這和 fibdev.c 透過 MODULE_VERSION
所指定的版本號碼相同。
這兩道命令的輸出都是 0
,意味著目前的 reference counting。
延伸閱讀:
MODULE_LICENSE
, MODULE_AUTHOR
, MODULE_DESCRIPTION
, MODULE_VERSION
等巨集做了什麼事,可以讓核心知曉呢? insmod
這命令背後,對應 Linux 核心內部有什麼操作呢?這些巨集本質上就是在編譯過後在 .ko
檔 (ko
即 kernel object 之意,對比使用者層級的 shared object [相當於 Microsoft Windows 的 DLL],可對照 你所不知道的 C 語言:動態連結器篇) 中提供相對應的資訊,由於性質相同,這邊就先專注於 MODULE_AUTHOR
。在範例程式中我們指定 module 的作者
以下摘自 include/linux/module.h:
上述註解說明 _author
的格式和若有多個 author 則應該呼叫多次 MODULE_AUTHOR
。
若將巨集展開應得 MODULE_INFO(author, "National Cheng Kung University, Taiwan")
繼續將上述展開得 __MODULE_INFO(author, author, "National Cheng Kung University, Taiwan")
以下摘自 include/linux/moduleparam.h:
上述巨集的定義根據 MODULE
是否有被定義,MODULE
是在此核心模組被編譯時期所定義,若此模組編譯時已內建於核心則不會被定義。繼續將上述巨集展開
以下摘自 include/linux/compiler-gcc.h:
繼續將 __UNIQUE_ID 展開,__COUNTER__
這個巨集由 GCC 自動更新,每當遇到使用到 __COUNTER__
就會將其值加一。
__UNIQUE_ID
會根據參數產生一個不重複的名字(參考linux/compiler.h),其中使用到的技術是利用巨集中的 ##
來將兩個引數合併成一個新的字串。__attribute__
關鍵字告訴編譯器,這段訊息
.modinfo
段__stringify
的目的是為了把引數轉換成字串形式。以 MODULE_LICENSE("Dual MIT/GPL")
為例,被展開後的 __stringify(tag) "=" info
會是 "license = Dual MIT/GPL"
字串。總結這部份,MODULE_XXX
系列的巨集在最後都會被轉變成
再放到 .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:
繼續展開:
注意到 #
和 ##
這兩個都是 preprocessor 語法,請參照 你所不知道的 C 語言:前置處理器應用篇 以得知詳細用法。
做最後的展開能夠得到以下的結果
根據 GNU GCC 文件說明對於 Variable attribute 的解說,section
會特別將此 variable 放到指定的 ELF section 中,這邊為 .modinfo
。關於 ELF 的資訊,請參照 你所不知道的 C 語言:連結器和執行檔資訊。
上述可以看到 author 的資訊被寫入到 .modinfo
section 中。
更進一步,用 vim 打開 fibdrv.ko
,並且使用 16 進位模式閱讀,可以在 readelf 輸出的 modinfo
區段的 offset
(560) 中找到下面內容:
再來看執行 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.
這邊利用 strace 追蹤執行 insmod fibdrv.ko
的過程有哪些系統呼叫被執行:
自上述第 18 行可以發現呼叫到 finit_module。去查看 linux 核心中如何宣告和實作 finit_module
。
在第 4 行可以發現執行 load_module
這個函式:
在 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 核心做了什麼事呢?首先,先看看原始碼
以下摘自 include/linux/module.h:
在第 5 行,可以看到 gcc 會在編譯過後將 initfn
設為 int init_module(void)
的別名。
__attribute__((alias( ..)))
的用法。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),改成上述命令可以產出一段結果 (但與教材的預期結果不一樣)
在 linux/module.h
裏面,有兩處定義 module_init
,分別是
還沒定義 MODULE
的
還有已定義 MODULE
的
從前置處理過後的結果可知,這裡選用沒定義 MODULE
的 module_init(x)
,也就是之後會繼續展開 __initcall(x)
。
__initcall(x)
巨集被定義在 linux/init.h
device_initcall
展開成 __define_initcall(fn, 6)
,最後會變成我們預處理看到的結果。最後展開的巨集
再來解讀: 首先是第一段 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
取一個別名。
之所以要這樣做,是因前面的地方有寫到
這行告訴我們說,有 init_module
可以使用,但是不在這個地方實作。那實作在什麼地方呢?就是我們寫的 init_fib_dev
,因為我們把 init_module
取了一個別名叫作 init_fib_dev
。
總結一下,module_init
巨集幫我們做 2 件事
init_module
和傳入的函式關聯起來,因為 insmod
指令實作內部會呼叫 init_module
。如此一來呼叫 init_module
就等同於呼叫我們自己寫的函式。透過以下實驗可確認否達到別名的效果:
編譯並執行:
因此執行 init_module()
就相當於執行使用者自定義的函式 initfn
。
再來繼續回到為什麼 init_module 會被執行?
就要回想系統會呼叫 finit_module
再來 load_module
。
摘自 kernel/module.c:
自第 11 行可發現,在 do_init_module
之前,核心先做 layout_and_allocate
為載入的 module 進行記憶體配置。最後在第 19 行對 module 做初始化。
摘自 kernel/module.c:
摘自 init/main.c:
可見到 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 個部分:
Section(s)
有系統預定義的 section,如 .text
, .data
, .bss
等等,但也有使用者定義的 section,在本例中就有 .modinfo
.
Section Header(s)
有對應的關於每個 section 的 metadata。例如某 Section 的 size。
再來看 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。
因此我們需要透過 insmod
這個程式(可執行檔)來將 fibdrv.ko
植入核心中。kernel module 是執行在 kernel space 中,但是 insmod fibdrv.ko
是一個在 user space 的程序,因此在 insmod
中應該需要呼叫相關管理記憶體的 system call,將在 user space 中 kernel module 的資料複製到 kernel space 中。
回頭看之前說 insmod
會使核心執行 finit_module
在第 10 行核心會讀取一個檔案,在本例中,就是 fibdrv.ko
:
可見上述執行 strace insmod fibdrv.ko
後,在第 5 行開啟 fibdrv.ko
這個檔案並得到其 file descriptor 為 3
。並在第 8 行傳入 finit_module
中。
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
:
sysfs_create_file
函式的第二個參數為 &temp_attr->attr
,而 temp_attr
是個指向
struct module_attribute 的指標:
第 1 行宣告 attr
,其型態為
struct attribute ( 定義於 /include/linux/sysfs.h ) :
延伸閱讀: 《The Linux Kernel Module Programming Guide》
所謂「可自我隱藏」的 Linux 核心模組就是指,一旦掛載後,難以利用工具 (如 lsmod
命令) 得知其蹤跡,許多 rootkit 會採取這個策略,以欺瞞 Linux 系統管理者,從而為所欲為。本文並非探討 rootkit,但解析為何一個 Linux 核心模組得以匿蹤,不啻是理解 Linux 核心的切入點 —— 我們要充分考慮到 Linux 核心的運作機制,才能有效隱藏 Linux 核心模組。
lkm-hidden 是個 Linux 核心模組,有限度地展現上述隱藏自己的存在。在 main.c 使用巨集宣告:
這則 Catch Me If You Can 訊息,暗示這個核心模組掛載後,難以找到其蹤跡。
主要程式碼在 main.c 的 hide_myself
函式:
前述提及 THIS_MODULE
巨集的作用,我們利用鏈結串列的行為,予以「脫鉤」,這樣才不會在 /proc/module
被發現。此外,當我們執行以下命令:
清楚可見 Linux 核心個別函式對應的記憶體地址範圍,這樣會洩漏 Linux 核心模組的執行,於是也該迴避。這裡利用取得 vmap_area_list
和 vmap_area_root
符號,並變更內容。
延伸閱讀: