--- tags: LINUX KERNEL, LKI --- # Linux 核心的 `/dev/mem` 裝置 > 改寫自 [Linux /dev/mem的新玩法](https://blog.csdn.net/dog250/article/details/102745181) 一文,採用 [CC BY-SA 授權](https://creativecommons.org/licenses/by-sa/4.0/) ## `/dev/mem` 裡有什麼 簡單來講,Linux 核心的 `/dev/mem` 是系統實體記憶體的映像檔案,這裡的「實體記憶體」需要進一步解釋。 ![](https://i.imgur.com/gR8gwNr.png) 實體記憶體是指插在主機板上的那些一條又一條的記憶體嗎?當然是,但實體記憶體不單單指條狀的記憶體儲存實體。 > 延伸閱讀: [像織毛衣一樣把記憶體織出來](https://youtu.be/mfEKIERbnc4) 嚴格來說,實體記憶體是指實體定址空間,而記憶體這樣的儲存裝置僅是映射到這個定址空間的一部分,其餘的還有各種 PCI 設備、IO port 等。我們可從 `/proc/iomem` 中看到這個映射: ```shell $ sudo cat /proc/iomem 00000000-00000fff : Reserved 00001000-0009d3ff : System RAM 0009d400-0009ffff : Reserved 000a0000-000bffff : PCI Bus 0000:40 000c0000-000dffff : PCI Bus 0000:00 000c0000-000cebff : Video ROM 000e0000-000fffff : Reserved 000f0000-000fffff : System ROM 00100000-09deffff : System RAM 09df0000-09ffffff : Reserved 0a000000-0affffff : System RAM 0b000000-0b01ffff : Reserved 0b020000-769befff : System RAM 769bf000-77fabfff : Reserved 77fac000-77fdefff : ACPI Tables 77fdf000-784d1fff : ACPI Non-volatile Storage 784d2000-79823fff : Reserved 79824000-7bffffff : System RAM 7c000000-7fffffff : Reserved ... ``` :::info 由於 Linux 安全考量,用一般使用者的權限來執行 `$ cat /proc/iomem` 和超級使用者 (即 `root`),會看到不同的內容,僅有後者能看到完整的資訊 ::: 其中,只有 RAM 才是指記憶體儲存裝置。關於實體定址空間的詳細情況,請參考 [BIOS-e820](https://en.wikipedia.org/wiki/E820) 相關的素材,後者是 BIOS 讓開機引導程式 (boot loader) 或 Linux 一類的作業系統核心得以存取的記憶體映射資訊。 明白實體記憶體的組成之後,我們接著觀察 `/dev/mem` 裡面有什麼。 事實上,它就是個執行中的 Linux 系統即時映像 (live image),所有行程的 `task_struct` 結構、`sock` 結構、`sk_buff` 結構,和行程資料等都在裡面的某個位置: ![實體記憶體示意圖](https://i.imgur.com/n02q8uX.png) 如果能夠定位它們在 `/dev/mem` 裡的位置,我們就能得到系統中這些資料結構的實時值,所謂的除錯工具所做的事情也是如此。其實我們透過 core dump 檔案進行除錯所用的 `vmcore` 也是個實體記憶體映像,和 `/dev/mem` 不同的是,它是一具**鹹魚**。 :::info 當 Linux 核心發生崩潰 (crash) 時,可經由 [kdump](https://en.wikipedia.org/wiki/Kdump_(Linux)) 等方式蒐集崩潰前記憶體的狀態,並建立一個 core dump 檔案,也就是 `vmcore` 來紀錄崩潰前的記憶體狀態,開發人員可以透過分析 `vmcore` 得知造成崩潰的原因。 也就是說,`vmcore` 雖然也是一個實體記憶體映像,但它紀錄的內容是崩潰前的記憶體狀態而不是當下的記憶體狀態,因此這邊戲稱為**鹹魚**。 粵語中「鹹魚」是「屍體」的意思:在處理屍體的過程中會「打包」,即是將屍體加上防腐劑後,再以白色麻布包裹著屍體,緊縛著以免屍體接觸空氣和水氣。這個形象和廣東人曬鹹魚的過程相仿:曬鹹魚前用鹽醃好魚類再曬乾,當魚類曬乾風乾之後,再用乾的白紙包裹鹹魚魚身。於是,廣東人將屍體稱作「鹹魚」。 ::: 但無論是活體,還是鹹魚,其功能與紀錄訊息的方式皆是相同的,分析它們的手段也相同。和靜態分析 `vmcore` 不同的是,`/dev/mem` 是一個動態的記憶體映像(會記錄當下的記憶體狀態),有時候可以借助它做一些正經的事情。 下面通過幾個範例,介紹和展示 `/dev/mem` 的一些玩法。 操作環境資訊 ``` 作業系統: Ubuntu 18.04.2 LTS 核心版本: 4.15.0-88-generic 記憶體: 12073632 kB (約 12G) CPU: Intel® Core™ i5-8250U CPU @ 1.60GHz × 8 ``` ## 映射系統保留的記憶體 Linux 核心的記憶體管理子系統非常強大,同時也非常複雜。我們享受其恩惠的同時,偶爾也會被其折磨得痛苦不堪。動輒記憶體不足 (Out Of Memory killer, OOM killer) 殺掉關鍵行程,動輒 flush 導致 CPU 使用率飆高… 為了避免任意行程任意使用記憶體,我們引入資源隔離的機制,比如 [cgroup](http://man7.org/linux/man-pages/man7/cgroups.7.html),但這樣反而會變得更加複雜。能否單純保留一部分記憶體,使其不受到核心的記憶體管理系統控制呢?就好像很多資料庫可以不經檔案系統直接訪問硬碟一樣,核心中有沒有什麼機制能讓我們不經過記憶體管理系統而直接使用記憶體呢? 當然有!加上 `mem` **啟動參數**即可實現。 這裡介紹一種關於保留記憶體的最簡單配置,在`cmdline` 中設置 `mem` 啟動參數: `mem=11G` :::info 此指令為 kernel cmdline 的指令,cmdline 藉由 bootloader 傳送給 kernel,kernel 按照解析的結果進行資源配置。 ::: 使用 `cat /proc/cmdline` 查看目前 `cmdline` 的參數 ```shell $ sudo cat /proc/cmdline BOOT_IMAGE=/boot/vmlinuz-4.15.0-88-generic root=UUID=8d259ef1-e1b6-45b1-947b-8c5aae1b32dd ro mem=11G ``` 系統總共有 12G 的記憶體(實體記憶體的總容量),那麼上述啟動參數將會保留 12G-11G 的記憶體空間不會被核心中的記憶體管理系統所管理,因此保留下來的記憶體就是 1G。 :::warning 目前是利用 grub2 來傳入 cmdline,透過編輯 `/etc/default/grub` 檔案中的 `GRUB_CMDLINE_LINUX_DEFAULT=""` 來達成,從目前啟用前後的差異來看,少掉的記憶體不只 1G,問題應該是出在 `mem=11G` 這項,尚未找到解決辦法。 ::: 觀察加入 `mem` 前後 `free` 跟 `/proc/iomem` 的差異: **配置前** ```shell $ free total used free shared buff/cache available Mem: 12073632 180836 11658444 1588 234352 11628592 Swap: 4194300 0 4194300 $ sudo cat /proc/iomem | grep RAM 00001000-00057fff : System RAM 00059000-00087fff : System RAM 00100000-56945fff : System RAM 56948000-58387fff : System RAM 58c88000-6ee9dfff : System RAM 6fffe000-6fffefff : System RAM 100000000-383ffffff : System RAM ``` **加入 `mem=11g` 後** ```shell $ free total used free shared buff/cache available Mem: 8913568 184448 8493344 1592 235776 8477288 Swap: 4194300 0 4194300 $ sudo cat /proc/iomem | grep RAM 00001000-00057fff : System RAM 00059000-00087fff : System RAM 00100000-56945fff : System RAM 56948000-58387fff : System RAM 58c88000-6ee9dfff : System RAM 6fffe000-6fffefff : System RAM 100000000-2bfffffff : System RAM ``` 可以觀察到,顯示的記憶體空間確實減少了,這是因為被保留下來的記憶體將不會記入核心的任何統計。 換句話說, 核心不再管理這 1G 的記憶體空間,程式可以任意修改,任意洩漏 (leak, 指程式未能釋放不再使用的記憶體),任意溢位 (超過能保存的最大空間),任意覆蓋,指的都不會對系統造成任何影響,所謂的系統保留的含意指的就是**核心不會為該段記憶體空間建立--映射表(x86_64 為原系统可以映射 64T 的實體記憶體)**。 我們經常使用的 crash 工具讀取記憶體使用的方式就是一一映射(mapping)。 > 在 x86_64 的平台上,每一個非保留的實體記憶體分頁可能會有多個映射,而被保留的實體記憶體分頁不會有下面第一種映射: > 1. 一一映射到 `0xffff880000000000` 開始虛擬記憶體地址。【保留的分頁無這種映射】 > 2. 映射到使用者空間,使用行程的記憶體空間。 > 3. 臨時映射到核心模式空間臨時 touch。 > 4. ... 我們試著用 `crash` 工具來讀取一下保留的記憶體: ```shell crash> rd -p 0x2C0000000 rd: seek error: physical address: 2c0000000 type: "64-bit PHYSADDR" ### 同時看一下查看未保留的空間 ### crash> rd -p 2bffffff1 2bffffff1: 5f6e6f6973726576 version_ ``` 顯然,核心並沒有對保留分頁建立一一映射的分頁表,所以讀取是失敗的。 我們已經知道了 `/dev/mem` 檔案是整個實體記憶體的映像,所以使用者行程可以使用 `mmap` 系統呼叫來重建使用者空間的頁表。方法如下: ```cpp #include <stdio.h> #include <unistd.h> #include <sys/mman.h> #include <fcntl.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,0x1000,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0x2C0000000); printf("addr = %p \n", addr); *(volatile unsigned int *)(addr + 0x00) = 0x1; // 0x2C0000000,令其值為1 *(volatile unsigned int *)(addr + 0x04) = 0x9; // 0x2C0080004,令其值為9 getchar(); munmap(addr,0x1000); close(fd); return 0; } ``` 是不是很簡單呢? 此時,在我們操作 `mmap` 的行程中便可以存取保留的記憶體了: ```shell crash> ps | grep test crash: current context no longer exists -- restoring "crash" context: > 31781 31780 4 ffff93d0ed35af00 RU 0.0 4512 1352 test crash> set 31781 PID: 31781 COMMAND: "test" TASK: ffff93d0ed35af00 [THREAD_INFO: ffff93d0ed35af00] CPU: 4 STATE: TASK_RUNNING (ACTIVE) crash> vtop 0x7f242a382000 VIRTUAL PHYSICAL 7f242a382000 2c0000000 PGD: 2ab03a7f0 => 8000000220585067 PUD: 220585480 => 2b0b63067 PMD: 2b0b63a88 => 2ad0b6067 PTE: 2ad0b6c10 => 80000002c0000267 PAGE: 2c0000000 PTE PHYSICAL FLAGS 80000002c0000267 2c0000000 (PRESENT|RW|USER|ACCESSED|DIRTY|NX) VMA START END FLAGS FILE ffff93d0ecf7a9c0 7f242a382000 7f242a383000 d0444fb /dev/mem ``` 這個範例中,我們展示了 `/dev/mem` 如何用來存取保留的記憶體。接下來我們將繼續用簡單的小例子展現 `/dev/mem` 的其它玩法。 **注意: 這邊的操作因為會存取 `/dev/mem` ,需要以超級使用者的權限來執行** :::warning 這邊的程式有稍作修改,先用 getchar(); 讓程式等待使用者輸入時操作 `crash` 做觀察; 但想展示修改後記憶體的值一直失敗,還在找顯示修改後數值方式。 ::: ## 行程間的分頁(page)交換 有一種需求: 我們不希望 process_A 和 process_B 共用任何分頁,這意味著它們不能同時操作同一份數據。 但偶爾我們也希望 process_A 和 process_B 交換資訊,卻又不想用低效率的傳統行程間通信的機制。 是不是覺得兩難了呢? 其實我們可以讓這==兩個行程進行分頁交換==來達到目的。 為了讓分頁表交換盡可能簡單,我們依然使用保留記憶體,解除核心記憶體管理系統對操作的限制。 下面給出範例程式的程式碼,首先看 process_A (master.c): ```c // gcc master.c -o master #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); // 建立一個分頁 P1 映射到保留記憶體 addr = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0x34000000); // 修改 P1 的内容 *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 1; } ``` 接下來看要與 process_A 做分頁表交換的 process_B (slave.c): ```cpp // gcc slave.c -o slave #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); // 建立分頁 P2 映射到保留的記憶體 addr = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0x34004000); // 修改 P2 的内容 *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 1; } ``` 分頁交換的原理非常簡單,互換兩個行程的兩個虛擬地址的頁表項即可。實作這件事意味著需要撰寫核心模組,但是由於我們僅進行展示,所以我們可以用 crash 工具輕鬆達到目標。 > 使用 `crash` 修改 `/dev/mem` 時,需要使用 `ststemtap` hook 住 devmeme_is_allowed, 使其返回值永遠為 1,之後才可直接修改。 > > 操作步驟: > 安裝 `systemtap` > 執行 `stap -g -e 'probe kernel.function("devmem_is_allowed").return { $return = 1 }'` > 接著再開啟 `crash` 使用 `crash` 修改 master 的分頁 ```shell crash> ps | grep master [8/287] 32334 32333 6 ffff93d0ed35c680 IN 0.0 4512 1384 master crash> set 32334 PID: 32334 COMMAND: "master" TASK: ffff93d0ed35c680 [THREAD_INFO: ffff93d0ed35c680] CPU: 6 STATE: TASK_INTERRUPTIBLE crash> vtop 0x7f8f3ba6a000 VIRTUAL PHYSICAL 7f8f3ba6a000 2c0000000 PGD: 2ae2f87f8 => 80000002af219067 PUD: 2af2191e0 => 2a9d3c067 PMD: 2a9d3cee8 => 2ac34b067 PTE: 2ac34b350 => 80000002c0000267 PAGE: 2c0000000 PTE PHYSICAL FLAGS 80000002c0000267 2c0000000 (PRESENT|RW|USER|ACCESSED|DIRTY|NX) VMA START END FLAGS FILE ffff93d0e894e000 7f8f3ba6a000 7f8f3ba6b000 d0444fb /dev/mem crash> wr -64 -p 2ac34b350 80000002c0004267 ``` 使用 `crash` 修改 slave 的分頁 ```shell crash> ps | grep slave 32348 32347 1 ffff93d0ed359780 IN 0.0 4512 1416 slave crash> set 32348 PID: 32348 COMMAND: "slave" TASK: ffff93d0ed359780 [THREAD_INFO: ffff93d0ed359780] CPU: 1 STATE: TASK_INTERRUPTIBLE crash> vtop 0x7f269fba3000 VIRTUAL PHYSICAL 7f269fba3000 2c0004000 PGD: 2ae2ca7f0 => 80000002ac354067 PUD: 2ac3544d0 => 2b45f6067 PMD: 2b45f67e8 => 2ac7db067 PTE: 2ac7dbd18 => 80000002c0004267 PAGE: 2c0004000 PTE PHYSICAL FLAGS 80000002c0004267 2c0004000 (PRESENT|RW|USER|ACCESSED|DIRTY|NX) VMA START END FLAGS FILE ffff93d0eea18820 7f269fba3000 7f269fba4000 d0444fb /dev/mem crash> wr -64 -p 2ac7dbd18 80000002c0000267 ``` master 執行結果 ```shell $ gcc -o master master.c && sudo ./master address at: 0x7f8f3ba6a000 content is: 0x1122334455667788 address at: 0x7f8f3ba6a000 content is: 0x8877665544332211 ``` slave 執行結果 ```shell $ gcc -o slave slave.c && sudo ./slave address at: 0x7f269fba3000 content is: 0x8877665544332211 address at: 0x7f269fba3000 content is: 0x1122334455667788 ``` 過程圖解如下: ![](https://i.imgur.com/Lk0gCQI.png) 1. 將兩支程式執行後顯示的虛擬記憶體地址透過 `crash` 找到對應的實體記憶體地址與分頁資訊 2. 改寫 master 所使用的分頁對應到的實體記憶體地址 3. 改寫 slave 所使用的分頁對應到的實體記憶體地址 4. 回到兩支程式輸入 enter 觀察交換分頁後的結果 這個範例非常適合用在設計微核心的行程間通信,搭配**快取一致性協議**,可以達到非常高的效率。 ## 安全竄改行程的記憶體 所謂的安全竄改的記憶體指的是用一種可靠的方法改行程的記憶體,而不是通過手工 hack 分頁的方式,簡單起見,這次我們藉助 `crash` 工具來完成。 首先我們看一個程式: ```cpp #include <stdio.h> #include <unistd.h> #include <sys/mman.h> #include <string.h> int main(int argc, char **argv) { unsigned char *addr; // 匿名映射一段記憶體空間 addr = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_ANONYMOUS|MAP_SHARED, -1, 0); // 修改內容 strcpy(addr, "浙江溫州皮鞋濕"); // 只是範例,所以直接顯示 address 實際操作時需要手工 hack 記憶體位置 printf("address at: %p content is: %s\n", addr, addr); getchar(); printf("address at: %p content is: %s\n", addr, addr); munmap(addr, 4096); return 1; } ``` 執行 ```shell $ gcc -o test test.c && ./test address at: 0x7f7d88693000 content is: 浙江溫州皮鞋濕 ``` 我們想要把「浙江溫州皮鞋濕」 這一塊記憶體的內容改成「下雨進水不會胖」的話,可以怎麼做呢? 方法很多,這裡介紹的是利用 `crash` 和 `/dev/mem` 的方法。 首先我們要找到 addr 對應的實體記憶體地址 (利用 `crash` 加上程式顯示的虛擬記憶體地址): ```shell crash> ps | grep test 11608 11607 1 ffff93d0ed378000 IN 0.0 4512 1408 test crash> set 11608 PID: 11608 COMMAND: "test" TASK: ffff93d0ed378000 [THREAD_INFO: ffff93d0ed378000] CPU: 1 STATE: TASK_INTERRUPTIBLE crash> vtop 0x7f7d88693000 VIRTUAL PHYSICAL 7f7d88693000 1f83ed000 PGD: 2a73ee7f0 => 8000000220a30067 PUD: 220a30fb0 => 2ae1f4067 PMD: 2ae1f4218 => 2b0c7e067 PTE: 2b0c7e498 => 80000001f83ed867 PAGE: 1f83ed000 PTE PHYSICAL FLAGS 80000001f83ed867 1f83ed000 (PRESENT|RW|USER|ACCESSED|DIRTY|NX) VMA START END FLAGS FILE ffff93d0ec033450 7f7d88693000 7f7d88694000 80000fb dev/zero PAGE PHYSICAL MAPPING INDEX CNT FLAGS ffffd377c7e0fb40 1f83ed000 ffff93d0f01a9290 0 2 17ffffc0040038 uptodate,dirty,lru,swapbacked ``` 我們得到了 addr 對應的實體記憶體地址是 0x1f83ed000 現在讓我們再寫另一支程式,映射 `/dev/mem`,然後修改偏移量 0x1f83ed000 處的記憶體即可竄改: ```cpp #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/mman.h> #include <string.h> #include <fcntl.h> int main(int argc, char **argv) { int fd; unsigned char *addr; unsigned long off; off = strtol(argv[1], NULL, 16); fd = open("/dev/mem", O_RDWR); addr = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, off); strcpy(addr, "下雨進水不會胖"); close(fd); munmap(addr, 4096); return 1; } ``` 直接執行: ```shell gcc -o hack hack.c && sudo ./hack 0x1f83ed000 ``` 回到按一下 Enter 觀察結果: ```shell $ gcc -o test test.c && ./test address at: 0x7f26698cd000 content is: 浙江溫州皮鞋濕 address at: 0x7f26698cd000 content is: 下雨進水不會胖 ``` 這個例子比較簡單,也顯得比較無趣,下面這個例子稍微有點意思。 ## 藉由變更 `/dev/mem` 來修改行程的名稱 這一個例子我們將不使用 `crash` 工具,僅僅依靠 hack `/dev/mem` 來修改一個行程的名字。 > 這對於一個網際網路產品的運行是有意義的。 > 特別是在一些託管的機器上,為了防止資訊洩漏,一般是不允許使用類似 `crash & gdb` 工具來 debug 的,當然了,`systemtap` API 有限制,所以相對安全,而核心模組一般也會被禁止。 > 但是有 `systemtap` 和 `/dev/mem` 就足夠了! 我們來做這樣一個實驗: - [ ] **修改在執行中的行程的名稱** 看看如何完成。先來看一個很簡單的程式: ```cpp // gcc pixie.c -o pixie #include <stdio.h> int main(int argc, char **argv) { getchar(); } ``` 編譯並執行 ```shell $ gcc -o pixie pixie.c && ./pixie ``` 現在我們想辦法把行程的名稱從 `pixie` 改成 `skinshoe`。 沒有`crash` 也沒有 `gdb` ,只有一個可以讀寫的 `/dev/mem` (假設我們已經 HOOK 了 devmem_si_allowed) 要怎麼做到呢? 我們知道,核心中所有的資料結構都可以在 `/dev/mem` 中找到,因此,我們要找到 `pixie` 行程的 `task_struct` 結構的位置,然後更改它的 `comm` 欄位。 問題是 `/dev/mem` 是實體記憶體空間,而作業系統操作的任何記憶體都是基於虛擬地址,如何建立兩者之間的關聯是關鍵的。 我們注意到三點事實: - x86_64 可直接映射 64TiB 的實體記憶體,足以一一映射當前常見的任意實體記憶體。 - Linux 核心對所有實體記憶體建立一一映射。實體地址和虛擬地址之間固定偏移量。 - Linux 核心的資料結構是彼此關聯的網狀結構,因此便可以此來順藤摸瓜。 這意味著,只要我們提供一個 Linux 核心空間數據結構的虛擬地址,我們就能求出它的實體地址,然後順藤摸瓜就能找到我們的 pixie 行程的task_struct 結構體。 在 Linux 系統中,很多地方都可以找到核心資料結構的地址: - `/proc/kallsyms` - `/boot/System.map` - `lsof` 的結果 最簡單的方法,那就是藉由在 `/proc/kallsyms` 或者 `System.map` 裡找到 `init_task` 的地址,比如在我的環境下: ```shell $ sudo cat /proc/kallsyms | grep init_task ffffffff97971270 T ftrace_graph_init_task ffffffff979cc3e0 T perf_event_init_task ffffffff989d7ea0 r __ksymtab_init_task ffffffff989f67c6 r __kstrtab_init_task ffffffff98c00000 D __start_init_task ffffffff98c04000 D __end_init_task ffffffff98c13480 D init_task ffffffff992ed8f8 b ext4_lazyinit_task ``` 然後在 [arch/x86/kernel/vmlinux.lds.S](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/vmlinux.lds.S) 裡找到 `init_task` 到實體記憶體的映射規則,從 `init_task` 開始走訪整個系統的 `task` linked list,找到我們的目標 `pixie` 行程,改之。 ```shell SECTIONS { ... /* Data */ .data : AT(ADDR(.data) - LOAD_OFFSET) { /* Start of data section */ _sdata = .; /* init_task */ INIT_TASK_DATA(THREAD_SIZE) ... ``` 但這種方法無法讓人體驗在 `/dev/mem` 裡順藤摸瓜的刺激感,所以我們最後再來說它,現在我們嘗試用一種稍微麻煩的方法來實現修改特定行程名字目標。 我的方法是建立一個 `tcpdump` 行程卻不抓任何的封包,它只是一個提供蛛絲馬跡的幌子,我們就它開始下手: ```shell $ sudo tcpdump -i lo -n tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on lo, link-type EN10MB (Ethernet), capture size 262144 bytes ``` 之所以建立 `tcpdump` 行程,是因為 `tcpdump` 會產生一個 `packet socket`,而該 `socket` 的虛擬地址可以從 `procfs` 中找到: ```shell $ sudo cat /proc/net/packet sk RefCnt Type Proto Iface R Rmem User Inode ffff93d0ecdec800 3 3 88cc 2 1 0 100 136312 ### 啟動 tcpdump 後確實新增一個新增的 packet socket $ sudo cat /proc/net/packet sk RefCnt Type Proto Iface R Rmem User Inode ffff93d0ecdec800 3 3 88cc 2 1 0 100 136312 ffff93d0605d1000 3 3 0003 1 1 0 0 345584 ``` OK,就是這個 `0xffff93d0605d1000 ` 作為我們的突破口,我們從它開始順藤摸瓜! 我們知道,`0xffff93d0605d1000 ` 是一個 `struct sock` 物件的記憶體地址,依據 `sock` 的結構可知道它的偏移量 224 byte 處是一個等待佇列 `wati_queue_head_t` 物件。 > 這一點需要你對 Linux 核心的資料結構非常熟悉,如果不熟悉,就去找對應的原始碼手算一下偏移量。【或者用一下 `crash` 的 `struct Xy -o` 計算也可】 而 `wait_queue_head_t` 物件的內部又含有一個 `wait_queue_t` 物件指向下一個節點,節點本身會被 `tcpdump` 的 `poll_wqueues` 結構所管理,最終它的 `polling_task` 欄位會指向 `tcpdump` 本身,而我們需要的正是 `tcpdump` 的 `task_struct` 物件本身,因為整個系統的所有 `task` 都被被連接在一個 linked list 中。 整體的關聯圖示如下: ![](https://hackmd.io/_uploads/ryN53mV73.png) 有了這個結構,我們就可以開始撰寫程式碼了。 由於 x86_64 可以直接一一映射 64T 的記憶體,而我只有區區 12G 的大小,可以保證的是,虛擬地址減去一一映射的基底 (在我的系統它就是 0x )後就可以得到實體地址了。 假設 `packet socket` 的地址是 0xffff88002c201000,我們就能確認其實體地址在 0x2c201000 處,撰寫程式映射 `/dev/mem`,並從 開始找起: :::warning 這邊做不出來 ... 照著他所提供的教學進行指標的位移會出錯,應該是不同系統版本所使用的結構有差異 ::: ### 解析 `/dev/mem` 以走訪系統的所有行程 ## 透過 `/dev/mem` 結束行程 無條件殺掉一個行程的方式不外乎兩種: - `SIGKILL` 殺掉它。 - 自身出了嚴重的問題而自毀。 不外乎從外部著手,或者由內部自行腐爛,但一個行程的結束均需要理由。然而,一個好好的行程整體被掏空的話意味著什麼?可以做到嗎?這就好比一個善良且健康的人,突然間遭遇了嚴重的意外事故那般不幸。 通過改寫 `/dev/mem` 可以輕而易舉地掏空一個行程,當行程再次準備執行時,就會發現自己什麼都沒有了。 我們可以定位到行程在 `/dev/mem` 的位置,進而刪除行程的 VMA (Virtual Memory Areas), 清空 stack … 有點殘忍,我便不再舉例細說。 ## 透過 `/dev/mem` 修改函式內容 作為最後一個範例,呼應〈[解決 Linux 核心問題實用技巧之 Crash 工具結合 `/dev/mem` 任意修改記憶體](https://mp.weixin.qq.com/s/040W19-CPF0VnUvwFSKiXw)〉的例子,修改 `devmem_is_allowed` 函式,使其返回值永遠為 1,現在,我們透過修改 `/dev/mem` 的方式把它還原回去,從而結束此範例。 我們可以從 `/proc/kallsyms` 中找到 `devmem_is_allowed` 的地址: ```shell $ sudo cat /proc/kallsyms | grep devmem_is_allowed ffffffff97873510 T devmem_is_allowed ``` 這是它原來的樣子: ```shell 0xffffffff97873510 <devmem_is_allowed>: nopl 0x0(%rax,%rax,1) [FTRACE NOP] 0xffffffff97873515 <devmem_is_allowed+5>: push %rbp 0xffffffff97873516 <devmem_is_allowed+6>: xor %ecx,%ecx 0xffffffff97873518 <devmem_is_allowed+8>: mov $0x1000200,%edx 0xffffffff9787351d <devmem_is_allowed+13>: mov $0x1000,%esi 0xffffffff97873522 <devmem_is_allowed+18>: mov %rsp,%rbp 0xffffffff97873525 <devmem_is_allowed+21>: push %r12 0xffffffff97873527 <devmem_is_allowed+23>: mov %rdi,%r12 0xffffffff9787352a <devmem_is_allowed+26>: push %rbx 0xffffffff9787352b <devmem_is_allowed+27>: shl $0xc,%r12 0xffffffff9787352f <devmem_is_allowed+31>: mov %rdi,%rbx 0xffffffff97873532 <devmem_is_allowed+34>: mov %r12,%rdi 0xffffffff97873535 <devmem_is_allowed+37>: callq 0xffffffff97896f70 <region_intersects> 0xffffffff9787353a <devmem_is_allowed+42>: cmp $0x1,%eax 0xffffffff9787353d <devmem_is_allowed+45>: je 0xffffffff97873552 <devmem_is_allowed+66> 0xffffffff9787353f <devmem_is_allowed+47>: xor %eax,%eax 0xffffffff97873541 <devmem_is_allowed+49>: cmp $0x100,%rbx 0xffffffff97873548 <devmem_is_allowed+56>: setb %al 0xffffffff9787354b <devmem_is_allowed+59>: pop %rbx 0xffffffff9787354c <devmem_is_allowed+60>: add %eax,%eax 0xffffffff9787354e <devmem_is_allowed+62>: pop %r12 0xffffffff97873550 <devmem_is_allowed+64>: pop %rbp 0xffffffff97873551 <devmem_is_allowed+65>: retq 0xffffffff97873552 <devmem_is_allowed+66>: mov %r12,%rdi 0xffffffff97873555 <devmem_is_allowed+69>: callq 0xffffffff978988f0 <iomem_is_exclusive> 0xffffffff9787355a <devmem_is_allowed+74>: cmp $0xff,%rbx 0xffffffff97873561 <devmem_is_allowed+81>: setbe %dl 0xffffffff97873564 <devmem_is_allowed+84>: test %eax,%eax 0xffffffff97873566 <devmem_is_allowed+86>: sete %al 0xffffffff97873569 <devmem_is_allowed+89>: or %edx,%eax 0xffffffff9787356b <devmem_is_allowed+91>: pop %rbx 0xffffffff9787356c <devmem_is_allowed+92>: movzbl %al,%eax 0xffffffff9787356f <devmem_is_allowed+95>: pop %r12 0xffffffff97873571 <devmem_is_allowed+97>: pop %rbp 0xffffffff97873572 <devmem_is_allowed+98>: retq 0xffffffff97873573 <devmem_is_allowed+99>: nopl (%rax) 0xffffffff97873576 <devmem_is_allowed+102>: nopw %cs:0x0(%rax,%rax,1) ``` :::warning 前一部分的修改成 nop nop 遇到 segamentaion falut,未修改成功,找原因中。 ::: ## 合法存取記憶體地址為 NULL 的空間 到底 NULL 地址能不能存取呢?到底是誰禁止存取 NULL 地址呢? 先說結論: NULL 地址完全可以存取,只要有分頁表映射它到一個實體記憶體分頁就行。 Linux 系統有一個參數控制能不能 mmap NULL地址: ```shell $ cat /proc/sys/vm/mmap_min_addr 65536 $ echo 0 >/proc/sys/vm/mmap_min_addr $ cat /proc/sys/vm/mmap_min_addr 0 ``` 我們做一個實驗,看個究竟,先看程式碼: ```cpp // gcc access0.c -o access0 #include <stdio.h> #include <stdlib.h> #include <sys/mman.h> int main(int argc, char **argv) { int i; unsigned char *niladdr = NULL; unsigned char str[] = "Zhejiang Wenzhou pixie shi,xiayu jinshui buhui pang!"; mmap(0, 4096, PROT_READ|PROT_WRITE, MAP_FIXED|MAP_ANONYMOUS|MAP_SHARED, -1, 0); perror("a"); for (i = 0 ; i < sizeof(str); i++) { niladdr[i] = str[i]; } printf("using assignment at NULL: %s\n", niladdr); for (i = 0 ; i < sizeof(str); i++) { printf ("%c", niladdr[i]); } printf ("\n"); getchar(); munmap(0, 4096); return 0; } ``` 執行: ```shell $ gcc -o access0 access0.c && ./access0 a: Success using assignment at NULL: (null) Zhejiang Wenzhou pixie shi,xiayu jinshui buhui pang! ``` 此時我們 `crash` 看看 NULL 的映射結果: ```shell crash> ps | grep access0 17515 13791 0 ffff93d0ebc41780 IN 0.0 4512 1444 access0 crash> set 17515 PID: 17515 COMMAND: "access0" TASK: ffff93d0ebc41780 [THREAD_INFO: ffff93d0ebc41780] CPU: 0 STATE: TASK_INTERRUPTIBLE crash> vtop 0 VIRTUAL PHYSICAL 0 1dfaef000 PGD: 2abe84000 => 80000002ae810067 PUD: 2ae810000 => 2ae09b067 PMD: 2ae09b000 => 2ac332067 PTE: 2ac332000 => 80000001dfaef867 PAGE: 1dfaef000 PTE PHYSICAL FLAGS 80000001dfaef867 1dfaef000 (PRESENT|RW|USER|ACCESSED|DIRTY|NX) VMA START END FLAGS FILE ffff93d04fb69ad0 0 1000 80000fb dev/zero PAGE PHYSICAL MAPPING INDEX CNT FLAGS ffffd377c77ebbc0 1dfaef000 ffff93d0ec4136b8 0 2 17ffffc0040038 uptodate,dirty,lru,swapbacked ``` 看起來並沒有不同。 之所以會無法存取 NULL ,是為了更好地區分什麼是合法地址,所以人為製造了一個特殊的地址稱為「NULL」並使其無法被存取,但是在 MMU (Memory Management Unit) 層面上,NULL 與其它記憶體並無不同。 ## 結論 好了,關於 `crash` 工具和 `/dev/mem` 的話題在此結束,結合前面一篇文章[連結](https://mp.weixin.qq.com/s/040W19-CPF0VnUvwFSKiXw),建議親自做一次這些實驗,可以獲得更加深刻的印象。 更重要的是,每個人在親自動手做這些實驗的過程中,會碰到各式各樣新的問題,分析以及最終解決掉這些問題,正是快樂感覺的由來,分享這種快樂本身也是一件快樂的事情,這也是我寫這兩篇文章的動力。