В былые времена люди использовали 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
, а после него — . Значение целочисленное, что даёт нам 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-входа, но у меня не было возможности протестировать.
Заключение
Это был мой первый эксплоит нулевого дня.
В начале было довольно трудно найти утечку. Пришлось анализировать снова и снова, пока не придумал решение.
Я узнал, что обработка сигналов гораздо сложнее, чем мне казалось, особенно потому, что нужно избегать не-реентерабельные функции, и что запрещены практически все функции из библиотеки С.
Автор: AloneCoder