Try   HackMD

Linux 核心的 /dev/mem 裝置

改寫自 Linux /dev/mem的新玩法 一文,採用 CC BY-SA 授權

/dev/mem 裡有什麼

簡單來講,Linux 核心的 /dev/mem 是系統實體記憶體的映像檔案,這裡的「實體記憶體」需要進一步解釋。

實體記憶體是指插在主機板上的那些一條又一條的記憶體嗎?當然是,但實體記憶體不單單指條狀的記憶體儲存實體。

延伸閱讀: 像織毛衣一樣把記憶體織出來

嚴格來說,實體記憶體是指實體定址空間,而記憶體這樣的儲存裝置僅是映射到這個定址空間的一部分,其餘的還有各種 PCI 設備、IO port 等。我們可從 /proc/iomem 中看到這個映射:

$ 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
...

由於 Linux 安全考量,用一般使用者的權限來執行 $ cat /proc/iomem 和超級使用者 (即 root),會看到不同的內容,僅有後者能看到完整的資訊

其中,只有 RAM 才是指記憶體儲存裝置。關於實體定址空間的詳細情況,請參考 BIOS-e820 相關的素材,後者是 BIOS 讓開機引導程式 (boot loader) 或 Linux 一類的作業系統核心得以存取的記憶體映射資訊。

明白實體記憶體的組成之後,我們接著觀察 /dev/mem 裡面有什麼。

事實上,它就是個執行中的 Linux 系統即時映像 (live image),所有行程的 task_struct 結構、sock 結構、sk_buff 結構,和行程資料等都在裡面的某個位置:
實體記憶體示意圖

如果能夠定位它們在 /dev/mem 裡的位置,我們就能得到系統中這些資料結構的實時值,所謂的除錯工具所做的事情也是如此。其實我們透過 core dump 檔案進行除錯所用的 vmcore 也是個實體記憶體映像,和 /dev/mem 不同的是,它是一具鹹魚

當 Linux 核心發生崩潰 (crash) 時,可經由 kdump 等方式蒐集崩潰前記憶體的狀態,並建立一個 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,但這樣反而會變得更加複雜。能否單純保留一部分記憶體,使其不受到核心的記憶體管理系統控制呢?就好像很多資料庫可以不經檔案系統直接存取硬碟一樣,核心中有沒有什麼機制能讓我們不經過記憶體管理系統而直接使用記憶體呢?
當然有!加上 mem 啟動參數即可實現。

這裡介紹一種關於保留記憶體的最簡單配置,在cmdline 中設置 mem 啟動參數:
mem=11G

此指令為 kernel cmdline 的指令,cmdline 藉由 bootloader 傳送給 kernel,kernel 按照解析的結果進行資源配置。

使用 cat /proc/cmdline 查看目前 cmdline 的參數

$ 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。

目前是利用 grub2 來傳入 cmdline,透過編輯 /etc/default/grub 檔案中的 GRUB_CMDLINE_LINUX_DEFAULT="" 來達成,從目前啟用前後的差異來看,少掉的記憶體不只 1G,問題應該是出在 mem=11G 這項,尚未找到解決辦法。

觀察加入 mem 前後 free/proc/iomem 的差異:
配置前

$ 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

$ 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。

我們試著用 crash 工具來讀取一下保留的記憶體:

crash> rd -p 0x2C0000000
rd: seek error: physical address: 2c0000000  type: "64-bit PHYSADDR"

### 同時看一下查看未保留的空間 ###
crash> rd -p 2bffffff1
       2bffffff1:  5f6e6f6973726576                    version_

顯然,核心並沒有對保留分頁建立一一映射的分頁表,所以讀取是失敗的。

我們已經知道了 /dev/mem 檔案是整個實體記憶體的映像,所以使用者行程可以使用 mmap 系統呼叫來重建使用者空間的頁表。方法如下:

#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 的行程中便可以存取保留的記憶體了:

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 ,需要以超級使用者的權限來執行

這邊的程式有稍作修改,先用 getchar(); 讓程式等待使用者輸入時操作 crash 做觀察;
但想展示修改後記憶體的值一直失敗,還在找顯示修改後數值方式。

行程間的分頁(page)交換

有一種需求:
我們不希望 process_A 和 process_B 共用任何分頁,這意味著它們不能同時操作同一份數據。
但偶爾我們也希望 process_A 和 process_B 交換資訊,卻又不想用低效率的傳統行程間通信的機制。

是不是覺得兩難了呢? 其實我們可以讓這兩個行程進行分頁交換來達到目的。
為了讓分頁表交換盡可能簡單,我們依然使用保留記憶體,解除核心記憶體管理系統對操作的限制。

下面給出範例程式的程式碼,首先看 process_A (master.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):

// 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 的分頁

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 的分頁

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 執行結果

$ gcc -o master master.c && sudo ./master
address at: 0x7f8f3ba6a000   content is: 0x1122334455667788

address at: 0x7f8f3ba6a000   content is: 0x8877665544332211                   

slave 執行結果

$ gcc -o slave slave.c && sudo ./slave
address at: 0x7f269fba3000   content is: 0x8877665544332211

address at: 0x7f269fba3000   content is: 0x1122334455667788

過程圖解如下:

  1. 將兩支程式執行後顯示的虛擬記憶體地址透過 crash 找到對應的實體記憶體地址與分頁資訊
  2. 改寫 master 所使用的分頁對應到的實體記憶體地址
  3. 改寫 slave 所使用的分頁對應到的實體記憶體地址
  4. 回到兩支程式輸入 enter 觀察交換分頁後的結果

這個範例非常適合用在設計微核心的行程間通信,搭配快取一致性協議,可以達到非常高的效率。

安全竄改行程的記憶體

所謂的安全竄改的記憶體指的是用一種可靠的方法改行程的記憶體,而不是通過手工 hack 分頁的方式,簡單起見,這次我們藉助 crash 工具來完成。

首先我們看一個程式:

#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;
}

執行

$ gcc -o test test.c && ./test

address at: 0x7f7d88693000   content is: 浙江溫州皮鞋濕

我們想要把「浙江溫州皮鞋濕」 這一塊記憶體的內容改成「下雨進水不會胖」的話,可以怎麼做呢?

方法很多,這裡介紹的是利用 crash/dev/mem 的方法。
首先我們要找到 addr 對應的實體記憶體地址 (利用 crash 加上程式顯示的虛擬記憶體地址):

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 處的記憶體即可竄改:

#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;
}

直接執行:

gcc -o hack hack.c && sudo ./hack  0x1f83ed000

回到按一下 Enter 觀察結果:

$ 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 就足夠了!

我們來做這樣一個實驗:

  • 修改在執行中的行程的名稱

看看如何完成。先來看一個很簡單的程式:

// gcc pixie.c -o pixie
#include <stdio.h>

int main(int argc, char **argv)
{
	getchar();
}

編譯並執行

$ 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 的地址,比如在我的環境下:

$ 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 裡找到 init_task 到實體記憶體的映射規則,從 init_task 開始走訪整個系統的 task linked list,找到我們的目標 pixie 行程,改之。

SECTIONS
{
    ...
	/* Data */
	.data : AT(ADDR(.data) - LOAD_OFFSET) {
		/* Start of data section */
		_sdata = .;

		/* init_task */
		INIT_TASK_DATA(THREAD_SIZE)
    ...

但這種方法無法讓人體驗在 /dev/mem 裡順藤摸瓜的刺激感,所以我們最後再來說它,現在我們嘗試用一種稍微麻煩的方法來實現修改特定行程名字目標。

我的方法是建立一個 tcpdump 行程卻不抓任何的封包,它只是一個提供蛛絲馬跡的幌子,我們就它開始下手:

$ 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 中找到:

$ 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 核心的資料結構非常熟悉,如果不熟悉,就去找對應的原始碼手算一下偏移量。【或者用一下 crashstruct Xy -o 計算也可】

wait_queue_head_t 物件的內部又含有一個 wait_queue_t 物件指向下一個節點,節點本身會被 tcpdumppoll_wqueues 結構所管理,最終它的 polling_task 欄位會指向 tcpdump 本身,而我們需要的正是 tcpdumptask_struct 物件本身,因為整個系統的所有 task 都被被連接在一個 linked list 中。

整體的關聯圖示如下:

有了這個結構,我們就可以開始撰寫程式碼了。

由於 x86_64 可以直接一一映射 64T 的記憶體,而我只有區區 12G 的大小,可以保證的是,虛擬地址減去一一映射的基底 (在我的系統它就是 0x )後就可以得到實體地址了。

假設 packet socket 的地址是 0xffff88002c201000,我們就能確認其實體地址在 0x2c201000 處,撰寫程式映射 /dev/mem,並從 開始找起:

這邊做不出來
照著他所提供的教學進行指標的位移會出錯,應該是不同系統版本所使用的結構有差異

解析 /dev/mem 以走訪系統的所有行程

透過 /dev/mem 結束行程

無條件殺掉一個行程的方式不外乎兩種:

  • SIGKILL 殺掉它。
  • 自身出了嚴重的問題而自毀。

不外乎從外部著手,或者由內部自行腐爛,但一個行程的結束均需要理由。然而,一個好好的行程整體被掏空的話意味著什麼?可以做到嗎?這就好比一個善良且健康的人,突然間遭遇了嚴重的意外事故那般不幸。

通過改寫 /dev/mem 可以輕而易舉地掏空一個行程,當行程再次準備執行時,就會發現自己什麼都沒有了。

我們可以定位到行程在 /dev/mem 的位置,進而刪除行程的 VMA (Virtual Memory Areas), 清空 stack … 有點殘忍,我便不再舉例細說。

透過 /dev/mem 修改函式內容

作為最後一個範例,呼應〈解決 Linux 核心問題實用技巧之 Crash 工具結合 /dev/mem 任意修改記憶體〉的例子,修改 devmem_is_allowed 函式,使其返回值永遠為 1,現在,我們透過修改 /dev/mem 的方式把它還原回去,從而結束此範例。

我們可以從 /proc/kallsyms 中找到 devmem_is_allowed 的地址:

$ sudo cat /proc/kallsyms | grep devmem_is_allowed
ffffffff97873510 T devmem_is_allowed

這是它原來的樣子:

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)

前一部分的修改成 nop nop 遇到 segamentaion falut,未修改成功,找原因中。

合法存取記憶體地址為 NULL 的空間

到底 NULL 地址能不能存取呢?到底是誰禁止存取 NULL 地址呢?

先說結論:
NULL 地址完全可以存取,只要有分頁表映射它到一個實體記憶體分頁就行。
Linux 系統有一個參數控制能不能 mmap NULL地址:

$ cat /proc/sys/vm/mmap_min_addr
65536
$ echo  0 >/proc/sys/vm/mmap_min_addr
$ cat /proc/sys/vm/mmap_min_addr
0

我們做一個實驗,看個究竟,先看程式碼:

// 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;
}

執行:

$ gcc -o access0 access0.c && ./access0
a: Success
using assignment at NULL: (null)
Zhejiang Wenzhou pixie shi,xiayu jinshui buhui pang!

此時我們 crash 看看 NULL 的映射結果:

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 的話題在此結束,結合前面一篇文章連結,建議親自做一次這些實驗,可以獲得更加深刻的印象。

更重要的是,每個人在親自動手做這些實驗的過程中,會碰到各式各樣新的問題,分析以及最終解決掉這些問題,正是快樂感覺的由來,分享這種快樂本身也是一件快樂的事情,這也是我寫這兩篇文章的動力。