# 利用 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;
}
```