Пов­семес­тное рас­простра­нение устрой­ств на базе плат­формы ARM при­водит к тому, что ата­ки на них ста­новят­ся для зло­умыш­ленни­ков все более «вкус­ными». Неуди­витель­но, что шелл‑коды под эту плат­форму дав­но ста­ли реаль­ностью. Нес­мотря на то что на обна­руже­нии шелл‑кодов под x86 иссле­дова­тели уже собаку съели, с ARM все не так прос­то. Боль­шинс­тво сущес­тву­ющих детек­торов ока­зались поп­росту неп­римени­мыми. При­чина это­му — раз­ница двух архи­тек­тур, поз­воля­ющая зло­умыш­ленни­кам сущес­твен­но изме­нить вид шелл‑кода. Об этих осо­бен­ностях шелл‑кодов под плат­форму ARM и пой­дет речь.

warning

Вни­мание! Информа­ция пред­став­лена исклю­читель­но с целью озна­ком­ления! Ни авто­ры, ни редак­ция за твои дей­ствия ответс­твен­ности не несут!

 

Кто такие шелл-коды и с чем их едят

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

Рис. 1. Как работает шелл-код
Рис. 1. Как работа­ет шелл‑код

Поп­робу­ем рас­смот­реть их «на паль­цах» — на при­мере наибо­лее прос­того типа, к которо­му при­нято отно­сить перепол­нение буфера в сте­ке. Итак, шелл‑коды работа­ют сле­дующим обра­зом. Пусть в коде есть вызов некото­рой уяз­вимой фун­кции, работа­ющей с поль­зователь­ски­ми дан­ными. При ее вызове со сте­ком про­исхо­дит одновре­мен­но нес­коль­ко вещей: там сох­раня­ется адрес воз­вра­та на сле­дующую инс­трук­цию (что­бы знать, куда передать управле­ние пос­ле того, как выпол­нение уяз­вимой фун­кции будет завер­шено); сох­раня­ется зна­чение фла­га BP; выделя­ется область опре­делен­ного раз­мера для локаль­ных дан­ных (что в нашем слу­чае и есть «поль­зователь­ский ввод»). Если количес­тво счи­тыва­емых дан­ных фун­кци­ей не про­веря­ется, а зло­умыш­ленник передал дан­ных боль­ше, чем нуж­но, то они перезат­рут слу­жеб­ные зна­чения на сте­ке — и зна­чения фла­гов, и адрес воз­вра­та. Куда будет переда­но управле­ние пос­ле выхода из фун­кции? Ров­но по тому адре­су, который теперь ока­зал­ся в зоне адре­са воз­вра­та. Так вот, цель зло­умыш­ленни­ка — сфор­мировать дан­ные таким обра­зом, что­бы передать управле­ние на вре­донос­ную наг­рузку, которая так­же содер­жится в переда­ваемых дан­ных. Поп­росту говоря — выпол­нить про­изволь­ный код на машине‑жер­тве.

Тра­дици­онно как шелл‑коды, так и методы их детек­тирова­ния при­нято ассо­цииро­вать с плат­формой x86 — вви­ду того, что тип ата­ки доволь­но ста­рый и был нацелен на наибо­лее рас­простра­нен­ную плат­форму. Пос­ледние нес­коль­ко лет ситу­ация меня­ется. Устрой­ств на базе плат­формы ARM уже в нес­коль­ко раз боль­ше, чем x86, и их чис­ло будет про­дол­жать рас­ти. Речь идет и о смар­тфо­нах, и о план­шетах, а Google с Samsung уже выпус­кают хром­буки на базе ARM. Архи­тек­тура ARM (Advanced RISC Machine) — семей­ство лицен­зиру­емых 32-бит­ных и 64-бит­ных мик­ропро­цес­сорных ядер раз­работ­ки ком­пании ARM Limited. ARM пред­став­ляет собой RISC-архи­тек­туру про­цес­сора. Казалось бы, плат­форма и шелл‑коды находят­ся на нес­коль­ко раз­ных уров­нях абс­трак­ции. Как сме­на плат­формы может изме­нить вид шелл‑кода? Ответ кро­ется в ряде сущес­твен­ных отли­чий двух плат­форм, которые пре­дос­тавля­ют зло­умыш­ленни­кам воз­можность исполь­зовать весь­ма раз­личные тех­ники написа­ния шелл‑кодов.

 

Что в ARM пошло «не так»

Су­щес­тву­ющие решения обна­руже­ния шелл‑кодов, работа­ющие для плат­формы x86, ана­лизи­руют типич­ные приз­наки шелл‑кодов. Тем не менее для плат­формы ARM они ока­зыва­ются неп­римени­мы — вид шелл‑кодов меня­ется. Рас­смот­рим более под­робно отли­чия двух архи­тек­тур, которые непос­редс­твен­но вли­яют на вид шелл‑кодов.

 

Фиксированный размер команд

В архи­тек­туре ARM все инс­трук­ции име­ют фик­сирован­ный раз­мер (32 бита в ARM-режиме, 16 бит в режиме Thumb), в отли­чие от архи­тек­туры х86, где раз­мер инс­трук­ций варь­иру­ется от одно­го до 16 байт.

Из‑за дан­ного свой­ства архи­тек­туры ARM дизас­сем­бли­рова­ние с раз­ных сме­щений не син­хро­низи­рует­ся (self-synchronizing disassembly). Self-synchronizing disassembly — осо­бен­ность дизас­сем­бли­рова­ния инс­трук­ций в архи­тек­туре х86. С какого бы бай­та оно ни началось, най­дет­ся точ­ка син­хро­низа­ции меж­ду дизас­сем­бли­рова­нием с про­изволь­ного бай­та и дизас­сем­бли­рова­нием от начала потока инс­трук­ций. Дан­ное свой­ство упро­щает поиск шелл‑кодов в тра­фике (он может находить­ся в про­изволь­ном мес­те потока), но усложня­ет ана­лиз шелл‑кода — сущес­тву­ет воз­можность про­пус­тить инс­трук­ции, кри­тичес­кие для шелл‑кода.

Рис. 2. Self-synchronizing disassembly
Рис. 2. Self-synchronizing disassembly

При детек­тирова­нии ARM шелл‑кодов в бай­товом потоке дос­таточ­но дизас­сем­бли­ровать с четырех сме­щений от начала потока: 0, 1, 2, 3. Так как инс­трук­ции име­ют фик­сирован­ную дли­ну, дизас­сем­бли­рова­ние всег­да будет идти с начала шелл‑кода, без про­пус­ка важ­ных инс­трук­ций.

 

Наличие нескольких режимов работы процессора и возможность динамического переключения между ними

Ра­нее мы уже упо­мяну­ли о наличии двух режимов про­цес­сора. В реаль­нос­ти же все еще слож­нее: помимо двух упо­мяну­тых, есть еще и Thumb2 (в 16-бит­ный Thumb добав­лено нес­коль­ко 32-бит­ных инс­трук­ций, такая тех­ника поз­воля­ет сущес­твен­но уве­личить плот­ность опко­дов) и Jazelle, под­держи­вающий аппа­рат­ное уско­рение выпол­нения Java-байт‑кода.

Ре­жим Thumb, в час­тнос­ти, исто­ричес­ки исполь­зовал­ся, что­бы сде­лать ком­пак­тнее прог­раммы на встро­енных сис­темах, для которых типич­ным явля­ется огра­ничен­ный раз­мер памяти. В дру­гих усло­виях исполь­зование Thumb-режима не так оправдан­но — он работа­ет мед­леннее режима ARM: нет под­дер­жки условно­го выпол­нения, хуже работа­ет модуль пред­ска­зания перехо­дов и так далее. Вви­ду это­го боль­шинс­тво при­ложе­ний исполь­зуют режим ARM, одна­ко при написа­нии шелл‑кодов воп­рос объ­ема дан­ных так­же вста­ет дос­таточ­но остро. Поэто­му тех­ника динами­чес­кого перек­лючения режима про­цес­сора — одна из самых рас­простра­нен­ных для шелл‑кодов. В час­тнос­ти, она при­сутс­тву­ет более чем в 80% пуб­лично дос­тупных образцов. При­мер шелл‑кодов, написан­ных в раз­ных режимах про­цес­сора, показан на рисун­ке.

Пе­рек­лючение про­изво­дит­ся при помощи инс­трук­ций перехо­да:

  • про­исхо­дит переход по мет­ке label и перек­лючение в режим, отличный от текуще­го;
  • про­исхо­дит переход по адре­су, записан­ному в регистр Rm;
  • а так­же в зависи­мос­ти от зна­чения пос­ледне­го бита регис­тра про­исхо­дит перек­лючение режимов (0 — ARM, 1 — Thumb).
Рис. 3. Шелл-коды в Thumb и ARM
Рис. 3. Шелл‑коды в Thumb и ARM

Ра­бота­ет это сле­дующим обра­зом. В шелл‑кодах про­явля­ется наличие коман­ды сме­ны режима про­цес­сора (BX Rm) на пересе­чении цепочек команд из раз­ных режимов про­цес­сора. Так как уяз­вимая прог­рамма может работать в ARM-режиме, то, что­бы выпол­нить дан­ный шелл‑код, необ­ходимо перек­лючить про­цес­сор в режим Thumb. Для того что­бы перек­лючить про­цес­сор в дру­гой режим, необ­ходимо счи­тать регистр счет­чика инс­трук­ций и исполь­зовать его зна­чение для перехо­да на начало шелл‑кода при помощи коман­ды BX Rm, где Rm — это регистр обще­го наз­начения, в который записы­вает­ся адрес начала шелл‑кода. Для сооб­щения про­цес­сору, в какой режим сле­дует перек­лючить­ся, в стар­шем бите адре­са ста­вит­ся зна­чение в зависи­мос­ти от тре­буемо­го режима.

Инс­трук­ции раз­ных режимов могут находить­ся на некото­ром рас­сто­янии друг от дру­га, но в силу огра­ничен­ности шелл‑кода мож­но сде­лать пред­положе­ние о том, что отступ меж­ду ARM-кодом перек­лючения про­цес­сора и Thumb-шелл‑кодом будет соиз­мерим с раз­мером шелл‑кода. При­мер такого шелл‑кода мож­но видеть на рисун­ке (шелл‑код взят с exploit-db.com).

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

Рис. 4. Шелл-код со сменой режима
Рис. 4. Шелл‑код со сме­ной режима
 

Возможность условного выполнения инструкций

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

Нап­ример, коман­да MOVEQ R0, R0 выпол­нится толь­ко тог­да, ког­да флаг Z = 1 (исполь­зует­ся суф­фикс EQ). Все­го сущес­тву­ет 15 зна­чений суф­фиксов. Так­же сущес­тву­ет суф­фикс S, который озна­чает, будет ли дан­ная коман­да изме­нять зна­чение регис­тра фла­гов.

Что дает эта тех­ника для шелл‑кодов? Во‑пер­вых, бла­года­ря это­му мож­но сущес­твен­но умень­шить объ­ем кода. Во‑вто­рых, воз­можен новый тип обфуска­ции: что, если выс­тро­ить вре­донос­ную наг­рузку из абсо­лют­но легитим­ной прог­раммы, меняя толь­ко зна­чения фла­гов на нуж­ные? Ведь совер­шенно оче­вид­но, что ста­тичес­кий ана­лиз при такой тех­нике будет зат­руднен в разы. Нап­ример, один из типич­ных и дос­таточ­но эффектив­ных под­ходов ста­тичес­кого ана­лиза — раз­личный ана­лиз гра­фов потока управле­ния (CFG — control flow graph) и гра­фов потока инс­трук­ций (IFG — instruction flow graph).

Рис. 5. Компактизация
Рис. 5. Ком­пакти­зация

Ус­ловное выпол­нение не даст воз­можнос­ти пра­виль­но ана­лизи­ровать CFG в любом методе, осно­ван­ном на ана­лизе CFG, так как при ста­тичес­ком ана­лизе невоз­можно узнать, какие имен­но инс­трук­ции выпол­нятся, а какие будут про­игно­риро­ваны. Граф потока инс­трук­ций так­же не будет отра­жать реаль­ную струк­туру ана­лизи­руемой прог­раммы, потому что мно­гие из вер­шин гра­фа могут быть про­игно­риро­ваны про­цес­сором. То есть мы потеря­ли один из самых инте­рес­ных методов детек­тирова­ния шелл‑кодов (это­му методу было пос­вящено уж очень мно­го ста­тей).

Рис. 6. Пример различных потоков инструкций в одном коде
Рис. 6. При­мер раз­личных потоков инс­трук­ций в одном коде

Дан­ное свой­ство так­же усложня­ет динами­чес­кий ана­лиз шелл‑кодов (при помощи эму­лято­ра). Все дело в том, что из‑за наличия условно­го выпол­нения инс­трук­ций воз­можно перенап­равлять поток выпол­нения инс­трук­ций таким обра­зом, что один и тот же код в зависи­мос­ти от началь­ного рас­пре­деле­ния зна­чений фла­гов может выпол­нять абсо­лют­но раз­ные дей­ствия (нап­ример, может сущес­тво­вать некото­рое началь­ное рас­пре­деле­ние фла­гов, которое ини­циирует вре­донос­ную полез­ную наг­рузку). Для получе­ния началь­ного рас­пре­деле­ния фла­гов шелл‑код исполь­зует зна­чение регис­тра фла­гов, которое было перед переда­чей управле­ния на шелл‑код, так называ­емая тех­ника non-selfcontained шелл‑кодов.

Ка­ким обра­зом это про­исхо­дит? Рас­смот­рим при­мер, показан­ный на рисун­ке. Здесь мы име­ем одну и ту же прог­рамму, запущен­ную два раза при раз­ных началь­ных усло­виях (для прос­тоты рас­смот­рим толь­ко два фла­га: ZERO и CARRY). В начале прог­раммы име­ется блок инс­трук­ций с суф­фиксом AL, это озна­чает, что все инс­трук­ции будут выпол­нены, нес­мотря на зна­чение фла­гов. Далее име­ется инс­трук­ция ADDEQS r0, r1, которая выпол­нится толь­ко тог­да, ког­да флаг ZERO = 1, то есть толь­ко в пра­вой эму­ляции. К тому же дан­ная инс­трук­ция изме­нит рас­пре­деле­ние фла­гов в пра­вой эму­ляции (бла­года­ря суф­фиксу S). Далее мы име­ем два бло­ка: CS: CARRY = 1, и NE: ZERO = 0. При­чем блок NE может пов­лиять на зна­чения регис­тров r3 и r4 таким обра­зом, что в пос­леду­ющей инс­трук­ции ADDCCS r3, r4 (выпол­ненной в обе­их эму­ляци­ях) фла­ги будут изме­нены раз­личным обра­зом. Обра­ти вни­мание на то, что одна и та же инс­трук­ция даст раз­личные резуль­таты при раз­ных началь­ных усло­виях эму­ляции. К тому же дан­ная инс­трук­ция вли­яет на воз­можные пос­леду­ющие инс­трук­ции. Таким обра­зом фор­миру­ется скры­тая пос­ледова­тель­ность инс­трук­ций, которая акти­виру­ется толь­ко при нуж­ных шелл‑коду усло­виях.

Так вот, ARM шелл‑код чрез­вычай­но хитер, что­бы быть обна­ружен­ным за один про­ход эму­лято­ра. Поэто­му нуж­но про­водить эму­ляцию при все­воз­можных началь­ных усло­виях (их 15 — столь­ко же, сколь­ко и условных суф­фиксов). К тому же мы не зна­ем, где имен­но в тра­фике находит­ся шелл‑код, поэто­му нуж­но про­водить ана­лиз с каж­дого сме­щения. В совокуп­ности эти фак­торы «очень отри­цатель­но» вли­яют на про­изво­дитель­ность ана­лиза тра­фика в реаль­ном вре­мени.

 

Возможность прямого обращения к счетчику инструкций

Ар­хитек­тура ARM поз­воля­ет нап­рямую обра­щать­ся к счет­чику инс­трук­ций. Что, конеч­но, силь­но облегча­ет жизнь зло­умыш­ленни­ку. И вот уже даже не обя­зате­лен GetPC code, который час­то исполь­зует­ся при написа­нии х86-шелл‑кодов. Как ты пом­нишь, дан­ная тех­ника поз­воля­ет получить зна­чение регис­тра счет­чика инс­трук­ций без пря­мого обра­щения к нему. В шелл‑код встав­ляет­ся инс­трук­ция вызова фун­кции call label по близ­кому сме­щению (внут­ри шелл‑кода), далее выпол­няет­ся коман­да взя­тия регис­тра со сте­ка pop bx, так как инс­трук­ция call сох­раня­ет адрес воз­вра­та на вер­шине сте­ка. Таким обра­зом, типич­ная эвристи­ка для x86-шелл‑кодов дол­жна быть изме­нена: теперь вмес­то GetPC-кода мы будем искать Get-UsePC-код.

Под Get-UsePC-кодом понима­ется наличие счи­тыва­ния зна­чения счет­чика инс­трук­ций PC и пос­леду­ющее исполь­зование это­го зна­чения. Что­бы счи­тать зна­чение счет­чика инс­трук­ций, мож­но непос­редс­твен­но обра­щать­ся к регис­тру PC (R15), мож­но так­же выз­вать фун­кцию с помощью инс­трук­ции BL и потом исполь­зовать зна­чение регис­тра LR (R14), в котором сох­ранил­ся адрес воз­вра­та.

Проб­лема сос­тоит в том, что в легитим­ных прог­раммах обя­затель­но будет исполь­зовать­ся GetPC-код: он нужен для того, что­бы вер­нуть управле­ние из фун­кции (MOV PC,LR или LDR PC,#Значение_со_стека). Поэто­му, что­бы отде­лить мух от кот­лет, нуж­но так­же учи­тывать исполь­зование зна­чения регис­тра счет­чика инс­трук­ций. То есть нуж­но узнать, какие регис­тры кос­венно ссы­лают­ся на счет­чик инс­трук­ций, и про­верять, где они исполь­зовались. Нап­ример, если дан­ный регистр исполь­зовал­ся в коман­де чте­ния из памяти, то мож­но утвер­ждать, что перед нами находит­ся дек­риптор, так как чте­ние будет про­исхо­дить по близ­кому сме­щению, а сле­дова­тель­но, из полез­ной наг­рузки шелл‑кода. При­мер такого шелл‑кода при­веден на рисун­ке (шелл‑код взят с exploit-db.com).

Рис. 7. Шелл-код с декриптором
Рис. 7. Шелл‑код с дек­рипто­ром
 

При вызове функций аргументы помещаются в регистры

В отли­чие от архи­тек­туры х86, где аргу­мен­ты фун­кци­ям переда­ются через стек, в архи­тек­туре ARM аргу­мен­ты записы­вают­ся в регис­тры R0–R3. Если аргу­мен­тов боль­ше, то оставши­еся аргу­мен­ты кла­дут­ся на стек, как и в х86.

Так что «пока», ROP-шелл‑коды! Ну, то есть не сов­сем «пока», конеч­но... По край­ней мере, дан­ная осо­бен­ность зат­рудня­ет их написа­ние. ROP-шелл‑код вызыва­ет фун­кции из сис­темных биб­лиотек, что­бы из их час­тей сос­тавить вре­донос­ную полез­ную наг­рузку. Исполь­зовать эту тех­нику зат­рудни­тель­но из‑за того, что пат­терны для сос­тавле­ния шелл‑кода дол­жны содер­жать в себе ини­циали­зацию аргу­мен­тов в регис­тры. То есть при­ходим к тому, что таких нуж­ных «кусоч­ков», которых и без того было не осо­бо мно­го, ста­нет еще мень­ше. Ну что ж, по край­ней мере, полет фан­тазии будет уже не таким высоким.

Так как иссле­дуемый объ­ект явля­ется исполни­мым кодом, он обла­дает харак­терис­тиками, спе­цифич­ными для раз­личных опе­раци­онных сис­тем. В час­тнос­ти, исполни­мый код дол­жен работать с вызова­ми опе­раци­онной сис­темы или биб­лиоте­ки ядра. Поэто­му для x86 типич­ным являлся детек­тор, осно­ван­ный на поис­ке спе­цифич­ных пат­тернов инс­трук­ций, в которые вхо­дил сис­темный вызов и под­счет их чис­ла. В ARM опять все работа­ет не так.

Для вызова фун­кции в ARM про­исхо­дит сле­дующая пос­ледова­тель­ность дей­ствий: аргу­мен­ты фун­кции кла­дут­ся в регис­тры обще­го наз­начения (R0–R3) и на стек (если количес­тво аргу­мен­тов боль­ше четырех), и далее сле­дует вызов фун­кции BL (BLX, если фун­кция написа­на с исполь­зовани­ем инс­трук­ций дру­гого про­цес­сорно­го режима). Для того что­бы сде­лать сис­темный вызов svc, нуж­но заг­рузить номер сис­темно­го вызова в регистр R7 и заг­рузить аргу­мен­ты вызова в регис­тры обще­го наз­начения (если аргу­мен­ты тре­буют­ся для сис­темно­го вызова).

По­это­му для детек­тирова­ния дан­ного приз­нака нуж­но при­менять тех­нику абс­трак­тно­го выпол­нения инс­трук­ций. В чем зак­люча­ется эта тех­ника? Про­водит­ся выпол­нение кода без сох­ранения зна­чения регис­тров. Единс­твен­ное, что мы можем отсле­дить, — это то, какие из регис­тров были ини­циали­зиро­ваны некото­рыми зна­чени­ями. Таким обра­зом, мы дол­жны про­верять, были ли ини­циали­зиро­ваны регис­тры обще­го наз­начения (и регистр R7 для сис­темных вызовов) перед сис­темным вызовом или вызовом фун­кции. Нап­ример, на рисун­ке показан код, в котором дела­ются сис­темные вызовы (часть шелл‑кода из Metasploit).

Рис. 8. Системные вызовы в шелл-коде
Рис. 8. Сис­темные вызовы в шелл‑коде
 

Что осталось в наследство от x86

Часть осо­бен­ностей шелл‑кодов под x86 оста­лась в нас­ледс­тво и для ARM. Соот­ветс­твен­но, детек­торы для таких осо­бен­ностей так­же мож­но оста­вить без сущес­твен­ных изме­нений на сво­ем наг­ретом мес­те. К ним мож­но отнести часть осо­бен­ностей шелл‑кодов, выделя­емых ста­тичес­ки, и тех­ники динами­чес­кого выяв­ления дек­рипто­ра (шелл‑коды, как пра­вило, обфусци­рова­ны).

  • Об­наруже­ние NOP-сле­да. Как пра­вило, такие детек­торы ана­лизи­руют кор­рек­тное дизас­сем­бли­рова­ние инс­трук­ций с каж­дого сме­щения — типич­ный приз­нак NOP-сле­да. Одна ого­вор­ка: количес­тво дизас­сем­бли­рован­ных инс­трук­ций счи­тает­ся толь­ко для каж­дого режима про­цес­сора отдель­но, так как, если исполь­зовать инс­трук­ции раз­ных режимов в NOP-sled, управле­ние может попасть на инс­трук­цию, исполь­зующую отличный от текуще­го режим про­цес­сора, и про­изой­дет пре­рыва­ние: unexpected instruction.
  • Ад­рес воз­вра­та находит­ся в опре­делен­ном диапа­зоне зна­чений. В шелл‑кодах адрес воз­вра­та переза­писы­вает­ся зна­чени­ем, которое находит­ся в диапа­зоне адресно­го прос­транс­тва исполни­мого про­цес­са. Ниж­няя гра­ница диапа­зона опре­деля­ется адре­сом начала переза­писы­ваемо­го буфера. Вер­хняя гра­ница опре­деля­ется как RA˘SL, где RA — адрес поля адре­са воз­вра­та и SL — дли­на вре­донос­ного исполни­мого кода, или как BS˘SL, где BS — адрес начала сте­ка. Этот приз­нак общий для всех иссле­дуемых объ­ектов, не исполь­зующих тех­нику ран­домиза­ции адресно­го прос­транс­тва (ASLR). Детект не изме­няет­ся для всех рас­смат­рива­емых плат­форм.
  • Ана­лиз количес­тва чте­ний полез­ной наг­рузки и количес­тва уни­каль­ных записей в память. В слу­чае если эти парамет­ры пре­выша­ют опре­делен­ный порог и поток управле­ния хотя бы один раз переда­ется из адресно­го прос­транс­тва вход­ного буфера на адрес, по которо­му ранее осу­щест­вля­лась запись, счи­тает­ся, что обна­ружен­ный объ­ект может быть полимор­фным шелл‑кодом (рис. 9). Количес­тво чте­ний счи­тает­ся не по количес­тву команд (так как ARM под­держи­вает одновре­мен­ную заг­рузку сра­зу нес­коль­ких регис­тров), а по количес­тву заг­ружен­ных регис­тров коман­дой LDM (коман­да LDR может счи­тывать толь­ко один регистр). Так­же для дешиф­ровки шелл‑кода в память по близ­кому адре­су записы­вает­ся рас­шифро­ван­ная полез­ная наг­рузка шелл‑кода. Количес­тво записей счи­тает­ся по количес­тву заг­ружен­ных в память регис­тров коман­дой STM (коман­да STR может записы­вать толь­ко один регистр). Пос­ле рас­шифров­ки тре­бует­ся передать управле­ние на рас­шифро­ван­ную полез­ную наг­рузку, то есть на адрес, по которо­му ранее про­изво­дилась запись.
Рис. 9. Расшифровка полезной нагрузки шелл-кода
Рис. 9. Рас­шифров­ка полез­ной наг­рузки шелл‑кода
Рис. 10. Передача управления на полезную нагрузку шелл-кода
Рис. 10. Переда­ча управле­ния на полез­ную наг­рузку шелл‑кода
 

Выводы

Ре­зюми­руя все, что ска­зано выше, мож­но сде­лать сле­дующее зак­лючение. Эра про­цес­соров ARM, на наш взгляд, пережи­вает вто­рое рож­дение. В бли­жай­шие годы задача защиты подоб­ных сис­тем будет вста­вать все более остро. Что же каса­ется шелл‑кодов... Вни­матель­но рас­смот­рев раз­ницу двух плат­форм, мож­но сде­лать сле­дующие выводы. Эти отли­чия, во‑пер­вых, отчасти изме­нили вид шелл‑кодов, во‑вто­рых, поз­волили зло­умыш­ленни­кам при­менять сущес­твен­но иные тех­ники обфуска­ции. Все это при­вело к тому, что сущес­тву­ющие методы детек­тирова­ния под x86 в слу­чае с ARM либо лома­ются, либо при­мени­мы с боль­шой натяж­кой — нап­ример, работа­ют доль­ше если не на порядок, то в разы. С уче­том того, что огра­ниче­ния на ско­рость обна­руже­ния очень сущес­твен­ны, выводы неуте­шитель­ны. С дру­гой сто­роны, эта раз­ница двух плат­форм поз­воля­ет най­ти некото­рые фичи, свой­ствен­ные толь­ко ARM-шелл‑кодам, что, безус­ловно, явля­ется плю­сом. Дру­гими сло­вами, раз­работ­ка детек­торов шелл‑кодов под ARM и нуж­на, и воз­можна.

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии