В предыдущей статье мы разобрали, как реверс-инжиниринг может помочь в получении какх-либо преимуществ перед остальными пользователями. Сегодня мы поговорим ещё об одном применении обратной разработки — исправлении багов в отсутствии исходных кодов приложения. Причин заниматься подобными вещами может быть целое море — разработка программы давным-давно заброшена, а её сорцы автор так и не предоставил общественности / разработка ведётся совершенно в другом русле, и авторам нет никакого дела до возникшего у вас бага / etc, но их объединяет общая цель — исправить сломанный функционал, который постоянно вам досаждает.
Что ж, ближе к делу. Есть такая широко известная в узких кругах программа под названием «Govorilka». Как объясняет её автор, это ничто иное, как «программа для чтения текстов голосом». По сути, так оно и есть. При помощи неё было озвучено множество популярных и не очень видео, рапространившихся по всей сети. Программа имеет консольную версию под названием «Govorilka_cp», которую удобно вызывать из своих собственных приложений, что, собственно, я и сделал в одном из своих проектов.
К сожалению, в процессе распространения моего софта был обнаружен довольно странный момент — на некоторых машинах говорилка падает абсолютно на любых фразах, причём падение было вызвано не моим взаимодействием с данной программой, а самой говорилкой. В попытках выяснить как можно больше деталей о происходящей ошибке я обнаружил, что на двух, казалось бы, совершенно одинаковых системах говорилка ведёт себя противоположным образом — на одной она стабильно работает без каких-либо ошибок, а на другой — падает на каждой переданной ей в качестве аргумента фразе. Эта ситуация мне изрядно поднадоела, и я решил во что бы то ни стало разобраться с данной проблемой.
Учитывая, что говорилка не обновлялась уже несколько лет, а сам автор оставил вот такое «послание» на своём сайте
, я понял, что надеяться мне не на кого, и решать проблему придётся самому.
Как протекал процесс, и что из этого вышло, читайте под катом (осторожно, много скриншотов).
Прежде чем загружать говорилку в OllyDbg, давайте посмотрим, не защищена ли она каким-нибудь протектором. Берём в руки DiE и видим следующую картину:
Судя по его выводу, программа запакована ASPack'ом. Для убедительности воспользуемся ещё одним анализатором — PEiD:
Надеясь, что два анализатора не могут ошибаться одновременно, приходим к выводу, что говорилка действительно накрыта ASPack'ом. Ну, ничего, давайте его снимем.
Избавление исполняемого файла от ASPack'а можно разделить на три основных этапа:
- Поиск OEP (Original Entry Point — адрес, с которого бы начинала выполняться программа, если бы не была упакована)
- Снятие дампа
- Восстановление IAT
Начнём, разумеется, с поиска OEP. Один из самых простых способов обнаружить оригинальную точку входа — поставить хардварный бряк на ESP-4, потому что большинство паковщиков после своей работы восстанавливают стек. Запускаем говорилку в OllyDbg, открываем Command Line при помощи Alt-F1 (окно стандартного плагина, который поставляется вместе с самим отладчиком), вводим команду hr esp-4, нажимаем F9 и останавливаемся вот на таком месте:
Теперь нам надо пробежаться до ближайшего return'а при помощи Ctrl-F9, нажать F8 и… оказаться на OEP, которая в данном случае находится по адресу 0x0045A210:
Пришло время снимать дамп. Скачиваем и устанавливаем плагин для OllyDbg под названием OllyDump, перезапускаем отладку при помощи Ctrl-F2, снова останавливаемся на OEP и выбираем пункт меню «Dump debugged process», который находится в Plugins -> OllyDump:
Нажимаем кнопку «Dump», выбираем имя выходного исполняемого файла и… получаем вот это при попытке его запустить:
Значит, что-то не так с импортами. Ничего, восстановим их вручную.
Дампим процесс говорилки ещё раз, но на этот раз снимаем галочку с CheckBox'а «Rebuild Import». Скачиваем ImpREC, выбираем процесс говорилки из списка, вводим адрес 5A210 в поле для OEP — 0x0045A210 — image base (0x400000) = 0x5A210 и нажимаем на кнопку «IAT AutoSearch»:
Делаем, как сказали, и наблюдаем следующую картину:
Очевидно, это ненормально. Пробуем указать RVA и size, которыми нам предлагала воспользоваться в случае неудачи сама программа, и нажать на кнопку «Get Imports» снова:
Что-то явно идёт не так, как ожидается. Давайте попробуем найти границы IAT вручную. Ищем в дизассемблированном листинге вызов любой WinAPI-функции. Например, вот этот:
Прыгаем на него (left click -> Enter) и видим, куда обращаются все JMP'ы:
Переходим по любому из указанных в данном месте адресов (right-click -> Follow in Dump -> Memory address), выбираем соответствующее представление (right-click по окну Memory Dump'а -> Long -> Address) и видим следующее:
Пробегаемся глазами до начала списка адресов функций:
Нажимаем двойным кликом мыши на ntdll.RtlDeleteCriticalSection и ищем конец списка:
Указываем адрес начала IAT (0005F168) и size (0000066C), снова нажимаем на кнопку «Get Imports» и получаем следующий результат:
Уже лучше, но всё равно остались невалидные импорты, причём самое странное, что мы вроде бы всё сделали правильно… Давайте воспользуемся другим приложением для восставления IAT — Scylla:
Да что такое происходит? А давайте попробуем проделать то же самое в другой системе — например, в Windows 7 x32 (до этого эксперименты проводились в Windows 8 x64).
Чудесным образом мы получили валидные импорты:
Честно говоря, не уверен, что это — баг ImpRec'а и Scylla, особенность Windows 8 x64 или что-то ещё, но главное, что мы восстановили IAT. Точнее, для этого нам надо нажать на кнопку «Fix Dump», выбрать сделанный ранее дамп и попробовать запустить получившийся исполняемый файл. Да, при запуске говорилки без аргументов она работает, как и положено, а вот в случае передачи каких-либо фраз для озвучивания оно падает, как и в запакованном варианте.
Итак, ASPack снят. Что дальше? А дальше, собственно, нам предстоит разобраться с возникающим на некоторых компьютерах багом.
Первым делом давайте посмотрим на стандартное окно Windows, сообщающее о краше приложения:
Вам должно быть заметно, что Exception Offset равен 0x000591D4. Загружаем говорилку в OllyDbg, ставим софтварный бряк на этот адрес (точнее, на image base + 0x000591D4 — в моём случае это 0x004591D4), нажимаем F9 и получаем следующее:
Как вы видите, значение регистра ESI на момент выполнения операции чтения по адресу ESI+38 равно нулю, что, разумеется, приводит к крашу приложения. Двумя инструкциями выше заметно, что в ESI значение попадает из регистра EAX. Значение регистра EAX до этого в данной процедуре не меняется, так что смотрим по Call Stack'у, откуда нас позвали:
Прыгаем туда (right click -> Show Call) и видим, что прямо перед вызовом процедуры находится команда MOV EAX,DWORD PTR DS:[45EC5C]:
Как видно из предыдущего скриншота, по адресу 0x45EC5C также находится ноль. Ставим хардварный бряк на запись по этому адресу (right click -> Breakpoint -> Hardware, on write -> Dword), запускаем приложение ещё раз и обнаруживаем, что никакой записи по данномуадресу не происходит. Давайте проанализируем, как себя ведёт приложение в случае успешной работы в другой системе. Запись по адресу 0x45EC5C в этом случае действительно происходит:
, в результате чего ESI+38 даёт какое-то осмысленное значение. Давайте узнаем, как мы попали в это место. Call Stack на момент выполнения данной инструкции пустой, так что у меня возникло впечатление, что это основная процедура программы. Чтобы понять, в каком именно месте приложение стало вести себя по-другому, давайте запустим Trace over (Ctrl-F12) на обеих системах (там, где приложение падает, и там, где оно работает).
В случае системы, где приложение постоянно падает, оно достигло бряка на доступе к ESI+38, где и упало. Последняя строчка кода в trace log'е была CALL'ом по адресу 0x004597C8:
, в то время как в системе, где приложение работает нормально, после вызова данной процедуры (0x004597C8) происходит выполнение других инструкций, на одной из которых и срабатывает хардварный бряк на запись по адресу 0x45EC5C:
Возможно, что-то идёт не так в процедуре 0x004597C8. Ставим софтварный бряк на её вызов (0x0045AD5B) и обнаруживаем, что его установка ведёт к тому, что на той системе, где всё работало нормально, теперь не срабатывает хардварный бряк на запись по адресу 0x45EC5C, в результате чего приложение падает, как и в случае другой системы. С хардварными бряками на выполнение всё обстоит так же.
Экспериментальным путём было выяснено, что установка бряков на нескольких предшествующих 0x0045AD5B инструкциях ведёт к такому же результату. Установить бряк с последующей нормальной работой приложения получилось лишь на данной инструкции:
Возможно, установкой бряков в данных местах мы мешали замерам времени, которые как раз и осуществляются при помощи вызовов функции GetTickCount.
Запускаем Trace into (Ctrl-F11), начиная с данной инструкции, и обнаруживаем, что различия в выполнении начинаются вот с этого места:
; если приложение выполняется в системе, где оно падает
004597EC Main MOV EAX,DWORD PTR DS:[45EC98] ; EAX=F2B47801
004597F1 Main ADD EAX,64 ; EAX=F2B47865
004597F4 Main CDQ ; EDX=FFFFFFFF
004597F5 Main CMP EDX,DWORD PTR SS:[ESP+4]
004597F9 Main JNZ SHORT Govorilk.00459807
00459807 Main POP EDX ; EDX=F2B47802
; если приложение выполняется в системе, где оно нормально работает
004597EC Main MOV EAX,DWORD PTR DS:[45EC98]
004597F1 Main ADD EAX,64 ; EAX=064A1501
004597F4 Main CDQ
004597F5 Main CMP EDX,DWORD PTR SS:[ESP+4]
004597F9 Main JNZ SHORT Govorilk.00459807
004597FB Main CMP EAX,DWORD PTR SS:[ESP]
Отсюда заметно, что результат выполнения инструкции CMP EDX,DWORD PTR SS:[ESP+4] влияет на дальнейшую работу процедуры. В случае системы, где оно работает, флаг Z регистра флагов был выставлен на момент выполнения данной инструкции, в то время как в случае той системы, где приложение падает, данный флаг не был выставлен:
Давайте посмотрим, что же на это влияет. Итак, что у нас здесь происходит?
- Вызывается функция GetTickCount, результат её вызова записывается в регистр EAX
- Обнуляется содержимое регистра EDX
- Содержимое регистров EDX (0x0) и EAX (результат выполнения функции GetTickCount) заносятся на стек
- В регистр EAX помещается значение из адреса 0x45EC98
- К нему прибавляется 0x64
- Выполняется инструкция CDQ
- И, наконец, сравнивается содержимое регистра EDX и значение по адресу ESP-4, где хранится предыдущее значение данного регистра, т.е. 0x0
Поставив хардварный бряк на запись по адресу 0x45EC98, можно увидеть, что туда также помещается результат выполнения функции GetTickCount:
Кстати, а почему GetTickCount возвращает такое большое значение? Ведь система выключалась мной ещё сегодня утром. На самом деле, тут играет свою роль hybrid boot и shutdown в Windows 8. Почитать об этом больше можно, например, тут.
Но вернёмся к основной теме нашего обсуждения. В чём же тут тогда проблема? Давайте внимательно посмотрим на содержимое регистров общего назначения в этом куске кода на обеих системах. Вам должно броситься в глаза, что после выполнения инструкции CDQ на системе, где приложение работает корректно, регистр EDX принимает значение 0x0, а в системе, где программа падает, 0xFFFFFFFF, что видно, например, на предыдущем скриншоте. Внимательно читаем описание инструкции CDQ:
Converts signed DWORD in EAX to a signed quad word in EDX:EAX by extending the high order bit of EAX throughout EDX
А теперь взглянем на сигнатуру функции GetTickCount:
DWORD WINAPI GetTickCount(void);
Как видите, она возвращает DWORD, который «раскрывается» в unsigned long, т.е. беззнаковый тип.
Уж не знаю, баг это разработчика или компилятора, но на такое поведение автор явно не рассчитывал, ведь в текущей ситуации говорилка будет падать на системах, которые работают дольше 24.8 и меньше 49.7 дней. Почему именно так? Нижняя граница определяется максимальным значением, которое может поместиться в знаковую 4-байтовую переменную — 2147483647:
2147483647 / 1000 / 60 / 60 / 24 = 24.8551348032
Верхняя же граница определяется самой функцией GetTickCount, о чём сказано в документации (собственно, она определяется максимальным значением, которое может поместиться в беззнаковую 4-байтовую переменную):
The elapsed time is stored as a DWORD value. Therefore, the time will wrap around to zero if the system is run continuously for 49.7 days
Ну что, пора исправить это допущение. Один из вариантов исправления проблемы — каким-то образом «подменить» вызов функции GetTickCount своим кодом, в котором мы будем «уменьшать» возвращаемое из неё значение настолько, чтобы оно поместилось в знаковую 4-байтовую переменную.
Вариантов решения данной задачи несколько, но давайте напишем code cave. Ищем свободное место (проще всего найти его в «конце» окна CPU) и помещаем туда следующий код:
; Сохраняем состояние регистров общего назначения на стеке
0045BAAA 60 PUSHAD
; Сохраняем состояние регистра флагов на стеке
0045BAAB 9C PUSHFD
; Получаем значение EIP
0045BAAC E8 00000000 CALL Govorilk.0045BAB1
0045BAB1 5E POP ESI
; Вызываем функцию GetTickCount
0045BAB2 FF96 2F380000 CALL DWORD PTR DS:[ESI+382F]
; Сохраняем результат её выполнения на по адресу ESP-4
0045BAB8 894424 FC MOV DWORD PTR SS:[ESP-4],EAX
; Восстанавливаем состояние регистра флагов
0045BABC 9D POPFD
; Восстанавливаем состояние регистров общего назначения
0045BABD 61 POPAD
; Помещаем в регистр EAX результат выполнения функции GetTickCount
0045BABE 8B4424 D8 MOV EAX,DWORD PTR SS:[ESP-28]
; Выполняем операцию битового "AND" над значением, хранящемся в регистре EAX
0045BAC2 25 FFFFFF0F AND EAX,0FFFFFFF
0045BAC7 C3 RETN
Значение 382F получилось в результате вычисления разницы между адресом в IAT, по которому хранится адрес функции GetTickCount (0x0045F2E0), и текущим адресом (0x0045BAB1).
Получить значение регистра EIP напрямую не получится — к нему нельзя обращаться ни на чтение, ни на запись.
Теперь переходим на место, где осуществляется JMP на функцию GetTickCount
и заменяем адрес, на который прыгает JMP, на адрес начала нашего code cave'а (в моём случае это 0x0045BAAA):
Сохраняем изменённый исполняемый файл (right-click по окну CPU -> Copy to executable -> All modifications -> Copy all -> right-click на появившемся окне -> Save file) и проверяем его работоспособность. Да, он действительно работает, однако при запуске говорилки теперь выдаётся знакомое практически всем пользователям Windows окно UAC. Почему именно это происходит можно почитать, например, тут, а для решения проблемы в нашем случае достаточно всего лишь дать нашему исполняемому файлу оригинальное название — «Govorilka_cp.exe».
Послесловие
Как видите, использовать реверс-инжиниринг можно не только для читерства или поиска уязвимостей, но и для вполне полезных целей. Старайтесь исправлять проблемы, а не забивать на них — вполне возможно, что в процессе исправления очередной ошибки вы откроете для себя что-то новое. А я в свою очередь снова надеюсь, что статья оказалась кому-нибудь полезной.
Также я выражаю огромную благодарность человеку с ником tihiy_grom — без него этой статьи просто не было бы.
This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.