The quiter you become,the more you are able to hear!

pagemap 解析

Author: geneblue

Blog: https://geneblue.github.io/

linux 下 /proc/[pid]/pagemap 文件保存了当前进程虚拟内存页的映射信息。用户态程序可以根据该文件计算出虚拟页映射到哪个物理页。

对于每一个虚拟页,pagemap 使用 64bit 的值表示,该值表示以下信息(fs/proc/task_mmu.c pagemap_read

1
2
3
4
5
6
7
8
9
* Bits 0-54  page frame number (PFN) if present
* Bits 0-4 swap type if swapped
* Bits 5-54 swap offset if swapped
* Bit 55 pte is soft-dirty (see Documentation/vm/soft-dirty.txt)
* Bit 56 page exclusively mapped (since 4.2)
* Bits 57-60 zero
* Bit 61 page is file-page or shared-anon (since 3.5)
* Bit 62 page swapped
* Bit 63 page present

为了安全性的考虑,自 Linux 4.0 开始,只有拥有 CAP_SYS_ADMIN 权限的进程才能读取 PFN 值,在 4.04.1 上无权限进程会得到 -EPERM 错误。从 4.2 开始,如果进程没有 CAP_SYS_ADMIN 权限,pagemap 中的 PFN 字段直接为 0。这主要是因为,PFN 值有助于 Rowhammer 漏洞的利用。

如果内存页在 swap 中,那 PFN 将被解析成两部分,一部分为 swap type,表示 swap 中的文件号,另一部分为 swap offset,表示在 swap 中的偏移。没有被映射的 page,PFN 为 null。可以利用这些值来判断一个虚拟页有没有被映射,是不是在 swap 中。

可以使用 /proc/pid/maps 先判断哪些内存被映射了,再去计算 PFN 值,这样可以提高效率。

获取了 PFN ,就可以根据 PFN 的值从 /proc/kpagecount, /proc/kpageflag, /proc/kpagecgroup 中获取内存页的其他信息。

那么给定一个虚拟地址 vaddr,如何从 pagemap 中读取该虚拟地址所属页对应的 PFN 值?如何计算该虚拟地址对应的物理地址 paddr?

用代码表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifdef __aarch64__
#define PFN_MIN 0x40000
#define PHYS_OFFSET 0xffffffc000000000
#endif
unsigned long data;
unsigned long offset = (vaddr / getpagesize()) * sizeof(uint64_t);
int fd = open("/proc/self/pagemap", O_RDONLY);
pread(fd, &data, sizeof(uint64_t), offset);
unsigned long PFN = data & 0x7fffffffffffff; // 0-54bits
int swapped = (data >> 62) & 1;
int present = (data >> 63) & 1;
unsigned long paddr = PFN * getpagesize() + address % getpagesize();
unsigned long kaddr = PHYS_OFFSET + getpagesize() * (PFN - PFN_MIN);

pagemap文件是按照虚拟页的序号排列映射信息的,所以可以得到 vaddr 所属的虚拟内存页在 pagemap 中的偏移 offset,所以可以获取虚拟内存页的映射信息 data,根据 pagemap 64bit信息的组织,就可以获取 PFN 值了,然后就可以获取物理地址 paddr 和内核 physmap 区的 kaddr

physmap 区直接映射了物理内存,我们在用户态 mmap 的内存并存放的数据也会出现在 physmap 中,2014年发表的 ret2dir 的攻击文章就是基于这一特性。ret2dir 会在另一篇文章中详细讲解。

为了验证physmap 区确实映射了我们的数据,可以将上述 kaddr 内存的值打印出来,如果就是在放置在用户态虚拟地址的数据,说明我们计算的 kaddr 是正确的。

以下是写的驱动获取的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[get_kaddr_value] kaddr=0xffffffc0368d3000 0xdeadbe00
[get_kaddr_value] kaddr=0xffffffc052f05000 0xdeadc000
[get_kaddr_value] kaddr=0xffffffc036b32000 0xdeadc200
[get_kaddr_value] kaddr=0xffffffc03565c000 0xdeadc400
[get_kaddr_value] kaddr=0xffffffc0359a9000 0xdeadc600
[get_kaddr_value] kaddr=0xffffffc05286d000 0xdeadc800
[get_kaddr_value] kaddr=0xffffffc053377000 0xdeadca00
[get_kaddr_value] kaddr=0xffffffc03bdc5000 0xdeadcc00
[get_kaddr_value] kaddr=0xffffffc029e1b000 0xdeadce00
[get_kaddr_value] kaddr=0xffffffc057760000 0xdeadd000
[get_kaddr_value] kaddr=0xffffffc0339fd000 0xdeadd200
[get_kaddr_value] kaddr=0xffffffc038c0e000 0xdeadd400
[get_kaddr_value] kaddr=0xffffffc0338bd000 0xdeadd600
[get_kaddr_value] kaddr=0xffffffc03d3bd000 0xdeadd800
[get_kaddr_value] kaddr=0xffffffc03c73d000 0xdeadda00

参考