# /dev/mem 裝置 contributed by < `vacontron` > ## 測試環境 ```shell $ uname -r 5.13.0-44-generic $ lscpu | grep "Model name" Model name: AMD Ryzen 7 4800H with Radeon Graphics $ cat /proc/meminfo | grep MemTotal MemTotal: 15587316 kB ``` ## 保留記憶體映射區段 在終端機中使用 `free` 命令可以查看已安裝的實體記憶體大小及使用量 ```shell $ free total used free shared buff/cache available Mem: 15587316 5332544 401648 162420 9853124 9767092 Swap: 2097148 1148 2096000 ``` 查看 /proc/iomem 可以發現並非全部的記憶體都被系統用於映射,還有保留給週邊裝置使用的區段,且這些用於映射的記憶體實體上也不是完全連續的 ```shell $ sudo cat /proc/iomem | grep RAM 00001000-0009ffff : System RAM 00100000-09bfefff : System RAM 0a000000-0a1fffff : System RAM 0a20d000-c82affff : System RAM c8410000-c8496017 : System RAM c8496018-c84a3457 : System RAM c84a3458-c84a4017 : System RAM c84a4018-c84b2057 : System RAM c84b2058-c84fefff : System RAM c8500000-c8544fff : System RAM c8546000-cb1d7fff : System RAM cd1ff000-cdffffff : System RAM 100000000-40f33ffff : System RAM ``` 參閱 [bootparam](https://man7.org/linux/man-pages/man7/bootparam.7.html) (新版本的文件移除了部份參數,詳見 [Documentation/admin-guide/kernel-parameters.txt](https://github.com/torvalds/linux/blob/master/Documentation/admin-guide/kernel-parameters.txt) ),在啟動時加入 `mem=14G` 保留 $16 - 14 = 2GB$ 不被系統用於記憶體映射 :::info 此操作為一次性,待下次啟動就會恢復成為未設定狀態。若是使用 grub 開機引導,也可以編輯 /etc/default/grub 中的 GRUB_CMDLINE_LINUX_DEFAULT 欄位變更預設值 查看此次啟動時使用的參數 ```shell $ sudo cat /proc/cmdline BOOT_IMAGE=/boot/vmlinuz-5.13.0-44-generic ro quiet mem=14G ``` ::: 再次使用 free 命令查看記憶體大小及使用的區段 ```shell $ free total used free shared buff/cache available Mem: 13277940 1404680 10287784 13808 1585476 11583796 Swap: 2097148 0 2097148 $ sudo cat /proc/iomem | grep RAM 00001000-0009ffff : System RAM 00100000-09bfefff : System RAM 0a000000-0a1fffff : System RAM 0a20d000-c82affff : System RAM c8410000-c8496017 : System RAM c8496018-c84a3457 : System RAM c84a3458-c84a4017 : System RAM c84a4018-c84b2057 : System RAM c84b2058-c84fefff : System RAM c8500000-c8544fff : System RAM c8546000-cb1d7fff : System RAM cd1ff000-cdffffff : System RAM 100000000-37fffffff : System RAM ``` 發現系統中可用於映射的記憶體確實少了高位元的 2GB 左右,而這些被保留下來的記憶體已經脫離系統的管理,可以自由的映射、存取、更改 ## crash utility 當 kernel 發生異常時 (panic),我們可以借助一些工具 (如 kdump) 產生錯誤發生前一刻的記憶體傾印幫助排除錯誤。 crash 套件不但可以載入分析這些傾印,還可以透過 /dev/mem 對正在使用中的記憶體進行讀寫。 ### 環境設定 若沒有安裝 debug symbol,依照 Ubuntuu 官方的 [指示](https://wiki.ubuntu.com/Debug%20Symbol%20Packages#Manual_install_of_debug_packages) 加入儲存庫來源,在執行 `apt update` 後使用 `sudo apt install linux-image-$(uname -r)-dbgsym` 命令安裝對應版本的 debug symbol 輸入以下命令 (預設會載入 /dev/mem) ```shell $ sudo crash /usr/lib/debug/boot/vmlinux-$(uname -r) ``` :::info 若使用的是 Fedora/RHEL,路徑應改為 /usr/lib/debug/modules/vmlinux-$(uname -r) ::: :::warning 試著在 5.13.0-44-generic (Ubuntu 20.04 LTS) 下使用 crash utility (7.2.8) 時會在載入 debug symbols 時出現錯誤,瀏覽該套件的 [github issue](https://github.com/crash-utility/crash/issues/99) 時找到解答,作者表示須使用較新版本的套件。因 apt 的 ubuntu 官方來源還未更新,故下載 crash 的 [原始碼](https://github.com/crash-utility/crash/releases/tag/7.3.2) 自行編譯安裝 ::: :::warning 之後使用 crash 載入 vmlinux 時出現 segmentation fault,可能是因為在別的地方使用了 apt upgrate 讓環境跟當初編譯 crash 時不一樣導致,重新編譯安裝即可 ::: ### 使用 crash 觀察被保留的記憶體區段 ```shell # 用於建立記憶體映射的實體記憶體區段 crash> rd -p 0x370000000 370000000: 0000000200480040 @.H..... # 保留的記憶體 crash> rd -p 0x400000000 rd: seek error: physical address: 400000000 type: "64-bit PHYSADDR" ``` 可以看到如果我們無法直接存取被保留的記憶體區段,因為系統尚未建立分頁表 (page table) 。而我們可以由以下的程式手動建立映射,再使用 crash 觀察其中的行為 ```c #include <stdio.h> #include <unistd.h> #include <sys/mman.h> #include <fcntl.h> #include <stdlib.h> int main(int argc, char *argv[]) { unsigned char *addr; int fd; fd = open("/dev/mem",O_RDWR); if (fd < 0){ printf("device file open error !\n"); return 0; } addr = mmap(0, 128, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0x400000000); printf("addr = %p \n", addr); *(volatile unsigned int *)(addr + 0x00) = 0x1; /* 將 1 指派給 0x400000000 */ *(volatile unsigned int *)(addr + 0x04) = 0x7; /* 將 7 指派給 0x400000004 */ printf("the address is %p, and the value is %d\n", addr + 0x00, *(addr + 0x00)); printf("the address is %p, and the value is %d\n", addr + 0x04, *(addr + 0x04)); system("read -p 'Press Enter to continue...' var"); munmap(addr,128); close(fd); return 0; } ``` ```shell $ sudo ./mmap addr = 0x7f4b4ff1c000 the address is 0x7f4b4ff1c000, and the value is 1 the address is 0x7f4b4ff1c004, and the value is 7 Press Enter to continue... ``` ```shell crash> ps | grep mmap 116541 116540 8 ffff904e69068000 IN 0.0 2500 1448 mmap crash> set 116541 PID: 116541 COMMAND: "mmap" TASK: ffff904e69068000 [THREAD_INFO: ffff904e69068000] CPU: 8 STATE: TASK_INTERRUPTIBLE crash> vtop 0x7f4b4ff1c000 VIRTUAL PHYSICAL 7f4b4ff1c000 400000000 PGD: 10aa2c7f0 => 10d2b7067 PUD: 10d2b7968 => 100fc2067 PMD: 100fc23f8 => 108d0c067 PTE: 108d0c8e0 => 8000000400000267 PAGE: 400000000 PTE PHYSICAL FLAGS 8000000400000267 400000000 (PRESENT|RW|USER|ACCESSED|DIRTY|NX) VMA START END FLAGS FILE ffff904b6f4be8f0 7f4b4ff1c000 7f4b4ff1d000 d0444bb /dev/mem ``` 可以看到用 vtop 將得到的虛擬記憶體地址 0x7f4b4ff1c000 轉為實體記憶體的地址跟我們在 mmap.c 中指派給 addr 的一樣,而其中的 PGD, PUD, PMD, PTE, PAGE 使用了 [5-level paging](https://en.wikipedia.org/wiki/Intel_5-level_paging) 的結構。 ### 5-level-paging ![image source: wikipedia](https://upload.wikimedia.org/wikipedia/commons/thumb/2/2b/Page_Tables_%285_levels%29.svg/1280px-Page_Tables_%285_levels%29.svg.png) 由上圖可以看出每張表都有 512 個欄位 (entries) ,底下的數字表示映射到的地址空間的大小。接續上面的例子,從 PTE 的 `108d0c8e0` 欄位找到對應的 `8000000400000267` 記憶體分頁。因為層級較多,造成查找分頁的時間增加,加上 cache ([translation lookaside buffer (TLB)](https://en.wikipedia.org/wiki/Translation_lookaside_buffer)) 可以改善這個問題 ### 交換行程的記憶體分頁 (page) 我們可以透過修改 PTE 指向的記憶體分頁來達成行程間的資料交換 ```c #include <stdio.h> #include <unistd.h> #include <sys/mman.h> #include <string.h> #include <fcntl.h> int main(int argc, char **argv) { int fd; unsigned long *addr; fd = open("/dev/mem", O_RDWR); // 在實體記憶體的 0x400000000 位置中寫入 0x1122334455667788 addr = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0x400000000); *addr = 0x1122334455667788; printf("address at: %p content is: 0x%lx\n", addr, addr[0]); // 等待修改記憶體分頁 getchar(); printf("address at: %p content is: 0x%lx\n", addr, addr[0]); close(fd); munmap(addr, 4096); return 0; } ``` ```c #include <stdio.h> #include <unistd.h> #include <sys/mman.h> #include <string.h> #include <fcntl.h> int main(int argc, char **argv) { int fd; unsigned long *addr; fd = open("/dev/mem", O_RDWR); // 在實體記憶體的 0x400004000 位置中寫入 0x1122334455667788 addr = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0x400004000); *addr = 0x8877665544332211; printf("address at: %p content is: 0x%lx\n", addr, addr[0]); // 等待修改記憶體分頁 getchar(); printf("address at: %p content is: 0x%lx\n", addr, addr[0]); close(fd); munmap(addr, 4096); return 0; } ``` ```shell $ sudo ./master address at: 0x7f3b31880000 content is: 0x1122334455667788 ``` ```shell $ sudo ./slave address at: 0x7f3f3cc6e000 content is: 0x8877665544332211 ``` 在 crash 中使用 vtop 查詢 master 中 addr 對應的分頁表 ```shell= crash> vtop 0x7f3b31880000 VIRTUAL PHYSICAL 7f3b31880000 400000000 PGD: 1079fa7f0 => 1073af067 PUD: 1073af760 => 19b08e067 PMD: 19b08ec60 => 10c828067 PTE: 10c828400 => 8000000400000267 PAGE: 400000000 PTE PHYSICAL FLAGS 8000000400000267 400000000 (PRESENT|RW|USER|ACCESSED|DIRTY|NX) VMA START END FLAGS FILE ffff8d36ccffb930 7f3b31880000 7f3b31881000 d0444bb /dev/mem ``` slave: ```shell= crash> vtop 0x7f3f3cc6e000 VIRTUAL PHYSICAL 7f3f3cc6e000 400004000 PGD: 206c267f0 => 206d1f067 PUD: 206d1f7e0 => 206cda067 PMD: 206cdaf30 => 1e74f9067 PTE: 1e74f9370 => 8000000400004267 PAGE: 400004000 PTE PHYSICAL FLAGS 8000000400004267 400004000 (PRESENT|RW|USER|ACCESSED|DIRTY|NX) VMA START END FLAGS FILE ffff8d37c6c949c0 7f3f3cc6e000 7f3f3cc6f000 d0444bb /dev/mem ``` 我們嘗試交換 master, slave 中第 8 行中 PTE 指向的內容 (指向的地址為十六進制,要加上 0x 前綴) ```shell crash> wr -p 10c828400 0x8000000400004267 wr: cannot write to /proc/kcore ``` ```shell crash> wr -p 1e74f9370 0x8000000400000267 wr: cannot write to /proc/kcore ``` 發現無法寫入 PTE 的實體記憶體地址,透過這篇 [參考資料](https://mp.weixin.qq.com/s?__biz=Mzg2OTc0ODAzMw==&mid=2247502373&idx=1&sn=29f55e365916eccda2e7d9b98b810889&source=41#wechat_redirect) 知道在編譯時開啟的 `CONFIG_STRICT_DEVMEM` 選項會讓寫入受到 `devmem_is_allowed` 的限制,可以使用 systemtap 工具暫時讓它回傳 1 :::warning 在安裝 systemtap 時也發生了在官方 apt 庫中的版本過舊問題,在他們的 [官方頁面](https://sourceware.org/systemtap/getinvolved.html) 中找到新發布的原始碼並依照 [教學](https://sourceware.org/git/?p=systemtap.git;a=blob_plain;f=README;hb=HEAD) 安裝需要的相依資源以及動態連結程式庫。在編譯 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 中讓它們繼續執行,發現回傳的內容確實改變了。 ```shell address at: 0x7f3b31880000 content is: 0x8877665544332211 ``` ```shell address at: 0x7f3f3cc6e000 content is: 0x1122334455667788 ``` ### systemtap 的替代方法 除了使用 systemtap 之外,我們也可以修改 `devmem_is_allowed()` 函式中的二進制指令讓它回傳 1 其中 `devmem_is_allowed()` 定義在 arch/x86/mm/init.c #821 。註解說明了前 1MB 的記憶體區段充常會保留給 BIOS 等啟動時載入的程式碼,真正被 kernel 管理的是其餘的部份。且因為在 kernel 中一個記憶體分頁的大小是 4KB ,所以是要排除前 $2 ^ {20} - 2 ^ {14} = 256$ 個分頁 ```c /* * devmem_is_allowed() checks to see if /dev/mem access to a certain address * is valid. The argument is a physical page number. * * On x86, access has to be given to the first megabyte of RAM because that * area traditionally contains BIOS code and data regions used by X, dosemu, * and similar apps. Since they map the entire memory range, the whole range * must be allowed (for mapping), but any areas that would otherwise be * disallowed are flagged as being "zero filled" instead of rejected. * Access has to be given to non-kernel-ram areas as well, these contain the * PCI mmio resources as well as potential bios/acpi data regions. */ int devmem_is_allowed(unsigned long pagenr) { if (region_intersects(PFN_PHYS(pagenr), PAGE_SIZE, IORESOURCE_SYSTEM_RAM, IORES_DESC_NONE) != REGION_DISJOINT) { /* * For disallowed memory regions in the low 1MB range, * request that the page be shown as all zeros. */ if (pagenr < 256) return 2; return 0; } /* * This must follow RAM test, since System RAM is considered a * restricted resource under CONFIG_STRICT_IOMEM. */ if (iomem_is_exclusive(pagenr << PAGE_SHIFT)) { /* Low 1MB bypasses iomem restrictions. */ if (pagenr < 256) return 1; return 0; } return 1; } ``` 使用 crash 找到它在核心中的虛擬記憶體位置及對應的指令 ```shell crash> dis devmem_is_allowed 0xffffffffac285240 <devmem_is_allowed>: nopl 0x0(%rax,%rax,1) [FTRACE NOP] 0xffffffffac285245 <devmem_is_allowed+5>: push %rbp 0xffffffffac285246 <devmem_is_allowed+6>: xor %ecx,%ecx 0xffffffffac285248 <devmem_is_allowed+8>: mov $0x1000200,%edx 0xffffffffac28524d <devmem_is_allowed+13>: mov $0x1000,%esi 0xffffffffac285252 <devmem_is_allowed+18>: mov %rsp,%rbp 0xffffffffac285255 <devmem_is_allowed+21>: push %r12 0xffffffffac285257 <devmem_is_allowed+23>: mov %rdi,%r12 0xffffffffac28525a <devmem_is_allowed+26>: push %rbx 0xffffffffac28525b <devmem_is_allowed+27>: shl $0xc,%r12 0xffffffffac28525f <devmem_is_allowed+31>: mov %rdi,%rbx 0xffffffffac285262 <devmem_is_allowed+34>: mov %r12,%rdi 0xffffffffac285265 <devmem_is_allowed+37>: callq 0xffffffffac2aac40 <region_intersects> 0xffffffffac28526a <devmem_is_allowed+42>: cmp $0x1,%eax 0xffffffffac28526d <devmem_is_allowed+45>: je 0xffffffffac285282 <devmem_is_allowed+66> 0xffffffffac28526f <devmem_is_allowed+47>: xor %eax,%eax 0xffffffffac285271 <devmem_is_allowed+49>: cmp $0x100,%rbx 0xffffffffac285278 <devmem_is_allowed+56>: pop %rbx 0xffffffffac285279 <devmem_is_allowed+57>: pop %r12 0xffffffffac28527b <devmem_is_allowed+59>: setb %al 0xffffffffac28527e <devmem_is_allowed+62>: pop %rbp 0xffffffffac28527f <devmem_is_allowed+63>: add %eax,%eax 0xffffffffac285281 <devmem_is_allowed+65>: retq 0xffffffffac285282 <devmem_is_allowed+66>: mov %r12,%rdi 0xffffffffac285285 <devmem_is_allowed+69>: callq 0xffffffffac2ac7c0 <iomem_is_exclusive> 0xffffffffac28528a <devmem_is_allowed+74>: cmp $0xff,%rbx 0xffffffffac285291 <devmem_is_allowed+81>: pop %rbx 0xffffffffac285292 <devmem_is_allowed+82>: pop %r12 0xffffffffac285294 <devmem_is_allowed+84>: mov %eax,%edx 0xffffffffac285296 <devmem_is_allowed+86>: setbe %al 0xffffffffac285299 <devmem_is_allowed+89>: pop %rbp 0xffffffffac28529a <devmem_is_allowed+90>: xor $0x1,%edx 0xffffffffac28529d <devmem_is_allowed+93>: or %edx,%eax 0xffffffffac28529f <devmem_is_allowed+95>: movzbl %al,%eax 0xffffffffac2852a2 <devmem_is_allowed+98>: retq 0xffffffffac2852a3 <devmem_is_allowed+99>: data32 nopw %cs:0x0(%rax,%rax,1) 0xffffffffac2852ae <devmem_is_allowed+110>: xchg %ax,%ax ``` 觀察程式碼及對應的組合語言,找到關鍵的地址應該是 `0xffffffffac285296` 的 `setbe`,試著更改指令將 al 暫存器直接寫入 1 ,因為要讓 stackframe 保持一致所以加上 nop 填充,預期的結果應該是: ```diff # ... # 0xffffffffac285294 mov %eax,%edx - # 0xffffffffac285296 setbe %al + # 0xffffffffac285296 or $0x1 %al (0x0c01) + # 0xffffffffac285298 nop (0x90) # 0xffffffffac285299 pop %rbp (0x5d) # ... ``` ```shell crash> wr -16 0xffffffffac285296 0x0c0190 ``` :::danger 但會導致 ubuntu 的 GUI 凍結、背景音訊變成持續一秒的循環播放,並沒有讓 kernel 崩潰進而觸發 kdump ,也無法使用 ctrl + alt + sysRq/r/e/i/s/u/b 組合鍵重啟。目前尚未找到原因,因此另尋他法使用 kernel module 改寫 ::: :::warning 重新閱讀 crash 的說明後發現 option -16 並非表示十六進位而是 16 個位元,且要注意 little-Endian,故重新改寫命令: ```shell crash> wr -32 0xffffffffac285296 0x5d90010c ``` 還是無法成功替換,但會觸發 kdump 後重啟 ``` [ 2810.361147] usercopy: Kernel memory overwrite attempt detected to linear kernel text (offset 545430, size 4)! [ 2810.361157] kernel BUG at mm/usercopy.c:99! [ 2810.361161] invalid opcode: 0000 [#1] SMP NOPTI ``` 回頭查詢 kernel 原始碼在 mm/usercopy.c 中發現這段註解: ```c /* * Some architectures have virtual memory mappings with a secondary * mapping of the kernel text, i.e. there is more than one virtual * kernel address that points to the kernel image. It is usually * when there is a separate linear physical memory mapping, in that * __pa() is not just the reverse of __va(). This can be detected * and checked: */ ``` 指出指向 kernel text 的虛擬記憶體並不是唯一的,如果只從其中一個虛擬地址去修改資料的話可能會導致其他對應的虛擬地址裡的資料不一致 ::: --- 建立核心模組修改上面的 `devmem_is_allowed()` 所在的位置的程式 ```c #include <linux/module.h> static int __init init(void) { char *movzbl = 0xffffffffbb08529d; // 從 "or %edx,%eax" 開始的 5 bytes 改成 "mov $0x1,%eax" movzbl[0] = 0xb8; movzbl[1] = 0x01; movzbl[2] = 0x00; movzbl[3] = 0x00; movzbl[4] = 0x00; // 不要真正掛載 return -1; } static void __exit exit(void) {} module_init(init); module_exit(exit); MODULE_LICENSE("GPL"); ``` 插入模組後系統崩潰重啟,使用 crash 查看 kdump 產生的傾印: ```shell $ sudo crash /usr/lib/debug/boot/vmlinux-5.13.0-44-generic /var/crash/202206061613/dump.202206061613 crash> log [ 194.195359] Code: Unable to access opcode bytes at RIP 0xffffffffc03adff2. [ 194.195359] RSP: 0018:ffffa5f442b57b88 EFLAGS: 00010246 [ 194.195359] RAX: ffffffff86a8529d RBX: 0000000000000000 RCX: 0000000000000027 [ 194.200575] RDX: 0000000000000000 RSI: 00000000ffffdfff RDI: ffff92e8ff620988 [ 194.200575] RBP: ffffa5f442b57b88 R08: ffff92e8ff620980 R09: ffffa5f442b57968 [ 194.203367] R10: 0000000000000001 R11: 0000000000000001 R12: ffffffffc03ae000 [ 194.203367] R13: ffff92e6065a9330 R14: ffffffffc0581040 R15: 0000000000000000 [ 194.207353] FS: 00007fabdfdd4740(0000) GS:ffff92e8ff600000(0000) knlGS:0000000000000000 [ 194.207353] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033 [ 194.211182] CR2: ffffffffc03adff2 CR3: 000000018cb26000 CR4: 0000000000350ee0 [ 194.211327] Call Trace: ... ``` 查詢資料後發現唯讀的分頁會受到 cr0 第 16 位元 (WP) 的管控,只有當第 16 位元為 0 時,具有 supervisor 等級的程式就可以對它操作。因此試著使用 `clearbit(16, &cr0); write_cr0(cr0);` 取得寫入權限,但又出現另一個問題: ```shell [ 1196.959693] CR0 WP bit went missing!? ``` 再度查詢資料發現 [8dbec27](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=8dbec27a242cd3e2816eeb98d3237b9f57cf6232) 將 x86 上任意修改 cr0 的功能關閉了,但我們還是有辦法繞過它,首先先來看一下這段原始碼 ([arch/x86/kernel/cpu/common.c #L361](https://elixir.bootlin.com/linux/v5.13/source/arch/x86/kernel/cpu/common.c#L361)) ```c= static inline void native_write_cr0(unsigned long val) { unsigned long bits_missing = 0; set_register: asm volatile("mov %0,%%cr0": "+r" (val), "+m" (__force_order)); if (static_branch_likely(&cr_pinning)) { if (unlikely((val & X86_CR0_WP) != X86_CR0_WP)) { bits_missing = X86_CR0_WP; val |= bits_missing; goto set_register; } /* Warn after we've set the missing bits. */ WARN_ONCE(bits_missing, "CR0 WP bit went missing!?\n"); } } ``` 觀察後發現關鍵的判斷是應該是在第九行的位置,若意圖將 cr0 的 WP 改為 0 就會跳到警告的部份了。我們可以用一段組合語言來完成這項功能 ```c static inline void _write_cr0(unsigned long val) { asm volatile("mov %0,%%cr0" : : "r"(val)); } ``` 使用這個函式重寫上面的核心模組: ```c #include <linux/module.h> static inline void _write_cr0(unsigned long val) { asm volatile("mov %0,%%cr0" : : "r"(val)); } static int __init init(void) { char *movzbl = 0xffffffffbb08529d; // 從 "or %edx,%eax" 開始的 5 bytes 改成 "mov $0x1,%eax" // unlock WP bit _write_cr0(read_cr0() & (~0x10000)); movzbl[0] = 0xb8; movzbl[1] = 0x01; movzbl[2] = 0x00; movzbl[3] = 0x00; movzbl[4] = 0x00; // lock WP bit _write_cr0(read_cr0() | 0x10000); // 不要真正加載 return -1; } static void __exit exit(void) {} module_init(init); module_exit(exit); MODULE_LICENSE("GPL"); ``` 接著重新執行 crash ,發現 `0xffffffffbb08529d` 的地方已經改成 xor 了 ```shell crash> dis devmem_is_allowed ... 0xffffffffac285299 <devmem_is_allowed+89>: pop %rbp 0xffffffffac28529a <devmem_is_allowed+90>: xor $0x1,%edx 0xffffffffac28529d <devmem_is_allowed+93>: mov $0x1,%eax 0xffffffffac2852a2 <devmem_is_allowed+98>: retq ... ``` 這時再重新做一次 "交換記憶體分頁" 的範例,這次應該就可以不用借助 systemtap 的幫助直接修改記憶體內的數值了 :triangular_flag_on_post: :::danger 做完修改 `devmem_is_allowed()` 函式後我們預期它的回傳值應該會變成 1 ,但再次使用 `wr` 寫入時還是發生了 `wr: cannot write to /proc/kcore` 的錯誤,再次使用 systemtap hook 住並重啟 crash 後就可以寫入了,這表示上面的修改並沒有照預期的改變函式的回傳值,原因待查 ::: ### 位址空間組態隨機載入 (ASLR) 如果在做上面的交換記憶體分頁的範例時有重新啟動過電腦,應該會發現在 crash 中顯示的記憶體地址改變了,這是由 [address space layout randomization (ASLR)](https://en.wikipedia.org/wiki/Address_space_layout_randomization) 又稱為 「位址空間組態隨機載入」的保護措施引起的,從 [Ubuntu 16.10](https://wiki.ubuntu.com/Security/Features#kASLR) 後預設為開啟 核心所使用的符號表 (system table) 存放於 system.map 中,可以透過 `/boot/System.map-$(uname -r)` 檔案找出核心程式碼所用的符號位於虛擬記憶體中的什麼位置,而一個惡意程式可能會藉由這個地址進行 [return-to-libc attack](https://en.wikipedia.org/wiki/Return-to-libc_attack) 之類的攻擊,透過計算位移量、溢位非法存取、修改該行程原本無法觸及的記憶體區段。要避免這種情況發生,我們可以在載入核心時給它一個位移量隨機化地址,讓意圖不軌的程式無法存取到它預期的地址。 找出 `devmem_is_allowed` 的原始地址 ```shell $ sudo grep devmem_is_allowed /boot/System.map-5.13.0-44-generic ffffffff81085240 T devmem_is_allowed ``` 這是 `devmem_is_allowed` 現在的地址 ```shell crash> dis devmem_is_allowed 0xffffffff85685240 <devmem_is_allowed>: nopl 0x0(%rax,%rax,1) [FTRACE NOP] ``` 即可算出位移量: 0xffffffff85685240 - ffffffff81085240 = 0x460000 重新啟動並在載入核心時使用 `norandmaps` 選項 ```shell crash> dis devmem_is_allowed 0xffffffffb3485240 <devmem_is_allowed>: nopl 0x0(%rax,%rax,1) [FTRACE NOP] ```