algoatson.github.io ~/posts
segfault@algoatson.github.io
back to index

kernel LPE — commit_creds(prepare_kernel_cred(0))

2026-04-123 min readkernel pwn

The modprobe_path overwrite is the clean, data-only way to escalate. But sometimes you have control flow, not a write-what-where, and the canonical kernel payload is two calls: make a fresh root credential, install it on the current task.

the entire goal, in Cc
commit_creds(prepare_kernel_cred(0));   // current task now has uid 0 creds

prepare_kernel_cred(0) allocates a cred with root ids; commit_creds swaps it onto the running task. After it returns you're root — you just have to get back to userland in one piece and execve("/bin/sh").

target./vuln.ko (a deliberately buggy CTF module)
arch
amd64-64-little
control
a kernel stack overflow -> kernel ROP
  • SMEPon
  • SMAPon
  • KPTIon
  • KASLRon

the four walls, and the door in each#

Every kernel mitigation here removes one easy version of the attack and leaves a narrower one:

target
  • SMEPon
  • SMAPon
  • KPTIon
  • KASLRon
  • SMEP — the kernel can't execute userland pages. So you can't point kernel rip at a userland function (ret2usr is dead); you ROP inside the kernel instead, with gadgets from the kernel image.
  • SMAP — the kernel can't casually read userland either, so stage your data in kernel memory or registers, not a userland buffer it'll dereference.
  • KASLR — the kernel base is randomized; you need a leak (an %pK/dmesg/info-leak bug) to rebase every gadget and symbol.
  • KPTI — userland and kernel run on separate page tables, so you can't just iretq back; you return through the kernel's own trampoline, swapgs_restore_regs_and_return_to_usermode, which switches CR3 for you.

the chain#

Save the userland state first (CPU cs/ss/rflags/rsp and a target rip), because the trampoline's iretq will restore exactly that and drop you back into your own process — now privileged:

sketch of the kernel ROP chainpython
base = leak_kernel_base()                       # defeat KASLR first
rop  = [
    pop_rdi, 0,                                 # prepare_kernel_cred(0)
    base + prepare_kernel_cred,
    mov_rdi_rax_ret,                            # rdi = the new cred (returned in rax)
    base + commit_creds,                        # commit_creds(cred) -> we're root
    # return to userland through the KPTI trampoline:
    base + kpti_trampoline + OFFSET,            # skips the mov rdi/swapgs preamble
    0, 0,                                        # padding the trampoline pops
    user_rip, user_cs, user_rflags, user_sp, user_ss,
]
the userland landing padc
1
2
3
4
5
6
void win(void) {            // user_rip points here; runs after we're root
    if (getuid() == 0)
        system("/bin/sh");  // root shell
}
// grab cs/ss/rflags/sp in a saved_state() before triggering the bug,
// so the trampoline's iretq returns cleanly into win().
uid=0

commit_creds returns, the trampoline iretqs back into win(), and the saved userland context is intact — except for the one thing that changed:

txt
1
2
3
# id
uid=0(root) gid=0(root) groups=0(root)
segfault{ring0_to_root}

notes from the trenches#

  • prepare_kernel_cred(0) is version-sensitive. On kernels ≥ 6.2 passing a task pointer changed semantics; the 0/NULL form still yields root creds in the versions most CTF modules ship, but check your target's source — some pwns switch to &init_cred directly.
  • The trampoline offset matters. Enter swapgs_restore_regs_and_return_to_usermode past its mov rdi, rsp / register-pop preamble, at the swapgs; iretq tail, with the stack laid out exactly how iretq wants it (rip cs rflags rsp ss).
  • Leak before you leap. With KASLR on, every address above is base + offset; without a leak you have nothing. The info-leak bug is usually the hard 80% of the exploit, the cred swap the easy 20%.

Two function calls separate an unprivileged task from root. The mitigations don't forbid those calls — they just make you assemble them from kernel gadgets, rebase everything off a leak, and tiptoe back across the KPTI boundary to enjoy the result.