contributed by < vacontron
>
在終端機中使用 free
命令可以查看已安裝的實體記憶體大小及使用量
查看 /proc/iomem 可以發現並非全部的記憶體都被系統用於映射,還有保留給週邊裝置使用的區段,且這些用於映射的記憶體實體上也不是完全連續的
參閱 bootparam (新版本的文件移除了部份參數,詳見 Documentation/admin-guide/kernel-parameters.txt ),在啟動時加入 mem=14G
保留 不被系統用於記憶體映射
此操作為一次性,待下次啟動就會恢復成為未設定狀態。若是使用 grub 開機引導,也可以編輯 /etc/default/grub 中的 GRUB_CMDLINE_LINUX_DEFAULT 欄位變更預設值
查看此次啟動時使用的參數
再次使用 free 命令查看記憶體大小及使用的區段
發現系統中可用於映射的記憶體確實少了高位元的 2GB 左右,而這些被保留下來的記憶體已經脫離系統的管理,可以自由的映射、存取、更改
當 kernel 發生異常時 (panic),我們可以借助一些工具 (如 kdump) 產生錯誤發生前一刻的記憶體傾印幫助排除錯誤。
crash 套件不但可以載入分析這些傾印,還可以透過 /dev/mem 對正在使用中的記憶體進行讀寫。
若沒有安裝 debug symbol,依照 Ubuntuu 官方的 指示 加入儲存庫來源,在執行 apt update
後使用 sudo apt install linux-image-$(uname -r)-dbgsym
命令安裝對應版本的 debug symbol
輸入以下命令 (預設會載入 /dev/mem)
若使用的是 Fedora/RHEL,路徑應改為 /usr/lib/debug/modules/vmlinux-$(uname -r)
試著在 5.13.0-44-generic (Ubuntu 20.04 LTS) 下使用 crash utility (7.2.8) 時會在載入 debug symbols 時出現錯誤,瀏覽該套件的 github issue 時找到解答,作者表示須使用較新版本的套件。因 apt 的 ubuntu 官方來源還未更新,故下載 crash 的 原始碼 自行編譯安裝
之後使用 crash 載入 vmlinux 時出現 segmentation fault,可能是因為在別的地方使用了 apt upgrate 讓環境跟當初編譯 crash 時不一樣導致,重新編譯安裝即可
可以看到如果我們無法直接存取被保留的記憶體區段,因為系統尚未建立分頁表 (page table) 。而我們可以由以下的程式手動建立映射,再使用 crash 觀察其中的行為
可以看到用 vtop 將得到的虛擬記憶體地址 0x7f4b4ff1c000 轉為實體記憶體的地址跟我們在 mmap.c 中指派給 addr 的一樣,而其中的 PGD, PUD, PMD, PTE, PAGE 使用了 5-level paging 的結構。
由上圖可以看出每張表都有 512 個欄位 (entries) ,底下的數字表示映射到的地址空間的大小。接續上面的例子,從 PTE 的 108d0c8e0
欄位找到對應的 8000000400000267
記憶體分頁。因為層級較多,造成查找分頁的時間增加,加上 cache (translation lookaside buffer (TLB)) 可以改善這個問題
我們可以透過修改 PTE 指向的記憶體分頁來達成行程間的資料交換
在 crash 中使用 vtop 查詢 master 中 addr 對應的分頁表
slave:
我們嘗試交換 master, slave 中第 8 行中 PTE 指向的內容 (指向的地址為十六進制,要加上 0x 前綴)
發現無法寫入 PTE 的實體記憶體地址,透過這篇 參考資料 知道在編譯時開啟的 CONFIG_STRICT_DEVMEM
選項會讓寫入受到 devmem_is_allowed
的限制,可以使用 systemtap 工具暫時讓它回傳 1
在安裝 systemtap 時也發生了在官方 apt 庫中的版本過舊問題,在他們的 官方頁面 中找到新發布的原始碼並依照 教學 安裝需要的相依資源以及動態連結程式庫。在編譯 elfutils 前需要在該目錄下執行 ./configure
命令檢查是否缺少其相依資源
在使用 $ stap -g -e 'probe kernel.function("devmem_is_allowed").return { $return = 1 }'
命令時出現 insmod: can‘t insert ‘xx.ko‘: invalid module format
錯誤,須加上 -B CONFIG_MODVERSIONS=y
參數
接下來回到 master, slave 中讓它們繼續執行,發現回傳的內容確實改變了。
除了使用 systemtap 之外,我們也可以修改 devmem_is_allowed()
函式中的二進制指令讓它回傳 1
其中 devmem_is_allowed()
定義在 arch/x86/mm/init.c #821 。註解說明了前 1MB 的記憶體區段充常會保留給 BIOS 等啟動時載入的程式碼,真正被 kernel 管理的是其餘的部份。且因為在 kernel 中一個記憶體分頁的大小是 4KB ,所以是要排除前 個分頁
使用 crash 找到它在核心中的虛擬記憶體位置及對應的指令
觀察程式碼及對應的組合語言,找到關鍵的地址應該是 0xffffffffac285296
的 setbe
,試著更改指令將 al 暫存器直接寫入 1 ,因為要讓 stackframe 保持一致所以加上 nop 填充,預期的結果應該是:
但會導致 ubuntu 的 GUI 凍結、背景音訊變成持續一秒的循環播放,並沒有讓 kernel 崩潰進而觸發 kdump ,也無法使用 ctrl + alt + sysRq/r/e/i/s/u/b 組合鍵重啟。目前尚未找到原因,因此另尋他法使用 kernel module 改寫
重新閱讀 crash 的說明後發現 option -16 並非表示十六進位而是 16 個位元,且要注意 little-Endian,故重新改寫命令:
還是無法成功替換,但會觸發 kdump 後重啟
回頭查詢 kernel 原始碼在 mm/usercopy.c 中發現這段註解:
指出指向 kernel text 的虛擬記憶體並不是唯一的,如果只從其中一個虛擬地址去修改資料的話可能會導致其他對應的虛擬地址裡的資料不一致
建立核心模組修改上面的 devmem_is_allowed()
所在的位置的程式
插入模組後系統崩潰重啟,使用 crash 查看 kdump 產生的傾印:
查詢資料後發現唯讀的分頁會受到 cr0 第 16 位元 (WP) 的管控,只有當第 16 位元為 0 時,具有 supervisor 等級的程式就可以對它操作。因此試著使用 clearbit(16, &cr0); write_cr0(cr0);
取得寫入權限,但又出現另一個問題:
再度查詢資料發現 8dbec27 將 x86 上任意修改 cr0 的功能關閉了,但我們還是有辦法繞過它,首先先來看一下這段原始碼 (arch/x86/kernel/cpu/common.c #L361)
觀察後發現關鍵的判斷是應該是在第九行的位置,若意圖將 cr0 的 WP 改為 0 就會跳到警告的部份了。我們可以用一段組合語言來完成這項功能
使用這個函式重寫上面的核心模組:
接著重新執行 crash ,發現 0xffffffffbb08529d
的地方已經改成 xor 了
這時再重新做一次 "交換記憶體分頁" 的範例,這次應該就可以不用借助 systemtap 的幫助直接修改記憶體內的數值了
做完修改 devmem_is_allowed()
函式後我們預期它的回傳值應該會變成 1 ,但再次使用 wr
寫入時還是發生了 wr: cannot write to /proc/kcore
的錯誤,再次使用 systemtap hook 住並重啟 crash 後就可以寫入了,這表示上面的修改並沒有照預期的改變函式的回傳值,原因待查
如果在做上面的交換記憶體分頁的範例時有重新啟動過電腦,應該會發現在 crash 中顯示的記憶體地址改變了,這是由 address space layout randomization (ASLR) 又稱為 「位址空間組態隨機載入」的保護措施引起的,從 Ubuntu 16.10 後預設為開啟
核心所使用的符號表 (system table) 存放於 system.map 中,可以透過 /boot/System.map-$(uname -r)
檔案找出核心程式碼所用的符號位於虛擬記憶體中的什麼位置,而一個惡意程式可能會藉由這個地址進行 return-to-libc attack 之類的攻擊,透過計算位移量、溢位非法存取、修改該行程原本無法觸及的記憶體區段。要避免這種情況發生,我們可以在載入核心時給它一個位移量隨機化地址,讓意圖不軌的程式無法存取到它預期的地址。
找出 devmem_is_allowed
的原始地址
這是 devmem_is_allowed
現在的地址
即可算出位移量: 0xffffffff85685240 - ffffffff81085240 = 0x460000
重新啟動並在載入核心時使用 norandmaps
選項