Этот пост в блоге необычный. Обычно я пишу посты о скрытых видах атак или интересном и сложном классе уязвимостей. На этот раз речь пойдёт о совершенно иной уязвимости. Впечатляет её простота. Её должны были заметить раньше, и я хочу выяснить, почему этого не произошло.
В 2021 году всем хорошим багам нужно цепляющее название, и у этой уязвимости появилось имя BigSig. Сначала объясню, как она нашлась, а затем попытаюсь понять, почему её так долго упускали.
struct VFYContextStr {
SECOidTag hashAlg; /* the hash algorithm */
SECKEYPublicKey *key;
union {
unsigned char buffer[1];
unsigned char dsasig[DSA_MAX_SIGNATURE_LEN];
unsigned char ecdsasig[2 * MAX_ECKEY_LEN];
unsigned char rsasig[(RSA_MAX_MODULUS_BITS + 7) / 8];
} u;
unsigned int pkcs1RSADigestInfoLen;
unsigned char *pkcs1RSADigestInfo;
void *wincx;
void *hashcx;
const SECHashObject *hashobj;
SECOidTag encAlg; /* enc alg */
PRBool hasSignature;
SECItem *params;
};
Структура VFYContext из NSS
Сигнатура максимального размера, которую обрабатывает эта структура, равна наибольшему элементу объединения, здесь это RSA в 2048 байтов, то есть 16 384 бита. Это достаточно много, чтобы вместить сигнатуры даже невероятно больших ключей. А что, если сделать сигнатуру больше этой? Произойдёт повреждение памяти. Да, так есть. Ненадёжная сигнатура просто копируется в этот буфер фиксированного размера, перезаписывая соседние элементы произвольными данными, которые контролируются злоумышленником.
Баг прост в воспроизведении и влияет на несколько алгоритмов. Проще всего показать RSA-PSS:
# We need 16384 bits to fill the buffer, then 32 + 64 + 64 + 64 bits to overflow to hashobj,
# which contains function pointers (bigger would work too, but takes longer to generate).
$ openssl genpkey -algorithm rsa-pss -pkeyopt rsa_keygen_bits:$((16384 + 32 + 64 + 64 + 64)) -pkeyopt rsa_keygen_primes:5 -out bigsig.key
# Generate a self-signed certificate from that key
$ openssl req -x509 -new -key bigsig.key -subj "/CN=BigSig" -sha256 -out bigsig.cer
# Verify it with NSS...
$ vfychain -a bigsig.cer
Уязвимость BigSig за три простых команд
Код, который вызывает повреждение, зависит от алгоритма. Вот код для RSA-PSS. Баг заключается в том, что проверки границ просто нет вообще: sig и key — это большие двоичные объекты произвольной длины, контролируемые злоумышленником, а cx->u — это буфер фиксированного размера:
case rsaPssKey:
sigLen = SECKEY_SignatureLen(key);
if (sigLen == 0) {
/* error set by SECKEY_SignatureLen */
rv = SECFailure;
break;
}
if (sig->len != sigLen) {
PORT_SetError(SEC_ERROR_BAD_SIGNATURE);
rv = SECFailure;
break;
}
PORT_Memcpy(cx->u.buffer, sig->data, sigLen);
break;
Уязвимость вызывает ряд вопросов:
NSS был одним из первых проектов, включённых в oss-fuzz: по крайней мере поддерживался официально с октября 2014 года. В Mozilla сами также проводят автоматизацию тестирования безопасности NSS с помощью libFuzzer и представили собственную коллекцию методов-модификаторов, а также основу корпуса покрытия. Есть обширный тестовый комплект и ночные сборки ASAN.
Я в общем скептически отношусь к статическому анализу, но это похоже на простую недостающую проверку границ, которую должно быть легко найти. Coverity отслеживает NSS по крайней мере с декабря 2008 года и тоже, кажется, не смогла обнаружить уязвимость.
До 2015 года в Google Chrome использовали NSS и поддерживали собственную инфраструктуру тестового комплекта и автоматизации тестирования безопасности, независимую от Mozilla. Сегодня в платформах Chrome используется BoringSSL, но порт NSS ещё поддерживается.
#include <stdio.h>
#include <string.h>
#include <limits.h>
static char buf[128];
void cmd_handler_foo(int a, size_t b) { memset(buf, a, b); }
void cmd_handler_bar(int a, size_t b) { cmd_handler_foo('A', sizeof buf); }
void cmd_handler_baz(int a, size_t b) { cmd_handler_bar(a, sizeof buf); }
typedef void (* dispatch_t)(int, size_t);
dispatch_t handlers[UCHAR_MAX] = {
cmd_handler_foo,
cmd_handler_bar,
cmd_handler_baz,
};
int main(int argc, char **argv)
{
int cmd;
while ((cmd = getchar()) != EOF) {
if (handlers[cmd]) {
handlers[cmd](getchar(), getchar());
}
}
}
Покрытие команды bar — это надмножество команды foo, поэтому ввод, содержащий foo, будет отброшен при минимизации корпуса. Есть уязвимость, недоступная через команду bar, которая может быть не обнаружена никогда. Покрытие стека корректно сохранит оба ввода1.
Чтобы решить эту проблему, я отслеживал стек вызовов во время выполнения.
Наивная реализация слишком медленная, но после многих оптимизаций я создал практичную библиотеку, достаточно быструю, чтобы интегрировать её в автоматизацию тестирования безопасности, ориентированную на покрытие. Я тестировал, как она работает с NSS и другими библиотеками.
Техника тестирования, которую я использовал, способна выделять и извлекать интересные новые идентификаторы объекта ASN.1, последовательности SEQUENCE, целые числа INTEGER и т. д. После извлечения они могут случайным образом комбинироваться или вставляться в данные шаблона. На самом деле идея не новая, новая — реализация. Планирую в будущем сделать этот код общедоступным.
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
char *dest[2048];
for (auto tpl : templates) {
PORTCheapArenaPool pool;
SECItem buf = {siBuffer, const_cast<unsigned char *>(Data),
static_cast<unsigned int>(Size)};
PORT_InitCheapArena(&pool, DER_DEFAULT_CHUNKSIZE);
(void)SEC_QuickDERDecodeItem(&pool.arena, dest, tpl, &buf);
PORT_DestroyCheapArena(&pool);
}
При использовании техники тестирования QuickDER объекты просто создаются и отбрасываются. Так проверяется синтаксический анализ ASN.1, но не корректность работы с получаемыми объектами других компонентов.
С этой техникой тестирования можно было создать SECKEYPublicKey, который бы достал в уязвимый код. Но баг так и не был бы обнаружен, ведь результат никогда не использовался для проверки сигнатуры.
Приемлемым вариантом может быть 224–1 байтов, т. е. максимально возможный сертификат, предоставляемый сервером во время согласования рукопожатия TLS.
Хотя в NSS обрабатываются объекты даже большего размера, TLS задействовать нельзя, а это снижает общую степень серьёзности любых пропущенных уязвимостей.
Почему? Потому что в техниках тестирования типа tls_server_target используются фиксированные, жёстко заданные сертификаты. При этом выполняется код, относящийся к проверке сертификата, но проводится автоматизированное тестирование лишь сообщений TLS и изменений состояния протокола.
Спасибо команде NSS, которая помогла проанализировать и разобраться с уязвимостью.
В 2021 году всем хорошим багам нужно цепляющее название, и у этой уязвимости появилось имя BigSig. Сначала объясню, как она нашлась, а затем попытаюсь понять, почему её так долго упускали.
Анализ
Network Security Services (NSS) — популярная кросс-платформенная криптографическая библиотека от Mozilla. Когда проверяется зашифрованная цифровая подпись ASN.1, в NSS создаётся структура VFYContext для хранения необходимых данных — открытого ключа, хеш-алгоритма и самой подписи.struct VFYContextStr {
SECOidTag hashAlg; /* the hash algorithm */
SECKEYPublicKey *key;
union {
unsigned char buffer[1];
unsigned char dsasig[DSA_MAX_SIGNATURE_LEN];
unsigned char ecdsasig[2 * MAX_ECKEY_LEN];
unsigned char rsasig[(RSA_MAX_MODULUS_BITS + 7) / 8];
} u;
unsigned int pkcs1RSADigestInfoLen;
unsigned char *pkcs1RSADigestInfo;
void *wincx;
void *hashcx;
const SECHashObject *hashobj;
SECOidTag encAlg; /* enc alg */
PRBool hasSignature;
SECItem *params;
};
Структура VFYContext из NSS
Сигнатура максимального размера, которую обрабатывает эта структура, равна наибольшему элементу объединения, здесь это RSA в 2048 байтов, то есть 16 384 бита. Это достаточно много, чтобы вместить сигнатуры даже невероятно больших ключей. А что, если сделать сигнатуру больше этой? Произойдёт повреждение памяти. Да, так есть. Ненадёжная сигнатура просто копируется в этот буфер фиксированного размера, перезаписывая соседние элементы произвольными данными, которые контролируются злоумышленником.
Баг прост в воспроизведении и влияет на несколько алгоритмов. Проще всего показать RSA-PSS:
# We need 16384 bits to fill the buffer, then 32 + 64 + 64 + 64 bits to overflow to hashobj,
# which contains function pointers (bigger would work too, but takes longer to generate).
$ openssl genpkey -algorithm rsa-pss -pkeyopt rsa_keygen_bits:$((16384 + 32 + 64 + 64 + 64)) -pkeyopt rsa_keygen_primes:5 -out bigsig.key
# Generate a self-signed certificate from that key
$ openssl req -x509 -new -key bigsig.key -subj "/CN=BigSig" -sha256 -out bigsig.cer
# Verify it with NSS...
$ vfychain -a bigsig.cer
Уязвимость BigSig за три простых команд
Код, который вызывает повреждение, зависит от алгоритма. Вот код для RSA-PSS. Баг заключается в том, что проверки границ просто нет вообще: sig и key — это большие двоичные объекты произвольной длины, контролируемые злоумышленником, а cx->u — это буфер фиксированного размера:
case rsaPssKey:
sigLen = SECKEY_SignatureLen(key);
if (sigLen == 0) {
/* error set by SECKEY_SignatureLen */
rv = SECFailure;
break;
}
if (sig->len != sigLen) {
PORT_SetError(SEC_ERROR_BAD_SIGNATURE);
rv = SECFailure;
break;
}
PORT_Memcpy(cx->u.buffer, sig->data, sigLen);
break;
Уязвимость вызывает ряд вопросов:
- Связана ли она с недавним изменением кода или это регрессия, которая проявилась только сейчас? Нет, исходный код был проверен поддержкой ECC 17 октября 2003 года, но его нельзя было использовать до рефакторинга, проведённого в июне 2012 года. В 2017 году была добавили поддержка RSA-PSS, при этом допустили ту же ошибку.
- Много ли времени требуется, чтобы сгенерировать ключ, который вызывает баг? Нет, в приведённом примере генерируются реальный ключ и сигнатура, но это может быть мусор: переполнение происходит до проверки сигнатуры. Несколько килобайтов символа А работают без проблем.
- Требуется ли для доступа к уязвимому коду сложное состояние, с которым у техник тестирования и статических анализаторов были бы трудности при синтезе, например хеши или контрольные суммы? Нет, должен быть правильный DER, вот и всё.
- Уязвимости трудно достичь в смысле кода? Нет, в Firefox этот путь кода не используется для сигнатур RSA-PSS, но точка входа по умолчанию для проверки сертификата в NSS — CERT_VerifyCertificate() — уязвима.
- Она характерна исключительно для алгоритма RSA-PSS? Нет, она влияет и на сигнатуры DSA.
- Этой уязвимостью нельзя воспользоваться или же она имеет ограниченное воздействие? Нет, может быть затёрт элемент hashobj. Этот объект содержит указатели функции, которые сразу же используются.
NSS был одним из первых проектов, включённых в oss-fuzz: по крайней мере поддерживался официально с октября 2014 года. В Mozilla сами также проводят автоматизацию тестирования безопасности NSS с помощью libFuzzer и представили собственную коллекцию методов-модификаторов, а также основу корпуса покрытия. Есть обширный тестовый комплект и ночные сборки ASAN.
Я в общем скептически отношусь к статическому анализу, но это похоже на простую недостающую проверку границ, которую должно быть легко найти. Coverity отслеживает NSS по крайней мере с декабря 2008 года и тоже, кажется, не смогла обнаружить уязвимость.
До 2015 года в Google Chrome использовали NSS и поддерживали собственную инфраструктуру тестового комплекта и автоматизации тестирования безопасности, независимую от Mozilla. Сегодня в платформах Chrome используется BoringSSL, но порт NSS ещё поддерживается.
- Было ли в Mozilla хорошее тестовое покрытие уязвимых областей? Да.
- Были ли в корпусе автоматизации тестирования безопасности Mozilla / Chrome / oss-fuzz соответствующие входные данные? Да.
- Есть ли метод-модификатор, способный расширить эти ASN1_ITEM? Да.
- Является ли это внутриобъектным переполнением или другой формой повреждения, которую ASAN было бы трудно обнаружить? Нет, это классическое переполнение буфера, которое ASAN может обнаружить легко.
Как я нашёл баг?
Экспериментировал с альтернативными методами измерения покрытия кода, чтобы узнать, можно ли их как-то использовать на практике в автоматизации тестирования безопасности. В технике тестирования, с помощью которой удалось обнаружить эту уязвимость, применялось сочетание двух подходов: покрытие стека и выделение объектов.Покрытие стека
Самый распространённый метод измерения покрытия кода — покрытие блоков или покрытие границ, когда доступен исходный код. Интересно, всегда ли этого достаточно? Например, возьмём простую таблицу диспетчеризации с сочетанием надёжных и ненадёжных параметров, как показано в листинге:#include <stdio.h>
#include <string.h>
#include <limits.h>
static char buf[128];
void cmd_handler_foo(int a, size_t b) { memset(buf, a, b); }
void cmd_handler_bar(int a, size_t b) { cmd_handler_foo('A', sizeof buf); }
void cmd_handler_baz(int a, size_t b) { cmd_handler_bar(a, sizeof buf); }
typedef void (* dispatch_t)(int, size_t);
dispatch_t handlers[UCHAR_MAX] = {
cmd_handler_foo,
cmd_handler_bar,
cmd_handler_baz,
};
int main(int argc, char **argv)
{
int cmd;
while ((cmd = getchar()) != EOF) {
if (handlers[cmd]) {
handlers[cmd](getchar(), getchar());
}
}
}
Покрытие команды bar — это надмножество команды foo, поэтому ввод, содержащий foo, будет отброшен при минимизации корпуса. Есть уязвимость, недоступная через команду bar, которая может быть не обнаружена никогда. Покрытие стека корректно сохранит оба ввода1.
Чтобы решить эту проблему, я отслеживал стек вызовов во время выполнения.
Наивная реализация слишком медленная, но после многих оптимизаций я создал практичную библиотеку, достаточно быструю, чтобы интегрировать её в автоматизацию тестирования безопасности, ориентированную на покрытие. Я тестировал, как она работает с NSS и другими библиотеками.
Выделение объектов
Многие типы данных создаются из записей меньшего размера. Файлы PNG состоят из фрагментов, файлы PDF — из потоков, файлы ELF — из разделов, а сертификаты X.509 — из элементов ASN.1 TLV. Если в технике тестирования заложено представление о базовом формате, то с её помощью можно выделить эти записи и извлечь те, что приводят к обнаружению новой трассировки стека.Техника тестирования, которую я использовал, способна выделять и извлекать интересные новые идентификаторы объекта ASN.1, последовательности SEQUENCE, целые числа INTEGER и т. д. После извлечения они могут случайным образом комбинироваться или вставляться в данные шаблона. На самом деле идея не новая, новая — реализация. Планирую в будущем сделать этот код общедоступным.
Работают ли эти подходы?
Возможно, обнаружение этого бага подтверждает мои идеи, но я не уверен, что это так. Я проводил относительно новую автоматизацию тестирования безопасности, но не вижу причин, почему этот баг не мог быть обнаружен раньше даже с помощью простейших методов тестирования.Что в итоге?
Как обширная, настраиваемая автоматизация тестирования безопасности с впечатляющими показателями покрытия не выявила этот баг?Что пошло не так?
1. Нет сквозного тестирования
NSS — модульная библиотека. Многоуровневый дизайн отражён в подходе автоматизации тестирования безопасности, ведь каждый компонент тестируется независимо. Например, декодер QuickDER проходит расширенное тестирование, но при использовании техники тестирования объекты просто создаются, отбрасываются и никогда не используются:extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
char *dest[2048];
for (auto tpl : templates) {
PORTCheapArenaPool pool;
SECItem buf = {siBuffer, const_cast<unsigned char *>(Data),
static_cast<unsigned int>(Size)};
PORT_InitCheapArena(&pool, DER_DEFAULT_CHUNKSIZE);
(void)SEC_QuickDERDecodeItem(&pool.arena, dest, tpl, &buf);
PORT_DestroyCheapArena(&pool);
}
При использовании техники тестирования QuickDER объекты просто создаются и отбрасываются. Так проверяется синтаксический анализ ASN.1, но не корректность работы с получаемыми объектами других компонентов.
С этой техникой тестирования можно было создать SECKEYPublicKey, который бы достал в уязвимый код. Но баг так и не был бы обнаружен, ведь результат никогда не использовался для проверки сигнатуры.
2. Произвольные ограничения по размеру
Для ввода автоматизированного тестирования задаётся произвольное ограничение в 10 000 байтов. В NSS такого ограничения нет: у многих структур этот размер возможно превысить. В случае с этой уязвимостью ошибки происходят на границах, поэтому ограничение следует выбирать с умом.Приемлемым вариантом может быть 224–1 байтов, т. е. максимально возможный сертификат, предоставляемый сервером во время согласования рукопожатия TLS.
Хотя в NSS обрабатываются объекты даже большего размера, TLS задействовать нельзя, а это снижает общую степень серьёзности любых пропущенных уязвимостей.
3. Метрики и заблуждения
Все техники тестирования NSS представлены в объединённых показателях покрытия oss-fuzz, а не в их индивидуальных покрытиях. Эти данные оказались неверными, так как уязвимый код проходит расширенное автоматизированное тестирование, но с помощью техник тестирования, которые не могли генерировать соответствующие входные данные.Почему? Потому что в техниках тестирования типа tls_server_target используются фиксированные, жёстко заданные сертификаты. При этом выполняется код, относящийся к проверке сертификата, но проводится автоматизированное тестирование лишь сообщений TLS и изменений состояния протокола.
Что всё-таки сработало?
- Благодаря дизайну библиотеки проверки корректности сертификатов mozilla:kix не было допущено ухудшение ситуации с этим багом. К сожалению, она не используется вне Firefox и Thunderbird.
Рекомендации
Эта проблема свидетельствует о том, что даже в очень хорошо поддерживаемом C/C++ могут быть фатальные, простейшие ошибки.Краткосрочные рекомендации
- Увеличить максимальный размер объектов ASN.1, создаваемых с помощью libFuzzer, с 10 000 до 224–1 = 16 777 215 байтов.
- При использовании техники тестирования QuickDER должны вызываться соответствующие API с любыми успешно созданными объектами, прежде чем они будут уничтожены.
- Показатели покрытия кода oss-fuzz нужно разделить по технике тестирования, а не по проекту.
Решение
Эта уязвимость, CVE-2021-43527, закрыта в NSS 3.73.0. Если вы вендор, распространяющий NSS в своих продуктах, то вам, скорее всего, потребуется заменить библиотеку или применить патч.Благодарности
Я бы не смог найти этот баг без помощи коллег из Chrome, Райана Слеви и Дэвида Бенджамина, которые помогли ответить на мои вопросы о кодировании ASN.1 и приняли участие в содержательном обсуждении этой темы.Спасибо команде NSS, которая помогла проанализировать и разобраться с уязвимостью.
Как Mozilla упустила (не)очевидную уязвимость
Этот пост в блоге необычный. Обычно я пишу посты о скрытых видах атак или интересном и сложном классе уязвимостей. На этот раз речь пойдёт о совершенно иной уязвимости. Впечатляет её простота. Её...
habr.com