2021 年暑期「Linux 核心」
contributed by <u1f383>
由於當初以為在 10 分鐘之內就要送出答案,因此沒能好好完成作答區的問題,在此補上答案:
AAA = list_for_each_entry_safe
BBB = list_for_each_entry_safe
CCC = list_add_tail(&proc->list_node, &hidden_proc);
DDD = list_del(&proc->list_node);
kernel module 在 init function 時透過 device_create()
註冊了 device,其中也包含定義對 device 執行 open
release
read
write
所呼叫到的 function,比較重要的為 read
以及 write
:
當執行 read
時會呼叫 device_read
,遍歷所有的 hidden_proc
,並且將每個 process 的 pid 連接後回傳給使用者。
當執行 write
時會呼叫 device_write
,當接收到的 data 格式為 add <pid>
,會執行 hide_process
把 <pid>
加入 hidden process 的 list 當中;當接收到的 data 格式為 del <pid>
,會執行 unhide_process
把 <pid>
從 hidden process 的 list 當中移除。
而在 init function 的最後呼叫 init_hook
,初始化 ftrace 相關 structure,流程大致如下:
init_hook
中初始化了 hook 的名稱 "find_ge_pid"
、hook function hook_find_ge_pid
以及要被 hook 的 function find_ge_pid
hook_install
中,設定了 ftrace rule,rule 大致上是當 kernel 執行到 find_ge_pid
時,會先去執行 filter function hook_ftrace_thunk
,hook_ftrace_thunk
會先判斷 find_ge_pid
是否由當前 module 所呼叫,若不是的話,會將 instruction pointer (ip) 指向先前設定的 hook function hook_find_ge_pid
register_ftrace_function
註冊 ftrace function,最後離開程式根據 find_ge_pid
的註解說明,此 function 用在找尋大於等於 (ge) 給定 pid 的 process 的 struct pid,在 userland 中找尋 process 相關的命令如 ps
皆會使用到,然而 ftrace 會在真正執行之前加上 hook function 來做處理,讓我們看一下 hook function hook_find_ge_pid
的行為:
hook_find_ge_pid
會先取得原本執行 real_find_ge_pid
的結果,如果回傳的 pid 存在於 hidden_proc,也就是先前透過 write
來定義的 pid list,就會將 pid + 1 後在執行 real_find_ge_pid
。該步驟重複執行到 pid 不在 hidden_proc
當中,就會將結果回傳,繼續執行。
做個總結:
write
傳入如 add <pid>
的字串,讓 module 把 <pid>
加到 hidden_proc
ps
相關取得 process 資訊的指令時,ftrace function 會被 trigger,並且回傳不在 hidden_proc
中大於等於 pid 的 process 的 struct pid2020 年的變更 Unexporting kallsyms_lookup_name()
Access to kallsyms on Linux 5.7+
linux kernel document 中有提到 linux kenrel 提供許多機制可以做程式執行重導向,目的是讓 user 能夠在不重開機的情況下,能夠修補引發錯誤的 critical function。透過這些機制能夠取得 function pointer 並使用,而在本實驗中也可以用來取得 kallsyms_lookup_name
的 function pointer。
而在這邊我選擇使用 livepatch 的方式,並參考 kallsyms-mod repo 說明並加以研究:
將上述程式碼加進 hideproc.c
後,並需要額外 include <linux/livepatch.h>
,以及定義 module info MODULE_INFO(livepatch, "Y");
就能順利執行。
而這邊有個匪夷所思的地方,如果 livepatch 沒有加上 obj kallsyms_failing_name
則無法順利執行,但是我有嘗試執行過官方提供的 sample code 來執行,除了最後要使用 echo 0 | sudo tee /sys/kernel/livepatch/hideproc/enabled
關閉 livepatch 才能順利 remove module 之外,也沒有特別加上 failing function:
當使用者傳送的資料格式為 add <pid1>{spaces}<pid2>{spaces}<pidN>
或是 del <pid1>{spaces}<pid2>{spaces}<pidN>
,hideproc 可以透過 strsep
parse 傳入的 pids 做到一次新增/刪除多個 pid。
在隱藏指定 pid 時,先去看是否 pid 有 parent,如果有的話也把他加到 hidden_proc 當中。而檢察是否有 parent 的方式為: 先用 pid_task()
取得對應 pid 的 task_struct
,而其中 member parent
則是指向 parent 的 task_struct
,如果 pointer 存在代表有 parent,即可從 member pid
取得 parent pid。
從功能方面來看,由於使用 lsmod
指令仍可以看到此 module,作為 rootkit 來使用容易被發現,因此效法 hidden process 的方法,透過 hook 的方式讓 module 不會在 lsmod
中顯示,但是仍會出現在目錄 /dev
底下。
資源使用方面補齊了 _hideproc_exit()
當中需要 release 資源的部分,以免造成 memory leak:
然後我發現 read
的部分可能會有 overflow 的問題:
其中 proc->id
的型態是 pid_t
:
pid 的上限為 4194304 (/proc/sys/kernel/pid_max
),而透過 sprintf()
產生出來的最長字串為 pid: 4194304\n
13 bytes,但是 sprintf()
結尾會有一個 NULL byte,可以透過簡單的 c 程式做範例:
在編譯時期 gcc 就會跳出警告:
寫入前執行 set *(int64_t*)(0x7fffffffdd2b+8)=0xdeadbeefdeadbeef
幫助我們觀察:
寫入後:
在第 14 個 byte 被寫入 00 (NULL byte),因此可能會造成
這只是正常情況下會發生的問題,然而我們沒辦法保證使用者傳入的 pid
是否是正常的 pid 範圍 (1~4194304),至少因為 pid 的型態是 int
,最多可以到 2147483647
,然而 10 bytes 的字串就足夠蓋到變數 message[]
正常能夠存取到的地方了。
而如果電腦是 32 位元,是否會有更大的問題?
解決方法有二:
/proc/sys/kernel/pid_max
所規範由於是從 kernel space 將資料 copy 到 userland,我們會好奇是否能透過 device_read
取得 kernel 記憶體位置,不過的是這邊使用的是 sprintf
,結尾必定會加上 NULL byte 來截斷,因此沒辦法來 leak kernel address;而因為 buffer 下面的就是 canary 了,因此能 overflow 的部分也沒辦法做利用。
最後有發現 hidden_proc 並沒有檢測是否出現 duplicate pid,因此可以在每次新增 list node 時做檢查。