Брудні рішення реверс-інжинірингу

image

Перед розробниками досить часто постає вибір — зробити все правильно, витративши на рішення завдання багато часу, або зробити так, щоб працювало, не особливо вдаючись у деталі того, як саме це вийшло. З боку замовника, зрозуміло, найбільш привабливою є якась золота середина, яка в даному випадку полягає одночасно і в хорошому розумінні програмістом виконаного таска, і в якомога меншій кількості витрачених на нього людино-годин. З розробниками теж не все так однозначно — з одного боку, розуміти, що відбувається в своєму власному коді, це цілком природне бажання (особливо якщо підтримка даного продукту також буде лежати на плечах), а з іншого боку, якщо результати роботи програми представлені в наочному вигляді (графіки / звукові або відео-фрагменти etc), розробка разова, і відділ тестування каже, що все добре, то чому б не проскроллить частину робочого часу Хабр, присвятивши час собі коханій?

Ближче до справи. одній з попередніх статей я вже згадував про програму під назвою «Говорилка». Незважаючи на назву, сама по собі вона нічого не озвучує, а лише є сполучною ланкою між користувачем і мовними движками, надаючи більш зручний інтерфейс і можливість конфігурації. Одним з найбільш популярних у вузьких колах движків є «Digalo 2000 text-to-speech engine» (далі — Digalo), посилання на який можна знайти на сайті «Говорилка». Як ви вже, напевно, здогадалися з тематик моїх попередніх статей, не всі з них так добре, і без багів тут також не обійшлося. На цей раз проблема проявилася при озвучуванні тексту «ааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааа». Трохи поекспериментувавши, я виявив, що при досягненні певної кількості «нерозривних» символів Digalo починає крашиться, пропонуючи налагодити свій процес. Ну, а що, чому б і ні?

Як протікав процес, і що з цього вийшло, читайте під катом (перед прочитанням цієї статті я настійно рекомендую ознайомитися з попередніми, які можна знайти, наприклад, тут).

Я думаю, зайве говорити, що исходников разом з Digalo не поставляється (більш того, скачати з офіційного сайту навіть бінарники мене цікавить версії вже не можна), так що нашими кращими друзями знову стануть дізассемблер, відладчик і hex-редактор, які, по суті, можна звести до однієї лише OllyDbg. Але перш ніж братися за вивчення дизассемблировааного лістингу, так-так, треба завантажити додаток і перевірити, не накрите воно протектором і не запаковано чи яким-небудь пакувальником.

Завантажуємо і встановлюємо Digalo, залазимо в директорію, куди він встановився, і досліджуємо виконуваний файл DiE і PEiD:

image

image

Нескладно помітити, що обидва аналізатора вирішили, що DIGALO_RUS.exe запакований PECompact'ом, і, в принципі, у нас немає особливих причин, щоб їм не вірити.

Незважаючи на те, що PECompact і ASPack (про який вже йшлося у одній з попередніх статей — абсолютно різні паковщік, принцип розпакування для них один і той же. Завантажуємо DIGALO_RUS.exe у OllyDbg, добегаем до інструкції PUSHFD, яка виконується відразу ж після першого JMP'а, відкриваємо Command Line за допомогою Alt-F1, ставимо хардварный бряк ESP-4 за допомогою команди hr esp-4, натискаємо F9 до тих пір, поки не опинимося на місці після виконання інструкції POPFD, добегаем до найближчого RETN'а, натискаємо F8 і опиняємося за адресою 0x0045B97B, який в даному випадку і є OEP:

image

Знімаємо дамп за допомогою плагіна OllyDump, залишаючи галочку на checkbox'і «Rebuild Import», перевіряємо працездатність досліджуваного додатка після розпакування і… Бачимо, що воно працює (зрозуміло, на тих рядках, які воно коректно обробляло і раніше).

Тепер перед нами постає важливе питання — як же можна налагодити цей мовної движок? Проблема полягає в тому, що падає він практично відразу після старту, відрізаючи можливість аттача до вже запущеного процесу. Що ж, тут є невелика хитрість — ми можемо поміняти перший байт, який знаходиться на OEP, на інструкцію INT3, яка в даному випадку (за відсутності підключеного до процесу відладчика) змусить ОС показати стандартне діалогове вікно з пропозицією налагодити процес в поточному JIT-налагоджувач. Робимо OllyDbg таким (Options -> Just-in-time debugging -> Make OllyDbg just-in-time-debugger) і замінюємо перший байт на OEP з 0x55 (PUSH EBP) 0xCC (INT3):

image

Зберігаємо зміни (right-click по вікну CPU -> Copy to executable -> All modifications -> Copy all -> right-click за відкрився вікна -> Save file), замінюємо оригінальний виконуваний файл і запускаємо консольну версію «Говорилка» з аргументом «ааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааа»:

image

Натискаємо кнопку «Debug the program», замінюємо INT3 назад PUSH EBP, натискаємо F9 і бачимо, що ми маємо справу з Access Violation:

image

Запускаємо програму ще раз, ставимо бряк за адресою, де відбувається Access Violation (в моєму випадку це 0x00428B9D), і намагаємося з'ясувати, як часто викликається це місце перед падінням. Виявляється, що бряк спрацьовує два рази перед тим, як впасти (після першого все нормально, а от в момент спрацьовування другого значення регістра ECX як раз містить адресу, звернення до якого і викликає дане виняток). Давайте запустимо трасування з цього місця після першого спрацьовування брейкпоинта і подивимося, що опиниться в вікні «Run trace» у разі успішної роботи програми (наприклад, при запуску говорилка з аргументом «Hello») і в разі падіння:

У разі падіння
Address Thread Command ; Registers and comments
Flushing gathered information
00428B9D 00002410 MOV EDX,DWORD PTR DS:[ECX+64] ; EDX=0051A820
00428BA0 00002410 LEA EAX,DWORD PTR DS:[EAX+EAX*2] ; EAX=00000003
00428BA3 00002410 MOV CL,BYTE PTR DS:[ESI] ; ECX=03007BA0
[...]
004303FB 00002410 CMP EDX,ECX
004303FD 00002410 JL digalo_r.00430282
00430282 00002410 MOV EDI,1 ; EDI=00000001
00430287 00002410 MOV EAX,DWORD PTR SS:[ESP+1C] ; EAX=00000028
0043028B 00002410 MOV EDX,DWORD PTR DS:[4A8BDC] ; EDX=004A8DC8
00430291 00002410 MOVSX ECX,BYTE PTR SS:[ESP+EAX+113] ; ECX=00000074

У випадку коректної роботи
Address Thread Command ; Registers and comments
Flushing gathered information
00428B9D 000024D8 MOV EDX,DWORD PTR DS:[ECX+64] ; EDX=0059A880
00428BA0 000024D8 LEA EAX,DWORD PTR DS:[EAX+EAX*2] ; EAX=00000003
00428BA3 000024D8 MOV CL,BYTE PTR DS:[ESI] ; ECX=02F37BE5
[...]
004303FB 000024D8 CMP EDX,ECX
004303FD 000024D8 JL digalo_r.00430282
00430403 000024D8 MOV EDI,DWORD PTR SS:[ESP+28] ; EDI=00000001
00430407 000024D8 MOV ESI,DWORD PTR SS:[ESP+244] ; ESI=02F37B20
0043040E 000024D8 LEA EDX,DWORD PTR SS:[ESP+114] ; EDX=024CF348

Якщо подивитися на виведення з кінця, то ми побачимо, що різниця полягає щонайменше в тому, що в разі падіння спрацьовує умовний перехід за адресою 0x00430282, чого не відбувається у випадку коректної роботи.

Що ж, давайте спробуємо занопить інструкції умовного переходу за цим посиланням і подивитися, що вийде. Так, Digalo тепер дійсно вимовляє це саме протяжне «а»! Але з'явилася інша проблема — після зачитування тексту движок знову падає з Access Violation, але вже в зовсім іншому місці:

image

Вже за адресами вам повинно бути помітно, що на цей раз мова йде про надра системних бібліотек. Поглянемо на call stack за допомогою Alt-K і дізнаємося, що падіння сталося всередині WinAPI-функції HeapFree:

image

Зрозуміло, з 99% ймовірністю ніякого бага в kernel32.dll ми не виявили, а лише передали невірні параметри. Якщо поставити бряки на виклики HeapFree, то ми побачимо, що у всіх інших випадках аргумент, що передається у якості параметра pMemory, містить адреса, значно відрізняється від того, який був переданий в момент падіння додатка:

image

Підозріло, чи не правда? Але що ми можемо зробити? Варіанта два — або довго і нудно вивчати причину потрапляння сюди даного адреси, або просто забити на звільнення пам'яті. Більшість з вас, напевно, вже починають покривати мене нецензурними виразами, але, якщо задуматися, це не може бути практично нічого жахливого. Так-так, ви не помилилися. Зрозуміло, я згоден з тим, що прибирати всі виклики HeapFree з коду — це, м'яко кажучи, неправильно, адже в процесі роботи програма може виділити божевільна кількість пам'яті (наприклад, при читанні довгого тексту або чогось подібного), неосвобождение якої може призвести до нових проблем. Однак що поганого в тому, що ми приберемо звільнення пам'яті при завершенні роботи програми? Оскільки мова йде тільки про Windows, ОС все одно звільнить ресурси (для якихось платформ і систем це могло б виявитися критичним, згоден).

Давайте подивимося по call stack', як ми сюди дісталися. Що ж, запустимо програму ще раз і поставимо бряки за адресами 0x0045A2B3 та 0x0041136C. Бряк за першою адресою спрацьовує багато разів, що говорить нам про те, що ця функція, найімовірніше, є враппером над HeapFree і служить для загального звільнення зазначеної пам'яті, а ось бряк по другому адресою спрацьовує тільки після прочитання голосовим движком переданого йому тексту, що, найімовірніше, означає, що дана процедура викликається тільки при завершенні роботи програми:

image

Занопим виклик процедури 0x0045A273, що знаходиться за адресою 0x0041136C і перевіримо, виправило це нашу проблему. Так, проблема виправлена — движок вимовляє вказану фразу і коректно завершується:

image

Оскільки моєю метою було отримання можливості виголошення конкретного протяжного звуку «а» за допомогою мовного движка Digalo, то, можна сказати, на це завдання було завершено. Так, ми не заглибилися в з'ясування причин падіння програми при виклику функції HeapFree, а також не до кінця зрозуміли, чи можна просто занопить умовний перехід для того, щоб уникнути початкової проблеми, але, врешті-решт, навіщо витрачати на рішення подібної задачі занадто багато часу? Звук виголосили? Вимовили. Для інших фраз і звуків можна продовжувати користуватися оригінальною версією виконуваного файлу Digalo, щоб не переживати, що ми своїми діями додали якихось непередбачених наслідків.

Післямова
Своєю статтею я хотів показати, що домогтися свого прямо тут і зараз для вас може бути не так складно, як ви про це, можливо, думаєте. Якщо якась програма відмовляється зберігати результати вашої роботи або, наприклад, не робить в певних ситуаціях те, чого від неї очікують, вирішити це питання ви цілком можете і самостійно, не чекаючи відповіді від тех. підтримки додатка (якої, до речі, може і взагалі не бути). Добре це чи погано, вирішуйте самі. В кінці-кінців, це взагалі досить дивне питання для реверс-інжинірингу, вірно?

Спасибі за увагу, і знову сподіваюся, що стаття була комусь корисною.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.