### Introduction
Back in the old days, people were using the `\a` character to emit a horrible 'beep' sound from their speaker.
It was a bit annoying, especially if you wanted more complicated stuff to do 8bits-like musics. That's why [Johnathan Nightingale](https://github.com/johnath/) made the beep software. A very simple and short software to let you fine-tune your beeps as you wish.
With the arrival of the X server, things started to be a bit more complicated.
For beep to work, the user must either be the superuser or own the current tty. It means that beep will always work for the root user or any local user, but not if it's a non-root remote user. And guess what, any terminal (like xterm) connected to the X server is considered as 'remote', and therefore beep won't work.
The solution that most people (and distros) are using is to set the SUID bit. The SUID bit is a special bit that, when set on a binary, launches it with its owner's rights (here root) instead of the normal user (you).
This bit is used quite a lot nowadays, mostly for convenience. An example is poweroff: it requires root privileges to work (only the root user can shut down the computer), but that would be overkill on a personal computer. If you were the sysadmin of a company and each user would need to ask you to shut down their computer, it would be annoying. On the other hand, on a server shared among multiple users, the ability for one malicious user to shut down the whole system is a serious security concern.
Of course, all SUID programs are potential security holes. Put it on bash, and it's free root-shell for everyone. That's why such programs are heavily reviewed by the community.
So one might think that a program like beep, only 375 lines of code, reviewed by a bunch of people, should be safe to install, even with a SUID bit, right?
Well, it's not!
### Understanding the code
Take a look at the beep source code, available [here](https://github.com/johnath/beep/blob/master/beep.c).
The main function sets some signal handlers, parses the arguments, and for each beep requested it calls 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;
}
```
On the other hand, play_beep() opens the targeted device, looks for its type and calls do_beep() for each repetitions.
```
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()` itself is simply calling the correct function to make the sound, depending on the type of the targeted 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 pretty straightforward: it frees the targeted device (a char *) and, if it was opened, stops 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);
}
}
}
```
So, what do we have?
The first thing that caught my attention was that if SIGINT and SIGTERM are sent at the same time, there is a risk of double free(). But apart from crashing the program, I couldn't find any useful way to exploit it, as console_device isn't used anywhere after that.
So, ideally, what do we want?
This write() in do_beep() looks good, right? Would be cool to use it to write in an arbitrary file!
But this write is protected by console_type, which must be BEEP_TYPE_EVDEV.
console_type is set in play_beep(), depending on the return value of ioctl(). To be considered a BEEP_TYPE_EVDEV, ioctl() must say so.
And that's the thing, we can't make ioctl() lie to beep. If the file isn't a device file, ioctl() will fail, device_type will not be BEEP_TYPE_EVDEV and do_beep() will not call write() (it will use ioctl() instead, which is, as far as I know, harmless in this context).`
But remember there is also a signal handler, and signals can happen at any time! They are perfect for race conditions.
### The race condition
This signal handler calls do_beep(). If we have the good values in console_fd and console_type at that moment, we will be able to write in the targeted file.
As signals can be called at any point, we need to find the exact location where both of these variables do not hold their correct values.
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 for each requested beep. If a previous call was successful, console_fd and console_type will still hold their old values.
Which means there is a small portion of code (line 285 to 293) where console_fd holds its new value and console_type still holds its old one.
This is it. This is our race condition. This is the moment where we want to trigger the signal handler.
### Writing an exploit
Writing the exploit wasn't easy. Finding the good timing was a real pain in the ass.
The path to the targeted device (console_device) cannot be changed after beep is started. The trick is to make a symlink, first pointing to a valid device, and then to the targeted file.
Now that we can write to the targeted file, we need to know what to write.
The call made to write is the following one:
```
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 struct input_event is defined in linux/input.h as follow:
```
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 time member is not assigned in beep's source code, and it's the first element of the structure, so its value will be the first bytes of the targeted file after the attack.
Maybe we can trick the stack to make it hold the value we want?
With a bit of luck and a lot of die-and-retry, I found out that the value of the -l parameter will be placed there, followed by a \0. It's an int, which gives us 4 bytes.
4 bytes that we can write in any existing file.
I decided to write /*/x. In a shell-script, it will execute the program (hand-crafted before) /tmp/x.
By targetting a file like /etc/profile or /etc/bash/bashrc, we will gain full access of each user logging in.
I wrote a small python script, (available [here](https://gist.github.com/Arignir/0b9d45c56551af39969368396e27abe8)), to automate the attack. It basically sets the symlink to /dev/input/event0, starts beep, waits a bit, re-sets the symlink, waits a bit more and signals 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
```
I saw alternatives using cron tasks instead. They look better, as they don't need that root logs in, but I didn't have the time to give them a chance.
暂无评论