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

Най­ден­ная уяз­вимость впос­ледс­твии получи­ла иден­тифика­тор 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-сооб­щения ука­зыва­ется еще и раз­мер все­го сооб­щения.

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

С помощью ста­тичес­кого ана­лиза мы изу­чили час­ти кода ОС модема, а имен­но учас­тки, отве­чающие за реали­зацию это­го про­токо­ла. Преж­де все­го мы выяс­нили, что в модеме исполь­зует­ся опе­раци­онная сис­тема 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[1] вмес­то магичес­кого зна­чения будет ука­затель на струк­туру, опи­сыва­ющую гло­баль­ное сос­тояние менед­жера кучи (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, которые необ­ходимо запол­нить для кор­рек­тной работы.

Итоговый вид «поддельной» структуры Thread
Ито­говый вид «под­дель­ной» струк­туры Thread

Ана­логич­ным обра­зом мы про­вери­ли, какие поля струк­туры HeapBase важ­но запол­нить, что­бы все работа­ло кор­рек­тно.

Реализация записи в память на практике

Итак, мы про­ана­лизи­рова­ли в ста­тике код менед­жера памяти ОС, и у нас появи­лось понима­ние того, как нуж­но сфор­мировать струк­туры в памяти модема, что­бы получить при­митив записи. Одна­ко реали­зовать это все на прак­тике было неп­росто. В час­тнос­ти, в про­цес­се обра­бот­ки вхо­дящих WAP SMS про­исхо­дит нес­коль­ко вызовов фун­кций free и malloc. Соот­ветс­твен­но, для пол­ноцен­ной экс­плу­ата­ции уяз­вимос­ти необ­ходимо было обес­печить:

На выпол­нение этих тре­бова­ний нап­рямую вли­яет стра­тегия работы менед­жера памяти со сво­бод­ными и заняты­ми бло­ками. Нам уже было извес­тно, что:

Да­лее мы уста­нови­ли, что про­исхо­дит при выделе­нии нового бло­ка фун­кци­ей 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-сооб­щения ука­зыва­ло ров­но на начало заголов­ка толь­ко что выделен­ного бло­ка.

Собираем все вместе

В ито­ге реали­зация при­мити­ва записи сос­тоит из сле­дующих шагов.

Исполнение кода

Целью всей слож­ной работы была запись единс­твен­ного 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 нет спе­циаль­ного кон­тек­ста выпол­нения. Фун­кция malloc при­нима­ет на вход толь­ко один параметр — раз­мер буфера, который необ­ходимо выделить. Фун­кция free — толь­ко ука­затель на буфер, который нуж­но осво­бодить.

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

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

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

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

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

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

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

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

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

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