Консольный вывод приложения pkzipc.exe

Как идентифицировать библиотечные функции

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

Как идентифицировать библиотечные функции

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

Ана­лиз дизас­сем­блер­ного лис­тинга — дело дру­гое. Имена функций отсутствуют, за редким исключением, и невозможно «с первого взгляда» определить, является ли это printf или что-то еще. Вы должны покопаться в алгоритме … Легко сказать — углубитесь! Тот же printf представляет собой сложный интерпретатор спецификационных строк — сразу не разобраться! Но есть и более чудовищные функции. Самое обидное, что алгоритм их работы не имеет ничего общего с анализом исследуемой программы. Тот же новый может выделять память из кучи Windows, а также реализовывать собственный менеджер. Что это значит для нас? Достаточно знать, что это именно новая, то есть функция выделения памяти, а не свободная или, например, открытая.

Доля библиотечных функций в программе составляет в среднем от пятидесяти до девяноста процентов. Он особенно высок для программ, написанных в средах визуальной разработки, использующих автоматическую генерацию кода (например, Microsoft Visual Studio, Delphi). Кроме того, библиотечные функции иногда намного сложнее и запутаннее, чем сам рутинный программный код. Обидно, что большая часть усилий по анализу тратится зря …Как бы опти­мизи­ровать это?

При идентификации библиотечных функций на помощь вновь приходит IDA

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

Впро­чем, неидентифицированная фун­кция — это пол­беды, неп­равиль­но рас­познан­ная фун­кция — мно­го хуже, ибо это при­водит к ошиб­кам (иног­да труд­ноуло­вимым) в ана­лизе иссле­дуемой прог­раммы или ста­вит иссле­дова­теля в глу­хой тупик. Нап­ример, вызыва­ется fopen, и воз­вра­щен­ный ею резуль­тат спус­тя некото­рое вре­мя переда­ется free — с одной сто­роны: почему бы и нет? Ведь fopen воз­вра­щает ука­затель на струк­туру FILE, а free ее уда­ляет. А если free никакой не free, а, ска­жем, fseek? Про­пус­тив опе­рацию позици­они­рова­ния, мы не смо­жем пра­виль­но вос­ста­новить струк­туру фай­ла, с которым работа­ет прог­рамма.

Опознание функций

Рас­познать ошиб­ки IDA будет лег­че, если пред­став­лять, как имен­но она выпол­няет рас­позна­вание. Мно­гие почему‑то счи­тают, что здесь задей­ство­ван три­виаль­ный под­счет CRC (кон­троль­ной сум­мы). Что ж, под­счет CRC — заман­чивый алго­ритм, но, увы, для решения дан­ной задачи он неп­ригоден. Основной камень прет­кно­вения — наличие непос­тоян­ных фраг­ментов, а имен­но переме­щаемых эле­мен­тов. И хотя при под­сче­те CRC переме­щаемые эле­мен­ты мож­но прос­то игно­риро­вать (не забывая про­делы­вать ту же опе­рацию и в иден­тифици­руемой фун­кции), раз­работ­чик IDA пошел дру­гим, более запутан­ным и вити­ева­тым, но и более про­изво­дитель­ным путем.

Ключевая идея состоит в том, что нет необходимости тратить время на вычисление CRC. Для предварительной идентификации функции подходит одиночное посимвольное сравнение за вычетом перемещаемых элементов (они игнорируются и не участвуют в сравнении). Точнее, не сравнение, а поиск заданной последовательности байтов в справочной базе данных, организованной в виде двоичного дерева. Известно, что время двоичного поиска пропорционально логарифму количества записей в базе данных. Здравый смысл подсказывает, что длина модели (другими словами, сигнатура, то есть сравниваемая последовательность) должна быть достаточной для однозначной идентификации функции. Однако разработчик IDA по причинам, неизвестным авторам, решил придерживаться первых тридцати двух байтов, что (особенно если вычесть пролог, который практически одинаков для всех функций) довольно мал.

И это правильно! Многие функции попадают на один и тот же лист дерева, возникает коллизия — неоднозначность идентификации функций. Чтобы разрешить ситуацию, CRC16 подсчитывается от тридцать второго байта до первого сдвинутого элемента для всех функций «коллизии» и сравнивается с CRC16 опорных функций. В большинстве случаев это работает, но если первый движущийся элемент находится слишком близко к тридцатисекундному байту, последовательность контрольных сумм слишком коротка или даже равна нулю (это может быть тридцать второй байт движущегося элемента, почему бы и нет?) … в случае повторного столкновения мы находим в функциях байт, в котором все они различаются и напоминают нам о своем смещении в базе.

Все это (да прос­тит авто­ров раз­работ­чик IDA!) напоми­нает сле­дующий анек­дот. Пой­мали тузем­цы нем­ца, аме­рикан­ца и укра­инца и говорят им: мол, или отку­пай­тесь чем‑нибудь, или съедим. На откуп пред­лага­ется: мил­лион дол­ларов (толь­ко не спра­шивай­те, зачем тузем­цам мил­лион дол­ларов, — может, кос­тер жечь), сто щел­банов или съесть мешок соли. Ну, аме­рика­нец дос­тает сотовый, зво­нит кому‑то… Прип­лыва­ет катер с мил­лионом дол­ларов, и аме­рикан­ца бла­гопо­луч­но отпуска­ют. Немец в это вре­мя геро­ичес­ки съеда­ет мешок соли, и его полумер­тво­го спус­кают на воду. Укра­инец же ел соль, ел‑ел, две тре­ти съел, не выдер­жал и говорит: а, лад­но, чер­ти, бей­те щел­баны. Бьет вождь его, и толь­ко девянос­то уда­ров отщелкал, тот не выдер­жал и говорит: да нате мил­лион, подави­тесь!

Так и с IDA, посим­воль­ное срав­нение не до кон­ца, а толь­ко трид­цати двух бай­тов, под­счет CRC не для всей фун­кции, а сколь­ко слу­чай на душу положит, наконец, пос­ледний клю­чевой байт — и тот‑то «клю­чевой», да не сов­сем. Дело в том, что мно­гие фун­кции сов­пада­ют байт в байт, но совер­шенно раз­личны по наз­ванию и наз­начению. Не веришь? Тог­да как тебе пон­равит­ся сле­дующее:

read: write:

sub rsp, 28h sub rsp, 28h
call _read call _write
add rsp, 28h add rsp, 28h
retn retn
 

Тут без ана­лиза переме­щаемых эле­мен­тов никак не обой­тись! При­чем это не какой‑то спе­циаль­но надуман­ный при­мер — подоб­ных фун­кций очень мно­го. В час­тнос­ти, биб­лиоте­ки от Embarcadero (в прош­лом от Borland) ими так и кишат. Поэто­му в былые вре­мена IDA час­то «спо­тыка­лась» и впа­дала в гру­бые ошиб­ки. Тем не менее сей­час IDA замет­но воз­мужала и уже не стра­дает дет­ски­ми боляч­ками. Для при­мера скор­мим ком­пилято­ру C++Builder такую фун­кцию:

void demo()
{
printf("DERIVEDn");
}
 

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

.text:0000000140001000 void demo(void) proc near
.text:0000000140001000 sub rsp, 28h
.text:0000000140001004 lea rcx, _Format ; "DERIVED\n"
.text:000000014000100B call printf
.text:0000000140001010 add rsp, 28h
.text:0000000140001014 retn
.text:0000000140001014 void demo(void) endp
 

То есть дизас­сем­блер пра­виль­но идентифицировал имя фун­кции. Но так быва­ет далеко не всег­да и не со все­ми биб­лиотеч­ными фун­кци­ями. А ког­да проб­лемы воз­ника­ют с ними, кодоко­пате­лю ана­лизи­ровать ста­новит­ся слож­новато. Быва­ет, сидишь, тупо уста­вив­шись в лис­тинг дизас­сем­бле­ра, и никак не можешь понять: что же этот фраг­мент дела­ет? И толь­ко потом обна­ружи­ваешь — одна или нес­коль­ко фун­кций опоз­наны неп­равиль­но!

Для умень­шения количес­тва оши­бок IDA пыта­ется по стар­товому коду рас­познать ком­пилятор, под­гру­жая толь­ко биб­лиоте­ку его сиг­натур. Из это­го сле­дует, что «осле­пить» IDA очень прос­то — дос­таточ­но слег­ка видо­изме­нить стар­товый код. Пос­коль­ку он, по обык­новению, пос­тавля­ется вмес­те с ком­пилято­ром в исходных тек­стах, сде­лать это будет нет­рудно. Впро­чем, хва­тит и изме­нения одно­го бай­та в начале startup-фун­кции. И все, хакер скле­ит лас­ты!

К счастью, в IDA пре­дус­мотре­на воз­можность руч­ной заг­рузки базы сиг­натур (FILELoad fileFLIRT signature file), но поп­робуй‑ка вруч­ную опре­делить, сиг­натуры какой имен­но вер­сии биб­лиоте­ки тре­бует­ся заг­ружать! Наугад — слиш­ком дол­го… Хорошо, если удас­тся визу­аль­но опоз­нать ком­пилятор. Опыт­ным иссле­дова­телям это обыч­но уда­ется, так как каж­дый из них (ком­пилятор, а не иссле­дова­тель) име­ет свой уни­каль­ный «почерк». К тому же сущес­тву­ет прин­ципи­аль­ная воз­можность исполь­зования биб­лиотек из пос­тавки одно­го ком­пилято­ра в прог­рамме, ском­пилиро­ван­ной дру­гим ком­пилято­ром. Сло­вом, будь готов к тому, что в один прек­расный момент ты можешь стол­кнуть­ся с необ­ходимостью самос­тоятель­но идентифицировать биб­лиотеч­ные фун­кции. Решение этой задачи сос­тоит из трех эта­пов. Пер­вое — опре­деле­ние самого фак­та «биб­лиотеч­ности» фун­кции, вто­рое — опре­деле­ние про­исхожде­ния биб­лиоте­ки и третье — иден­тифика­ция фун­кции по этой биб­лиоте­ке.

Учи­тывая, что лин­кер обыч­но рас­полага­ет фун­кции в поряд­ке перечис­ления obj-модулей и биб­лиотек, а боль­шинс­тво прог­раммис­тов ука­зыва­ют сна­чала собс­твен­ные obj-модули, а биб­лиоте­ки — потом (кста­ти, так же пос­тупа­ют и ком­пилято­ры, самос­тоятель­но вызыва­ющие лин­кер пос­ле окон­чания работы), мож­но зак­лючить: идентифицированые биб­лиотеч­ные фун­кции раз­меща­ются в кон­це прог­раммы, а собс­твен­но ее код — в начале. Конеч­но, из это­го пра­вила есть исклю­чения, но все же сра­баты­вает оно дос­таточ­но час­то.

Рас­смот­рим, к при­меру, струк­туру кон­соль­ной вер­сии обще­извес­тной прог­раммы pkzip.exe пос­ледней на дан­ный момент 14-й вер­сии. Она сущес­твен­но лег­че, чем ее окон­ная вер­сия. Ее вывод в кон­соли име­ет сле­дующий вид.

Консольный вывод приложения pkzipc.exe

Кон­соль­ный вывод при­ложе­ния pkzipc.exe

Во вре­мя заг­рузки при­ложе­ния в IDA та дела­ет пред­положе­ние, что это ROM для SNES. Забав­но, но выберем AMD64, если, конеч­но, уста­нов­лена 64-раз­рядная вер­сия ути­литы.

Загрузка pkzipc.exe в IDA

Заг­рузка pkzipc.exe в IDA

На диаг­рамме, пос­тро­енной IDA Pro 7.2, вид­но, что все биб­лиотеч­ные фун­кции находят­ся в сег­менте кода сре­ди обыч­ных фун­кций до начала сег­мента дан­ных.

Диаграмма для pkzipc.exe

Ди­аграмма для pkzipc.exe

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

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

Ес­ли не рас­позна­на ни одна фун­кция, ско­рее все­го, IDA не сумела опоз­нать ком­пилятор или исполь­зовались неиз­вес­тные ей вер­сии биб­лиотек. Для при­мера под­сунем IDA дру­гой популяр­ный архи­ватор — peazip.exe, который, как извес­тно, написан на объ­ектном пас­кале.

Диаграмма для peazip.exe

Ди­аграмма для peazip.exe

На диаг­рамме вид­но, что IDA не опоз­нала ни одной биб­лиотеч­ной фун­кции! Вот так новость! Смот­рим, какой ком­пилятор опре­делен дизас­сем­бле­ром: «Plan FLIRT signature: SEH for vc64 7-14». Кар­тина про­ясня­ется: IDA неп­равиль­но опре­дели­ла ком­пилятор! Но что она ска­жет, если мы пред­ложим ей pkzipw.exe (for Windows)?

Диаграмма для pkzipw.exe

Ди­аграмма для pkzipw.exe

Го­раз­до луч­ше! Мы зна­ем, что пре­пари­руемое при­ложе­ние написа­но на C++ и откомпи­лиро­вано в Visual Studio. Поэто­му IDA выда­ет снос­ные резуль­таты. Меж­ду тем тех­ника рас­позна­вания ком­пилято­ров — раз­говор осо­бый, а вот рас­позна­ние вер­сий биб­лиотек — это то, чем мы сей­час и зай­мем­ся.

Определение библиотек и их версий

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

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

Тупик, да не сов­сем! Конечно, больше нельзя будет автоматически восстанавливать имена функций, но все же есть надежда, что назначение функций будет быстро выяснено. Имена функций Windows API, которые вызываются из библиотек, позволяют идентифицировать хотя бы категорию библиотеки (например, работа с файлами, памятью, графикой). Как обычно, математические функции содержат множество инструкций сопроцессора.

Ди­зас­сем­бли­рова­ние очень похоже на раз­гадыва­ние крос­свор­да (хотя не факт, что хакеры любят раз­гадывать крос­свор­ды) — неиз­вес­тные сло­ва уга­дыва­ются за счет извес­тных. При­мени­тель­но к дан­ному слу­чаю в некото­рых кон­тек­стах наз­вание фун­кции вытека­ет из ее исполь­зования. Нап­ример, зап­рашива­ем у поль­зовате­ля пароль, переда­ем его фун­кции X вмес­те с эта­лон­ным паролем, если резуль­тат завер­шения нулевой — пишем «пароль ОК», и, соот­ветс­твен­но, наобо­рот. Не под­ска­зыва­ет ли твоя инту­иция, что фун­кция X не что иное, как strcmp? Конеч­но, это прос­тей­ший слу­чай, но, стол­кнув­шись с нез­накомой под­прог­раммой, никог­да не спе­ши впа­дать в отча­яние и при­ходить в ужас от ее «монс­тру­озности». Прос­мотри все вхож­дения, обра­щая вни­мания, кто вызыва­ет ее, ког­да и сколь­ко раз.

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

Дру­гие зацеп­ки: аргу­мен­ты и кон­стан­ты. Ну, с аргу­мен­тами все более или менее ясно. Если фун­кция получа­ет стро­ку, то это, оче­вид­но, фун­кция из биб­лиоте­ки работы со стро­ками, а если вещес­твен­ное зна­чение — воз­можно, фун­кция матема­тичес­кой биб­лиоте­ки. Количес­тво и тип аргу­мен­тов (если их учи­тывать) весь­ма сужа­ют круг воз­можных кан­дидатов. С кон­стан­тами же еще про­ще — очень мно­гие фун­кции при­нима­ют в качес­тве аргу­мен­та флаг, допус­кающий огра­ничен­ное количес­тво зна­чений. За исклю­чени­ем битовых фла­гов, которые все похожи друг на дру­га, доволь­но час­то встре­чают­ся уни­каль­ные зна­чения, пус­кай не однознач­но иден­тифици­рующие фун­кцию, но все рав­но сужа­ющие круг «подоз­рева­емых». Да и сами фун­кции могут содер­жать харак­терные кон­стан­ты. Ска­жем, встре­тив стан­дар­тный полином для под­сче­та CRC, мож­но быть уве­рен­ным, что «под­следс­твен­ная» вычис­ляет кон­троль­ную сум­му…

Мне могут воз­разить: мол, все это час­тнос­ти. Воз­можно. Но, идентифицировав  часть фун­кций, наз­начения осталь­ных мож­но вычис­лить «от про­тив­ного». И уж по край­ней мере понять, что это за биб­лиоте­ка такая и где ее искать.

Заключение

Идентификация алгоритмов (то есть наз­начения фун­кции) становится намного проще, если знать сами эти алгоритмы. В частности, код, выполняющий LZ-сжатие (декомпрессию), настолько типичен, что его можно распознать с первого взгляда. Достаточно знать этот механизм упаковки. Напротив, если вы об этом не подозреваете, проанализировать программу будет непросто! Зачем изобретать велосипед, если можно взять уже готовое? Хотя есть мнение, что хакер — это прежде всего хакер, а затем программист (а почему он должен уметь программировать?). В жизни все наоборот — программист, который не умеет программировать, будет жить — он там много библиотек, подключите — и все заработает! С другой стороны, хакеру необходимо знать основы, без них далеко не уплывешь (конечно, серийный номер программы можно сломать без сложных математических расчетов).

Очевидно, библиотеки были созданы для этой цели, чтобы избавить разработчиков от необходимости копаться в этих областях, без которых они чувствуют себя хорошо. Увы, у исследователей программы нет простых средств, им приходится думать руками и головой, и даже … пятая точка опоры, спинной мозг, — единственный способ демонтировать серьезные программы. . Иногда готовое решение приходит в поезде или во сне …

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

 

 

 

 

 

 

Click to rate this post!
[Total: 0 Average: 0]

Leave a reply:

Your email address will not be published.