(C)
contributed by < cwl0429
>
這次測驗的目標是利用 lkm 來變更特定 Linux 行程的內部狀態,需要瞭解 workqueue
, signal
等機制的原理及使用方法,以下內容皆以 Linux v5.13 為基礎
workqueue 是 linux 內用來處理非同步行程 execution context 的機制
在 workqueue 中,work item 是一種簡易的結構,定義在 workqueue.h
可以觀察到 work_struct
包含一個指標 func
,其指到欲執行的 function,能讓 worker 在處理 work 時能藉此找到要完成的工作
worker 是 workqueue 中執行 work item 的個體,struct worker
定義在 workqueue_internal.h
struct worker
內有多個 struct
記錄執行工作的狀態或內容,像是
*current_work
指向當前 work*pool
指向此 worker 所屬的 worker_pool*task
則紀錄當前 process 的狀態其中 tast_struct
是本次測驗的重點
task_struct
在核心中,行程或執行緒都可歸類於 task,後者對應到一個 task_struct
,task_struct
可視為 process descriptor 或是恐龍書提到的 PCB,所有關於 task 的資訊皆被存放於此。
舉例來說,task_struct
有成員 __state
,tsk->__state
能記錄此 task 的狀態為何,可能是 TASK_RUNNING, TASK_INTERRUPTIBLE
或 TASK_UNINTERRUPTIBLE
等,state machine 變化如下圖所示
其中 states of process 定義在 include/linux/sched.h
TASK_RUNNING
代表 task 已經準備好執行或正在執行,也就是 task 已排入 run queue 內TASK_INTERRUPTIBLE
及 TASK_UNINTERRUPTIBLE
皆代表 task 正在等待某些條件成立而處於休眠狀態,前者可被 signal 喚醒,後者不行,但二者都會定期地被 try_to_wake_up()
嘗試喚醒__TASK_TRACED
代表有其他的 process 正在追蹤此 task,常用於 ptrace 和 ftrace 等 debugger__TASK_STOPPED
代表 task 已停止執行且不再被排程,通常是收到 stop signal 或任何來自追蹤中程式的 signal另外,還需特別提到 task_struct
內的兩名成員 ptraced
及 ptrace_entry
在 dont_trace lkm 中,便是利用上述二成員找出正在使用 ptrace() 的 tracer 及其 tracee
task_struct 完整程式碼
在介紹 jiffy 前,需先瞭解 linux kernel HZ 及 tick 的定義
linux kernel 會在固定周期發出 timer interrupt (IRQ 0),HZ 是用來定義一秒內會有幾次 timer interrupt,假設 HZ 為 100 則代表每秒發生 timer interrupt 100 次,可利用 getconf CLK_TCK 得知當前裝置的 HZ
而 tick 是 HZ 的倒數,也就是 timer interrupt 每過多少時間會觸發,舉例來說當 HZ 為 100 時,則 tick 為 10 毫秒
jiffies 是個全域變數,用於紀錄開機後產生了幾次 tick。
開機時,kernel 會將 jiffies 初始成 0,並且在每次 timer interrupt 發生後,jiffies 便加一。
以 32 位元的系統為例,當 HZ 為 100,一天會產生 86400 * 100 次 timer interrupt
jiffies 約在 497.1 天產生溢位,若是 HZ 為 1000,則 jiffies 會在 49.7 天產生溢位
為了解決這個問題,linux/jiffies.h 內提供 4 個 marco
舉 time_after(unknown, known)
為例,
則可以得到下列式子
(int8_t) 253 - (int8_t) 2 == -3 - 2 == -5 < 0
利用有號數的機制判斷
為了方便計算,將 long 改為 int_8 處理
所以即便是 jiffies 發生溢位,也能藉由這些 macro 取得正確的 jiffies
最後,有了以上資訊,可以推論出 jiffies, HZ, seconds 三者之間的關係
signal 是 UNIX 及 Linux 為了回應某些狀況所生成的 event,process 可能會根據收到的 signal 做出對應行為
常見的 signal 如下表所示
Signal Name | Signal Number | Description |
---|---|---|
SIGHUP | 1 | Hang up detected on controlling terminal or death of controlling process |
SIGINT | 2 | Issued if the user sends an interrupt signal (Ctrl + C) |
SIGQUIT | 3 | Issued if the user sends a quit signal (Ctrl + D) |
SIGFPE | 8 | Issued if an illegal mathematical operation is attempted |
SIGKILL | 9 | If a process gets this signal it must quit immediately and will not perform any clean-up operations |
SIGALRM | 14 | Alarm clock signal (used for timers) |
SIGTERM | 15 | Software termination signal (sent by kill by default) |
在 task_struct 內,包含以下成員
一個 thread group 內的所有 task 會使用相同的 signal 及 sighand,用途分別為:
linux 不論是 system call 或 user program 發送 signal 時,最終都會使用到 __send_signal
來處理
__send_signal 完整程式碼
首先 __send_sig
會根據 pid_type 來決定 pending 的對象
當 type 不為 PIDTYPE_PID 時,送出的 signal 會作用 thread group 的所有 thread,反之則作用於單一 thread
接著,假設要送出的 signal 為 SIGKILL,因為 SIGKILL 緣故,不需對 siginfo 作資源分配,故使用 goto 技巧,跳到 tag: out_set
siginfo 內會記錄此 signal 的相關資訊
在 tag: out_set 階段
利用 macro sigaddset(&pending->signal, sig)
將 SIGKILL 存放至 peding->signal 內
當前置作業皆完成後,kernel 利用 complete_signal(sig, t, type)
通知 task 盡快處理收到的 signal
細看 complete_signal
的實際做法
首先建立二指標,*signal
及 *t
,其中 signal 會指向 p->signal
,也就是先前 pending 過的 signal
接著會出現二種情況,分別為
最後檢查 task 是否佇列於 runqueue 內
若是 task 不在 runqueue 內,則嘗試將其放入 runqueue,否則將 task 從 runqueue 內移除,希望能在下次放入 runqueue 時做 signal 的處理
(待補..)
ptrace()
是一種 system call,能使一個 process (the "tracer") 去觀察和控制另一個 process (the "tracee"),主要用於偵錯時的中斷點設置及追蹤 system call
追蹤 process 的方式如下
fork
一個 child processexecve
執行 PTRACE_TRACEME
,此時 parent process 便開始追蹤 traceePTRACE_PEEKTEXT
, PTRACE_PEEKUSER
等等以下面範例做說明,範例改自Playing with ptrace, Part I
PTRACE_PEEKUSER
觀察 child process 的 ORIG_RAX
內容為何PTRACE_CONT
使 child process 繼續執行因此,可觀察到在 tracer 執行 PTRACE_CONT
後,ls 命令才被 child process 執行
有了上述基礎,開始探討本次考試內容
dont_trace
這個 Linux 核心模組利用 task_struct 內部成員 ptrace 來偵測給定的行程是否被除錯器或其他使用 ptrace 系統呼叫的程式所 attach,一旦偵測到其他行程嘗試追蹤該行程,dont_trace 就會主動 kill 追蹤者行程。流程:
瞭解 module 的詳細流程後,觀察實作方式
首先,該如何找出 "ptracing" 中的程式呢?
check()
利用 macro for_each_process
逐一檢查此 task 是否為 tracer,若是找到,則用 kill_task()
對 tracee 及 tracer 送出 SIGKILL
is_tracer()
實際內容如下
&task->ptraced
為 list_head
,其代表 task 正在追蹤的 tracee,能藉由檢查這串 list 是否有對應 tracee task 存在,便可得知 task 是否為 tracer
kill_tracee()
將 tracee 找出並發送 SIGKILL,其找出 tracee 的方式與 is_tracer
類似
瞭解如何刪除 tracer 及 tracee 的方式後,下一個問題是如何定期地執行此 lkm ?
為了定期地執行,此處引入 workqueue 機制,利用 macro DECLARE_DELAYED_WORK
建立一個 work item
此 work item 的工作便是 periodic_routine
另外,在 dont_trace_init
內觀察到 queue_delayed_work
函式及 flag loaded
loaded
代表 dont_trace lkm 已正確載入queue_delayed_work
能在指定的 jiffies 後,將 work 排入 workqueue 執行Description
Returns false if work was already on a queue, true otherwise.
We queue the work to the CPU on which it was submitted, but if the CPU dies it can be processed by another CPU.
bool queue_delayed_work(struct workqueue_struct * wq, struct delayed_work * dwork, unsigned long delay)
queue work on a workqueue after delayParameters
struct workqueue_struct * wq workqueue to use
struct delayed_work * dwork delayable work to queue
unsigned long delay number of jiffies to wait before queueing
接著,觀察 periodic_routine
,發現有二處需實作,根據老師提供的線索,可以推論兩件事
periodic_routine
必須定期被執行periodic_routine
執行時必須確保 dont_trace 是否載入至 kernelloaded
確認 dont_trace 是否載入periodic_routine
內再次呼叫 queue_delayed_work
,重新將 periodic_routine
排入 workqueue 內,達到定期執行的效果最後執行結果如下
dont_trace 完整程式碼