前言
最近完成了一個需要修改和編譯linux內核源碼的操作系統實驗,個人感覺這個實驗還是比較有意思的。這次實驗總共耗時4天,從對linux實現零基礎,通過查閱資料和不斷嘗試,直到完成實驗目標,在這過程中確實也收獲頗豐,特此記錄
實驗內容
- 實現系統調用int hide(pid_t pid, int on),在進程pid有效的前提下,如果on置1,進程被隱藏,用戶無法通過ps或top觀察到進程狀態;如果on置0且此前為隱藏狀態,則恢復正常狀態(考慮權限問題,只有root用戶才能隱藏進程)
- 設計一個新的系統調用int hide_user_processes(uid_t uid, char *binname),參數uid為用戶ID號,當binname參數為NULL時,隱藏該用戶的所有進程;否則,隱藏二進制映像名為binname的用戶進程
- 在/proc目錄下創建一個文件/proc/hidden,該文件可讀可寫,對應一個全局變量hidden_flag,當hidden_flag為0時,所有進程都無法隱藏,即便此前進程被hide系統調用要求隱藏。只有當hidden_flag為1時,此前通過hide調用要求被屏蔽的進程才隱藏起來
- 在/proc目錄下創建一個文件/proc/hidden_process,該文件的內容包含所有被隱藏進程的pid,各pid之間用空格分開
實現思路
對于要求1,首先要修改PCB,對應到源碼里面就是task_struct,在其中添加一個屬性hide,用來表示該進程是否需要隱藏;然后修改復制進程的系統調用,用于給hide屬性設置默認值0;最后修改列舉所有進程的系統調用,在其中加入一個判斷,如果進程的hide是1則不展示這個進程
(注:也有方法說是可以通過把pid設置為0來達到隱藏的效果,但是實測下來,在5.15.60的kernel里面,這樣做不能隱藏,所以只能通過劫持系統調用來實現)
對于要求2,則可以遍歷所有進程,把符合條件的進程的hide設置為1即可
對于要求3,最開始以為可以通過用戶態的文件操作來實現,結果后來發現/proc是個虛擬文件系統,所以需要在初始化proc文件系統時,添加一個hide條目,然后設置這個條目的write函數,來達到創建該文件的目的
對于要求4,也是用和要求3一樣的思路,只是這里需要設置read函數,然后遍歷所有進程,把hide為1的pid全部返回
實驗環境
操作系統使用的ubuntu 22.04
linux kernel代碼版本是5.15.60
虛擬機使用的是VM Ware Workstation Pro 16
注意:虛擬機硬盤大小建議為60GB,編譯內核代碼非常吃硬盤,本人在實驗中前前后后擴容了幾次硬盤,最終發現60GB是個比較合適的大小,內核源碼編譯安裝之后還能剩15GB左右(下一次編譯安裝還需要一些硬盤空間做緩存,所以剩15GB是比較合適的)
實驗流程
編譯與安裝內核
參考https://www.cnblogs.com/robotech/p/16152269.html 即可,如果這部分出錯了,網上可以找到的資料很多,這里不再贅述了
不過這一步一定要有耐心,源碼編譯很慢,第一次全量編譯估計會耗時一個多小時,可以用這個閑暇時間玩玩原神
完成要求一
把編譯和安裝的流程跑通以后,就開始進行源代碼的修改了
修改PCB
linux的PCB結構體是task_struct
,這個定義位于include/linux/sched.h
Tips:如果想在linux源碼里找東西,可以用https://elixir.bootlin.com/ 這個網站,左邊選擇版本,右邊輸入關鍵字,即可查詢到
在linux使用vim打開這個文件,往下翻,看到這段注釋
按照提示添加屬性即可
修改fork
這部分的源碼是在kernel/fork.c
中的copy_process
函數里面
閱讀源碼,在合適的地方插入初始化hide屬性的代碼即可,這里我選擇的位置是復制完進程信息之后,即下圖所示的位置
添加系統調用
這部分我是看網上的各種文章,東拼西湊,進行多次實驗之后才跑通的,事后想想,我應該最先去看linux kernel的官方手冊
https://docs.kernel.org/process/adding-syscalls.html (附上官方手冊)
以下是我自己添加系統調用的過程,這里用添加一個輸出Hello World的簡單系統調用來舉例子
首先找到kernel/sys.c
,在文件末尾使用SYSCALL_DEFINE
宏來定義系統調用的函數體
SYSCALL_DEFINE0(hello)
{printk("hello world.114514\n");return 0;
}
解釋一下,SYSCALL_DEFINE0(hello)
表示定義一個含有0個參數的系統調用,名字是hello,通過查看sys.c
里面其它函數的定義代碼可以得知,如果想要添加一個只有一個參數的系統調用,那么應該使用SYSCALL_DEFINE1(hide,pid_t,pid)
,其中hide是系統調用的名字,pid_t是第一個參數類型,pid是第一個參數的名字,2個參數的同理
printk是輸出日志,這個日志可以在sudo dmesg
里面看到,printk支持使用%d,%s等對輸出進行格式化,用法類似于printf
接下來修改系統調用表,在arch/x86/entry/syscalls/syscall_64.tbl
中合適的地方添加剛才寫的系統調用,這里我是添加在了334號系統調用之后的
仿照上面334寫即可,其中hello是自己隨便起的名字,而sys_hello是系統調用的函數名,這個函數名是上面SYSCALL_DEFINE0
里寫的函數名前面加上前綴sys_得到的
添加完成后可以嘗試編譯運行一下新的內核
可以使用uname -a
查看當前內核是不是最新編譯的(看時間即可)
下面將使用一段代碼來測試一下新添加的335號系統調用
#include <linux/kernel.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <stdio.h>int main(int argc,char **argv)
{printf("System call return %ld\n",syscall(335));return 0;
}
運行程序
然后使用sudo dmesg
查看
編寫hide系統調用
hide系統調用的實現有2個思路,一個是遍歷所有進程,找到pid相符的進程,然后設置hide,另一個思路是通過pid找到進程,然后直接設置,這里采取后者
通過查找資料,可以得知,根據pid查找進程是用這段代碼
pid_task(find_vpid(pid),PIDTYPE_PID);
最終完整的系統調用代碼如下
SYSCALL_DEFINE2(hide,pid_t,pid,int,on)
{struct task_struct * me = NULL;me=pid_task(find_vpid(pid),PIDTYPE_PID);if(current->uid != 0){//User is not rootreturn 0;}if(me == NULL){return 0;}if( on == 1 ){me->hide = 1;}else{if( me->hide == 1 ){me->hide = 0;}}return 0;
}
接下來再修改系統調用表即可完成系統調用的添加
劫持獲取所有進程的函數
現在已經可以通過系統調用來設置PCB里面的hide,下一步就是修改列舉所有進程的函數,讓它在列舉時判斷一下,如果hide==1就不列舉
proc文件系統
在劫持之前,需要簡單介紹一下proc文件系統。在linux根目錄下,有一個/proc文件夾,這其實并不是在磁盤上真實存在的文件,而是一個虛擬文件系統。
proc文件夾里面有很多個以pid為名字的文件夾,這些文件夾里面又有若干個文件,讀取這些文件就可以獲取這個進程的相關信息,例如想查看pid為1的程序的名字可以使用sudo cat /proc/1/comm
這一系列操作在系統底層的實現是:系統在啟動的時候就掛載了一個proc虛擬文件系統,當用戶訪問proc文件夾下的文件時,系統會調用proc文件系統里面相關的函數,而不是常規文件系統的函數,例如在執行ls /proc
時,實際上系統會調用位于s/proc/base.c
里面的proc_pid_readdir
函數,這個函數會獲取當前系統中所有的進程,隨后會有函數把這個函數的返回值寫入到讀取文件操作的緩沖區中
修改代碼
所以,我們的突破口就是proc_pid_readdir
函數,在閱讀這個函數的代碼之后,可以找到突破口是一個put_task_struct
函數的調用,如下圖
那么只需要在這個if里面加上一個條件,即必須這個進程不被隱藏才能put,即可完成劫持
結果驗證
在修改完源代碼之后,重新編譯和安裝內核,啟動新的內核
使用下面這段代碼來測試
#include <linux/kernel.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>int main(int argc,char **argv)
{int pid;int hide;scanf("%d %d",&pid,&hide);printf("System call return %ld\n",syscall(336,pid,hide));return 0;
}
編譯運行程序
從圖中可以看出,進程順利隱藏,并且能夠重新展示,要求一順利實現
完成要求二
要求二是在要求一的基礎上進行一些簡單的擴展,這里可以使用一個比較暴力的思路,就是遍歷所有進程,然后挨個判斷uid和進程名稱,把符合要求的進程的hide設置為1即可
這里只有三點需要注意一下
1.遍歷所有進程可以使用for_each_process
這個宏來完成,這個宏有類似于for循環的作用,用法如下
struct task_struct* p;
for_each_process(p){//Do something.....
}
這個宏的定義在include/linux/sched/signal.h
里面,定義如下
#define for_each_process(p) \for (p = &init_task ; (p = next_task(p)) != &init_task ; )
2.用戶態的字符串不能在內核態直接使用,需要調用strncpy_from_user
把用戶態的字符串復制到內核態的緩沖區才能使用,方法如下
char tmp_buf[256];
if(binname != NULL)strncpy_from_user(tmp_buf,binname,256);
最終的系統調用代碼如下
SYSCALL_DEFINE2(hide_user_processes,uid_t,uid,char*,binname)
{uid_t curr_uid=current->uid;if(curr_uid != 0){//User is not rootreturn 0;}char tmp_buf[256];if(binname != NULL)strncpy_from_user(tmp_buf,binname,256);struct task_struct* p=NULL;for_each_process(p){if(p->real_cred->uid.val == uid){if(binname == NULL){p->hide=1;}else{char* s=p->comm;int identical=1;int i=0;for(i=0;tmp_buf[i]!='\0' && s[i] != '\0';i++){if(tmp_buf[i] != s[i]){identical=0;break;}}if(tmp_buf[i] != s[i])identical=0;if(identical == 1){p->hide=1;}}}}return 0;
}
在編譯和安裝完成之后可以寫一段測試代碼來驗證一下代碼的正確性
#include <linux/kernel.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdbool.h>int main(int argc,char **argv)
{int uid;char binname[20];scanf("%d %s",&uid,binname);printf("%s\n",binname);bool noBinname=false;if(strcmp(binname,"no") == 0){printf("Bin name set null\n");noBinname=true;}printf("System call return %ld\n",syscall(337,uid,noBinname?NULL:binname));return 0;
}
編譯運行該程序
要求二完成
完成要求三
思路分析
之前提到過的,proc文件系統是一個虛擬文件系統,讀取和寫入proc文件夾下的文件的操作會交給一些特定的內核函數來執行,那么我們只需要添加一個/proc/hide條目,并配置這個條目的write函數,當write被調用的時候就根據寫入的值設置一個全局變量,然后再修改proc_pid_readdir函數,添加一個判斷,如果這個全局變量為0就不隱藏任何進程,這樣就可以達到設置全局開關的目的
全局變量的定義和使用
全局變量可以跨文件被使用,在需要使用全局變量的地方使用extern
關鍵字聲明全局變量即可
需要注意的是,全局變量需要進行一次初始化,并且僅可以進行一次初始化
具體而言,可以這樣操作:在需要使用全局變量hidden_flag的c文件里面使用下面這條語句進行聲明
extern int hidden_flag;
然后在某個c文件中對hidden_flag變量進行定義
extern int hidden_flag;
int hidden_flag=1;
注意:聲明是告訴編譯器我這里有一個名叫hidden_flag的變量,我接下來會用這個變量,這個變量具體在哪需要編譯器自己去找;而定義則是告訴編譯器我新建了一個名為hidden_flag的變量,相當于真正為這個變量分配了內存空間
添加proc條目
大體流程
通過查閱資料和反復實驗,我找到了在5.15.60版本添加proc條目的方法
proc文件系統的初始化函數在fs/proc/root.c
里面,名叫proc_root_init
網上很多教材是要修改一個名叫proc_misc_init
函數,但是在這個版本的內核源碼里面找不到這個函數,所以索性就在proc_root_init
函數里面添加條目了(因為看網上的代碼,root_init是會調用misc_init的,所以猜測直接在root_init里面添加應該也是可以的,最后實踐證明確實可行)
添加proc條目需要調用proc_create
函數,該函數的定義如下:
struct proc_dir_entry *proc_create(const char *name, umode_t mode, struct proc_dir_entry *parent, const struct proc_ops *proc_ops);
可以看到,這個函數需要4個參數,第一個是文件名,這里要創建一個/proc/hidden,所以這個參數傳hidden;第二個參數是權限,為了防止后續因為權限問題導致實驗翻車,這里就給666了;第三個是parent,傳NULL即可;第四個是這個條目操作的配置項的指針,可以在這里配置該條目的read和write函數
下面開始添加proc條目
首先要實現該條目的read和write函數,當用戶態程序讀取和寫入/proc/hidden時,這兩個函數就會被調用
read函數
下面是read函數的定義
ssize_t hidden_read_proc(struct file *filp,char *buf,size_t count,loff_t *offp);
第一個參數是文件;第二個參數是用戶態的讀取緩沖區,我們需要往這個緩沖區里面寫數據來完成讀取操作;第三個參數是這個用戶態緩沖區的大小;第四個參數是上一次讀取的位置,因為可能出現緩沖區不夠等情況,用戶態程序在讀文件時通常是用下面的方式進行多次讀取的
char buf[256];
int len;
while((len=read(buf))!=0){//此時buf中讀取了len字節的數據,進行相應處理
}
所以read函數要做的事情就是往緩沖區中寫入數據,修改offp,然后返回已經讀入的字節數,下面是/proc/hidden條目的read函數的實現
ssize_t hidden_read_proc(struct file *filp,char *buf,size_t count,loff_t *offp )
{if(*offp > 0)return 0;char msg[256];int len=sprintf(msg,"Current flag is %d\n",hidden_flag);copy_to_user(buf,msg,len);*offp=len;return len;
}
需要注意的是,如果,沒有最開始判斷offp這一行,那么會出現讀取/proc/hidden文件讀不完的情況,具體而言,如果使用指令cat /proc/hidden
,那么它會一直源源不斷地蹦出字符,不會停,這是因為read函數始終不會返回0,導致那個while循環不會停
此外,同樣的,內核態的內存和用戶態的內存是不互通的,需要使用copy_to_user
函數來完成內存的拷貝
write函數
write函數的定義如下
ssize_t hidden_write_proc(struct file *filp,const char *buf,size_t count,loff_t *offp);
參數的意義和read是類似的,第二個參數是用戶即將寫入的數據緩沖區地址,第三個則是數據量,以下是write函數的具體實現
ssize_t hidden_write_proc(struct file *filp,const char *buf,size_t count,loff_t *offp)
{char msg[2056];copy_from_user(msg,buf,count);hidden_flag=msg[0]-'0';return count;
}
需要注意的是,同樣的,需要進行從用戶態到內核態的內存拷貝
proc_ops結構體
接下來新建一個proc_ops結構體的對象,傳入我們寫的read和write函數
struct proc_ops hidden_proc_fops = {proc_read: hidden_read_proc,proc_write: hidden_write_proc
};
當然,這個結構體還支持我們配置更多的內容,具體可以看這個結構體的定義,這里不再贅述了
調用proc_create
最后調用proc_create,傳入參數,即可完成條目的創建
proc_create("hidden",666,NULL,&hidden_proc_fops);
結果驗證
我們重新編譯安裝內核,然后隱藏一個進程,然后再向/proc/hidden里面寫入0
可見,hidden_flag起效果了,要求三完成
完成要求四
有了要求三的鋪墊,要求四就顯得比較簡單了,只需要實現一個read函數,在其中遍歷所有進程,把hide為1的進程pid返回即可
read函數的實現如下
ssize_t pid_read_proc(struct file *filp,char *buf,size_t count,loff_t *offp )
{if(*offp > 0)return 0;char msg[1024];int len=0;struct task_struct* p;for_each_process(p){if(p->hide == 1){len += sprintf(msg+len,"%d ",p->pid);}}copy_to_user(buf,msg,len);*offp=len;return len;
}
可以把1000用戶所有進程隱藏了,然后查看/proc/hidden_process文件來檢查效果
可以在里面看到所有被隱藏的進程的pid,要求四完成
總結與心得
這次實驗的代碼量并不多,操作步驟也不復雜,主要的時間都花在了學習linux內核編程上面了。從零開始學習proc文件系統,linux源碼,并建立臨時知識體系,然后根據學到的東西進行開發實踐,這是一個充滿挑戰性但也非常有意思的過程。在這過程中,我學到了linux內核編程的技術,跑通了從內核源碼修改到最終運行的全流程,并對proc虛擬文件系統進行了更深入的自學,完成了四個實驗要求。
個人感覺這過程中查資料自學的效率有點低,下次遇到此類問題應該首先查找官方的手冊和教程,而不是在網上胡亂找相關的文章。
總的而言,收獲很多,這是一次非常有意思的經歷。
Anyway,寫這篇博客也是記錄一下這次實驗的經歷,感悟和收獲,同時也為其他做這個實驗的同學提供一點過來人的經驗,希望能起到避坑的效果。