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

«Фундаментальные основы хакерства»

Мы пуб­лику­ем эту статью в честь начала пред­заказов обновлен­ной вер­сии кни­ги Кри­са Кас­пер­ски «Фун­дамен­таль­ные осно­вы хакерс­тва», получив­шей под­заголо­вок «Ана­лиз прог­рамм в сре­де Win64».

В пер­вой час­ти статьи ты вкрат­це поз­накомишь­ся с основны­ми инс­тру­мен­тами отладки, а вто­рая — это отры­вок из кни­ги. Он уже пуб­ликовал­ся в «Хакере», но теперь дос­тупен без плат­ной под­писки.

Ку­пить кни­гу ты можешь на сай­те изда­тель­ства «Солон‑пресс».

При­меча­ние: Текст статьи обновлен с уче­том работы с x86-64.

 

Этап 1

Ес­ли работа ведет­ся в Linux, сре­ди инс­тру­мен­тов повер­хностно­го ана­лиза мож­но отме­тить такие при­ложе­ния:

  • file опре­деля­ет тип фай­ла, ана­лизи­руя его поля;
  • readelf отоб­ража­ет информа­цию о сек­циях фай­ла;
  • ldd выводит спи­сок динами­чес­ких биб­лиотек, от которых зависит дан­ный исполня­емый файл;
  • nm выводит спи­сок декори­рован­ных имен фун­кций, которые соз­дают­ся ком­пилято­рами язы­ков, под­держи­вающих перег­рузку фун­кций. Затем ути­лита может декоди­ровать эти име­на;
  • c++filt пре­обра­зует декори­рован­ные име­на фун­кций в пер­воначаль­ные с уче­том переда­ваемых и получа­емых аргу­мен­тов;
  • strings выводит стро­ки, содер­жащи­еся в фай­ле, с уче­том задан­ного шаб­лона.

В Windows со мно­гими из этих задач справ­ляет­ся ути­лита dumpbin, вхо­дящая в пос­тавку Visual Studio.

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

 

Этап 2

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

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

В качес­тве отладчи­ка в Linux мож­но вос­поль­зовать­ся ста­рым доб­рым GDB либо средс­тва­ми трас­сиров­ки в Radare2. В Windows выбор тоже невелик: OllyDbg пос­тепен­но уста­рева­ет и не обновля­ется. В нем отла­живать при­ложе­ния мож­но толь­ко в режиме поль­зовате­ля. Пос­ле смер­ти SoftICE единс­твен­ным нор­маль­ным отладчи­ком в Windows стал WinDbg. В нем мож­но отла­живать драй­веры на уров­не ядра.

Ста­тичес­кие дизас­сем­бле­ры тоже делят­ся на две груп­пы: линей­ные и рекур­сивные. Пер­вые переби­рают все сег­менты кода в дво­ичном фай­ле, декоди­руют и пре­обра­зуют их в мне­мони­ки язы­ка ассем­бле­ра. Так работа­ет боль­шинс­тво прос­тых дизас­сем­бле­ров, вклю­чая objdump и dumpbin. Может показать­ся, что так и дол­жно быть. Одна­ко, ког­да сре­ди исполня­емо­го кода встре­чают­ся дан­ные, воз­ника­ет проб­лема: их не надо пре­обра­зовы­вать в коман­ды, но линей­ный дизас­сем­блер не в сос­тоянии отли­чить их от кода! Мало того, пос­ле того как сег­мент дан­ных завер­шится, дизас­сем­блер будет рас­син­хро­низи­рован с текущей позиции кода.

С дру­гой сто­роны, рекур­сивные дизас­сем­бле­ры, такие как IDA Pro, Radare2 и Ghidra, ведут себя ина­че. Они дизас­сем­бли­руют код в той же пос­ледова­тель­нос­ти, в которой его будет исполнять про­цес­сор. Поэто­му они акку­рат­но обхо­дят дан­ные, и в резуль­тате боль­шинс­тво команд бла­гопо­луч­но рас­позна­ется.

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

 

Этап 3

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

Су­щес­тву­ет мно­жес­тво HEX-редак­торов на любой вкус и цвет, нап­ример 010 Editor, HexEdit, HIEW. Воору­жай­ся одним из них, и тебе оста­нет­ся толь­ко переза­писать коман­ду по най­ден­ному адре­су. Но в дво­ичном фай­ле силь­но не раз­бежишь­ся! Сущес­тву­ющий код не дает прос­тора для манипу­ляций, поэто­му нам при­ходит­ся уме­щать­ся в име­ющем­ся прос­транс­тве.

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

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

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

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

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

Од­нако во вре­мя динами­чес­кого осна­щения, так как прог­рамма выпол­няет­ся «под наб­людени­ем», ее про­изво­дитель­ность замет­но пада­ет. Для динами­чес­кого осна­щения исполь­зуют­ся сис­темы DynamoRIO (сов­мес­тный про­ект HP и MIT) и Pin (Intel).

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

 

Практический взлом

Что­бы поуп­ражнять­ся на прак­тике, про­ведем ана­лиз и рас­кусим эле­мен­тарную защиту.

 

Шаг первый. Разминочный

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

Дос­тоинс­тво такой защиты — край­не прос­тая прог­рам­мная реали­зация. Ее ядро сос­тоит фак­тичес­ки из нес­коль­ких строк, которые на язы­ке С/C++ мож­но записать так:

if (strcmp(введенный пароль, эталонный пароль))
{/* Пароль неверен */}
else
{/* Пароль ОК*/}

Да­вай допол­ним этот код про­цеду­рами зап­роса пароля и вывода резуль­татов срав­нения, а затем испы­таем получен­ную прог­рамму на проч­ность, т. е. на стой­кость к взло­му. В Visual Studio соз­дай кон­соль­ный про­ект и ском­пилируй сле­дующий код, уста­новив целевую плат­форму x64.

При­мер прос­тей­шей сис­темы аутен­тифика­ции:

#include "stdafx.h"
// Простейшая система аутентификации
// Посимвольное сравнение пароля
#include <stdio.h>
#include <string.h>
#define PASSWORD_SIZE 100
#define PASSWORD "myGOODpassword\n"
// Этот перенос нужен затем, чтобы
// не выкусывать перенос из строки,
// введенной пользователем
int main()
{
// Счетчик неудачных попыток аутентификации
int count=0;
// Буфер для пароля, введенного пользователем
char buff[PASSWORD_SIZE];
// Главный цикл аутентификации
for(;;)
{
// Запрашиваем и считываем пользовательский пароль
printf("Enter password:");
fgets(&buff[0],PASSWORD_SIZE,stdin);
// Сравниваем оригинальный и введенный пароль
if (strcmp(&buff[0],PASSWORD))
// Если пароли не совпадают «ругаемся»
printf("Wrong password\n");
// Иначе (если пароли идентичны)
// выходим из цикла аутентификации
else break;
// Увеличиваем счетчик неудачных попыток
// аутентификации и, если все попытки
// исчерпаны, завершаем программу
if (++count>3) return -1;
}
// Раз мы здесь, то пользователь ввел правильный пароль
printf("Password OK\n");
}

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

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

Раз эта­лон­ный пароль хра­нит­ся в теле прог­раммы, то, если он не зашиф­рован каким‑нибудь хит­рым обра­зом, его мож­но обна­ружить три­виаль­ным прос­мотром дво­ично­го кода прог­раммы. Переби­рая все встре­тив­шиеся в ней тек­сто­вые стро­ки, начиная с тех, что боль­ше все­го сма­хива­ют на пароль, мы очень быс­тро под­берем нуж­ный ключ и откро­ем им прог­рамму! При­чем область прос­мотра мож­но сущес­твен­но сузить — в подав­ляющем боль­шинс­тве слу­чаев ком­пилято­ры раз­меща­ют все ини­циали­зиро­ван­ные перемен­ные в сег­менте дан­ных (в PE-фай­лах он раз­меща­ется в сек­ции .data или .rdata). Исклю­чение сос­тавля­ют, пожалуй, ран­ние ком­пилято­ры Borland с их мани­акаль­ной любовью всо­вывать тек­сто­вые стро­ки в сег­мент кода — непос­редс­твен­но по мес­ту их вызова. Это упро­щает сам ком­пилятор, но порож­дает мно­жес­тво проб­лем. Сов­ремен­ные опе­раци­онные сис­темы, в отли­чие от ста­руш­ки MS-DOS, зап­реща­ют модифи­кацию кодово­го сег­мента, и все раз­мещен­ные в нем перемен­ные дос­тупны лишь для чте­ния. К тому же на про­цес­сорах с раз­дель­ной сис­темой кеширо­вания они «засоря­ют» кодовый кеш, попадая туда при упрежда­ющем чте­нии, но при пер­вом же обра­щении к ним вновь заг­ружа­ются из мед­ленной опе­ратив­ной памяти в кеш дан­ных. В резуль­тате — тор­моза и падение про­изво­дитель­нос­ти.

Что ж, пусть это будет сек­ция дан­ных! Оста­ется толь­ко най­ти удоб­ный инс­тру­мент для прос­мотра дво­ично­го фай­ла. Мож­но, конеч­но, нажать кла­вишу F3 в сво­ей любимой обо­лоч­ке (FAR, DOS Navigator) и, при­давив кир­пичом Page Down, любовать­ся бегущи­ми цифер­ками до тех пор, пока не надо­ест.

Мож­но вос­поль­зовать­ся любым HEX-редак­тором (qView, HIEW... — кому какой по вку­су), но в дан­ном слу­чае, по сооб­ражени­ям наг­ляднос­ти, при­веден резуль­тат работы ути­литы dumpbin из штат­ной пос­тавки Microsoft Visual Studio. Запус­тить dumpbin мож­но из Developer Command Prompt или дру­гой кон­соли.

Нат­равим ути­литу на исполня­емый файл нашей прог­раммы, содер­жащей пароль, и поп­росим ее рас­печатать содер­жащую толь­ко для чте­ния ини­циали­зиро­ван­ные дан­ные сек­цию — rdata (ключ /SECTION:.rdata) в «сыром» виде (ключ /RAWDATA:BYTES), ука­зав зна­чок > для перенап­равле­ния вывода в файл (ответ прог­раммы занима­ет мно­го мес­та, и на экра­не помеща­ется один лишь «хвост»).

dumpbin /RAWDATA:BYTES /SECTION:.rdata passCompare1.exe > rdata.txt
0000000140002240: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000000140002250: 40 40 00 40 01 00 00 00 E0 40 00 40 01 00 00 00 @@.@....□@.@....
0000000140002260: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF □□□□□□□□□□□□□□□□
0000000140002270: 45 6E 74 65 72 20 70 61 73 73 77 6F 72 64 3A 00 Enter password:.
0000000140002280: 6D 79 47 4F 4F 44 70 61 73 73 77 6F 72 64 0A 00 myGOODpassword..
0000000140002290: 57 72 6F 6E 67 20 70 61 73 73 77 6F 72 64 0A 00 Wrong password..
00000001400022A0: 50 61 73 73 77 6F 72 64 20 4F 4B 0A 00 00 00 00 Password OK.....
00000001400022B0: 40 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 @...............
00000001400022C0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................

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

Один из спо­собов сде­лать это — насиль­но помес­тить эта­лон­ный пароль в собс­твен­норуч­но выб­ранную нами сек­цию. Такая воз­можность не пре­дус­мотре­на стан­дартом, и потому каж­дый раз­работ­чик ком­пилято­ра (стро­го говоря, не ком­пилято­ра, а лин­кера, но это не суть важ­но) волен реали­зовы­вать ее по‑сво­ему или не реали­зовы­вать вооб­ще. В Microsoft Visual C++ для этой цели пре­дус­мотре­на спе­циаль­ная праг­ма data_seg, ука­зыва­ющая, в какую сек­цию помещать сле­дующие за ней ини­циали­зиро­ван­ные перемен­ные. Неини­циали­зиро­ван­ные перемен­ные по умол­чанию рас­полага­ются в сек­ции .bss и управля­ются праг­мой bss_seg соот­ветс­твен­но.

В при­мере аутен­тифика­ции выше перед фун­кци­ей main добавим новую сек­цию, в которой будем хра­нить наш пароль (для удобс­тва я соз­дал отдель­ный про­ект — passCompare2):

// С этого момента все инициализированные
// переменные будут размещаться в секции .kpnc
#pragma data_seg(".kpnc")
#define PASSWORD_SIZE 100
#define PASSWORD "myGOODpassword\n"
char passwd[] = PASSWORD;
#pragma data_seg()

Внут­ри фун­кции main про­ини­циали­зиру­ем мас­сив:

// Теперь все инициализированные переменные
// вновь будут размещаться в секции по умолчанию,
// т. е. .rdata
char buff[PASSWORD_SIZE]="";

Нем­ного изме­нилось усло­вие срав­нения строк в цик­ле:

if (strcmp(&buff[0],&passwd[0]))

Нат­равим ути­литу dumpbin на новый исполня­емый файл:

dumpbin /RAWDATA:BYTES /SECTION:.rdata passCompare2.exe > rdata.txt
0000000140002230: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000000140002240: 40 30 00 40 01 00 00 00 E0 30 00 40 01 00 00 00 @0.@....□0.@....
0000000140002250: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF □□□□□□□□□□□□□□□□
0000000140002260: 45 6E 74 65 72 20 70 61 73 73 77 6F 72 64 3A 00 Enter password:.
0000000140002270: 57 72 6F 6E 67 20 70 61 73 73 77 6F 72 64 0A 00 Wrong password..
0000000140002280: 50 61 73 73 77 6F 72 64 20 4F 4B 0A 00 00 00 00 Password OK.....
0000000140002290: 40 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 @...............
00000001400022A0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................

Ага, теперь в сек­ции дан­ных пароля нет и хакеры «отды­хают»! Но не спе­ши с вывода­ми. Давай сна­чала выведем на экран спи­сок всех сек­ций, име­ющих­ся в фай­ле:

dumpbin passCompare2.exe
Summary
1000 .data
1000 .kpnc
1000 .pdata
1000 .reloc
1000 .rsrc
1000 .text

Нес­тандар­тная сек­ция .kpnc сра­зу же при­ковы­вает к себе вни­мание. А ну‑ка пос­мотрим, что там в ней.

dumpbin /SECTION:.kpnc /RAWDATA passCompare2.exe
RAW DATA #5
0000000140005000: 6D 79 47 4F 4F 44 70 61 73 73 77 6F 72 64 0A 00 myGOODpassword..

Вот он, пароль! Спря­тали, называ­ется... Мож­но, конеч­но, извра­тить­ся и засунуть сек­ретные дан­ные в сек­цию неини­циали­зиро­ван­ных дан­ных (.bss) или даже сек­цию кода (.text) — не все там догада­ются поис­кать, а работос­пособ­ность прог­раммы такое раз­мещение не нарушит. Но не сто­ит забывать о воз­можнос­ти авто­мати­зиро­ван­ного поис­ка тек­сто­вых строк в дво­ичном фай­ле. В какой бы сек­ции ни содер­жался эта­лон­ный пароль, филь­тр без тру­да его най­дет (единс­твен­ная проб­лема — опре­делить, какая из мно­жес­тва тек­сто­вых строк пред­став­ляет собой иско­мый ключ; воз­можно, пот­ребу­ется переб­рать с десяток‑дру­гой потен­циаль­ных кан­дидатов).

 

Шаг второй. Знакомство с дизассемблером

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

Хак­нуть, говори­те?! Что ж, это нес­ложно! Куда проб­лематич­нее опре­делить­ся, чем имен­но ее хакать. Инс­тру­мен­тарий хакеров чрез­вычай­но раз­нооб­разен, чего тут толь­ко нет: и дизас­сем­бле­ры, и отладчи­ки, и API-, и message-шпи­оны, и монито­ры обра­щений к фай­лам (пор­там, реес­тру), и рас­паков­щики исполня­емых фай­лов, и... Слож­новато начина­юще­му кодоко­пате­лю со всем этим хозяй­ством разоб­рать­ся!

Впро­чем, шпи­оны, монито­ры, рас­паков­щики — вто­рос­тепен­ные ути­литы зад­него пла­на, а основное ору­жие взлом­щика — отладчик (динами­чес­кий дизас­сем­блер) и дизас­сем­блер (ста­тичес­кий).

Итак, дизас­сем­блер при­меним для иссле­дова­ния откомпи­лиро­ван­ных прог­рамм и час­тично при­годен для ана­лиза псев­доком­пилиро­ван­ного кода. Раз так, он дол­жен подой­ти для вскры­тия пароль­ной защиты passCompare1.exe. Весь воп­рос в том, какой дизас­сем­блер выб­рать.

Не все дизас­сем­бле­ры оди­нако­вы. Есть сре­ди них и «интеллек­туалы», авто­мати­чес­ки рас­позна­ющие мно­гие конс­трук­ции, как то: про­логи и эпи­логи фун­кций, локаль­ные перемен­ные, перек­рес­тные ссыл­ки и т. д., а есть и «прос­таки», чьи спо­соб­ности огра­ниче­ны одним лишь перево­дом машин­ных команд в ассем­блер­ные инс­трук­ции.

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

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

dumpbin /SECTION:.text /DISASM passCompare1.exe > code-text.txt

Заг­лянем еще раз в сек­цию дан­ных (или в дру­гую — в зависи­мос­ти от того, где хра­нит­ся пароль).

За­пом­ним най­ден­ный пароль: myGOODpassword. В зависи­мос­ти от вер­сии и нас­тро­ек дизас­сем­бле­ра dumpbin ини­циали­зиро­ван­ные перемен­ные, к которым обра­щает­ся код, могут быть пред­став­лены по‑раз­ному: на их мес­те могут быть или сим­воль­ные кон­стан­ты, или непос­редс­твен­но шес­тнад­цатерич­ное сме­щение. Поп­робу­ем най­ти выяв­ленный ранее пароль в дизас­сем­бли­рован­ном лис­тинге три­виаль­ным кон­текс­тным поис­ком с помощью любого тек­сто­вого редак­тора.

00000001400010B2: 48 8D 15 C7 11 00 lea rdx, [??_C@_0BA@PCMCJPMK@myGOODpassword?6@]
00
00000001400010B9: 48 8D 4C 24 20 lea rcx,[rsp+20h]
00000001400010BE: E8 8C 0D 00 00 call strcmp
00000001400010C3: 85 C0 test eax,eax
00000001400010C5: 74 58 je 000000014000111F

Есть сов­падение! Цен­траль­ная часть это­го лис­тинга — вызов фун­кции strcmp. В качес­тве парамет­ров ей переда­ются две стро­ки, раз­мещен­ные в регис­трах RCX и RDX. Как мы видим, в пер­вой стро­ке лис­тинга в RDX помеща­ется эта­лон­ный пароль, а во вто­рой стро­ке в регистр RCX копиру­ется зна­чение из локаль­ной перемен­ной — оче­вид­но, стро­ка, вве­ден­ная поль­зовате­лем.

В резуль­тате выпол­нения, если стро­ки рав­ны, фун­кция strcmp воз­вра­щает 0 в регис­тре RAX или EAX, как в дан­ном слу­чае. Если же стро­ки раз­лича­ются, воз­вра­щает­ся зна­чение, отли­чающееся от нуля.

В сле­дующей стро­ке лис­тинга инс­трук­ция test про­веря­ет зна­чение в регис­тре EAX на равенс­тво нулю. Если ответ положи­тель­ный, что может про­изой­ти, толь­ко ког­да стро­ки оди­нако­вые, флаг ZF уста­нав­лива­ется в еди­ницу.

Сле­дующая инс­трук­ция JE осу­щест­вля­ет переход по ука­зан­ному адре­су толь­ко в том слу­чае, ког­да флаг ZF равен еди­нице. Пос­мотрим, на какой код ука­зыва­ет этот адрес:

000000014000111F: 48 8D 0D 7A 11 00 lea rcx, [??_C@_0N@MBEFNJID@Password?5OK?6@]
00
0000000140001126: E8 E5 FE FF FF call printf

Это код вывода стро­ки Password OK. В регистр RCX помеща­ется ука­зан­ная стро­ка, затем зна­чение это­го регис­тра переда­ется фун­кции printf, которая выводит передан­ную стро­ку на экран. Из это­го сле­дует, что перед нами валид­ная ветвь прог­раммы.

Рас­смот­рим обратный исход, ког­да стро­ки раз­лича­ются, strcmp воз­вра­щает ненуле­вое зна­чение, флаг ZF оста­ется сбро­шен­ным (рав­ным нулю) и переход не осу­щест­вля­ется. Тог­да мы про­вали­ваем­ся в такой код:

00000001400010C7: 66 0F 1F 84 00 00 nop word ptr [rax+rax]
00 00 00
00000001400010D0: 48 8D 0D B9 11 00 lea rcx, [??_C@_0BA@EHHIHKNJ@Wrong?5password?6@]
00
00000001400010D7: E8 34 FF FF FF call printf

Он выводит стро­ку о неп­равиль­ном пароле — тоже с помощью фун­кции printf, в парамет­ре при­нима­ющей стро­ку для отоб­ражения.

Опе­ратив­ные сооб­ражения сле­дующие: если коман­ду JE заменить JNE, то прог­рамма отвер­гнет истинный пароль как неп­равиль­ный, а любой неп­равиль­ный пароль вос­при­мет как истинный. А если TEST EAX,EAX заменить XOR EAX,EAX, то пос­ле исполне­ния этой коман­ды регистр EAX будет всег­да равен нулю, какой бы пароль ни вво­дил­ся.

Де­ло за малым — най­ти эти самые бай­тики в исполня­емом фай­ле и малость под­пра­вить их.

 

Хирургическое вмешательство

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

Ну, с «выкиды­вани­ем зап­частей» спра­вить­ся как раз таки прос­то — дос­таточ­но забить код коман­дами NOP (опкод которой 0x90, а вов­се не 0x0, как почему‑то дума­ют мно­гие начина­ющие кодоко­пате­ли), т. е. пус­той опе­раци­ей (вооб­ще‑то NOP — это прос­то дру­гая фор­ма записи инс­трук­ции XCHG EAX,EAX, если инте­рес­но). С «раз­движ­кой» куда слож­нее! К счастью, в PE-фай­лах всег­да при­сутс­тву­ет мно­жес­тво «дыр», оставших­ся от вырав­нивания, в них‑то и мож­но раз­местить свой код или свои дан­ные.

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

При­ходит­ся резать прог­рамму вжи­вую. Лег­че все­го это делать с помощью ути­литы HIEW, которая «перева­рива­ет» PE-фор­мат фай­лов и упро­щает тем самым поиск нуж­ного фраг­мента. Обра­ти вни­мание, так как мы работа­ем в 64-бит­ной сре­де, подой­дет толь­ко одна из новых вер­сий прог­раммы с под­дер­жкой фай­лов PE32+. Нап­ример, я исполь­зую вер­сию 8.67, прек­расно ужи­вающуюся с Windows 10. Запус­тим ее, ука­зав имя фай­ла в коман­дной стро­ке (hiew32 passCompare1.exe), двой­ным нажати­ем кла­виши Enter, перек­лючим­ся в режим ассем­бле­ра и при помощи кла­виши F5 перей­дем к тре­буемо­му адре­су. Как мы пом­ним, коман­да TEST, про­веря­ющая резуль­тат на равенс­тво нулю, рас­полага­лась по адре­су 0x1400010C3.

Что­бы HIEW мог отли­чить адрес от сме­щения в самом фай­ле, пред­варим его сим­волом точ­ки: .1400010C3.

00000001400010C3: 85 C0 test eax,eax
00000001400010C5: 74 58 je 000000014000111F

Ага, как раз то, что нам надо! Наж­мем кла­вишу F3 для перево­да HIEW в режим прав­ки, под­ведем кур­сор к коман­де TEST EAX, EAX и, нажав кла­вишу Enter, заменим ее коман­дой XOR EAX,EAX.

00000001400010C3: 31 C0 xor eax,eax
00000001400010C5: 74 58 je 000000014000111F
HIEW в режиме правки ассемблерной команды
HIEW в режиме прав­ки ассем­блер­ной коман­ды

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

>passCompare1
Enter password:Привет, шляпа!
Password OK

По­лучи­лось! Защита пала! Хорошо, а как бы мы дей­ство­вали, если бы у нас не было HIEW? Тог­да, воору­жив­шись каким‑нибудь шес­тнад­цатерич­ным редак­тором (нап­ример, HxD), приш­лось бы при­бег­нуть к кон­текс­тно­му поис­ку.

Заг­рузим подопыт­ный файл в редак­тор. Конеч­но, если пытать­ся най­ти пос­ледова­тель­ность 85 C0 — код коман­ды TEST EAX, EAX, ничего хороше­го из это­го не вый­дет — этих самых «тес­тов» в прог­рамме может быть нес­коль­ко сотен, а то и боль­ше. А вот адрес перехо­да, ско­рее все­го, во всех вет­ках прог­раммы раз­личен, и подс­тро­ка TEST EAX,EAX/je 000000014000111F име­ет хорошие шан­сы на уни­каль­ность. Поп­робу­ем най­ти в фай­ле соот­ветс­тву­ющий ей код: 85 C0 74 58. В HxD для это­го надо нажать Ctrl-F, что­бы открыть окно поис­ка, затем перей­ти на вклад­ку «Hex-зна­чения» и искать уже отсю­да.

Оп‑с! Най­дено толь­ко одно вхож­дение, что нам, собс­твен­но, и нуж­но. Давай теперь поп­робу­ем модифи­циро­вать файл непос­редс­твен­но в HEX-режиме, не перехо­дя в ассем­блер. Возь­мем себе на замет­ку: инверсия млад­шего бита кода коман­ды при­водит к изме­нению усло­вия перехо­да на про­тиво­полож­ное, т. е. 74 JE75 JNE. Под­веди кур­сор к зна­чению 74 и вве­ди 75. Зна­чение будет выделе­но крас­ным. Далее надо сох­ранить резуль­тат работы. Готово!

Правка шестнадцатеричного кода с помощью HxD
Прав­ка шес­тнад­цатерич­ного кода с помощью HxD
Взломанная программа принимает любые пароли
Взло­ман­ная прог­рамма при­нима­ет любые пароли

Ра­бота­ет? В смыс­ле защита свих­нулась окон­чатель­но — не приз­нает истинные пароли, зато радос­тно при­ветс­тву­ет осталь­ные. Замеча­тель­но!

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

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

    Подписаться

  • Подписаться
    Уведомить о
    5 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии