HoleyBeep: explanation and exploit

Original author: Pirhack
  • Transfer
  • Tutorial


In the old days, people used \ato generate unpleasant “beeps” from speakers of system units. This was especially inconvenient if you wanted to generate more complex sound sequences like 8-bit music. Therefore, Jonathan Nightingale wrote the program beep. It was a short and very simple program that allowed you to finely tune the sound from the speaker.

With the advent of the X server, things got a lot more complicated.

To beep work, the user must either be superuser or be the owner of the current tty. I.ebeep It will always work with a root user or any local user, but it will not work with a non-root remote user. Moreover, any terminal (for example, xterm) connected to the X-server is considered to be “remote”, and therefore beep will not work.

Many users (and distributions) solve the problem with a bit SUID. This is a special bit, if you set it for the binary, the file is executed with the rights of the owner (in this case root), and not of the ordinary user (yours).

Today, this bit is used widely, mainly for convenience. For example, to workpoweroff root privileges are needed (only the root user can turn off the computer), but this would be too much for a personal computer. Imagine that you are a system administrator and all users in the company ask you to turn off their computers to them. On the other hand, if one attacker can shut down a server with a large number of users, this is a serious security breach.

Of course, all programs using SUID are potential flaws. Take the same bash, a free root shell. Therefore, such programs are very carefully reviewed by the community.

You might think that a program like beepthat, consisting of only 375 lines of code, viewed by a bunch of people, can be installed safely, despite it SUID, right?

Not at all!

We understand the code


Let's see the source code beep, it lies here: https://github.com/johnath/beep/blob/master/beep.c .

The main function sets the signal handlers, parses the arguments, and calls for each sound requested play_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;
}

In turn, it play_beep()opens the target device, searches for its types and calls for each repeat 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() just calls the desired function to generate the signal depending on the target device:

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");
     }
  }
}

The signal handler is simple: it frees the target device ( char *), and if it works, it interrupts the sound by calling 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);
    }
  }
}

First of all, my attention was drawn to the fact that if SIGINT they are SIGTERM sent at the same time, there is a chance to call twice free(). But I do not see other useful applications except for the program crash, because after that console_deviceit will not be used anywhere else.

What would we ideally achieve?

This feature write()in do_beep()looks appropriate. It would be great to use it to write to an intermediate file!

But this entry is protected console_type, which should be BEEP_TYPE_EVDEV.

console_typeis set in play_beep()depending on the return value ioctl(). That is, it ioctl()must be allowed to be BEEP_TYPE_EVDEV.

But we cannot make ioctl()lies. If the file does not belong to the device, it will ioctl()fail,device_typewill not BEEP_TYPE_EVDEV, but do_beep()will not call write()(instead, it uses ioctl(), which, as far as I know, is safe in this context).

But we still have a signal handler, and signals can be generated at any time !

Race condition


This signal handler calls do_beep(). If at this moment in console_fdand console_typewe have the correct values, then we can write to the target file.

Since signals can be called anywhere, you need to find a specific place where both variables do not contain the correct values.

Do you remember play_beep()? Here is the code:

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);
}

It is called whenever requested beep. If the previous call is successful, console_fdand console_typewill still have their old values.

This means that in a small piece of code (from line 285 to line 293) it console_fdhas a new value, but console_type- it still has the old value.

Here it is. Here is our race condition. It is at this point that we will launch the signal handler.

We write an exploit


Writing an exploit was not easy. It was very difficult to calculate the right moment.
After the beep starts, the path to the target device ( console_device) cannot be changed . But you can make a symlink, first leading to the correct device, and then to the target file.

And since now we can write to this file, we need to understand what to write.

Call to record:

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");
}

The structure is struct input_eventdefined in linux/input.h:

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.

The element is time not assigned in the source code beep, and this is the first element of the structure, so its value will be the first bytes of the target file after the attack.

Perhaps we can trick the stack so that it retains the desired value?

After a bunch of trial and error, I found out that the value of the parameter will be stored there -l, and after it - \0. The value is integer, which gives us 4 bytes.

Four bytes that we can write to any existing file.

I decided to record /*/x. In a shell script, this will lead to the execution of the program (pre-made) /tmp/x.

If you attack the file /etc/profileor /etc/bash/bashrc, then we will achieve complete success with any logged-in user.

To automate the attack, I wrote a small Python script (lies here: https://gist.github.com/Arignir/0b9d45c56551af39969368396e27abe8 ). It assigns a symlink leading to /dev/input/event0, starts beep, waits a little, reassigns the link, waits again, and then generates a signal.

$ 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

I came across solutions using cron tasks. This approach looks better because it does not require root login, but I did not have the opportunity to test.

Conclusion


This was my first zero day exploit.

In the beginning it was pretty hard to find a leak. I had to analyze again and again, until I came up with a solution.

I found out that signal processing is much more complicated than it seemed to me, especially because non-reentrant functions should be avoided, and almost all functions from the C library are forbidden.

Also popular now: