Упа­ков­щики и крип­торы хорошо извес­тны любите­лям усложнить обратную раз­работ­ку кода. Для вин­ды таких инс­тру­мен­тов целый зоопарк, в Linux все куда слож­нее: сей­час про­верен­но работа­ет с эль­фами под раз­личные плат­формы лишь UPX. Его исполь­зуют вирусо­писа­тели для упа­ков­ки бо­тов, май­неров и SSH-бэк­доров, а потому заос­трим на нем вни­мание и мы.

info

Эта статья написа­на по мотивам док­лада с новогод­ней сход­ки, орга­низо­ван­ной сооб­щес­твом SPbCTF в декаб­ре 2020 года. За­пись сход­ки со все­ми док­ладами дос­тупна на канале SPbCTF в YouTube.

 

Зачем это все?

На стра­нич­ке одно­го хороше­го пакера мож­но про­честь, что основная цель его сущес­тво­вания — умень­шить раз­мер исполня­емо­го фай­ла с вытека­ющей отсю­да эко­номи­ей дис­кового прос­транс­тва и сетево­го тра­фика. Нап­ример, UPX поз­воля­ет сжать исполня­емый файл на 50–70%, и он оста­нет­ся пол­ностью самодос­таточ­ным, потому что код, выпол­няющий рас­паков­ку в память, добав­ляет­ся к получив­шемуся бинарю. Для это­го же упа­ков­ка исполь­зует­ся и в PyInstaller при сбор­ке кода на Python в незави­симые исполня­емые фай­лы PE или ELF (при этом он может работать и в тан­деме с UPX).

Од­нако, как ты догады­ваешь­ся, пакеры отлично под­ходят еще и для того, что­бы нем­ного под­портить жизнь реверс‑инже­неру. В этом клю­че родс­твен­ны им крип­торы. В мире Linux, одна­ко, это ско­рее про­екты, боль­ше похожие на proof-of-concept, либо же прос­то что‑то ста­рое и, пря­мо ска­жем, в живой при­роде поч­ти не встре­чающееся.

Из упа­ков­щиков для фай­лов ELF в нас­тоящее вре­мя наибо­лее популя­рен UPX (в час­тнос­ти, сре­ди вирусо­писа­телей), пос­коль­ку осталь­ные пакеры либо под­держи­вают пол­торы архи­тек­туры, либо уже очень дав­но не обновля­лись (оце­нить количес­тво канув­ших в Лету про­ектов мож­но, полис­тав дав­ний об­зор упа­ков­щиков для Linux/BSD от Кри­са Кас­пер­ски).

 

Принцип работы среднестатистического пакера

Кон­цепту­аль­но упа­ков­щик работа­ет так. Код и дан­ные прог­раммы сжи­мают­ся без потерь каким‑либо алго­рит­мом (с исполь­зовани­ем lzma, zlib или чего‑либо еще), добав­ляет­ся код, выпол­няющий рас­паков­ку того, что получи­лось, затем добав­ляют­ся собс­твен­ные заголов­ки, и вуаля — у нас сжа­тый бинарь. Схе­матич­но про­цесс упа­ков­ки пред­став­лен ниже.

Упаковка исполняемого файла
Упа­ков­ка исполня­емо­го фай­ла

При запус­ке такого фай­ла нач­нет выпол­нять­ся заг­рузчик, отве­чающий за рас­паков­ку сжа­того кода и дан­ных в память, пос­ле чего он переда­ет управле­ние в ори­гиналь­ную точ­ку вхо­да. Гру­бо говоря, получа­ется саморас­паковы­вающий­ся архив. Детали реали­зации в раз­ных пакерах могут раз­личать­ся — нап­ример, PyInstaller при соз­дании exe-фай­ла помеща­ет упа­кован­ные дан­ные в овер­лей, а заг­рузчик находит­ся перед упа­кован­ными дан­ными, а не пос­ле, как на схе­ме. Более изощ­ренные упа­ков­щики (ско­рее боль­ше напоми­нающие крип­торы), могут еще боль­ше усложнять этот про­цесс, но в целом это не меня­ет сути.

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

 

The Ultimate Packer for eXecutables

Этот про­ект праз­дну­ет в нынеш­нем году 25-летие. Его исходни­ки дос­тупны на Гит­хабе, он написан на плю­сах с при­месью ассем­бле­ра, поэто­му раз­бирать­ся в коде доволь­но‑таки весело. UPX под­держи­вает мно­жес­тво типов фай­лов и архи­тек­тур — не зря он такой популяр­ный. Более того, PyInstaller име­ет опцию --upx-dir, поз­воля­ющую при соз­дании архи­ва упа­ковать его при помощи UPX, тем самым умень­шая раз­мер резуль­тиру­юще­го фай­ла.

Как про­исхо­дит упа­ков­ка прог­раммы в UPX? Вна­чале опре­деля­ется ее фор­мат — PE, ELF, образ ядра Linux или что‑либо еще (го­ворят, в UPX мож­но паковать даже sh-скрип­ты!). Пос­ле это­го нуж­но опре­делить архи­тек­туру, под которую ском­пилиро­ван файл. Это свя­зано с тем, что заг­рузчик, который называ­ется в коде UPX stub loader («заг­лушка», стаб), плат­формен­но зависим. Его код написан на язы­ке ассем­бле­ра, потому что при сжа­тии раз­работ­чики ста­рают­ся эко­номить бук­валь­но каж­дый байт и добить­ся мак­сималь­ной ком­прес­сии. В свя­зи с этим быва­ет и так, что сам заг­рузчик тоже час­тично упа­кован. Так что UPX под­держи­вает те архи­тек­туры, под которые у него есть реали­зация ста­ба. Оце­нить спи­сок архи­тек­тур и типов фай­лов мож­но, взгля­нув на со­дер­жимое дирек­тории src/stub в сор­цах UPX.

Итак, если архи­тек­тура упа­ковы­ваемо­го фай­ла под­держи­вает­ся UPX, то для него фор­миру­ется заг­рузчик, сам файл сжи­мает­ся, и в целом наш сжа­тый бинарь готов. Для воз­можнос­ти ста­тичес­кой рас­паков­ки, которая выпол­няет­ся коман­дой upx -d, UPX добав­ляет в файл собс­твен­ные заголов­ки.

 

Заголовки UPX

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

  • loader info (l_info) содер­жит кон­троль­ную сум­му, магичес­кие сиг­натуры «UPX!», раз­мер и некото­рые парамет­ры заг­рузчи­ка;
  • packed program info (p_info). В этом заголов­ке находят­ся раз­меры неупа­кован­ного бло­ка p_blocksize и неупа­кован­ной (исходной) прог­раммы p_filesize, которые, как пра­вило, рав­ны;
  • block info (b_info) пред­варя­ет каж­дый сжа­тый блок и содер­жит информа­цию о раз­мерах до и пос­ле сжа­тия, алго­рит­ме (методе), уров­не сжа­тия бло­ка и дру­гих парамет­рах. На рисун­ке ниже ты можешь видеть по сме­щению 0x110 сиг­натуру ELF: это и есть начало упа­кован­ного фай­ла.

    Заголовки в начале
    За­голов­ки в начале
  • packheader добав­ляет­ся в кон­це фай­ла, обыч­но занимая нем­ного боль­ше 0x24 байт в зависи­мос­ти от вырав­нивания. Его выделя­ют две сиг­натуры UPX!, вто­рая из которых выров­нена по четырех­бай­товой гра­нице. В packheader записы­вает­ся информа­ция, необ­ходимая самому UPX, что­бы ста­тичес­ки рас­паковать бинарь, не при­бегая к коду заг­рузчи­ка. В нее вхо­дят, в час­тнос­ти, уже наз­ванные дан­ные — раз­мер рас­пакован­ного фай­ла и некото­рые парамет­ры сжа­тия. В резуль­тате это­го получа­ется некото­рая избы­точ­ность. Как видим, обве­ден­ное зна­чение сов­пада­ет с хра­нящи­мися в p_info раз­мерами неупа­кован­ных бло­ка и фай­ла. Это может нем­ного помочь нам в даль­нейшем при рас­смот­рении спо­собов защиты от ста­тичес­кой рас­паков­ки.

    Заголовок в конце файла
    За­голо­вок в кон­це фай­ла

Ес­ли тебе хочет­ся луч­ше понять про­цесс ста­тичес­кой рас­паков­ки UPX, то нарав­не с чте­нием сор­цов ты можешь вос­поль­зовать­ся дебаж­ным фла­гом. Тог­да ты уви­дишь, что на осно­ве име­ющих­ся в бинаре заголов­ков упа­ков­щик выбира­ет фун­кцию, которой будет рас­паковы­вать файл. Здесь наб­люда­ется то же огра­ниче­ние, что и для stub loader: для каких фор­матов и аппа­рат­ных плат­форм они сущес­тву­ют, те и под­держи­вают­ся. Спи­сок весь­ма вну­шите­лен.

UPX перебирает все возможные функции упаковки перед тем, как сдаться
UPX переби­рает все воз­можные фун­кции упа­ков­ки перед тем, как сдать­ся
 

Распаковываем себя

Что же про­исхо­дит, ког­да бинарь рас­паковы­вает­ся не коман­дой upx -d, а сво­им собс­твен­ным кодом? В этом слу­чае за про­исхо­дящее отве­чает заг­рузчик. Рас­паковы­вать он может либо сра­зу в память, либо с исполь­зовани­ем вре­мен­ных фай­лов с пос­леду­ющим их запус­ком — это будет зависеть от кон­крет­ной реали­зации.

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

Продолжение доступно только участникам

Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».

Присоединяйся к сообществу «Xakep.ru»!

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

Оставить мнение