---
tags: linux2019
---
# 2019q1 Homework2 (fibdrv)
contributed by < `HexRabbit` >
## Environment
下方參考的 kernel source 皆出自 linux kernel v5.0.2
## 自我檢查事項
### 1. 檔案 `fibdrv.c` 裡頭的 `MODULE_LICENSE`, `MODULE_AUTHOR`, `MODULE_DESCRIPTION`, `MODULE_VERSION` 等巨集做了什麼事,可以讓核心知曉呢? `insmod` 這命令背後,對應 Linux 核心內部有什麼操作呢?請舉出相關 Linux 核心原始碼並解讀
macro 的展開相對繁瑣,我們可以先利用 `nm` 指令來觀察展開後的模樣
```
0000000000000030 r __UNIQUE_ID_author24
000000000000000c r __UNIQUE_ID_description25
000000000000005e r __UNIQUE_ID_license23
0000000000000000 r __UNIQUE_ID_version26
```
:::info
這邊應該要用 `gcc -E` 直接觀察 preprocessor 展開後的輸出比較好,可是因為這是 kernel module,不太清楚要怎麼做
:::
有關 kernel module 的 macro 被放在 `/include/linux/module.h` 以及 `/include/linux/moduleparam.h` 中,下面以 `MODULE_LICENSE` 為例列出相關的 macro
- #### `MODULE_LICENSE`
```cpp
#define MODULE_LICENSE(_license) MODULE_INFO(license, _license)
#define MODULE_INFO(tag, info) __MODULE_INFO(tag, tag, info)
#define __MODULE_INFO(tag, name, info) \
static const char __UNIQUE_ID(name)[] \
__used __attribute__((section(".modinfo"), unused, aligned(1))) \
= __stringify(tag) "=" info
#define __UNIQUE_ID(prefix) __PASTE(__PASTE(__UNIQUE_ID_, prefix), __COUNTER__)
#define __PASTE(a, b) a##b
#define __stringify(x...) #x
```
光是展開一個 macro 就跑出一堆相關的 code,我們從最下方開始一步步組裝
- **`__PASTE(a, b)`**
`a##b` 是一個 gcc preprocessor feature: **Token pasting**,會將 token a 和 token b 黏合,例如: `__PASTE(Hello, world) == Helloworld`,要注意的是這裡的輸出並不是一個字串而是一個新的 token
- **`__COUNTER__`**
這個就好玩了,這是一個 gcc predefined macro,可以參考 [Common-Predefined-Macros](https://gcc.gnu.org/onlinedocs/cpp/Common-Predefined-Macros.html),第一次展開時 `__COUNTER__` 會是 `0`,再來每一次展開該 macro 其值就會遞增,是個方便使用者搭配 `##` 製造變數的好工具
- **`__UNIQUE_ID_`**
沒什麼,就是個 token,用來作為 `##` 黏合之後形成的變數的前綴
- **`__used`**
這是 gcc 提供的一個 extension,根據 [gcc 文件](https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html) 可以知道,這是為了讓一些只有在 inline assembly 中被使用到的變數不會被 optimizer 優化而被刪除
> This attribute, attached to a function, **means that code must be emitted for the function even if it appears that the function is not referenced**. This is useful, for example, when the function is referenced only in inline assembly.
:::warning
不知道為甚麼可以不寫在 `__attributes__` 裡面
:::
- **`__stringify(x...)`**
這裡的 `x...` 是利用 [Variadic-Macros](https://gcc.gnu.org/onlinedocs/cpp/Variadic-Macros.html) 的技巧,`#` 則是會把參數變成字串, 所以 `#x` 會將所有參數變成字串(包含分隔他們的那些逗號),建議可以用 `gcc -E` 試驗一遍比較清楚
- **`__attribute__ ((attribute-list))`**
這是個 gcc 擴展 [Function-Attributes](https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#Common-Function-Attributes),可以在編譯時為變數指定一些 attribute 進而能夠直接讓 compiler 針對設定的 attribute 進行優化,而不是在進行一系列分析後才做出優化,不過在使用時也必須要小心,因為 compiler 會根據設定的 attribute 做優化不會另作分析,所以如果沒有設定好可能出現危險的 bug (例如設定 pure 但讓 function 進行 non-pure 的指標操作)
- **`section("section-name")`**
指定變數在編譯時該被放入哪個 section
- **`unused`**
用於消除 compile 時產生出該變數未被使用的 warning
注意這與前面提到的 `used` 是完全不同的概念
- **`aligned`**
告知編譯器該變數在記憶體中的位置需要向多少 byte 對齊
最後以 `MODULE_LICENSE("GPL");` 為例把它們組起來,就會形成這樣一個宣告:
```clike
static const char __UNIQUE_ID_license23[] \
__used __attribute__((section(".modinfo"), unused, aligned(1))) \
= license="GPL";
```
### 2. 當我們透過 `insmod` 去載入一個核心模組時,為何 `module_init` 所設定的函式得以執行呢?Linux 核心做了什麼事呢?
先了解 macro `module_init` 做了甚麼吧,以 `module_init(init_fn);` 為例,最終會被展開為:
```clike
static inline initcall_t __maybe_unused __inittest(void) \
{ return init_fn; } \
int init_module(void) __attribute__((alias("init_fn")))
```
`initcall_t` 為 function pointer 型別,由 `typedef int (*initcall_t)(void);` 定義,
接著注意到 `__inittest` 是個 function,猜測在這裡用於在編譯時期檢查 `init_fn` 的型別是否跟 `initcall_t` 相同 (回傳值必須要是`initcall_t`)
所以這個 macro 基本上只是將給定的 `init_fn` 設定為 `init_module` 這個 function 的 alias。
再來看到編譯中額外產生的檔案 `fibdrv.mod.c`,透過查閱一些[文件](https://www.ibm.com/developerworks/cn/linux/l-cn-kernelmodules/index.html)可以得知這是用於將一些必要的 metadata 和 version 資訊一併在編譯時附加進去,在其中可以發現 init_module 被放入 `__this_module` 這個結構體變數中,看來會在載入時使用
```cpp
__visible struct module __this_module
__attribute__((section(".gnu.linkonce.this_module"))) = {
.name = KBUILD_MODNAME,
.init = init_module,
#ifdef CONFIG_MODULE_UNLOAD
.exit = cleanup_module,
#endif
.arch = MODULE_ARCH_INIT,
};
```
接著來看看我們到底編出了甚麼東西
```
fibdrv.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV)
```
看起來 kernel module 似乎就像是個 dynamically linked library,會被 kernel 在 runtime 載入。不知道該從哪裡開始看起,總之先 strace 一次看看到底用了些甚麼 syscall
```clike
execve("/sbin/insmod", ["insmod", "fibdrv.ko"], [/* 19 vars */])
:
:
open("/lib/modules/4.15.0-46-generic/modules.softdep", O_RDONLY|O_CLOEXEC) = 3
:
:
stat("/home/user/fibdrv/fibdrv.ko", {st_mode=S_IFREG|0664, st_size=8416, ...}) = 0
open("/home/user/fibdrv/fibdrv.ko", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0664, st_size=8416, ...}) = 0
mmap(NULL, 8416, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7ff23ed79000
finit_module(3, "", 0) = 0
```
可以發現一個不常見(對我來說啦)的 syscall `finit_module`,透過查詢 man [finit_module](https://linux.die.net/man/2/finit_module) 得知`finit_module`當中的 "f" 是 file descriptor 的意思,而這是自從 linux kernel 3.8 後才新加入的 syscall,在這之前使用的是 `init_module` syscall 來載入 kernel modules
既然找到了對應的 syscall 那就直接看 [kernel source](https://elixir.bootlin.com/linux/latest/source/kernel/module.c#L3878) 來理解吧
```cpp=3878
SYSCALL_DEFINE3(finit_module, int, fd, const char __user *, uargs, int, flags)
{
struct load_info info = { };
loff_t size;
void *hdr;
int err;
err = may_init_module();
if (err)
return err;
pr_debug("finit_module: fd=%d, uargs=%p, flags=%i\n", fd, uargs, flags);
if (flags & ~(MODULE_INIT_IGNORE_MODVERSIONS
|MODULE_INIT_IGNORE_VERMAGIC))
return -EINVAL;
err = kernel_read_file_from_fd(fd, &hdr, &size, INT_MAX,
READING_MODULE);
if (err)
return err;
info.hdr = hdr;
info.len = size;
return load_module(&info, uargs, flags);
}
```
有關 `SYSCALL_DEFINE` 的解釋可以參考 [Linux系统调用之SYSCALL_DEFINE](https://blog.csdn.net/hxmhyp/article/details/22699669)
這裡在做的事情就是透過`kernel_read_file_from_fd` 把整個編譯好的 kernel module 從 userspace 讀到一個 buffer `*hdr` 然後放進 `info` 中。
接著會呼叫`load_module` 來解析 kernel module 中的資訊,並將其放入 `struct module mod` 變數中對應的欄位上,在 ` setup_load_info(info, flags)` 中會將先前提到用於存放 metadata 的 `__this_module` 指標複製到 `info->mod`
```cpp=2981
info->index.mod = find_sec(info, ".gnu.linkonce.this_module");
if (!info->index.mod) {
pr_warn("%s: No module found in object\n",
info->name ?: "(missing .modinfo name field)");
return -ENOEXEC;
}
/* This is temporary: point mod into copy of data. */
info->mod = (void *)info->hdr + info->sechdrs[info->index.mod].sh_offset;
```
接著在 `layout_and_allocate(info, flags)` 時將所有 `info` 中的資訊讀取到 `mod` 裡
在所有資訊都讀取完成後,函式會呼叫 `do_init_module(mod)` 進行 module 的初始化。
```cpp=3458
/* Start the module */
if (mod->init != NULL)
ret = do_one_initcall(mod->init);
```
而在其中若是 `mod->init != NULL` 則我們定義的 init function 就會在這裡被呼叫到。
### 3.解釋像 fibdrv.ko 這樣的 ELF 執行檔案是如何「植入」到 Linux 核心

要瞭解 ELF 是如何被載入到 kernel 的,首先必須對它的格式有所瞭解,所以接著就來閱讀 glibc/kernel 中 ELF 的相關代碼吧,以下代碼取自 [elf/elf.h](https://code.woboq.org/userspace/glibc/elf/elf.h.html), [include/uapi/linux/elf.h](https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/elf.h),詳細的解釋也可以透過 [man elf](http://man7.org/linux/man-pages/man5/elf.5.html) 找到
### ELF header
```cpp
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf64_Half e_type; /* Object file type */
Elf64_Half e_machine; /* Architecture */
Elf64_Word e_version; /* Object file version */
Elf64_Addr e_entry; /* Entry point virtual address */
Elf64_Off e_phoff; /* Program header table file offset */
Elf64_Off e_shoff; /* Section header table file offset */
Elf64_Word e_flags; /* Processor-specific flags */
Elf64_Half e_ehsize; /* ELF header size in bytes */
Elf64_Half e_phentsize; /* Program header table entry size */
Elf64_Half e_phnum; /* Program header table entry count */
Elf64_Half e_shentsize; /* Section header table entry size */
Elf64_Half e_shnum; /* Section header table entry count */
Elf64_Half e_shstrndx; /* Section header string table index */
} Elf64_Ehdr;
```
ELF header 放置了一些與程式相關的 metadata,以及接下來的各個 table 跟 ELF header 之間的 offest,這裡比較有趣的是 `e_entry` 會在之後 kernel 載入 ELF 時決定運行時的 entry point address,對一般的程式來說就是 _start 的位置,可以用 `readelf -a elf` 觀察看看。
不過 kernel module 比較特別,在這個欄位的數值是零,我想這有很大部分是跟他的實作方式有關,畢竟在 insmod 時,kernel 已經將不少前置作業完成並「插入」到 kernel 中,也就不需要 userspace 中程式入口點的設計了
### Program header
```cpp
typedef struct
{
Elf64_Word p_type; /* Segment type */
Elf64_Word p_flags; /* Segment flags */
Elf64_Off p_offset; /* Segment file offset */
Elf64_Addr p_vaddr; /* Segment virtual address */
Elf64_Addr p_paddr; /* Segment physical address */
Elf64_Xword p_filesz; /* Segment size in file */
Elf64_Xword p_memsz; /* Segment size in memory */
Elf64_Xword p_align; /* Segment alignment */
} Elf64_Phdr;
```
program header 的資訊多用於建立 runtime 的相關資料,是用來告訴 kernel 該 ELF 中的各個 segment 在載入記憶體時該被放置到甚麼位置、讀寫權限、以及類型相關的資訊,要注意的是 segment 和 section 的關係,一個 segment 可以包含多個 section。
:::warning
有關於讀寫權限,實際上 kernel 似乎不太一定會照做 ?
(之前有一次奇怪的經驗)
:::
### Section header
```cpp
typedef struct
{
Elf64_Word sh_name; /* Section name (string tbl index) */
Elf64_Word sh_type; /* Section type */
Elf64_Xword sh_flags; /* Section flags */
Elf64_Addr sh_addr; /* Section virtual addr at execution */
Elf64_Off sh_offset; /* Section file offset */
Elf64_Xword sh_size; /* Section size in bytes */
Elf64_Word sh_link; /* Link to another section */
Elf64_Word sh_info; /* Additional section information */
Elf64_Xword sh_addralign; /* Section alignment */
Elf64_Xword sh_entsize; /* Entry size if section holds table */
} Elf64_Shdr;
```
section header 中的資訊可以協助 kernel 辨識/載入整個 ELF 中的資料,在程式剛被讀進 kernel 時常會看到它的身影
有了這些概念後,就可以回頭檢視 `load_module()` 中各個 function 是如何一步步的將 kernel module 載入的
### elf_header_check
檢查 ELF header 中各變數是否有被妥善的設定,從資安觀點來看這相當重要,若是沒有進行檢查則極有可能受攻擊者利用,進而越界讀取洩漏 kernel stack 上的資訊
```cpp=2805
static int elf_header_check(struct load_info *info)
{
// 檢查讀入的大小是否大於 Elf64_Ehdr 的大小
if (info->len < sizeof(*(info->hdr)))
return -ENOEXEC;
// 注意這邊 info->hdr 雖然讀進了整個 binary,但是 elf header 的結構是相同的
// 所以下方可以直接存取結構中相關的變數
// 檢查各個 offset 以及 size
if (memcmp(info->hdr->e_ident, ELFMAG, SELFMAG) != 0
|| info->hdr->e_type != ET_REL
|| !elf_check_arch(info->hdr)
|| info->hdr->e_shentsize != sizeof(Elf_Shdr))
return -ENOEXEC;
if (info->hdr->e_shoff >= info->len
|| (info->hdr->e_shnum * sizeof(Elf_Shdr) >
info->len - info->hdr->e_shoff))
return -ENOEXEC;
return 0;
}
```
### setup_load_info
~~可以利用 `objdump -s --section .strtab elf` (錯了它不能用,因為它太廢ㄌ...~~ [參考資料](https://stackoverflow.com/questions/22160621/why-does-objdump-not-show-bss-shstratab-symtab-and-strtab-sections)
可以利用 `readelf -p "section-name" elf` 以字串方式印出該 section 中的資料
```cpp
static int setup_load_info(struct load_info *info, int flags)
{
unsigned int i;
/* Set up the convenience variables */
info->sechdrs = (void *)info->hdr + info->hdr->e_shoff;
info->secstrings = (void *)info->hdr
+ info->sechdrs[info->hdr->e_shstrndx].sh_offset;
/* Try to find a name early so we can log errors with a module name */
// find_sec 利用了上方 setup 好的資訊來搜尋,return 目標於 section table 中的 index
info->index.info = find_sec(info, ".modinfo");
if (!info->index.info)
info->name = "(missing .modinfo section)";
else
info->name = get_modinfo(info, "name");
/* Find internal symbols and strings. */
for (i = 1; i < info->hdr->e_shnum; i++) {
if (info->sechdrs[i].sh_type == SHT_SYMTAB) {
info->index.sym = i;
info->index.str = info->sechdrs[i].sh_link;
info->strtab = (char *)info->hdr
+ info->sechdrs[info->index.str].sh_offset;
break;
}
}
if (info->index.sym == 0) {
pr_warn("%s: module has no symbols (stripped?)\n", info->name);
return -ENOEXEC;
}
info->index.mod = find_sec(info, ".gnu.linkonce.this_module");
if (!info->index.mod) {
pr_warn("%s: No module found in object\n",
info->name ?: "(missing .modinfo name field)");
return -ENOEXEC;
}
/* This is temporary: point mod into copy of data. */
info->mod = (void *)info->hdr + info->sechdrs[info->index.mod].sh_offset;
/*
* If we didn't load the .modinfo 'name' field earlier, fall back to
* on-disk struct mod 'name' field.
*/
if (!info->name)
info->name = info->mod->name;
if (flags & MODULE_INIT_IGNORE_MODVERSIONS)
info->index.vers = 0; /* Pretend no __versions section! */
else
info->index.vers = find_sec(info, "__versions");
info->index.pcpu = find_pcpusec(info);
return 0;
}
```
先到這邊,之後繼續更新
## fibdrv kernel module
在看 fibdrv kernel module 的實作之前總之先來觀察一下要怎麼跟 fibdrv 互動吧
```cpp
fd = open(FIB_DEV, O_RDWR);
if (fd < 0) {
perror("Failed to open character device");
exit(1);
}
for (i = 0; i <= offset; i++) {
sz = write(fd, write_buf, strlen(write_buf));
printf("Writing to " FIB_DEV ", returned the sequence %lld\n", sz);
}
for (i = 0; i <= offset; i++) {
lseek(fd, i, SEEK_SET);
sz = read(fd, buf, 1);
printf("Reading from " FIB_DEV
" at offset %d, returned the sequence "
"%lld.\n",
i, sz);
}
```
看起來 fibdrv kernel module 被設計成一個 [character device](https://linux-kernel-labs.github.io/master/labs/device_drivers.html),粗淺的看可以理解成像是一個能夠循序存取的文件,透過定義相關的函數,可以利用存取檔案的 system call 去存取他 (e.g. open, read, write, mmap...等)。
不過等等,這似乎跟我想像中的 fibdrv 的實作不太一樣,原以為應該會是一種類似 function 的互動模式,送一個參數 x 給 fibdrv 然後他會透過 socket 或類似的管道回傳 fib(x) 給我,沒有想到要透過 "read" 他來得到輸出,實在是相當新奇。接著就來看看這到底是怎麼實現的
```cpp
/* calculate the fibonacci number at given offset */
static ssize_t fib_read(struct file *file,
char *buf,
size_t size,
loff_t *offset)
{
return (ssize_t) fib_sequence(*offset);
}
const struct file_operations fib_fops = {
.owner = THIS_MODULE,
.read = fib_read,
.write = fib_write,
.open = fib_open,
.release = fib_release,
.llseek = fib_device_lseek,
};
```
發現其實是透過 `fib_fops` 中的自定義的 read 來實作讀取操作,而 fib_read 最終會 return `fib_sequence(*offset)`,所以其實就是透過讓使用者指定不同的 offest 作為費伯納契數列的 $X$ 然後透過 read 的回傳值輸出給使用者。呃,這種做法看起來很 hack XD
### 測量時間
接著利用 ktime 提供的方便計時工具來測量時間,我們可以發現到 write 在這個 module 中毫無用武之地,可以拿來做點小 hack 改造成輸出上一次 fib 的執行時間。
```cpp
static ktime_t kt;
static long long fib_time_proxy(long long k)
{
kt = ktime_get();
long long result = fib_sequence(k);
kt = ktime_sub(ktime_get(), kt);
return result;
}
static ssize_t fib_read(struct file *file,
char *buf,
size_t size,
loff_t *offset)
{
return (ssize_t) fib_time_proxy(*offset);
}
static ssize_t fib_write(struct file *file,
const char *buf,
size_t size,
loff_t *offset)
{
return ktime_to_ns(kt);
}
```