# 利用 proc_dir_entry 進行資訊洩漏 contributed by < `rota1001` > ## 想法 在 [rooty](https://github.com/jermeyyy/rooty) 這個專案可以看到它利用 `filep_open` 去找到特定路徑的 `file_operations` 結構體,以劫持對應目錄的 VFS 界面。這個方法的價值是不管對應的檔案操作函式的 symbol 有沒有被釋放出來,只要我知道那個目錄的路徑我都能獲得它所有的檔案操作函式。然而,專案內使用的 `readdir` 函式在 linux 3.11 之後就已經從 `file_operations` 裡面移除了。而目前我實驗上,如果我建立一個 process file,使用 `filep_open` 打開之後,那個 `file` 結構體裡面的 `file_operations` 結構體和我用來註冊的 `proc_ops` 結構體裡面的函式是不一樣的,所以需要去找到新的方法做這件事情。 以下做的事情是去了解 `proc_dir_entry` 的資料結構,並且利用創建惡意檔案來得到根目錄的地址。另外,在 linux 原始碼中這個結構體有 `__randomize_layout`,所以以下會在不知道結構體內部實作的前提下,利用紅黑樹結點的結構去計算出偏移量。 這樣可以做到什麼事情呢?舉個例子,在 `/proc` 底下有 `kallsyms`,可以讀 symbol,那我利用這個方法獲取了 `proc_dir_entry` 結構體,又計算出 `proc_ops` 的偏移量,所以我能找到 `proc_read` 的函式指標,所以就能獲取 symbol。 ## 實驗環境 測試環境是使用 `qemu` 來跑,使用 `6.11.0-25-generic` 的映像檔: ``` # uname -a Linux buildroot 6.11.0-25-generic #25~24.04.1-Ubuntu SMP PREEMPT_DYNAMIC Tue Apr 15 17:20:50 UTC 2 x86_64 GNU/Linux ``` 另外,在確定使用 `qemu` 跑起來沒有問題就會在我的電腦上直接跑,版本也是 `6.11.0-25-generic`(因為映像檔是直接從我的電腦上抓的): ``` $ uname -a Linux rota1001 6.11.0-25-generic #25~24.04.1-Ubuntu SMP PREEMPT_DYNAMIC Tue Apr 15 17:20:50 UTC 2 x86_64 x86_64 x86_64 GNU/Linux ``` ## 觀察 procfs 實作 首先去追蹤 `proc_create` 的[實作](https://github.com/torvalds/linux/blob/3ce9925823c7d6bb0e6eb951bf2db0e9e182582d/fs/proc/generic.c#L587),可以發現他是要讓一個結構體 `p` 變成另一個結構體 `parent` 的子節點,我們繼續追蹤 `p` 與 `parent` 的關係,會發現最後進到了 `pde_subdir_insert` 這個函式裡面: ```cpp static bool pde_subdir_insert(struct proc_dir_entry *dir, struct proc_dir_entry *de) { struct rb_root *root = &dir->subdir; struct rb_node **new = &root->rb_node, *parent = NULL; /* Figure out where to put new node */ while (*new) { struct proc_dir_entry *this = rb_entry(*new, struct proc_dir_entry, subdir_node); int result = proc_match(de->name, this, de->namelen); parent = *new; if (result < 0) new = &(*new)->rb_left; else if (result > 0) new = &(*new)->rb_right; else return false; } /* Add new node and rebalance tree. */ rb_link_node(&de->subdir_node, parent, new); rb_insert_color(&de->subdir_node, root); return true; } ``` 會發現,這是一個紅黑樹的插入,使用名字字典序來做大小比較。 這裡統整一下這段程式碼可以看到的事情,`proc_dir_entry` 中的 `subdir` 是一個紅黑樹的根,這棵紅黑樹裡面裝的是所有在這個目錄底下的東西(就這一層,目錄底下的目錄中的東西不算),而紅黑樹的節點是內嵌在 `proc_dir_entry` 中的 `subdir_node`。 那所以我創建一個 process file 就能把所有 process file 的資訊都洩漏出來了嗎?去看了 `proc_dir_entry` 的結構發現他有一個 [`__randomize_layout` 巨集](https://hackmd.io/@sysprog/linux-macro-randstruct),在某些組態上這個結構體會根據編譯階段決定的種子隨機分佈,所以我應該在不知道結構體內部實作的前提下來做到這件事情。 那我想,我有方法能判斷一個指標是否為有效指標,又能對指標進行讀取,那如果創建一些惡意的檔案與目錄,就能利用樹的結構去枚舉偏移量了。 ## 偏移量計算 首先是創建 `parent`、`child` 和 `grandchild` 這樣的檔案結構: ```cpp static struct proc_dir_entry *parent, *child, *grandchild; parent = proc_mkdir("parent", NULL); if (!parent) goto DONE; child = proc_mkdir("child", parent); if (!child) goto REMOVE; grandchild = proc_create("grandchild", 0644, child, &proc_file_fops); if (!grandchild) goto REMOVE; unsigned long *parent_arr = (unsigned long *)parent; unsigned long *child_arr = (unsigned long *)child; unsigned long *grandchild_arr = (unsigned long *)grandchild; ``` 另外定義了這樣的獲取成員的巨集: ```cpp #define get_member_ptr(base, offset, type) ((type *)(((char *)(base)) + (offset))) ``` ### 目標 我要去計算以下這些偏移量或地址: ```cpp static int name_offset = 0; static int parent_offset = 0; static int subdir_offset = 0; static int subdir_node_offset = 0; static int proc_ops_offset = 0; static struct proc_dir_entry *proc_root = 0; ``` ### `name` 這就枚舉看看哪個指標是名字: ```cpp // find `char *name` offset for (int i = 0; i < 100; i++) { char buf[7]; if (copy_from_kernel_nofault(buf, parent_arr[i], 6) < 0) continue; buf[6] = 0; if (!strcmp(buf, "parent")) { name_offset = i * sizeof(unsigned long); break; } } printk("name_offset: 0x%x\n", name_offset); ``` ### `parent` 在 child 中枚舉看哪個是 parent: ```cpp // find `struct proc_dir_entry *parent` offset for (int i = 0; i < 100; i++) if (child_arr[i] == (unsigned long)parent) { parent_offset = i * sizeof(unsigned long); break; } printk("parent_offset: 0x%x\n", parent_offset); ``` ### `subdir` 這是紅黑樹的根。在 `parent` 和 `child` 的樹中,現在都只有一個節點,分別是 `child` 和 `grandchild` 的 `subdir_node`,而他們內嵌在結構體中,所以相對於結構體開頭的偏移量是相同的,可以利用這樣去計算: ```cpp for (int i = 0; i < 100; i++) if (parent_arr[i] - (unsigned long)child == child_arr[i] - (unsigned long)grandchild) { subdir_offset = i * sizeof(unsigned long); break; } printk("subdir_offset: 0x%x\n", subdir_offset); ``` ### `subdir_node` 直接把前述的那個偏移量拿來用就好: ```cpp // find `struct rb_node subdir_node` subdir_node_offset = *get_member_ptr(parent, subdir_offset, unsigned long) - (unsigned long)child; printk("subdir_node: 0x%x\n", subdir_node_offset); ``` ### `proc_ops` 在創建 process file 的時候,註冊的那個 `proc_ops` 會被複製一份,所以指標不會一樣,不過裡面的函式指標會是一樣的,只要計算好偏移量去對照函式指標就好了: ```cpp // find `struct proc_ops *proc_ops` for (int i = 0; i < 100; i++) { unsigned long addr; if (copy_from_kernel_nofault(&addr, grandchild_arr[i] + offsetof(struct proc_ops, proc_read), sizeof(addr)) < 0) continue; if (addr == (unsigned long)proc_file_fops.proc_read) { proc_ops_offset = i * sizeof(unsigned long); break; } } printk("proc_ops_offset: 0x%lx\n", proc_ops_offset); ``` ### `proc_root` 這是要找到 `/proc` 這個目錄的 `proc_dir_entry`,這樣以後要什麼目錄就能直接用二元搜尋樹的查詢就好。因為之前已經有把結構體中的 `parent` 的偏移量計算出來了,所以就能簡單的計算: ```cpp // find root proc_root = *get_member_ptr(parent, parent_offset, struct proc_dir_entry *); ``` ## demo 我首先去計算偏移量與獲取根節點,並且呼叫以下的 `test` 函式: ```cpp void dfs(struct rb_node *node) { if (!node) return; dfs(node->rb_left); printk("dfs: %s\n", *get_member_ptr(node, -subdir_node_offset + name_offset, char *)); dfs(node->rb_right); } void test(struct proc_dir_entry *parent) { struct rb_node *node = get_member_ptr(parent, subdir_offset, struct rb_root)->rb_node; printk("test: %lx\n", node); dfs(node); } ``` 這個函式能把這個目錄底下所有東西印出來,去掛載之後用 `dmesg` 查看: ``` [97187.098941] name_offset: 0xa0 [97187.098952] parent_offset: 0x78 [97187.098955] subdir_offset: 0x80 [97187.098957] subdir_node: 0x88 [97187.098959] proc_ops_offset: 0x30 [97187.098967] test: ffff9e56c0301108 [97187.098970] dfs: fb [97187.098973] dfs: fs [97187.098975] dfs: bus [97187.098977] dfs: dma [97187.098979] dfs: irq [97187.098981] dfs: mtd [97187.098982] dfs: net [97187.098984] dfs: sys [97187.098986] dfs: tty [97187.098988] dfs: acpi [97187.098990] dfs: keys [97187.098992] dfs: kmsg ... ``` 可以發現在那裡面的東西都被印出來了,且在中序走訪的過程中是按照字典序排列(長度不一樣時以長度優先),下面可以看到確實有 `kallsyms`: ``` $ sudo dmesg | grep "97187" | grep "kallsyms" [97187.099088] dfs: kallsyms ``` 另外也可以去寫一個二元搜尋的函式就能找到特定目錄或檔案: ```cpp! struct proc_dir_entry *find_child(struct proc_dir_entry *parent, char *name) { struct rb_node *node = get_member_ptr(parent, subdir_offset, struct rb_root)->rb_node; while (node) { int k = cmp(name, *get_member_ptr(node,-subdir_node_offset + name_offset, char *)); if (k < 0) node = node->rb_left; else if (k > 0) node = node->rb_right; else return get_member_ptr(node, -subdir_node_offset, struct proc_dir_entry); } return NULL; } ```