Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
249 changes: 249 additions & 0 deletions pocs/linux/kernelctf/CVE-2026-64560_lts/docs/exploit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
# Exploit

The `lts-6.12.89` exploit races deletion of a process POSIX CPU timer against
non-leader exec, reclaims the released timer slab with packetized pipe pages,
and forges the stale timerqueue node so `rb_erase_cached()` changes the
`core_pattern` sysctl permissions. Running exactly `./exploit` installs a
memfd-backed root coredump helper and prints `/flag`.

## Exploit Summary

- **Leaking KASLR** -> Three prefetch-timing scans select the 40-slot kernel
mapping window at 2 MiB granularity, then a majority vote yields the base.
- **Preparing timer slabs** -> 8,192 `k_itimer` objects populate 512 source
pages, with six reusable allocation holes prepared per page.
- **Triggering the UAF** -> `timer_delete()` races a target's non-leader
`execve()`; a fresh-boot warning taint bit confirms that the deleted timer
remains queued.
- **Reclaiming the slab page** -> Source and filler timers are freed after RCU,
then 512 packet-mode pipes retain sixteen controlled order-0 pages each.
- **Finding the stale slot** -> The 8,192 pages are refilled for candidate
`k_itimer` slots 0 through 15 while preserving the live queue metadata after
byte 151.
- **Building a pointer write** -> An expiry-zero fake rb node makes
`rb_erase_cached()` store one selected kernel pointer at one selected address
inside `coredump_sysctls`.
- **Executing the flag helper** -> The write makes `kernel.core_pattern`
writable, after which a deliberate crash executes fd 111 as root and prints
the flag.

## Exploit Details

### Leaking KASLR

The final write targets the relocated static `coredump_sysctls` table. The
exploit therefore performs an integrated x86 prefetch timing scan before
creating vulnerable kernel state. It scans all 512 possible 2 MiB KASLR slots
and keeps the minimum of 100 measurements for each slot. The median represents
the much larger unmapped region; a 40-slot sliding window then selects the
region with the largest total absolute deviation from that median. Three full
scans must agree by majority, yielding the base without a separate leak.

The 40-slot window comes from the `lts-6.12.89` runtime LOAD span, which ends
at `kernel_base + 0x4fff000` and therefore occupies forty 2 MiB slots.

The target offset used after relocation is:

```text
coredump_sysctls = kernel_base + 0x03611600
```

The offset is obtained through kernelXDK `TargetDb` and
`GetSymbolOffset()`. The exploit adds it to the leaked kernel base when
building the two addresses used by the forged rb node.

### Preparing posix_timers_cache

The target's `struct k_itimer` is 256 bytes, giving sixteen objects per 4 KiB
`posix_timers_cache` slab page. The exploit allocates 8,192 `SIGEV_NONE`
monotonic timers. In every logical group of sixteen, it deletes slots 0, 3, 6,
9, 12, and 15. The remaining ten timers keep each source page resident while
the six holes are available for race-attempt allocations.

Timer objects are released through `call_rcu()`. After bulk deletion the
exploit performs 256 short yield/nanosleep rounds before depending on reuse.

Each race attempt allocates 48 filler timers, one vulnerable process CPU
timer, and 64 more filler timers. These allocations bracket the target in the
prepared source pages.

### Winning the dangling timer race

The exploit wins the bug only when deletion caches the former leader before
the target transfers its TGID, but tries to lock that leader after
`__exit_signal()` clears its `sighand`. The allocation layout, process
synchronization, CPU placement, and kernel warning oracle below retain the one
child whose shared process timerqueue crosses that lifetime boundary.

### Triggering the exec()/timer_delete() race

The creator constructs the target child's process profiling clock ID as:

```text
clockid = ((~child_tgid) << 3) | CPUCLOCK_PROF
```

The vulnerable timer uses `SIGEV_SIGNAL` and blocked `SIGUSR1`, because the
embedded `cpu_timer.node` must actually be armed in the child's process-wide
timerqueue. The child has a leader and a second pthread. The non-leader waits
on a one-byte gate, then executes `/proc/self/exe --worker`, entering the
`de_thread()` identity-transfer path.

Creator and target run on CPUs 0 and 1. Before releasing exec, the parent arms
the vulnerable timer at the child's sampled process CPU time plus 60 seconds.
This keeps the genuine node queued but prevents natural expiry during the
race. The parent evicts a 1 MiB working set, releases the exec thread, burns 15
microseconds, arms a 15.5-microsecond timerfd event, and calls
`timer_delete()`.

The useful interleaving is:

```text
creator CPU 0 target CPU 1
------------- ------------
cpu_timer_task_rcu() -> old leader
exchange_tids()
transfer PIDTYPE_TGID
release_task(old leader)
preserve inherited process queue
old_leader->sighand = NULL
lock_task_sighand(old leader) -> NULL
WARN: cpu_timer.node is still queued
return success without disarm_timer()
call_rcu(k_itimer)
target tree still contains the node
```

The target leader queues up to 256 blocked real-time signals before exec. One
timerfd is duplicated into 96 epoll registrations.

The exploit reads `/proc/sys/kernel/tainted` before the loop. On the tested
otherwise untainted fresh boots, bit 9 first changes when the
`WARN_ON_ONCE(ctmr->head || queued(node))` branch is reached.

### Reclaiming the timer slab with packetized pipe pages

After a confirmed race, the exploit deletes all attempt fillers and all
remaining source timers, then waits for RCU again. A completely empty timer
slab can now be returned to the page allocator.

The reclaim uses `pipe2(O_NONBLOCK | O_DIRECT)`. Packet mode prevents adjacent
sub-page writes from merging, so each write obtains one separate order-0 page.
Each pipe holds sixteen packets. The exploit creates 512 pipes and fills every
slot, retaining:

```text
512 pipes * 16 packet pages = 8,192 controlled pages
```

The stale timer may occupy any of the sixteen 256-byte positions in its old
page. For candidate slot `s`, every pipe page receives a packet of length:

```text
s * 256 + 152
```

The candidate fake timer therefore starts at byte `s*256`. The largest packet
is 3,992 bytes and remains within one page. To advance to the next candidate,
the exploit reads one old packet from every ring position and writes one new
packet, keeping the 8,192-page reclaim pressure constant. After each refill it
triggers target CPU-timer collection and tests whether `core_pattern` became
writable.

### Building the fake timerqueue node

The pipe payload overwrites old `k_itimer+0..+151` and stops exactly before
`cpu_timer.head` at offset 152. Preserving `head`, `pid` at 160, and `elist` at
168 is necessary because the target timerqueue and collection path still use
those fields.

The controlled values are:

| `k_itimer` offset | Value | Role |
| ---: | ---: | --- |
| `+0..+31` | `0` | Clear stale list and hash metadata |
| `+32` | `1` | Keep the raw `it_lock` qspinlock locked after erase |
| `+40..+119` | `0` | Neutralize timer metadata before `it.cpu` |
| `+120` | `write_target - 8` | Fake `rb_node.__rb_parent_color` |
| `+128` | `write_value` | Fake `rb_node.rb_right` |
| `+136` | `0` | NULL `rb_node.rb_left` |
| `+144` | `0` | Earliest timerqueue expiry |
| `+152` onward | Preserved | Live `head`, `pid`, `elist`, and later state |

### Turning rb_erase() into a core_pattern permission write

The fake rb node has no left child and one right child. On x86-64,
`rb_node.rb_right` is at offset 8. The payload uses:

```text
parent = write_target - 8
right = write_value
left = NULL
```

The one-child erase path executes `__rb_change_child()`. The fake parent does
not contain the old node in its `rb_left`, so the function selects
`parent->rb_right` and performs:

```text
*(uint64_t *)write_target = write_value
```

The erase also stores the fake parent in `right->__rb_parent_color`, producing
one correlated write at `write_value`.

The target's `struct ctl_table` is 56 bytes, with `maxlen` at offset 16 and
`poll` at offset 32. The selected addresses are:

```text
write_target = coredump_sysctls + 56 + 16 = coredump_sysctls + 0x48
write_value = coredump_sysctls + 32 = coredump_sysctls + 0x20
```

`write_target` covers the second table entry's 32-bit `maxlen` and following
16-bit `mode`, which describe `core_pattern`. Writing a canonical kernel
pointer there gives `maxlen` a large low 32-bit value and changes `mode` to
`0xffff`. The uid-1000 process can then open
`/proc/sys/kernel/core_pattern` for writing. This proc open is also the
candidate-slot success check.

### Installing and executing the pipe core helper

Before exploitation, the binary copies `/proc/self/exe` to a memfd and
duplicates it to descriptor 111. After the sysctl metadata write, it installs:

```text
|/proc/%P/fd/111 helper
```

The exploit then deliberately dereferences NULL. The coredump path expands
`%P` to the crashing process ID and executes the inherited memfd through
`/proc/<pid>/fd/111`. The `helper` entry point runs as root, reads `/flag`, and
prints it to the console.

### KASAN vulnerability trigger

Running `./exploit --vuln-trigger` skips kernelXDK target setup, the KASLR
scan, slab grooming, page reclaim, and the final overwrite. It performs only
the non-leader `execve()` versus `timer_delete()` race, waits for the queued
timer object to pass through RCU, and asks the target to collect the dangling
timerqueue node so a KASAN kernel reports the use-after-free.

## Additional Notes

The exploit embeds `target_db.kxdb` and links kernelXDK statically.
`setup_target()` uses `GetSymbolOffset()`, `GetStructSize()`, and
`GetFieldOffset()` for `coredump_sysctls`, `k_itimer`, `cpu_timer`,
`timerqueue_node`, `rb_node`, and `ctl_table`. The target-specific entries
missing from the bundled database are added as a normal `Target` before
auto-detection. The bundled stable libxdk v0.1 release does not expose its
newer `leak_kaslr_base()` helper, so the exploit contains the same windowed
max-absolute-difference scan locally. The data-only rb-tree corruption has no
corresponding kernelXDK primitive.

The normal command is exactly `./exploit`; no user namespace, capability,
`io_uring`, or netfilter operation is required. Testing used KASLR-enabled
`runs/remote/lts-6.12.89/bzImage` with SHA256
`3962083a895545dd51c83d714f1fd6746bffb9e2536bf0866021eb9794685090`,
3,584 MiB of RAM, two vCPUs, and a fresh VM for every attempt. The exploit
printed a flag in 30 of 30 runs; the slowest success took 70.36 seconds.
13 changes: 13 additions & 0 deletions pocs/linux/kernelctf/CVE-2026-64560_lts/docs/vulnerability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Vulnerability Details

- **Requirements**:
- **Capabilities**: None
- **Kernel configuration**: `CONFIG_POSIX_TIMERS=y`
- **User namespaces required**: No
- **Introduced by**: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=55e8c8eb2c7b6bf30e99423ccfe7ca032f498f59
- **Fixed by**: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=920f893f735e92ba3a1cd9256899a186b161928d
- **Affected Version**: `v5.7-rc1 - v6.12.89`
- **Affected Component**: posix-cpu-timers
- **Syscall to disable**: `timer_create`
- **Cause**: Use-After-Free
- **Description**: A POSIX process CPU timer is owned by the process that creates its `struct k_itimer`, while its embedded `cpu_timer.node` can be queued in a different target process's shared `signal->posix_cputimers` rbtree. During a non-leader `execve()`, `de_thread()` transfers the TGID to the execing thread and releases the old leader. The TGID-targeted timer and process-wide queue are intentionally inherited by the new leader, but a concurrent `posix_cpu_timer_del()` can retain the old leader from an earlier PID lookup. When `lock_task_sighand()` then observes the old leader's `sighand == NULL`, the vulnerable code does not retry the PID lookup to find the new leader; it returns success without `disarm_timer()`, and `timer_delete()` RCU-frees the still-enqueued 256-byte `k_itimer`. Later CPU-timer execution or timerqueue add/delete operations access its freed embedded node. The same lookup/lock race can make `posix_cpu_timer_set()` return transient `-ESRCH`, leave the stack timer used by `do_cpu_nanosleep()` queued, or make `posix_cpu_timer_rearm()` silently lose a rearm. On weakly ordered architectures, observing `sighand == NULL` without ordering can also make the queued-node warning a false positive. The fix changes the `sighand = NULL` store to `smp_store_release()`, adds `smp_acquire__after_ctrl_dep()` to the NULL path in `lock_task_sighand()`, introduces a retrying `timer_lock_sighand()` helper for delete, set, and rearm, and uses `smp_rmb()` before the queued-state check when the initial task lookup fails.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
XDK ?= ./libxdk-v0.1
CXXFLAGS ?= -O2 -Wall -static -pthread -I$(XDK)
LDLIBS ?= -L$(XDK) -lkernelXDK

exploit: exploit.c target_db.kxdb $(XDK)/libkernelXDK.a
g++ $(CXXFLAGS) -x c++ $< -o $@ $(LDLIBS)

all: exploit

.PHONY: prerequisites
prerequisites:

.PHONY: run
run: exploit
./exploit

.PHONY: clean
clean:
rm -f exploit
Binary file not shown.
Loading
Loading