or
or
By clicking below, you agree to our terms of service.
New to HackMD? Sign up
Syntax | Example | Reference | |
---|---|---|---|
# Header | Header | 基本排版 | |
- Unordered List |
|
||
1. Ordered List |
|
||
- [ ] Todo List |
|
||
> Blockquote | Blockquote |
||
**Bold font** | Bold font | ||
*Italics font* | Italics font | ||
~~Strikethrough~~ | |||
19^th^ | 19th | ||
H~2~O | H2O | ||
++Inserted text++ | Inserted text | ||
==Marked text== | Marked text | ||
[link text](https:// "title") | Link | ||
 | Image | ||
`Code` | Code |
在筆記中貼入程式碼 | |
```javascript var i = 0; ``` |
|
||
:smile: | ![]() |
Emoji list | |
{%youtube youtube_id %} | Externals | ||
$L^aT_eX$ | LaTeX | ||
:::info This is a alert area. ::: |
This is a alert area. |
On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?
Please give us some advice and help us improve HackMD.
Syncing
xxxxxxxxxx
Linux 核心模組運作原理
本文從分析 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?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
- 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 →sudo
之後的實驗中,若濫用 root 權限,可能會破壞 GNU/Linux 開發環境 (當然,你還是可重新安裝),現在開始養成好習慣
程式碼準備
建立
hello
目錄並在其中建立Makefile
及hello.c
hello.c
Makefile
建構核心模組
編譯核心模組的命令:
成功後會產出許多檔案,這邊我們會用到的只有
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。延伸閱讀:
Linux 核心模組掛載機制
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:大致的意思是把 string literal 並排,等同於一個合併起來的字串。
以下摘自 include/linux/compiler_types.h:
繼續展開:
摘自 include/linux/stringify.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 章的資訊:
可知
module_init
巨集在編譯出來的 object 中,加入初始化模組函數的起始位置。類似地,module_exit
的相關敘述:這邊利用 strace 追蹤執行
insmod fibdrv.ko
的過程有哪些系統呼叫被執行:自上述第 18 行可以發現呼叫到 finit_module。去查看 linux 核心中如何宣告和實作
finit_module
。kernel/module.c
在第 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.hdevice_initcall
展開成__define_initcall(fn, 6)
,最後會變成我們預處理看到的結果。最後展開的巨集再來解讀: 首先是第一段
static inline ....
,這邊用了一個小技巧讓我們可在編譯時期就知道傳入的 function pointer 是不是合法的。我們回傳的return init_fib_dev
,他的資料型態必須要和initcall_t
相同,否則編譯器會報錯。再來是 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 個部分:存放了有關於此 object file 的訊息
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
的描述。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
摘自 linux/kernel/module.c:
在第 10 行核心會讀取一個檔案,在本例中,就是
fibdrv.ko
:可見上述執行
strace insmod fibdrv.ko
後,在第 5 行開啟fibdrv.ko
這個檔案並得到其 file descriptor 為3
。並在第 8 行傳入finit_module
中。Patrick Mochel 撰寫的報告 The sysfs Filesystem 第 1 頁提到:
並在第 5 頁的 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 核心模組
所謂「可自我隱藏」的 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
符號,並變更內容。延伸閱讀: