导语:本文对Holeybeep漏洞进行了代码级的分析和利用复现。
简介
过去,人们用\a来从扬声器发出beep的声音。如果想要beep工作,用户需要是root超级用户或者拥有当前tty。也就是说beep只有在root用户和任何本地用户情况下才工作,在非root远程用户登录情况下是不工作的。所以,连接X server的终端会被认为是远程用户,beep也是不工作的。
大多数人的解决方法是设定SUID位,SUID位是一个特殊的位,设定后相当于对用户进行提权。因为很便利,所以SUID的应用也很广泛。所有的SUID程序都有潜在的安全问题。放在bash上,对每个人来说都是自由的root-shell。这也是为什么社区要严格审查这些程序。所以像beep这样只有375行代码的程序,虽然有SUID位,但是经过一大群人的检查,应该是安全的吧?NO,答案是否定的。
代码
下面我们看一下beep的源码,主函数用来设置一些信号handler,对请求的beep会调用play_beep()函数。
下面是beep的源码。
int main(int argc, char **argv) { /* ... */ signal(SIGINT, handle_signal); signal(SIGTERM, handle_signal); parse_command_line(argc, argv, parms); while(parms) { beep_parms_t *next = parms->next; if(parms->stdin_beep) { /* ... */ } else { play_beep(*parms); } /* Junk each parms struct after playing it */ free(parms); parms = next; } if(console_device) free(console_device); return EXIT_SUCCESS; }
另一方面,play_beep()打开目标设备,寻找设备类型,对于每个重复会调用do_beep()。
void play_beep(beep_parms_t parms) { /* ... */ /* try to snag the console */ if(console_device) console_fd = open(console_device, O_WRONLY); else if((console_fd = open("/dev/tty0", O_WRONLY)) == -1) console_fd = open("/dev/vc/0", O_WRONLY); if(console_fd == -1) { /* ... */ } if (ioctl(console_fd, EVIOCGSND(0)) != -1) console_type = BEEP_TYPE_EVDEV; else console_type = BEEP_TYPE_CONSOLE; /* Beep */ for (i = 0; i < parms.reps; i++) { /* start beep */ do_beep(parms.freq); usleep(1000*parms.length); /* wait... */ do_beep(0); /* stop beep */ if(parms.end_delay || (i+1 < parms.reps)) usleep(1000*parms.delay); /* wait... */ } /* repeat. */ close(console_fd); }
do_beep()函数本身只是简单的调用正确的函数来发出声音,对不同的设备调用的函数不同,所以调用的函数是依赖目标设备类型的。
void do_beep(int freq) { int period = (freq != 0 ? (int)(CLOCK_TICK_RATE/freq) : freq); if(console_type == BEEP_TYPE_CONSOLE) { if(ioctl(console_fd, KIOCSOUND, period) < 0) { putchar('\a'); perror("ioctl"); } } else { /* BEEP_TYPE_EVDEV */ struct input_event e; e.type = EV_SND; e.code = SND_TONE; e.value = freq; if(write(console_fd, &e, sizeof(struct input_event)) < 0) { putchar('\a'); /* See above */ perror("write"); } } }
信号handler也是很直接的,它会释放目标设备(a char *),如果打开的话,通过调用do_beep(0)来停止出声。
/* If we get interrupted, it would be nice to not leave the speaker beeping in perpetuity. */ void handle_signal(int signum) { if(console_device) free(console_device); switch(signum) { case SIGINT: case SIGTERM: if(console_fd >= 0) { /* Kill the sound, quit gracefully */ do_beep(0); close(console_fd); exit(signum); } else { /* Just quit gracefully */ exit(signum); } } }
通过代码发现,如果SIGINT和SIGTERM同时发送,就可能会有两个free()的风险。除了程序会奔溃以外,研究人员还没有发现利用该漏洞的方法。
不过do_beep()中的write()看起来不错,可以尝试用它来在任意文件中写。但这种写是通过console_type保护的,只有console_type是BEEP_TYPE_EVDEV时才可以写。console_type根据ioctl()的返回值是在play_beep()中设置的。
但问题是不能让ioctl()假装beep,如果该文件不是设备文件,ioctl()就会失败,device_type就不是BEEP_TYPE_EVDEV,do_beep()也不会调用write()进行写操作。但需要注意的是只是一个信号handler,信号可能会随时产生。这对竞争条件来说几乎是完美的。
竞争条件
信号handler会调用do_beep(),如果同时有console_fd和console_type的值,那么就可以在目标文件中进行写操作。
因为信号可以在任何点被调用,所以需要找到这些值不正确的变量的精确位置。下面是play_beep()的代码:
void play_beep(beep_parms_t parms) { /* ... */ /* try to snag the console */ if(console_device) console_fd = open(console_device, O_WRONLY); else if((console_fd = open("/dev/tty0", O_WRONLY)) == -1) console_fd = open("/dev/vc/0", O_WRONLY); if(console_fd == -1) { /* ... */ } if (ioctl(console_fd, EVIOCGSND(0)) != -1) console_type = BEEP_TYPE_EVDEV; else console_type = BEEP_TYPE_CONSOLE; /* Beep */ for (i = 0; i < parms.reps; i++) { /* start beep */ do_beep(parms.freq); usleep(1000*parms.length); /* wait... */ do_beep(0); /* stop beep */ if(parms.end_delay || (i+1 < parms.reps)) usleep(1000*parms.delay); /* wait... */ } /* repeat. */ close(console_fd); }
对于每个请求的beep都会调用play_beep()。如果之前的调用成功了,那么console_fd和console_type就会保持原来的值。也就是说console_type的值不变,而console_fd会变成一个新的值。
这就是想要的竞争条件,此刻可以触发信号handler。
写利用
写利用非常不容易,beep开启后到目标设备的路径就不能改变了。有一个技巧就是做一个symlink,首先指向一个有效的设备,然后指向目标文件。这样就可以向目标文件中写了,那么就需要知道写入什么。
执行写的调用为:
struct input_event e; e.type = EV_SND; e.code = SND_TONE; e.value = freq; if(write(console_fd, &e, sizeof(struct input_event)) < 0) { putchar('\a'); /* See above */ perror("write"); }
linux/input.h文件中定义了input_event结构体:
struct input_event { struct timeval time; __u16 type; __u16 code; __s32 value; }; struct timeval { __kernel_time_t tv_sec; /* seconds */ __kernel_suseconds_t tv_usec; /* microseconds */ }; // On my system, sizeof(struct timeval) is 16.
时间参数值beep的源代码中是没有分配的,而且是结构的第一个元素,所以它的值是攻击后的目标文件中的第一字节。
可以尝试去欺骗栈来使栈中的值变成我们想要的。
经过多次尝试,研究人员发现-l参数的值后面加\0。这是一个整数类型,就有4字节可以利用。而4字节可以在任何已有的文件中进行写操作。
研究人员决定写入/*/x。在shell脚本中,写入的内容会执行程序/tmp/x。通过写入/etc/profile,/etc/bash/bashrc这样的文件,就可以获取登录用户的所有权限。
研究人员写了一个python脚本来自动执行攻击。该脚本会在/dev/input/event0中设置symlink,开启beep,过一会,重新设置symlink,然后(等的时间再长一点)会继续beep。
$ echo 'echo PWND $(whoami)' > /tmp/x $ ./exploit.py /etc/bash/bashrc # Or any shell script Backup made at '/etc/bash/bashrc.bak' Done! $ su PWND root
使用cron任务也可以完成,而且不需要root登录,但是研究人员因为时间关系没有尝试,有想实践的同学可以试一下。