
Эксплойт 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().