Атака по SMS
Как мы нашли уязвимость в популярном GSM-модеме и раскрутили ее до RCE
Найденная уязвимость впоследствии получила идентификатор CVE-2023-47610 и высокую оценку критичности.
Pentest Award
Этот текст получил первое место на премии Pentest Award 2024 в категории «Девайс». Это соревнование ежегодно проводится компанией Awillix.
Модемы Cinterion стоят в огромном числе самых разных устройств. Даже примерный список девайсов составить сложно, поскольку модель модема не всегда указана в документации. Мы изучали сайты производителей и таможенные декларации импортеров разных изделий и установили, что такие модемы можно встретить в умных устройствах, банкоматах, индустриальных шлюзах, автомобилях и даже медицинском оборудовании.
Результатами исследования мы уже делились на конференциях OffensiveCon и Hardware.io.
warning
Статья имеет ознакомительный характер и предназначена для специалистов по безопасности, проводящих тестирование в рамках контракта. Автор и редакция не несут ответственности за любой вред, причиненный с применением изложенной информации. Распространение вредоносных программ, нарушение работы систем и нарушение тайны переписки преследуются по закону.
Исследуем модем

Итак, наш модем — это модуль в форм‑факторе LGA (Land Grid Array), предназначенный для встраивания в печатные платы. Сам модуль состоит из baseband-процессора Intel X-Gold 625 (XMM 6260), микросхемы NAND-флеш‑памяти (совмещенной с 512 Мбит памяти LPDDR), микросхемы RF-усилителя и трансивера. Модем поддерживает стандарты GSM, GPRS, EDGE, UMTS и HSPA+.
Чтобы получить прошивку, мы сняли защитный экран и выпаяли микросхему флеш‑памяти, после чего считали данные с нее на программаторе и восстановили логические блоки из сырого образа.
Общаемся с модемом удаленно
Cinterion EHS5 поддерживает геопозиционирование с помощью подсистемы SUPL. Она отвечает за обмен специальными сообщениями между H-SLP (Home SUPL Location Platform) и SET (SUPL Enabled Terminal). Сам модем при этом — это объект типа SET.

Для коммуникации используется бинарный протокол ULP. Его данные в сети GSM передаются при помощи push-сообщений через стек протоколов WAP (Wireless Application Protocol).
На уровне push-сообщений WSP (Wireless Session Protocol) в протокол ULP заложена возможность фрагментации передаваемого сообщения. Это сделано для того, чтобы можно было передавать большие бинарные сообщения через ограниченный канал передачи SMS. Чтобы фрагментированные послания можно было собрать на стороне SET, они индексируются. При этом в самом начале SUPL-сообщения указывается еще и размер всего сообщения.


С помощью статического анализа мы изучили части кода ОС модема, а именно участки, отвечающие за реализацию этого протокола. Прежде всего мы выяснили, что в модеме используется операционная система ThreadX, затем нашли функции создания процессов и в одном из них обнаружили код обработки тех самых «магических» значений из SMS-сообщений со скриншотов выше.

После этого мы углубились в изучение алгоритма работы.
Переполнение кучи
Изучая драйвер, отвечающий за обработку фрагментации ULP-сообщений, мы нашли уязвимость переполнения кучи.

На скриншоте выше переменная ULPSizeFromPacket
отвечает за размер всего пакета ULP, а за размер принятого WAP-сообщения — переменная wapTpduLen
. Обработка WAP-сообщений заключается в последовательном копировании фрагментов ULP-сообщения в буфер, размер которого равен ULPSizeFromPacket
.
В соответствии с протоколом передачи переменные ULPSizeFromPacket
и wapTpduLen
вычисляются независимо. Эти переменные имеют отношение к разным уровням представления данных и косвенно связаны только в части алгоритма приема WAP-сообщений: сумма размеров всех принятых WAP-сообщений одного UPL-сообщения не должна превышать ULPSizeFromPacket
. Но в алгоритме приема WAP-сообщений отсутствует такая проверка. Соответственно, принятое WAP-сообщение размера wapTpduLen
будет безусловно копироваться в буфер размера ULPSizeFromPacket
. Это классическая уязвимость типа Heap-based Buffer Overflow.
Мы сформировали соответствующее SMS-сообщение и с первой же попытки вызвали переполнение буфера на стороне модема, что привело к его полной перезагрузке.
Что делать с black box окружением?
Мы нашли переполнение буфера, и это хорошо. Но нам неизвестен его контекст, и это плохо. У нас нет никакой возможности хотя бы прочитать память модема (RAM), чтобы разобраться в происходящем... или есть?
При переполнении буфера мы получали стабильный Hardware Fault и перезагрузку модема, а в качестве обратной связи смогли извлечь только адрес места падения и состояние регистров процессора с помощью AT-команды AT+XLOG
.

Чтобы продвинуться в эксплуатации, нужно было проанализировать место падения, получить контекст исполнения (состояние оперативной памяти на момент падения) и разобраться в устройстве менеджера кучи операционной системы ThreadX. Мы не могли читать память или отлаживать прошивку, однако мы обнаружили, что при падении в регистр R0 попадают контролируемые нами данные.
Дальнейший анализ кода прошивки показал, что падение происходит внутри функции malloc
, когда программа пытается разыменовать указатель, содержащий контролируемый нами адрес в регистре R0.

Значение 0xFFFFEEEE
— «магическое» и означает начало свободного блока памяти в куче. В случае если блок занят, в Curr_Chunk[
вместо магического значения будет указатель на структуру, описывающую глобальное состояние менеджера кучи (HeapBase
).

Если вызвать переполнение таким образом, чтобы в регистре R0 оказался корректный адрес памяти модема, то падения в этом месте не произойдет и в R0 сохранится значение из памяти по этому адресу. Однако если значение не окажется корректным адресом, то программа упадет и значение, записанное ранее в R0, можно будет прочитать после перезагрузки командой AT+XLOG
. Модем загружается всегда по одному сценарию, поэтому между перезагрузками его оперативная память будет иметь приблизительно одинаковый вид.
Эта находка позволила нам очень медленно, со скоростью 4 байта в минуту, считывать память модема путем отправки SMS-сообщений, вызывающих переполнение кучи. Для автоматизации процесса мы собрали стенд, и в итоге считывание интересующих областей памяти таким изощренным способом заняло несколько недель.

Поиск примитива записи
Пока память считывалась, мы занялись детальным изучением устройства кучи в ОС ThreadX с целью найти примитив записи. Мы полностью проанализировали код функций malloc
и free
и определили, что единственное подходящее место, в котором происходит запись в память через двойное разыменование, находится в коде free
, и связано это с особенностями ее реализации.
В отличие от традиционной функции free
, наша не только освобождает память, возвращая выделенный блок обратно в пул свободной памяти, но и после этого проверяет, есть ли необработанный запрос на выделение памяти от текущего процесса. И, если только что освобожденный блок памяти подходит по размеру, происходит его выделение прямо внутри функции free
, после чего обновляется состояние в соответствующей структуре Thread
.

Рассмотрим код со скриншота выше. Здесь указатель на найденный свободный блок для текущего процесса будет записан по адресу, указанному в этой структуре по смещению 0х80. Поэтому, если вместо исходной структуры Thread
текущего процесса в функцию free
будет передан указатель на управляемую нами область памяти, имитирующую структуру Thread
, можно будет обеспечить запись по произвольному адресу в памяти модема. При этом запишутся не произвольные данные, а указатель на некоторый свободный блок памяти. Этого вполне достаточно для того, чтобы перехватить поток управления исполняемой программы.
Указатель на Thread
берется из структуры HeapBase
, расположенной по статическому адресу. Указатель на HeapBase
содержится в каждом занятом блоке по смещению 0x4 и используется для того, чтобы при работе с занятым блоком можно было адресовать HeapBase
. В итоге если переписать указатель по смещению 0x4 занятого блока указателем на наши данные, то можно полностью подменить структуру HeapBase
, с которой будет работать функция free
, а следовательно, и структуру Thread
.
Остается выяснить, какие поля структур необходимо заполнить, чтобы выполнение free
дошло до интересующей нас точки в программе. Далее на рисунке — нормальное состояние системных структур и то, к которому их нужно привести для записи в память по произвольному адресу указателя на свободный блок.

В процессе работы free
использует лишь несколько полей структуры Thread
. По смещению 0x7C находится размер памяти (блока), которую пытался выделить процесс. Именно его будет пытаться найти функция free
в текущей куче. С помощью этого поля можно управлять тем, какой блок будет выделен алгоритмом как подходящий.
Это очень важное обстоятельство, так как наши данные, присланные в SUPL SMS, располагаются в куче и, если выбрать подходящий размер выделяемой в функции free
памяти, можно писать по произвольному адресу не просто адреса какого‑то подходящего свободного блока, а адреса блока, в котором лежат наши данные.
По смещению 0x80 находится адрес, по которому запишется указатель на блок в случае его успешного выделения. Сюда мы можем подставить произвольный адрес, и он будет использован для записи. Функция free
также использует некоторые другие смещения внутри Thread
, которые необходимо заполнить для корректной работы.

Аналогичным образом мы проверили, какие поля структуры HeapBase
важно заполнить, чтобы все работало корректно.
Реализация записи в память на практике
Итак, мы проанализировали в статике код менеджера памяти ОС, и у нас появилось понимание того, как нужно сформировать структуры в памяти модема, чтобы получить примитив записи. Однако реализовать это все на практике было непросто. В частности, в процессе обработки входящих WAP SMS происходит несколько вызовов функций free
и malloc
. Соответственно, для полноценной эксплуатации уязвимости необходимо было обеспечить:
- отсутствие ошибок при вызовах функций
free
иmalloc
; - достаточно долгое хранение сформированных структур
Thread
иHeapBase
в памяти.
На выполнение этих требований напрямую влияет стратегия работы менеджера памяти со свободными и занятыми блоками. Нам уже было известно, что:
- поиск свободного блока начинается с указателя на первый свободный блок в структуре
HeapBase
(смещение 0х05); - размер текущего блока вычисляется как разность между адресом следующего блока (блок, на который указывает текущий) и адресом текущего блока;
- свободным считается только блок, у которого есть волшебное значение 0xFFFFEEEE.
Далее мы установили, что происходит при выделении нового блока функцией malloc
. Его область пользовательских данных затирается нулями. Это очень важный момент, и его нужно учитывать, чтобы обеспечить достаточно долгое хранение наших структур.
При этом выделяется всегда первый блок подходящего размера. Но если размер найденного свободного блока слишком большой (превышает размер пользовательских данных на 20 байт), то происходит фрагментация: создается еще один блок сразу после конца пользовательских данных. Если в процессе выделения найдется свободный блок, но меньшего, чем нужно, размера и следующий за ним блок тоже свободен, произойдет дефрагментация — два свободных блока объединятся в один.
При освобождении занятого блока функцией free
, если его адрес меньше, чем текущий адрес первого свободного блока в буфере HeapBase
(смещение 0х05), его адрес становится новым адресом первого свободного блока. Так что если размер SUPL-сообщения всегда задавать равным одному байту, то выделение памяти в куче будет производиться всегда в самом первом свободном блоке, поскольку он всегда будет удовлетворять критериям. А если при этом будет успевать отрабатывать free
, то отправка сообщений SUPL будет приводить к записи в промежуточный буфер, находящийся по одному и тому же адресу. Остается установить, как можно выстроить вызовы free
и malloc
в нужной последовательности.
При получении первого WAP SMS выделяется буфер в куче размером ULPSizeFromPacket
, куда будут копироваться все фрагменты ULP-сообщения. Однако в коде отсутствует проверка на то, что порядковый номер текущего WAP-сообщения (фрагмент ULP-сообщения) уже присылался ранее. Это тоже приводит к переполнению в куче, но ошибка возникнет в другой части алгоритма.
После первого сообщения всегда приходит не последнее. Используя этот факт, можно переполнять выделенный буфер для ULP-сообщения сколько угодно большим объемом данных. При этом мы управляем указателем внутри переполненного буфера на то, по какому смещению будет записано следующее SMS-сообщение. Для этого нам нужно посылать SMS нужного размера, и указатель будет смещаться на ту же величину при копировании. Каждый следующий фрагмент ULP-сообщения будет размещаться в памяти сразу за предыдущим. В итоге, чтобы сформировать в куче нужные нам структуры данных, потребуется всего три типа WAP-сообщений.
Если после отправки первого фрагментированного WAP-сообщения послать снова первое (индекс первого сообщения проверяется при обработке), то программа проверит, что ранее для этого ULP-сообщения уже был выделен буфер, и произойдет вызов free
для освобождения — так как было получено новое ULP-сообщение и нет смысла хранить старое.
За счет того, что выделенный ранее буфер был первым свободным, он снова станет первым свободным (у него младший адрес из всех свободных буферов). Далее снова вызывается malloc
для только что пришедшего первого фрагмента ULP-сообщения. Но программа снова выделит все тот же буфер! Таким образом, все первые WAP-сообщения будут приводить к выделению одного и того же буфера для ULP-сообщения.

Буфер для нашего первого WAP SMS выделяется всегда в одном и том же месте в памяти, причем его размер очень маленький, поэтому произойдет обнуление только первых 4 байт нашего ULP-сообщения (то есть при повторном выделении одного и того же указателя в памяти будут затерты только 4 байта из ранее записанных данных). Мы управляем размером копируемых в этот буфер данных, поэтому можно прислать сначала несколько фрагментированных WAP-сообщений, чтобы создать в памяти нужные структуры данных, а также разместить код для дальнейшего исполнения.
В WAP-сообщении мы указываем размер ULP-сообщения равным одному байту. Поэтому размер выделяемого буфера, в силу выравнивания в malloc
размера выделяемой памяти по границе DWORD, будет равен 4 байтам — независимо от размера блока, в который нас поместят. А благодаря фрагментации можно быть уверенным, что при переполнении мы обязательно перетрем следующий блок нашими данными.
Это очень важно, потому что в итоге, чтобы сработала вся выстроенная цепочка вызовов free
и malloc
, она должна завершиться вызовом free
для блока, данные заголовка в котором переписаны указателем на нашу структуру HeapBase
. Обеспечить это можно, если мы сможем управлять данными в заголовке свободных блоков.
Используя описанную стратегию записи в кучу, можно добавить не только структуры и код, но и свободный блок в цепочку всех блоков кучи. В первом WAP-сообщении происходит перезапись указателя следующего блока. Мы можем переписать этот указатель правильным адресом следующего за нашим блока. Созданный таким образом искусственный блок будет находиться внутри нашего большого блока. А за счет его расположения он окажется первым при поиске свободных.
В итоге мы создадим в памяти модема нужные структуры данных и свободные блоки, а также разместим исполняемый код. Теперь можно снова начать присылать WAP-сообщения с индексом, соответствующим первому SMS-сообщению, и таким образом получить возможность переписывать данные в ранее созданном свободном блоке. Так мы обеспечим подмену указателя на HeapBase
в занятом блоке нашим.

После создания свободного блока внутри большого буфера остается заставить кого‑то его занять до того, как мы пришлем новое SMS-сообщение, которое будет этот блок перетирать. В этом нам поможет алгоритм обработки принятого WAP-сообщения.
Особенности обработки каждого WAP-сообщения
В начале обработки любого WAP-сообщения только что принятое сообщение копируется во временный буфер, выделяемый в куче. Именно из этого буфера потом выполняется копирование в буфер для всего ULP-сообщения (он создается при обработке первого WAP SMS). При этом, если WAP-сообщение не было последним, после его копирования происходит только освобождение выделенного временного буфера. Буфер, содержащий часть ULP-сообщения, остается занятым.
Это тот самый вызов free
, который мы хотим использовать для записи в память. Чтобы добиться этого, нам достаточно при получении очередного WAP-сообщения скопировать его именно в созданный нами свободный блок внутри нашего буфера. Для этого нужно прислать WAP-сообщение, размер данных которого равен размеру свободного блока, созданного нами внутри нашего буфера.
Поскольку malloc
начнет поиск подходящего блока для обработки пришедшего WAP-сообщения с этого свободного блока, мы гарантированно заставим malloc
вернуть указатель именно на наш блок. Остается только сделать так, чтобы текущее смещение внутри буфера ULP-сообщения указывало ровно на начало заголовка только что выделенного блока.
Собираем все вместе
В итоге реализация примитива записи состоит из следующих шагов.
- Присылаем первое WAP SMS. Путем переполнения буфера ULP-сообщения переписываем указатели и данные следующего блока, создаем свободные блоки размера 0x10 байт внутри нашего буфера SUPL-сообщения. Важно после созданного нами свободного блока положить занятый, иначе используемый нами блок могут дефрагментировать.
- Присылаем следующее фрагментированное WAP-сообщение. В нем размещаем структуры
HeapBase
иThread
. - Снова присылаем первое WAP-сообщение. На этот раз такого размера, чтобы следующее фрагментированное SMS-сообщение перетерло начало нашего свободного блока.
- Присылаем еще одно (не последнее по номеру!) фрагментированное SMS-сообщение. Оно должно быть размера 8 байт. Эти 8 байт перезапишут указатель на структуру
HeapBase
в нашем блоке. За счет этого при выходе из функции обработки WAP-сообщения будет вызвана функцияfree
для блока, только что выделенного внутри нашего буфера. Это позволит создать временный буфер для нашего же WAP-сообщения. Эта функция будет использовать подготовленные структурыHeapBase
иThread
вместо системных.
Исполнение кода
Целью всей сложной работы была запись единственного DWORD — указателя на наши данные — по произвольному адресу в памяти модема. Разработку эксплоита значительно усложняло то, что у нас не было возможности отладки и мы строили все примитивы, работая с модемом как с черным ящиком.
Мы можем записать всего один DWORD, поэтому нужно было найти такое место в коде ОС модема, которое бы передавало управление сразу по адресу, взятому из оперативной памяти. При этом желательно, чтобы код, который будет делать переход, выполнялся безусловно и гарантированно. Такое место нашлось в менеджере процессов ОС.
Менеджер процессов работает со структурой Thread
. В этой структуре по смещению 0х94 от начала может храниться указатель на функцию, которая будет вызвана, если он не равен нулю.

Таким образом, для исполнения своего кода достаточно записать по смещению 0х94 некоторого процесса указатель на наш ARM-код в памяти кучи с помощью полученного ранее примитива записи.
В итоге мы сможем выполнять произвольный код с максимальными привилегиями, достаточно лишь отправить несколько SMS.
Разблокируем память
Итак, мы можем исполнить свой код на уровне операционной системы модема. Что дальше? А дальше нужно понять, можно ли закрепиться в ОС, ведь от этого зависит, насколько эта уязвимость действительно критическая.
Фрагмент кода, в котором происходит перехват управления, выполняется в критической секции. В этой части отключены все прерывания. Поэтому такой код нельзя использовать для закрепления в ОС — он должен выполняться как можно быстрее, чтобы не сработал сторожевой таймер. При этом ни одна операция работы с внешними компонентами в этом режиме не доступна (прерывания отключены).
Поэтому для модификации процесса ОС мы решили использовать код, выполняемый в контексте менеджера памяти. Вся секция была отображена в режиме RX (Read/Execute), поэтому перед дальнейшими действиями нужно было как‑то обеспечить возможность записи в секцию кода.
Мы узнали, что в MMU модема хранится полная таблица трансляции по логическому адресу 0x00088000. Секция кода и секция данных оказались отображены в режиме page
, где размер одной страницы равен 0х100000 байт (1 Мбайт). Для этих секций использовалась трансляция 1:1. Это хорошо видно в области данных настройки MMU.

Настройка режима доступа к секциям — нестандартная для архитектуры ARM. Поэтому мы решили просто скопировать настройку доступа из секции данных, которая, как мы уже убедились, отображена в режиме RWX (Read/Write/Execute). После этого мы получили возможность записи в любую секцию кода.
Бонусом к этому мы теперь можем получить карту физической памяти модема. По смещению 0x00089900 начинались свободные логические адреса, на которые гарантированно никто не ссылается в коде, потому что они не были отображены. Но мы же можем сделать их отображенными вручную! Такой подход позволил дополнить уже имеющиеся сведения о карте памяти модема информацией о точном размере доступной оперативной памяти.
Запускаем свое приложение, без регистрации, но через SMS
После того как мы разблокировали секцию кода для записи, оставалось только выбрать подходящий для модификации процесс. Чтобы организовать полудуплексный канал связи с модемом, мы выбрали процесс UTACAT, отвечающий за обработку SMS-сообщений. Именно отвечающую за это функцию мы и изменили в его коде.

Определив место внесения изменений, мы составили список функций, которые работают с файловой системой модема. С их помощью можно обеспечить установку произвольного приложения. Кроме того, были нужны функции, которые дадут возможность работать с памятью модема, потому что большинство функций драйвера файловой системы работает с локальными буферами. В итоговый список вошли:
- функция выделения памяти (
malloc
); - функция освобождения памяти (
free
); - функция создания/открытия файла на файловой системе (
createFile
).
У malloc
и free
нет специального контекста выполнения. Функция malloc
принимает на вход только один параметр — размер буфера, который необходимо выделить. Функция free
— только указатель на буфер, который нужно освободить.

У нас осталась функция работы с файловой системой, она должна была помочь создавать новые файлы или перезаписывать существующий. Такая функция в процессе JVM используется для работы с файловой системой и принимает на вход всего два параметра:
- абсолютный путь к файлу на файловой системе;
- режим работы (0x6C для записи).

В итоге мы разработали небольшой драйвер на ARM-ассемблере, обеспечивающий работу со всеми описанными функциями через SMS-сообщения. Чтобы можно было отличить наши SMS, мы решили использовать специальное значение 0x6AA677BB в заголовке.

Когда мы применили описанную технику исполнения произвольного кода, нам удалось успешно запустить драйвер в контексте процесса UTACAT. После нам оставалось только создать и запустить с его помощью наше приложение на модеме.

Таким образом, мы собрали full chain PoC эксплуатации переполнения кучи, начиная от посылки первого SMS-сообщения и заканчивая закреплением (установка нашего приложения) на модеме.
Возможные риски
Модем — это важный компонент разных устройств, которые должны оставаться на связи. Среди них как бытовые приборы и привычные гаджеты, так и телекоммуникационные блоки автомобилей, банкоматы, компоненты АСУ ТП.
При разработке конечных устройств производители зачастую не уделяют должного внимания их защите от компрометации модема. А захватив модем, злоумышленник может не только контролировать информационный поток между устройством и внешним миром, но и получить практически неограниченный доступ к наиболее важным компонентам конечного устройства.
Например, при компрометации модема, используемого в электронном блоке автомобиля, злоумышленник может получить удаленный доступ к тормозной системе, рулевому управлению или КПП. А управляя модемом, используемым в АСУ ТП, — вызвать техногенную катастрофу.
Проблему усугубляет то, что при обнаружении серьезной уязвимости в модеме может понадобиться значительное время на обновление всех устройств, в которых он установлен. При этом в каких‑то устройствах удаленное обновление может быть вообще не заложено как функция.
Подобную проблему мы наблюдали, например, в одной из систем управления телематическими данными автомобиля. В таких случаях установка обновления требует дополнительных усилий и затрат со стороны производителя конечного устройства для того, чтобы обновить вручную каждый из уязвимых модемов.