|
| 1 | +// This file is part of KASLD - https://github.com/bcoles/kasld |
| 2 | +// |
| 3 | +// Leak the kernel virtual text base (_text) from the debugfs page-table dump. |
| 4 | +// |
| 5 | +// With CONFIG_PTDUMP_DEBUGFS=y the kernel exposes a walk of init_mm's page |
| 6 | +// tables. On x86 it is /sys/kernel/debug/page_tables/kernel; on arm64 |
| 7 | +// /sys/kernel/debug/kernel_page_tables. The walker prints one line per |
| 8 | +// contiguous run of same-protection mappings as a raw virtual range |
| 9 | +// "0x<start>-0x<end>", with region headers "---[ name ]---" between runs. The |
| 10 | +// addresses are printed raw (%lx), with no kptr_restrict / kallsyms_show_value |
| 11 | +// gate — the only access control is the file mode (0400) and the debugfs mount. |
| 12 | +// |
| 13 | +// The randomized text base is recovered from the x86 "High Kernel Mapping" |
| 14 | +// region. That region begins at the fixed __START_KERNEL_map and holds an |
| 15 | +// unmapped gap [__START_KERNEL_map, _text) followed by the mapped kernel image |
| 16 | +// [_text, ...) — the gap width is exactly the KASLR slide. The run addresses |
| 17 | +// alone do not distinguish the gap from the image (both are printed), so the |
| 18 | +// image is located by the protection column: a present entry prints one of |
| 19 | +// "RW "/"ro ", a non-present entry prints only spaces. The first run in the |
| 20 | +// region that carries a protection flag starts at _text. |
| 21 | +// |
| 22 | +// Data leaked: _text (virtual text base) |
| 23 | +// Kernel subsystem: mm/ptdump + arch page-table dumper |
| 24 | +// Address type: virtual (kernel text) |
| 25 | +// Method: parsed (debugfs page-table dump) |
| 26 | +// Gate: file mode 0400 and the debugfs mount (0700 by default); |
| 27 | +// no kptr_restrict / kallsyms_show_value check. Reachable |
| 28 | +// from an already-privileged vantage, a debugfs mount |
| 29 | +// relaxed by mode=/gid=, or a container bind-mount. |
| 30 | +// Config: CONFIG_PTDUMP_DEBUGFS |
| 31 | +// |
| 32 | +// The dump format carries no ABI guarantee, so parsing is conservative: a base |
| 33 | +// is pinned only when a mapped run is unambiguously located inside the kernel- |
| 34 | +// text window; anything else declines rather than risk a wrong pin. Only the |
| 35 | +// text base is recoverable — the direct-map / vmalloc / vmemmap region bases |
| 36 | +// are held in the walker's marker table and never printed, and the first mapped |
| 37 | +// run of those regions sits above the region base, so no exact base is |
| 38 | +// available there. proc_kcore recovers the same text base (and page_offset) |
| 39 | +// from a stable binary format; this covers the vantage where kcore is masked |
| 40 | +// but debugfs is readable. |
| 41 | +// |
| 42 | +// Sound only where the kernel text has a dedicated high mapping distinct from |
| 43 | +// the direct map (TEXT_TRACKS_DIRECTMAP == 0). On arm64 the kernel image is |
| 44 | +// mapped inside the vmalloc region with no distinct header, so its runs cannot |
| 45 | +// be told from module/vmalloc runs — the parse finds no "High Kernel Mapping" |
| 46 | +// region and declines there. |
| 47 | +// --- |
| 48 | +// <bcoles@gmail.com> |
| 49 | + |
| 50 | +#define _GNU_SOURCE |
| 51 | +#include "include/kasld/api.h" |
| 52 | +#include "include/kasld/cli.h" |
| 53 | + |
| 54 | +#include <errno.h> |
| 55 | +#include <fcntl.h> |
| 56 | +#include <stdio.h> |
| 57 | +#include <string.h> |
| 58 | +#include <unistd.h> |
| 59 | + |
| 60 | +KASLD_EXPLAIN( |
| 61 | + "Reads the debugfs page-table dump (/sys/kernel/debug/page_tables/kernel " |
| 62 | + "on " |
| 63 | + "x86, kernel_page_tables on arm64), a raw walk of the kernel page tables " |
| 64 | + "with CONFIG_PTDUMP_DEBUGFS. In the x86 'High Kernel Mapping' region an " |
| 65 | + "unmapped gap precedes the kernel image, so the first mapped run — found " |
| 66 | + "via " |
| 67 | + "the protection column — starts at the randomized _text. Addresses are " |
| 68 | + "printed raw with no kptr_restrict gate; access is bounded by the 0400 " |
| 69 | + "file " |
| 70 | + "mode and the debugfs mount."); |
| 71 | + |
| 72 | +KASLD_META("method:parsed\n" |
| 73 | + "phase:inference\n" |
| 74 | + "discloses:virtual\n" |
| 75 | + "config:CONFIG_PTDUMP_DEBUGFS\n"); |
| 76 | + |
| 77 | +#if !TEXT_TRACKS_DIRECTMAP |
| 78 | + |
| 79 | +/* x86 dumps init_mm under page_tables/kernel; arm64 uses kernel_page_tables. |
| 80 | + * The arm64 file is opened too, but its output carries no "High Kernel Mapping" |
| 81 | + * region, so the parse below declines on it. */ |
| 82 | +static const char *const PATHS[] = { |
| 83 | + "/sys/kernel/debug/page_tables/kernel", |
| 84 | + "/sys/kernel/debug/kernel_page_tables", |
| 85 | + NULL, |
| 86 | +}; |
| 87 | + |
| 88 | +/* A present page-table entry prints exactly one of "RW "/"ro " in the |
| 89 | + * protection column; a non-present entry prints only spaces. Either token |
| 90 | + * marks a mapped run. */ |
| 91 | +static int line_is_mapped(const char *line) { |
| 92 | + return strstr(line, "RW ") != NULL || strstr(line, "ro ") != NULL; |
| 93 | +} |
| 94 | + |
| 95 | +int main(int argc, char **argv) { |
| 96 | + kasld_cli(argc, argv); |
| 97 | + |
| 98 | + FILE *f = NULL; |
| 99 | + int denied = 0; |
| 100 | + for (int i = 0; PATHS[i]; i++) { |
| 101 | + int fd = kasld_open(PATHS[i], O_RDONLY); |
| 102 | + if (fd < 0) { |
| 103 | + if (errno == EACCES || errno == EPERM) |
| 104 | + denied = 1; |
| 105 | + continue; |
| 106 | + } |
| 107 | + f = fdopen(fd, "r"); |
| 108 | + if (!f) { |
| 109 | + close(fd); |
| 110 | + continue; |
| 111 | + } |
| 112 | + kasld_info("reading kernel page-table dump from %s", PATHS[i]); |
| 113 | + break; |
| 114 | + } |
| 115 | + if (!f) |
| 116 | + return denied ? kasld_disp_mitigation_denied( |
| 117 | + "debugfs", "kernel page-table dump not readable") |
| 118 | + : kasld_disp_absent( |
| 119 | + "no kernel page-table dump (CONFIG_PTDUMP_DEBUGFS)"); |
| 120 | + |
| 121 | + char line[512]; |
| 122 | + int in_text_region = 0; |
| 123 | + unsigned long text = 0; |
| 124 | + while (fgets(line, sizeof(line), f)) { |
| 125 | + /* Region header: "---[ name ]---". Only the x86 high-kernel-image region |
| 126 | + * bounds the randomized text run. */ |
| 127 | + if (strstr(line, "---[")) { |
| 128 | + in_text_region = strstr(line, "High Kernel Mapping") != NULL; |
| 129 | + continue; |
| 130 | + } |
| 131 | + if (!in_text_region) |
| 132 | + continue; |
| 133 | + |
| 134 | + /* Run lines start with the raw range "0x<start>-0x<end>"; headers and |
| 135 | + * "... skipped ..." lines do not. Only the start is needed. */ |
| 136 | + if (strncmp(line, "0x", 2) != 0) |
| 137 | + continue; |
| 138 | + unsigned long a; |
| 139 | + const char *end; |
| 140 | + if (!kasld_addr_parse(line + 2, 16, &a, &end) || *end != '-') |
| 141 | + continue; |
| 142 | + /* First mapped run in the region starts at _text (nothing below _text in |
| 143 | + * the high mapping is mapped). An unmapped gap line is skipped. */ |
| 144 | + if (line_is_mapped(line)) { |
| 145 | + text = a; |
| 146 | + break; |
| 147 | + } |
| 148 | + } |
| 149 | + fclose(f); |
| 150 | + |
| 151 | + if (text == 0 || !kasld_addr_is_kernel_text(text)) |
| 152 | + return kasld_disp_inconclusive( |
| 153 | + "no mapped kernel-text run in the page-table dump"); |
| 154 | + |
| 155 | + kasld_found("kernel _text from page-table dump: 0x%lx", text); |
| 156 | + /* The first mapped run of the high kernel mapping begins at _text, the image |
| 157 | + * base — the start of the image, not _stext (which sits past the head gap). |
| 158 | + * REGION_KERNEL_IMAGE names the image base directly; REGION_KERNEL_TEXT would |
| 159 | + * be read as _stext and shifted down by the head gap. */ |
| 160 | + kasld_result_base(KASLD_TYPE_VIRT, REGION_KERNEL_IMAGE, text, "_text", |
| 161 | + CONF_PARSED); |
| 162 | + return 0; |
| 163 | +} |
| 164 | + |
| 165 | +#else /* TEXT_TRACKS_DIRECTMAP: the kernel text sits inside the direct map, so \ |
| 166 | + * a page-table run in the text window can start below _text — no sound \ |
| 167 | + * pin. Inert on coupled arches. */ |
| 168 | + |
| 169 | +int main(void) { return 0; } |
| 170 | + |
| 171 | +#endif |
0 commit comments