• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Атака по SMS. Как мы нашли уязвимость в популярном GSM-модеме и раскрутили ее до RCE

pablo

(L2) cache
Пользователь
Регистрация
01.02.2019
Сообщения
433
Реакции
1 524
В прошлом году моя я и моя команда исследовали модем Cinterion EHS5. Мы обнаружили в его прошивке уязвимость переполнения кучи при обработке сообщений протокола Secure UserPlane Location (SUPL), передаваемых в виде SMS. Этот баг позволяет выполнить произвольный код на уровне операционной системы модема с максимальными привилегиями — достаточно отправить всего пять SMS через сеть оператора.

Найденная уязвимость впоследствии получила идентификатор CVE-2023-47610 и высокую оценку критичности.

Модемы Cinterion стоят в огромном числе самых разных устройств. Даже примерный список девайсов составить сложно, поскольку модель модема не всегда указана в документации. Мы изучали сайты производителей и таможенные декларации импортеров разных изделий и установили, что такие модемы можно встретить в умных устройствах, банкоматах, индустриальных шлюзах, автомобилях и даже медицинском оборудовании.

Результатами исследования мы уже делились на конференциях OffensiveCon и Hardware.io.


Исследуем модем​

image1.png

Итак, наш модем — это модуль в форм‑факторе 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.
image2.png

Для коммуникации используется бинарный протокол ULP. Его данные в сети GSM передаются при помощи push-сообщений через стек протоколов WAP (Wireless Application Protocol).

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


Пример последующих SMS


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

После этого мы углубились в изучение алгоритма работы.

Переполнение кучи​

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

На скриншоте выше переменная 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.
image7.png

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

Дальнейший анализ кода прошивки показал, что падение происходит внутри функции malloc, когда программа пытается разыменовать указатель, содержащий контролируемый нами адрес в регистре R0.
image8.png


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

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

Эта находка позволила нам очень медленно, со скоростью 4 байта в минуту, считывать память модема путем отправки SMS-сообщений, вызывающих переполнение кучи. Для автоматизации процесса мы собрали стенд, и в итоге считывание интересующих областей памяти таким изощренным способом заняло несколько недель.
image10.png


Поиск примитива записи​

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

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

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

Указатель на Thread берется из структуры HeapBase, расположенной по статическому адресу. Указатель на HeapBase содержится в каждом занятом блоке по смещению 0x4 и используется для того, чтобы при работе с занятым блоком можно было адресовать HeapBase. В итоге если переписать указатель по смещению 0x4 занятого блока указателем на наши данные, то можно полностью подменить структуру HeapBase, с которой будет работать функция free, а следовательно, и структуру Thread.

Остается выяснить, какие поля структур необходимо заполнить, чтобы выполнение free дошло до интересующей нас точки в программе. Далее на рисунке — нормальное состояние системных структур и то, к которому их нужно привести для записи в память по произвольному адресу указателя на свободный блок.
image12.png

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

Это очень важное обстоятельство, так как наши данные, присланные в SUPL SMS, располагаются в куче и, если выбрать подходящий размер выделяемой в функции free памяти, можно писать по произвольному адресу не просто адреса какого‑то подходящего свободного блока, а адреса блока, в котором лежат наши данные.

По смещению 0x80 находится адрес, по которому запишется указатель на блок в случае его успешного выделения. Сюда мы можем подставить произвольный адрес, и он будет использован для записи. Функция free также использует некоторые другие смещения внутри Thread, которые необходимо заполнить для корректной работы.

Итоговый вид «поддельной» структуры 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-сообщения.

image14.png

Буфер для нашего первого WAP SMS выделяется всегда в одном и том же месте в памяти, причем его размер очень маленький, поэтому произойдет обнуление только первых 4 байт нашего ULP-сообщения (то есть при повторном выделении одного и того же указателя в памяти будут затерты только 4 байта из ранее записанных данных). Мы управляем размером копируемых в этот буфер данных, поэтому можно прислать сначала несколько фрагментированных WAP-сообщений, чтобы создать в памяти нужные структуры данных, а также разместить код для дальнейшего исполнения.

В WAP-сообщении мы указываем размер ULP-сообщения равным одному байту. Поэтому размер выделяемого буфера, в силу выравнивания в malloc размера выделяемой памяти по границе DWORD, будет равен 4 байтам — независимо от размера блока, в который нас поместят. А благодаря фрагментации можно быть уверенным, что при переполнении мы обязательно перетрем следующий блок нашими данными.

Это очень важно, потому что в итоге, чтобы сработала вся выстроенная цепочка вызовов free и malloc, она должна завершиться вызовом free для блока, данные заголовка в котором переписаны указателем на нашу структуру HeapBase. Обеспечить это можно, если мы сможем управлять данными в заголовке свободных блоков.

Используя описанную стратегию записи в кучу, можно добавить не только структуры и код, но и свободный блок в цепочку всех блоков кучи. В первом WAP-сообщении происходит перезапись указателя следующего блока. Мы можем переписать этот указатель правильным адресом следующего за нашим блока. Созданный таким образом искусственный блок будет находиться внутри нашего большого блока. А за счет его расположения он окажется первым при поиске свободных.

В итоге мы создадим в памяти модема нужные структуры данных и свободные блоки, а также разместим исполняемый код. Теперь можно снова начать присылать WAP-сообщения с индексом, соответствующим первому SMS-сообщению, и таким образом получить возможность переписывать данные в ранее созданном свободном блоке. Так мы обеспечим подмену указателя на HeapBase в занятом блоке нашим.

image15.png

После создания свободного блока внутри большого буфера остается заставить кого‑то его занять до того, как мы пришлем новое 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 от начала может храниться указатель на функцию, которая будет вызвана, если он не равен нулю.
image16.png

Таким образом, для исполнения своего кода достаточно записать по смещению 0х94 некоторого процесса указатель на наш ARM-код в памяти кучи с помощью полученного ранее примитива записи.

В итоге мы сможем выполнять произвольный код с максимальными привилегиями, достаточно лишь отправить несколько SMS.

Разблокируем память​

Итак, мы можем исполнить свой код на уровне операционной системы модема. Что дальше? А дальше нужно понять, можно ли закрепиться в ОС, ведь от этого зависит, насколько эта уязвимость действительно критическая.

Фрагмент кода, в котором происходит перехват управления, выполняется в критической секции. В этой части отключены все прерывания. Поэтому такой код нельзя использовать для закрепления в ОС — он должен выполняться как можно быстрее, чтобы не сработал сторожевой таймер. При этом ни одна операция работы с внешними компонентами в этом режиме не доступна (прерывания отключены).

Поэтому для модификации процесса ОС мы решили использовать код, выполняемый в контексте менеджера памяти. Вся секция была отображена в режиме RX (Read/Execute), поэтому перед дальнейшими действиями нужно было как‑то обеспечить возможность записи в секцию кода.

Мы узнали, что в MMU модема хранится полная таблица трансляции по логическому адресу 0x00088000. Секция кода и секция данных оказались отображены в режиме page, где размер одной страницы равен 0х100000 байт (1 Мбайт). Для этих секций использовалась трансляция 1:1. Это хорошо видно в области данных настройки MMU.
image18.png

Настройка режима доступа к секциям — нестандартная для архитектуры ARM. Поэтому мы решили просто скопировать настройку доступа из секции данных, которая, как мы уже убедились, отображена в режиме RWX (Read/Write/Execute). После этого мы получили возможность записи в любую секцию кода.

Бонусом к этому мы теперь можем получить карту физической памяти модема. По смещению 0x00089900 начинались свободные логические адреса, на которые гарантированно никто не ссылается в коде, потому что они не были отображены. Но мы же можем сделать их отображенными вручную! Такой подход позволил дополнить уже имеющиеся сведения о карте памяти модема информацией о точном размере доступной оперативной памяти.

Запускаем свое приложение, без регистрации, но через SMS​

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


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


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


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


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

Таким образом, мы собрали full chain PoC эксплуатации переполнения кучи, начиная от посылки первого SMS-сообщения и заканчивая закреплением (установка нашего приложения) на модеме.

Возможные риски​

Модем — это важный компонент разных устройств, которые должны оставаться на связи. Среди них как бытовые приборы и привычные гаджеты, так и телекоммуникационные блоки автомобилей, банкоматы, компоненты АСУ ТП.

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

Например, при компрометации модема, используемого в электронном блоке автомобиля, злоумышленник может получить удаленный доступ к тормозной системе, рулевому управлению или КПП. А управляя модемом, используемым в АСУ ТП, — вызвать техногенную катастрофу.

Проблему усугубляет то, что при обнаружении серьезной уязвимости в модеме может понадобиться значительное время на обновление всех устройств, в которых он установлен. При этом в каких‑то устройствах удаленное обновление может быть вообще не заложено как функция.

Подобную проблему мы наблюдали, например, в одной из систем управления телематическими данными автомобиля. В таких случаях установка обновления требует дополнительных усилий и затрат со стороны производителя конечного устройства для того, чтобы обновить вручную каждый из уязвимых модемов.

Автор @n0um3n0n
Источник xakep.ru
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх