导语:本文对Holeybeep漏洞进行了代码级的分析和利用复现。

holeybeep.png

简介

过去,人们用\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登录,但是研究人员因为时间关系没有尝试,有想实践的同学可以试一下。

源链接

Hacking more

...