# Linux 核心專題: 統計學和資訊安全
> 執行人: fcu-D0812998
> [專題解說錄影](https://youtu.be/7o6YluXnzIA)
### Reviewed by `rota1001`
在 [host 端直接實作及問題](https://hackmd.io/@sysprog/Skxv0AVEll#host-%E7%AB%AF%E7%9B%B4%E6%8E%A5%E5%AF%A6%E4%BD%9C%E5%8F%8A%E5%95%8F%E9%A1%8C) 有提到在 `/proc/kallsyms` 裡面沒找到 `module_alloc`,代表它沒有被 export。事實上,kallsyms 裡面有的東西是比有被 export 的 symbols 還要多的(如果你指的是 `EXPORT_SYMBOL` 和 `EXPORT_SYMBOL_GPL` 的話)。另外是你有沒有想過自己去分配空間,然後去設定 page 的可執行標誌?我在 x86-64 上有去取得 pte 然後清除 `_PAGE_NX`,成功執行過一段程式,但會用這個方法是因為當時我被規定有些函式不能用的情況。如果要支援多種架構的話,那麼 `set_memory_x` 是一個好選擇。雖然它沒有被 export,但是你可以使用 `kprobe` 去找到它。
#### Reply by `fcu-D0812998`
謝謝你提供的意見!這些方法對我來說很有幫助,未來如果再實作的話,我會用你提到的這些方式來試試看。
### Reviewed by `thwuedwin`
> `jhash` 經過 多輪 bit mixing,對任意位元變化都能擴散,避免 clustering ,對比之下 `Fibonacci hashing` 只對尾段 bit 改變的 key 有良好分布,但高位變化小時很容易 clustering ,甚至發生 collision 。
關於擴散和高敏感度的性質,數學上要如何去量化?
#### Reply by `fcu-D0812998`
在密碼學裡,常用 `avalanche effect` 來表達擴散這個詞,這個是可以透過計算輸入微擾後輸出和原始輸出的 `Hamming distance` 來量化。同理高敏感度也可以用 `Hamming distance` 來量化。
### Reviewed by `Hande1004`
專題報告須精準用詞,不要用[應該](https://hackmd.io/1FHYMOWrQdmYUd8-001eOQ?both=&stext=5213%3A55%3A0%3A1751555093%3AH_C5H-)這類不準確的詞語。
另外請你對於你最後的洗牌結果作出解釋,結果是否有符合預期,提供對應得數學分析,亂度是否足夠等等。
### Reviewed by `h0w726`
>Linux 其實使用多種 hash fucntion 在不同的場景,而 jhash 最常使用就是在不牽涉到外部使用者資料、不需要抗攻擊能力,但又常常需要快速查找、分類、配對的情境, dcache 就是其中一個場景,可用於檔案路徑的雜湊查找,那為甚麼相較於黃金比例的 Fibonacci hash ,linux 傾向使用 jhash 呢?
可以附上 Linux 核心使用到 jhash 程式碼的連結,這樣想看比較方便。
## 簡述
[lab0](https://hackmd.io/@sysprog/linux2025-lab0/%2F%40sysprog%2Flinux2025-lab0-d) 提及若干統計學議題,其中涉及到資訊安全 (如 dudect),並反映在 ASLR 的安全強度上,本專案回顧相關的理論背景,並以 Linux 核心為探討標的。
## TODO: 回顧統計學
> 統計學裡頭 uniform 的定義
> 為何 Linux 核心用到 jhash?為何多種雜湊函數存在於核心?如何以統計學解釋
### 為何 Linux 核心用到 jhash?
首先先理解 jhash 的運作流程
1. 初始化變數 : 將三個 32-bit 的變數 a, b, c 設定為某些初始常數,這會當作內部的 hash 狀態。
2. 分段處理輸入資料 : 每次讀取 12 bytes(3 個 32-bit word),依序加到 a、b、c 三個暫存器中。
3. 進行 mix 運算:將 a、b、c 做一系列的加、減、XOR、rotate 的操作,以達成 entropy 的擴散與高敏感度。**擴散**的意思 : 資料中每個輸入 bit 所帶來的隨機資訊,會在 hash 函數中經過多輪複雜的運算後,平均分布到最終輸出的每一個位元上。這表示輸入中每個變動,都能有效地影響輸出中大部分甚至所有位元。**高敏感度**的意思 : 當輸入資料發生極小的變化,哪怕只是一個 bit ,hash 的輸出結果就會有極大且無規律的改變。
4. 重複處理直到輸入吃完 : 如果最後剩餘不到 12 bytes,則依據剩餘長度進行最後一次特殊的填補與 mix。
5. 最後回傳的 c (通常是 c )雜湊值
那 jhash 的核心就在於 rotate
```c
a -= c; a ^= rol32(c, 4); c += b;
```
hash function 的重點是要不漏掉資訊同時能重新洗牌,那 rotation 本身只是重排 bit 位置,但如果將 rotation 的結果參與 XOR、加法、減法、再次 rot 等混合操作,簡單來說, rotation 可以讓某個高位 bit 可以變成低位,靠著後續 XOR 的操作可以完全打亂整體 bit 結構,避免 `bit correlation` 的發生。
Linux 其實使用多種 hash fucntion 在不同的場景,而 `jhash` 最常使用就是在不牽涉到外部使用者資料、不需要抗攻擊能力,但又常常需要快速查找、分類、配對的情境, `dcache` 就是其中一個場景,可用於檔案路徑的雜湊查找,那為甚麼相較於黃金比例的 `Fibonacci hash` ,linux 傾向使用 jhash 呢?
* `jhash` 只用加減、XOR、rotate且無需使用乘法或 lookup 表
* `jhash` 經過 多輪 bit mixing,對任意位元變化都能擴散,避免 clustering ,對比之下 `Fibonacci hashing` 只對尾段 bit 改變的 key 有良好分布,但高位變化小時很容易 clustering ,甚至發生 collision 。
* 許多 kernel 結構使用 字串作為 key ,但 `Fibonacci hash` 完全不支援
在 git log 中找不到相關有指出 hash 函式選用依據,但有在註解中找到
```c
/*
* This is the single most critical data structure when it comes
* to the dcache: the hashtable for lookups. Somebody should try
* to make this good - I've just made it work.
*
* This hash‑function tries to avoid losing too many bits of hash
* information, yet avoid using a prime hash‑size or similar.
*/
#define D_HASHBITS d_hash_shift
#define D_HASHMASK d_hash_mask
```
後來發現 dcache 使用的其實是 `full_name_hash()` 這個 function 。
```c
unsigned long hash = 0;
for (i = 0; i < len; i++) {
hash = (hash + (c << 4) + (c >> 4)) * 11;
}
hash = (unsigned int) hash;
```
根據註解的句子做解釋 :
* `avoid losing too many bits of hash information` : 一般來說,遮罩的話可能會丟失高位資訊,所以這個函式會先進行如 `hash += hash >> shift` 的操作,把高位資訊折疊回來,盡量保留原本雜湊值中的分布特性與 entropy 。
* `yet avoid using a prime hash-size or similar` : 一般來說,用質數作為表大小可以減少碰撞,使用 `2^n` 大小的表,可以用位元遮罩`(& (2^n - 1))`來快速計算 bucket index,這比 `% prime` 快得多,Linux 核心考慮的是效能與 cache locality,不是數學上的理想分佈。
### 為何多種雜湊函數存在於核心?
Linux 中不同子系統對雜湊函數的需求不同,例如
* `dentry` 利用了 hash 可以達到快速查詢的目標
* `netfilter` 利用 hash 封包流分類
* `random.c` 也會利用 hash 來處理 `entropy`
## TODO: ASLR 探討
> 整理 2025-03-25 問答簡記,討論 entropy 的來源、ASLR 的侷限
### ASLR 的侷限
閱讀完[ nyraa ](https://github.com/nyraa?tab=repositories)同學整理 ASLR 的筆記後,這本書標題為 `"Exploiting Linux and PaX ASLR’s Weaknesses on 32- and 64-bit Systems"` 這本書主要去深入分析 Linux 與 PaX 的 ASLR 實作弱點。
1. Low Entropy
* Page Alignment : 原理是記憶體必須依照 page 大小對齊才能配置,~~普通~~ 常見的 page 大小為 4KB , 4KB 為 $2^{12}$ = 4096 bytes 換算成二進位為 `1000000000000` (後面 12 個 0 ),因為若不以 4096 bytes 為單位放的話,作業系統會不知道要把那個 page 放在哪裡。而 Huge page 大小為 2MB = $2^{21}$ =2097152 = `1000000000000000000000` (後面 21 個 0 ),這些被固定的 bits 就是 ASLR 無法利用的 entropy 位元,即使你有 32-bit 或 64-bit 的地址空間,但真正能用來做隨機的 bits 被減少了。
* 為了相容性與避免 Fragmentation,Linux 限制了分佈範圍PaX 允許物件隨機在整個 VMA,但 Linux 會讓 mmap、libs、heap 各用自己的一小塊區域來隨機。這會導致一個記憶體區段只有 $2^8$ = 256 種可能位置 ⇒ 實際 entropy 僅約 8 bits ,這麼低的隨機性使得攻擊者可以透過暴力猜測嘗試所有可能位址,在極短時間內就可能命中目標區段(如 libc 或 heap)。若目標系統使用如 `Apache` 或 `Android Zygote` 等 fork 模型,攻擊者甚至可以透過反覆建立子行程並保留相同記憶體配置,以逐步測試並突破 ASLR 的保護機制。
2. Non-uniform distribution
* ASLR 理想狀況下,希望各種載入位址是 uniform 的,==而 uniform 在統計學的定義是所有可能的數值都有「相等的機率」被選到==,也就是每個可能的記憶體位置被選到的機率都相等。但根據下列圖顯示,舉例來說 PaX 32-bit 因為是兩個隨機變數相加,會導致產生三角形分布,也就是中心極限定理(CLT),意思是若一個隨機變數 𝑋1,𝑋2,…,𝑋𝑛 是獨立同分布(i.i.d.),且期望值與變異數有限,則當 𝑛→∞,它們的「平均」或「總和」會趨近於常態分布,根據 CLT 的數學表示式 : $$
Z=\frac{S_n - \mathbb{E}[S_n]}{\sqrt{\mathrm{Var}(S_n)}} \xrightarrow{d} \mathcal{N}(0, 1)
$$這個式子的意思是
> 把多個隨機變數加總後,先減掉它們的平均期望值,再除以標準差,這個結果就會越來越接近「標準常態分布」——也就是中間值機率高、兩邊機率低的鐘形曲線。這樣的結果可以進一步用來「查標準常態分布表」,計算某個值落在特定範圍內的機率。舉例來說,若經過標準化後得到的 Z 值為 1,查表可知其機率約為 0.8413,代表該值小於平均值加上一個標準差的機率為 84.13%。這種查表方式可用來預估落在某區間內的機率大小,也說明為何分布集中的 ASLR 位址可能被更容易猜中。
3. Correlation Between Maps
* 在理想的 ASLR 中,每個記憶體區段(如 libc、heap、stack、mmap)應該都是獨立隨機配置的。但實際上,很多情況下這些區段的位址是有關聯的,攻擊者若知道某一區段的位址,就可能推測出其他區段的大致位置。
1. Total Correlation : 只要知道一個函式庫的位址,就能知道所有函式庫、heap、stack 的相對位址
2. Positive Correlation : 意思是有部分位址資訊是有關的,但不能完全確定整體位址
3. Useless Correlation : 不同區段之間毫無關係,知道一個區段完全無法推論另一個。這才是理想 ASLR 應有的目標。
4. Memory Layout Inheritance
* 在 Linux 中,當程式使用 `fork()` 系統呼叫建立子行程時,新的子行程會繼承親代節點的整個記憶體佈局,這代表即使系統開啟了 ASLR ,這些位址在子行程中也不會重新隨機化,而是完全複製親代節點當前的記憶體分佈。這種行為在安全上產生嚴重風險,特別是在常見的伺服器應用中,如 Apache 或 PHP-FPM,這些服務常採用 `pre-fork` 模型,先 fork 出大量子行程等待請求。攻擊者若能導致其中一個子行程崩潰親代節點通常會重新 fork 一個新的子行程來替補,但由於新的子行程繼承了與上一個完全相同記憶體佈局,攻擊者即可反覆對相同記憶體位置進行爆破,極大提升成功率。這種「記憶體位址可重生」的行為破壞了 ASLR 應有的隨機性保障,使得原本應該難以預測的位置,變成攻擊者可以一再嘗試的固定目標。
### 討論 entropy 的來源
1948年, Claude Shannon 在其論文[《A Mathematical Theory of Communication》](https://people.math.harvard.edu/~ctm/home/text/others/shannon/entropy/entropy.pdf)中,將熵的概念引入資訊理論,提出了資訊熵的概念,用以衡量訊息的不確定性。他指出這個不確定性函數應該滿足以下三項基本條件:
1. 連續性 : 熵 $H(p_1, \ldots, p_n)$ 對於所有機率變數 $Pi$ 是連續的,意思是機率的微小變化不應造成熵的不連續跳動。
2. 極大性 : 若所有事件是「等可能」的(例如 pi = 1/n ),那麼事件總數 n 越多,熵 H 越大。越多。簡單來說意思是選擇越多,不確定性就應該越大。
3. 可加性 : 若某個事件的選擇是兩階段決定的,則總熵應等於第一階段熵,加上該階段下條件熵的加權平均。
根據以上三個條件可定義出公式為 :
$S = - \sum^{n}_{i=1} P(x_i) log_b P(x_i)=\sum^{n}_{i=1} P(x_i)I(x_i)$
在資訊理論中,熵越高,表示資訊的不確定性越大;熵越低,表示資訊越有序、可預測。
而之中為甚麼使用 log 而不是其他數學理論呢?
1. 資訊量與加法性( Additivity )來看 :
當你有多個獨立事件(例如兩枚硬幣),你希望 $H(X,Y) = H(X) + H(Y)$ 意思是總資訊量 = 個別資訊量相加,對數函數有乘法轉加法的性質 $log(xy)=logx+logy$ 所以用對數,才能讓機率乘積對應到資訊量加總。
2. 遞增性 :
在information theory中,$-log2(1/p)$ 也稱之為資訊本體( self-information ),也就是說,越不常出現的訊息往往帶著越大的資訊含量,同理來說,如果當一個事件一定會發生(機率 p = 1),那就不帶來任何新資訊,用對數表示就是 $log(1) = 0$。
3. 公式可以表達成當每件事機率相同, entropy 會越大,舉例假設一個三面骰子,他的 entropy 加總後會是 1.585 bits ,但如果出現不均勻的現象,則 entropy 就會小於 1.585 bits。
[參考資料](https://www.quora.com/Why-is-there-a-logarithmic-function-in-the-entropy-formula)
## TODO: 論文回顧和重現
> 重現 [Chaos: Function Granularity Runtime Address Layout Space Randomization for Kernel Module](https://dl.acm.org/doi/10.1145/3678015.3680476) 實驗並檢討論
論文內容 :
* 將 ASLR 細化到函式等級,兼顧隨機性與執行效率。
* 在執行期定時重排函式記憶體位置,有效對抗持續性攻擊。
* 從原本相對位置固定,到使用 `Fisher-Yates` 演算法重新排列所有函式段順序,導致 gadget 相對位址不斷變動進而讓 entropy 增加。

### host 端直接實作及問題
原本的想法是利用 `module_alloc()` 的方式去將函式的機器碼完整複製到新的記憶體空間,但後來編譯時出現以下錯誤
```gcc
ERROR: modpost: "module_alloc" [/home/junan/chaos/chaos_module.ko] undefined!
```
使用 command 尋找模組
```
grep module_alloc /proc/kallsyms
```
輸出空白,代表 Linux 核心沒有將它對外 export 。
### virtme-ng 實作及問題
後來聽取老師的建議,嘗試使用 KVM 來實作,參考資料為[測試 Linux 核心的虛擬化環境](https://hackmd.io/@sysprog/linux-virtme),論文中的環境為 `Linux 5.10 LTS on ARM64`
* 安裝 virtme-ng
```shell
$ sudo apt install virtme-ng
```
* 下載與編譯 Linux kernel
```shell
$ git clone https://github.com/torvalds/linux.git
$ cd linux
$ git checkout v5.10
$ vng --build
```
遇到問題
```bash
error: pointer ‘ptr’ may be used after ‘realloc’ [-Werror=use-after-free]
```
意思是呼叫 `realloc(ptr, size)` 之後,又用到了 ptr ,GCC 13 不允許這種危險寫法,解決方法為以下,[參考資料](https://unix.stackexchange.com/questions/709671/linux-kernel-5-15-54-compilation-errors-with-gcc-12-1)
```
edit Makefile(s) and remove -Werror=use-after-free
```
接下來又遇到問題
```bash
arch/x86/entry/thunk_64.o: warning: objtool: missing symbol table
make[2]: *** [scripts/Makefile.build:360:arch/x86/entry/thunk_64.o] 錯誤 1
```
解決方法為先進入目錄
```bash
cd ~/linux/tools/objtool
```
開啟 `elf.c`
```bash
nano elf.c
```
找到以下這段的程式碼做修改
```diff
symtab = find_section_by_name(elf, ".symtab");
if (!symtab) {
WARN("missing symbol table");
- return -1;
+ return 0;
}
```
即可解決此問題。[參考資料](https://blog.csdn.net/qq_40552624/article/details/131401326)
後來又遇到問題
```pgsql
unable to access /boot/vmlinuz-6.11.0-26-generic (check for read permissions)
```
代表 `vng --run` 嘗試載入主機系統的 kernel 映像檔,而不是載入自己編譯的 Linux 核心。網路上找不到相關解決方法,於是求助大型語言模型。
```bash!
virtme-run --kdir ~/linux --memory 512M --mods=none --verbose --qemu-opts -nographic
```
意思是利用 `--kdir ~/linux` 去指定 kernel source 的路徑。
```bash
[ 1.183602] virtme-ng-init: waiting for udev to settle
[ 1.363287] virtme-ng-init: udev is done
[ 1.368347] virtme-ng-init: initialization done
_ _
__ _(_)_ __| |_ _ __ ___ ___ _ __ __ _
\ \ / / | __| __| _ _ \ / _ \_____| _ \ / _ |
\ V /| | | | |_| | | | | | __/_____| | | | (_| |
\_/ |_|_| \__|_| |_| |_|\___| |_| |_|\__ |
|___/
kernel version: 5.10.0-virtme x86_64
(CTRL+d to exit)
root@(none):/#
```
這邊又遇到問題
```pgsql
mkdir: cannot create directory '/share': Read-only file system
```
:::info
`virtme` 若無設置則它會自動使用 `busybox` ,這邊的問題是 `busybox` 無法使用 `virtfs` 的方法,就是在 host 端編寫 .ko 檔再去虛擬機裡共用資料夾去做掛載,而利用 `busybox` 建構的環境也無法編譯,必須使用其他能在虛擬機裡進行編譯的 `rootfs` 才能去做重現。
:::
### QEMU 搭配 Debian ARM64 rootfs 實作及問題
感謝 [devarajabc](https://hackmd.io/@devaraja/debian-rootfs#%E5%88%A9%E7%94%A8-qemu-system-riscv64-%E9%81%8B%E4%BD%9C-Debian-GNULinux) 和 [Yiwei Lin](https://hackmd.io/@RinHizakura/SJ8GXUPJ6#tip1) 的筆記
1. 首先你可以用 [clone](https://github.com/qemu/qemu) 或者 apt 來安裝 QEMU
2. Clone 你要的 linux kernal 版本
```sh!
git clone --depth=1 --branch v5.10 https://github.com/torvalds/linux.git linux-5.10
cd linux-5.10
make ARCH=arm64 defconfig
make -j$(nproc) ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- Image
```
3. 建立一個空的映像檔並格式化,掛載並解壓 rootfs ,再來卸載映像
```sh
dd if=/dev/zero of=rootfs.img bs=1M count=1024
mkfs.ext4 rootfs.img
mkdir ~/mnt-rootfs
sudo mount -o loop rootfs.img ~/mnt-rootfs
sudo tar -xpf debian-rootfs.tar.xz -C ~/mnt-rootfs
sudo umount ~/mnt-rootfs
```
4. 啟動 QEMU
:::info
在這裡則又遇到錯誤 `Kernel panic - not syncing: Requested init /init failed` 意思是 `rootfs` 裡根本沒有 `/init` 可執行檔。
:::
### [程式實作](https://github.com/fcu-D0812998/chaos_reproduce)
論文提及
> Chaos utilizes the symbol and relocation information in the ELF file to update the target address of branch instructions for various function arrangements.
這表示論文的作法是針對 ELF 檔中每個函式都劃分為獨立的 section,方便在重排時利用符號表與重定位資訊快速搬移或重建函式位址。
但由於 Linux 核心記憶體有保護機制,在搞定虛擬機的問題之前先實作看看邏輯層級的函式等級隨機化,就是先改變 trampoline 指標指向的函式位置,意思就是將 trampoline 的記憶體位址在載入時固定不變,但其內部指向的函式地址在載入後經過洗牌重新指派。
1. 撰寫 GCC Plugin
用 GCC Plugin 掃描每個函式,並為其加上 `__attribute__((section(".chaos_func_<編號>")))`
```c!
// 判斷是否為 Chaos 處理的函式
static bool should_chaos_process(tree fndecl) {
const char *name = IDENTIFIER_POINTER(DECL_NAME(fndecl));
return name && strncmp(name, "chaos_", 6) == 0;
}
// 加上 section 屬性
static void add_chaos_section_attribute(tree fndecl) {
static int func_counter = 0;
char section_name[256];
snprintf(section_name, sizeof(section_name), ".chaos_func_%d", func_counter++);
DECL_ATTRIBUTES(fndecl) = tree_cons(
get_identifier("section"),
build_string_literal(strlen(section_name)+1, section_name),
DECL_ATTRIBUTES(fndecl));
}
```
這些函式將會變成 shuffle 的目標
```c!
#define __chaos_func __attribute__((section(".chaos_func"), used))
void __chaos_func chaos_func1(void) { printk(...); }
void __chaos_func chaos_func2(void) { printk(...); }
void __chaos_func chaos_func3(void) { printk(...); }
...
```
建置 GCC Plugin
```c!
$(CHAOS_PLUGIN_SO): chaos_gcc_plugin.cpp
$(CXX) -fPIC -shared -I$(GCC_PLUGIN_DIR)/include -o $@ $<
```
可以利用此命令確認 section 是否獨立
```bash!
readelf -S chaos_core.o | grep chaos_func
```
可以看到輸出
```
[26] .chaos_func_0
[27] .chaos_func_1
[28] .chaos_func_2
...
```
代表每個函式都有自己的 ELF section。
2. 先定義函式表以及 trampoline 陣列
```c!
static void (*chaos_func_table[])(void) = {
chaos_func1, chaos_func2, chaos_func3, chaos_func4, chaos_func5
};
#define CHAOS_FUNC_NUM (sizeof(chaos_func_table)/sizeof(chaos_func_table[0]))
struct chaos_trampoline {
void (*func_addr)(void); // 實際被呼叫的函式指標
int func_id; // 本身在表中的索引
unsigned long call_count; // 被呼叫次數統計
};
static struct chaos_trampoline trampolines[CHAOS_FUNC_NUM];
```
3. 使用 Fisher–Yates 洗牌演算法重排 `chaos_func_table[]`
```c!
static void chaos_shuffle_functions(void) {
int i;
void *tmp;
for (i = CHAOS_FUNC_NUM - 1; i > 0; i--) {
unsigned int j;
get_random_bytes(&j, sizeof(j));
j = j % (i + 1);
// swap chaos_func_table[i] ↔ chaos_func_table[j]
tmp = chaos_func_table[i];
chaos_func_table[i] = chaos_func_table[j];
chaos_func_table[j] = tmp;
}
// 更新 trampoline 指標
for (i = 0; i < CHAOS_FUNC_NUM; i++) {
trampolines[i].func_addr = chaos_func_table[i];
}
chaos_ctx.shuffle_count++;
}
```
4. 定時自動洗牌
```c
INIT_WORK(&chaos_ctx.shuffle_work, chaos_shuffle_work);
timer_setup(&chaos_ctx.shuffle_timer, chaos_timer_callback, 0);
mod_timer(&chaos_ctx.shuffle_timer, jiffies + msecs_to_jiffies(50));
```
```c!
static void chaos_timer_callback(struct timer_list *t) {
schedule_work(&chaos_ctx.shuffle_work);
}
static void chaos_shuffle_work(struct work_struct *work) {
mutex_lock(&chaos_ctx.variant_mutex);
chaos_shuffle_functions();
mutex_unlock(&chaos_ctx.variant_mutex);
mod_timer(&chaos_ctx.shuffle_timer, jiffies + msecs_to_jiffies(50));
}
```
輸入命令
```shell
sleep 3
sudo dmesg | grep '\[Chaos\]' | tail -30
```
結果為
```bash
[33977.805411] [Chaos] 洗牌前函式位址:
[33977.805417] [Chaos] [0] 00000000a4c87289
[33977.805426] [Chaos] [1] 00000000cf264ec0
[33977.805431] [Chaos] [2] 0000000036e40a28
[33977.805436] [Chaos] [3] 0000000048780368
[33977.805440] [Chaos] [4] 0000000011f31c71
[33977.805449] [Chaos] 洗牌後函式位址:
[33977.805452] [Chaos] [0] 00000000cf264ec0
[33977.805456] [Chaos] [1] 0000000036e40a28
[33977.805460] [Chaos] [2] 00000000a4c87289
[33977.805464] [Chaos] [3] 0000000011f31c71
[33977.805469] [Chaos] [4] 0000000048780368
```
結果可以看到每個 trampoline 的位址在經歷洗牌後都呼叫到不同的 func ,而我的方法是把函式放進函式指標表中,洗牌後更新 trampoline 指向的目標,改變函式的呼叫順序,但函式的機器碼位置並未改變。
論文的方法則是在載入時把每個函式的機器碼搬到隨機的位置,改變整個記憶體佈局。
### 更新版本
感謝 `rota1001` 同學的建議,已成功實作了動態函式搬移,利用 kprobe 取得未 export 的 set_memory_x 符號,將 vmalloc 分配的頁面標記為可執行,並把函式機器碼複製到這些 vmalloc 頁上執行。,以下為執行結果,更詳細的未來再補充。
```bash
Chaos 虛擬記憶體
洗牌次數: 9021
函式表:
[0] 目前: 00000000e5e2da43 (搬移: 是)
過往位址:
000000000c40f742 -> 00000000e5e2da43
00000000e5e2da43 -> 00000000e751981e
00000000e751981e -> 00000000f6a5f393
00000000f6a5f393 -> 00000000db99a8ab
00000000db99a8ab -> 00000000e5e2da43
[1] 目前: 00000000db99a8ab (搬移: 是)
過往位址:
00000000f6a5f393 -> 00000000db99a8ab
00000000db99a8ab -> 00000000548fa8fd
00000000548fa8fd -> 00000000ae0665bb
00000000ae0665bb -> 00000000f6a5f393
00000000f6a5f393 -> 00000000db99a8ab
[2] 目前: 00000000f6a5f393 (搬移: 是)
過往位址:
00000000ae0665bb -> 000000000c40f742
000000000c40f742 -> 00000000e5e2da43
00000000e5e2da43 -> 00000000e751981e
00000000e751981e -> 00000000ae0665bb
00000000ae0665bb -> 00000000f6a5f393
[3] 目前: 00000000ae0665bb (搬移: 是)
過往位址:
00000000e751981e -> 00000000f6a5f393
00000000f6a5f393 -> 00000000db99a8ab
00000000db99a8ab -> 00000000548fa8fd
00000000548fa8fd -> 00000000e751981e
00000000e751981e -> 00000000ae0665bb
[4] 目前: 00000000e751981e (搬移: 是)
過往位址:
00000000548fa8fd -> 00000000ae0665bb
00000000ae0665bb -> 000000000c40f742
000000000c40f742 -> 00000000e5e2da43
00000000e5e2da43 -> 00000000548fa8fd
00000000548fa8fd -> 00000000e751981e
```