# 2021q3 Homework1 (quiz1)
contributed by <conc95xh>
## 開發環境介紹
本次作業嘗試在 Apple Silicon (aarch64架構) 的機器上架設QEMU+Arch Linux for ARM的環境,原因為以下幾個優點
* Apple Silicon 為ARM架構,未來驗證ARM處理器相關議題時,理論上在arm機器上直接使用arm虛擬器相比於在x86機器上跑arm模擬器可得到較快的效能,不需要做binary translation [1](https://stackoverflow.com/questions/6044978/full-emulation-vs-full-virtualization).
* [QEMU](https://en.wikipedia.org/wiki/QEMU)為 emulator 或virtulizer , 選擇使用QEMU除了是全命令模式易於利用script簡化重複性的操作
* 例如寫Linux kernel module死當要重開機,我只須在host os上下一個簡單的kill指令終止qemu程式,再跑一次寫好的start up script即可在5秒鐘內回復一個可運作的Linux (或用savevm/loadvm指令,但目前MacOS的HVF仍不支援)
* 也可減少一些操作GUI界面的繁瑣導致的錯誤
* Arch Linux for ARM 預設root file system就是一套可用的Linux環境,在全命令模式就可以把Linux裝好,不需操作GUI,且套件管理簡單預裝容量小,需要用的套件再安裝,節省硬碟空間
* Arch Linux的Wiki文件非常完整且詳盡
* ~~X86機器跑Virtual Box或VMWare風扇很吵,用Macbook M1跑得差不多快且較安靜不會發熱~~
## QEMU及Arch Linux for ARM架設
QEMU 在Apple Silicon運行可參考以下step-by-step tutorial
[How to boot Arch Linux ARM in QEMU (patched for M1) or Parallels Technical Preview for M1](https://gist.github.com/thalamus/561d028ff5b66310fac1224f3d023c12)
簡述文件提到的幾個步驟
1. 在Linux系統上利用loopback device先製造好以下檔案~(因為MacOS使用loop\ back\ device或mkfs.ext4指令較不方便所以這一步驟在x86\ Linux上做)~
* rootfs image(part 0為boot partition/uefi partition, part 1為ArchLinux root fs)
* flash images(0為uefi bios本身, 1為儲存bios設定)
2. 將製作好的三個檔案拷貝進MacOS
3. 利用patch過的for Apple Silicon的QEMU執行(請自行編譯,勿用brew安裝QEMU)
照著上述文件操作可能會遇到幾個問題
1. edk2 UEFI bios等候PXE開機的時間很久,所以一開始我以為安裝失敗卡關,但過了一段不短的時間,忽然成功進入Linux登入畫面,閱讀edk2文件得知edk2支援好幾種開機方式,在qmeu運行edk2 bios時,我們可以把它想像成一台個人電腦bios,因此我們應該在一開始進入畫面時按ESC設定開機順序,把裝有root fs的disk設定到第一順位,如此它才不會花太多時間等待DHCP Server給定網路IP及下載作業系統
2. 一步都不能少,例如在將edk2 bios寫到一個flash0.img的時候,我忘了先把從壓縮檔解壓縮就直接寫進去,得到了完全不能開機的黑色畫面(沒有吐出任何log就卡關是很令人挫折的),回去一步一步檢查也曠日廢時,不如一開始就細心照著tutorial寫的做
3. tutorial沒有寫清楚網路如何使用,但照QEMU的文件可以得知以下部份指令
> -nic user,model=virtio-net-pci,hostfwd=tcp::50022-:22
為命令QEMU去聽port 50022並將所有到此埠的所有TCP封包轉到guest os的22 port, 因此我們在host os下可以利用ssh root@localhost -p 50022就可連進QEMU裡的Linux
## 延伸問題 1. 解釋上述程式碼運作原理,包含 ftrace 的使用
在Linux kernel關於ftrace的文件[ftrace - Function Tracer](https://www.kernel.org/doc/Documentation/trace/ftrace.txt)
裡概述中提到
> Ftrace is an internal tracer designed to help out developers and
designers of systems to find what is going on inside the kernel.
It can be used for debugging or analyzing latencies and
performance issues that take place outside of user-space.
簡言之,a function tracer
我們在初學程式時追蹤一個程式的流程最簡單直觀的方法就是在程式裡每一個function前面加一串printf/printk("%s:xxxx\n",\_\_function\_\_),但這樣做會有很大的效能的問題,通常只是在驗證階段使用
若在高度運行的程式裡加一行列印的程式碼,整體速度會被拖慢,因此若我們能動態地在function開頭開啟關閉,且在需要時才印或只將必要資訊寫入log,就能在不影響效率的情況下,仍然能獲得執行時的資訊且不用重新編譯
CONFIG_DYNAMIC_TRACER即可符合我們實際應用需求
> dynamic ftrace
>
>If CONFIG_DYNAMIC_FTRACE is set, the system will run with
virtually no overhead when function tracing is disabled. The way
this works is the mcount function call (placed at the start of
every kernel function, produced by the -pg switch in gcc),
starts of pointing to a simple return. (Enabling FTRACE will
include the -pg switch in the compiling of the kernel.)
在這次作業的範例程式即用到[ftrace hook](https://www.kernel.org/doc/html/v4.17/trace/ftrace-uses.html#introduction)
藉由跟Linux ftrace子系統註冊一個hook function,在每次執行到某個ip位置或function時,即呼叫註冊的callback function - hook_ftrace_thunk
並傳入攔截當下的register set,以利在callback function根據當下情況決定要不要更改ip/pc的位置
```c=
static int hook_install(struct ftrace_hook *hook)
{
int err = hook_resolve_addr(hook);
if (err)
return err;
hook->ops.func = hook_ftrace_thunk; //callback function
hook->ops.flags = FTRACE_OPS_FL_SAVE_REGS | FTRACE_OPS_FL_RECURSION |
FTRACE_OPS_FL_IPMODIFY; //告知系統我們有可能更改ip/pc位置
//且需保護防止重覆進入及傳入當下register set
err = ftrace_set_filter(&hook->ops, "find_ge_pid", strlen("find_ge_pid"), 0);
//此為告知系統在執行到find_get_pid時,先進入hook function
if (err) {
printk("ftrace_set_filter() failed: %d\n", err);
return err;
} else {
printk("successful \n");
}
#if 0
err = ftrace_set_filter_ip(&hook->ops, hook->address, 0, 1);
if (err) {
printk("ftrace_set_filter_ip() failed: %d\n", err);
return err;
}
/* 將此函式取代為ftrace_set_filter的原因是在arm系統
直接設定ip位置會傳回-EINVAL:ftrace系統檢查此位置發現
不是tracable,猜測是架構的差異導致在ftrace檢查輸入位址
時,發現function前幾個instruction的特徵的位置可能有差異
一時找不到在arm使用此函式的範例,因此改成ftrace_set_filter
(反可達到portable的效果(?))
*/
#endif
err = register_ftrace_function(&hook->ops); //註冊且啟動一個hook
if (err) {
printk("register_ftrace_function() failed: %d\n", err);
ftrace_set_filter_ip(&hook->ops, hook->address, 1, 0);
return err;
}
return 0;
}
```
我們註冊的hook function:
```c=
static void notrace hook_ftrace_thunk(unsigned long ip,
unsigned long parent_ip,
struct ftrace_ops *ops,
struct ftrace_regs *regs)
{
struct ftrace_hook *hook = container_of(ops, struct ftrace_hook, ops);
if (!within_module(parent_ip, THIS_MODULE))
regs->regs.pc = (unsigned long) hook->func;
/* parent_ip為function caller的位置 */
/* 某行程呼叫了一個正在被我們追蹤的函式,則在進入該函式前
的位置的下一個instruction的記億體位置即為parent_ip */
/* 這段程式碼檢查是否是我們自己這個模組去呼叫了被追蹤的function,
以免重覆進入hook function */
/* 原因:我們攔截了所有進入find_ge_pid()的事件,將其ip/pc改成我們自己撰
寫的函式,但我們做完壞事後仍然要不動聲色的再度呼叫原始的find_ge_pid函式
這時候ftrace會再觸發一次攔截,且其parent ip就是我們自己,因此若parent
ip 是我們自己,則不作任何事讓它正常呼叫原始的find_ge_pid()以免陷入無窮
輪迴
*/
}
```
前面的hook function為的就是將find_ge_pid()代換成我們自己的find_ge_pid(),也就是做壞事的地方,注意在arm架構沒有ip這個register,因此改成pc,測試之後可成功隱藏
```c=
static struct pid *hook_find_ge_pid(int nr, struct pid_namespace *ns)
{
struct pid *pid = real_find_ge_pid(nr, ns);
while (pid && is_hidden_proc(pid->numbers->nr))
pid = real_find_ge_pid(pid->numbers->nr + 1, ns);
/* is_hidden_proc 用來檢查nr是不是在我們的名單之內
若為隱藏的名單之一,則跳過這個pid,從pid+1開始找 */
return pid;
}
```
### 如何輸入該隱藏的pid名單
在教授給定的範例程式中,利用了註冊一個character device的方式,並實作了file read/write callback
```c=
static const struct file_operations fops = {
.owner = THIS_MODULE,
.open = device_open,
.release = device_close,
.read = device_read,
.write = device_write,
};
```
因此我們可以透過
讀取:列出隱藏pid名單
```c=
static ssize_t device_read(struct file *filep,
char *buffer,
size_t len,
loff_t *offset)
```
寫入:讓user space寫入pid,將該pid加入我們想隱藏的名單
```c=
static ssize_t device_write(struct file *filep,
const char *buffer,
size_t len,
loff_t *offset)
```
device_write即為[延伸問題3](https://hackmd.io/u0zLNafiT2ehtbEzmoPxfw#%E5%BB%B6%E4%BC%B8%E5%95%8F%E9%A1%8C3-%E6%9C%AC%E6%A0%B8%E5%BF%83%E6%A8%A1%E7%B5%84%E5%8F%AA%E8%83%BD%E9%9A%B1%E8%97%8F%E5%96%AE%E4%B8%80-PID%EF%BC%8C%E8%AB%8B%E6%93%B4%E5%85%85%E7%82%BA%E5%85%81%E8%A8%B1%E5%85%B6-PPID-%E4%B9%9F%E8%B7%9F%E8%91%97%E9%9A%B1%E8%97%8F%EF%BC%8C%E6%88%96%E5%85%81%E8%A8%B1%E7%B5%A6%E5%AE%9A%E4%B8%80%E7%B5%84-PID-%E5%88%97%E8%A1%A8%EF%BC%8C%E8%80%8C%E9%9D%9E%E5%83%85%E6%9C%89%E5%96%AE%E4%B8%80-PID)的主角:我們需要將其改寫以支援一次多筆pid輸入
## 延伸問題2:本程式僅在 Linux v5.4 測試,若你用的核心較新,請試著找出替代方案
在本次作業中,我使用了Arch Linux,因為arch linux預設不支援dynamic tracer,因此重編譯kernel並更改config勢在必行,不然連本次作業範例都編不過(CONFIG_DYNAMIC_TRACER沒開,很多相關structure定義不存在),遑論修改執行,
也因為要重編kernel,所以就順手暴力把kallsyms_lookup_name()加回去EXPORTED SYMBOL以求快速先驗証作業module的正確性
所以本延伸問題只有碰到Arch Linux重編譯核心並取代現行Kernel的議題
在ARM的版本,原先Arch Wiki提到的簡易核心編譯法只有支援x86版本,裡面直接預設了使用x86架構,預期修改script會比重編vanilla kernel花更多的時間,因此我還是選擇了直接去github checkout一份kernel source下來編
幾個要點:
1. 選跟現在所使用系統接近的核心版本(uname -r查看現行版本)
2. checkout一份一樣版本的Linux, e.g. git checkout v5.12
3. 直接抓現行可執行Linux系統的config.gz,放到Linux kernel資料夾裡,保證可以跑,不然可能遇到driver沒編到開不了機的問題
* zcat /proc/config.gz > [linux資料夾]/.config
4. 在linux資料夾下 make oldconfig;make;sudo make modules_install
5. arch/arm64/boot/Image即為編譯成功的核心,將其拷貝到boot partition取代原來的核心
* Image: Linux kernel ARM64 boot executable Image, little-endian, 4K pages
6. 參照[arch linux traditional compilation](https://wiki.archlinux.org/title/Kernel/Traditional_compilation)文件,完成mkinitcpio產出init ramdisk並放到boot partition取代原來的
若使用原系統的toolchain且版本相近,很大的機率可以編譯過且執行成功,若版本差太遠或toolchain版本差太多很可能遇到語法錯誤編不過的問題,或一些undefined reference的問題
TODO:
- [ ] https://github.com/h33p/kallsyms-mod
## 延伸問題3: 本核心模組只能隱藏單一 PID,請擴充為允許其 PPID 也跟著隱藏,或允許給定一組 PID 列表,而非僅有單一 PID
設計一段程式碼,可支援多筆輸入
輸入格式範例為:
echo "add 5132\ndel 3232\nadd 5323"
如下執行結果:在執行之前先利用其他視窗的bash shell呼叫兩個sleep 3000,獲得兩個睡3000秒的行程,在輸入echo "add 5132\n 5111" > ./hideproc 後,兩個行程是看不到的,將2個pid del掉,ps又可以看到兩個行程了
```
/dev% pidof sleep
5132 5111
/dev% echo "add 5132\n 5111" > ./hideproc
/dev% cat hideproc
pid: 5111
pid: 5132
/dev% ps aux | grep sleep
root 5149 0.0 0.0 6060 2076 pts/1 S+ 15:53 0:00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=
.svn --exclude-dir=.idea --exclude-dir=.tox sleep
/dev% echo "del 5132\ndel 5111" > ./hideproc
/dev% ps aux | grep sleep
k 5111 0.0 0.0 4932 552 pts/5 S+ 15:53 0:00 sleep 3000
k 5132 0.0 0.0 4932 504 pts/6 S+ 15:53 0:00 sleep 3000
root 5155 0.0 0.0 6060 2024 pts/1 S+ 15:53 0:00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=
.svn --exclude-dir=.idea --exclude-dir=.tox sleep
```
以上的功能由改寫以下程式碼完成
```c=
static ssize_t device_write(struct file *filep,
const char *buffer,
size_t len,
loff_t *offset)
{
long pid;
char *message;
char add_message[] = "add", del_message[] = "del";
if (len < sizeof(add_message) - 1 && len < sizeof(del_message) - 1)
return -EAGAIN;
if (len >= MAX_WRITE_BUFFER_SIZE) {
printk("Too large to handle\n");
return -EINVAL;
}
#if defined(DEBUG) && defined(CONFIG_PRINTK)
print_hex_dump(KERN_DEBUG, "input buffer", DUMP_PREFIX_ADDRESS, 32,4,buffer, len,0);
#endif
message = kmalloc(len + 1, GFP_KERNEL);
memset(message, 0, len + 1);
copy_from_user(message, buffer, len);
char *p=message;
char *q=strstr(p,"\n");
while (p < message+len && q!= NULL) {
*q = '\0';
#if defined(DEBUG) && defined(CONFIG_PRINTK)
print_hex_dump(KERN_DEBUG,"partial string", DUMP_PREFIX_ADDRESS, 32, 4, p, q-p+1,0);
#endif
if (!memcmp(p, add_message, sizeof(add_message) - 1)) {
kstrtol(p+ sizeof(add_message), 10, &pid);
hide_process(pid);
} else if (!memcmp(p, del_message, sizeof(del_message) - 1)) {
kstrtol(p+ sizeof(del_message), 10, &pid);
unhide_process(pid);
}
p = q+1;
q=strstr(p,"\n");
}
*offset = len;
kfree(message);
return len;
#if 0
if (!memcmp(message, add_message, sizeof(add_message) - 1)) {
kstrtol(message + sizeof(add_message), 10, &pid);
hide_process(pid);
} else if (!memcmp(message, del_message, sizeof(del_message) - 1)) {
kstrtol(message + sizeof(del_message), 10, &pid);
unhide_process(pid);
} else {
kfree(message);
return -EAGAIN;
}
else {
kfree(message);
return -EAGAIN;
}
#endif
}
```
基本上,只是把message擴充到1024的長度,用'\n'分割每一筆資料,讀取到傳入message的尾巴為止
TODO:
- [ ] 1.device_write有沒有可能呼叫好幾次?然後一筆資料被分成兩段buffer傳進來?
- [ ] 2.能否直接讀寫buffer? 在沒有其他程序進入的假設下,我們能不能直接讀取buffer?
## 延伸問題4: 指出程式碼可改進的地方,並動手實作
### module exit
範例程式沒有實作module exit,所以若rmmod後會造成系統不正常,需重新啟動(這正好說明了老師上課提到的microkernel的優勢)
以下程式碼在module_exit反向依序呼叫所有反安裝函式將所有裝置及hook移除,釋放資源以維持系統正常(注意在module_init和module_exit共享的變數需移到外面成為global的變數)
```c=
static void _hideproc_exit(void)
{
printk(KERN_INFO "@ %s\n", __func__);
hook_remove(&hook);
device_destroy(hideproc_class,MKDEV(dev_major, MINOR_VERSION));
//cdev_del(&dev);
class_destroy(hideproc_class);
unregister_chrdev(dev_major,DEVICE_NAME);
}
```
### link list
一些link list操作的時候除了有使用list_del的地方,其餘地方不需要使用_safe及多宣告一個tmp
改進的程式參考[GitHub conc95xh](https://github.com/conc95xh/linux2021-summer-quiz/tree/main/hideproc)
~~來不及完成~~TODO
- [ ] thread safe? 應可用mutex去鎖定link list防止多個行程操作同一個檔案,或使用RCU達成lock-free?