![image](https://habrastorage.org/getpro/habr/post_images/96c/cfb/9b6/96ccfb9b6c80f0aac8a5a460de064f04.png)
В предыдущей статье мы разобрали, как реверс-инжиниринг может помочь в получении каких-либо преимуществ перед остальными пользователями. Сегодня мы поговорим ещё об одном применении обратной разработки — исправлении багов в отсутствии исходных кодов приложения. Причин заниматься подобными вещами может быть целое море — разработка программы давным-давно заброшена, а её сорцы автор так и не предоставил общественности / разработка ведётся совершенно в другом русле, и авторам нет никакого дела до возникшего у вас бага / etc, но их объединяет общая цель — исправить сломанный функционал, который постоянно вам досаждает.
Что ж, ближе к делу. Есть такая широко известная в узких кругах программа под названием «Govorilka». Как объясняет её автор, это ничто иное, как «программа для чтения текстов голосом». По сути, так оно и есть. При помощи неё было озвучено множество популярных и не очень видео, рапространившихся по всей сети. Программа имеет консольную версию под названием «Govorilka_cp», которую удобно вызывать из своих собственных приложений, что, собственно, я и сделал в одном из своих проектов.
К сожалению, в процессе распространения моего софта был обнаружен довольно странный момент — на некоторых машинах говорилка падает абсолютно на любых фразах, причём падение было вызвано не моим взаимодействием с данной программой, а самой говорилкой. В попытках выяснить как можно больше деталей о происходящей ошибке я обнаружил, что на двух, казалось бы, совершенно одинаковых системах говорилка ведёт себя противоположным образом — на одной она стабильно работает без каких-либо ошибок, а на другой — падает на каждой переданной ей в качестве аргумента фразе. Эта ситуация мне изрядно поднадоела, и я решил во что бы то ни стало разобраться с данной проблемой.
Учитывая, что говорилка не обновлялась уже несколько лет, а сам автор оставил вот такое «послание» на своём сайте
![image](https://habrastorage.org/getpro/habr/post_images/296/9f7/89c/2969f789c5ffd6d98a21111d20e0a032.png)
, я понял, что надеяться мне не на кого, и решать проблему придётся самому.
Как протекал процесс, и что из этого вышло, читайте под катом (осторожно, много скриншотов).
Прежде чем загружать говорилку в OllyDbg, давайте посмотрим, не защищена ли она каким-нибудь протектором. Берём в руки DiE и видим следующую картину:
![image](https://habrastorage.org/getpro/habr/post_images/5a3/8c6/d54/5a38c6d54a52ffb824eaab7a0964b3c6.png)
Судя по его выводу, программа запакована ASPack'ом. Для убедительности воспользуемся ещё одним анализатором — PEiD:
![image](https://habrastorage.org/getpro/habr/post_images/f6d/8f7/de9/f6d8f7de997c04ff803e269e5cd45473.png)
Надеясь, что два анализатора не могут ошибаться одновременно, приходим к выводу, что говорилка действительно накрыта ASPack'ом. Ну, ничего, давайте его снимем.
Избавление исполняемого файла от ASPack'а можно разделить на три основных этапа:
- Поиск OEP (Original Entry Point — адрес, с которого бы начинала выполняться программа, если бы не была упакована)
- Снятие дампа
- Восстановление IAT
Начнём, разумеется, с поиска OEP. Один из самых простых способов обнаружить оригинальную точку входа — поставить хардварный бряк на ESP-4, потому что большинство паковщиков после своей работы восстанавливают стек. Запускаем говорилку в OllyDbg, открываем Command Line при помощи Alt-F1 (окно стандартного плагина, который поставляется вместе с самим отладчиком), вводим команду hr esp-4, нажимаем F9 и останавливаемся вот на таком месте:
![image](https://habrastorage.org/getpro/habr/post_images/48a/c29/34f/48ac2934fb4c6c2d5a5661bbe4374897.png)
Теперь нам надо пробежаться до ближайшего return'а при помощи Ctrl-F9, нажать F8 и… оказаться на OEP, которая в данном случае находится по адресу 0x0045A210:
![image](https://habrastorage.org/getpro/habr/post_images/2f5/f5d/3f5/2f5f5d3f5c5260ca5f8c912da0ea6c00.png)
Пришло время снимать дамп. Скачиваем и устанавливаем плагин для OllyDbg под названием OllyDump, перезапускаем отладку при помощи Ctrl-F2, снова останавливаемся на OEP и выбираем пункт меню «Dump debugged process», который находится в Plugins -> OllyDump:
![image](https://habrastorage.org/getpro/habr/post_images/57c/42d/79c/57c42d79cd8404e1dc32adea90c59ec8.png)
Нажимаем кнопку «Dump», выбираем имя выходного исполняемого файла и… получаем вот это при попытке его запустить:
![image](https://habrastorage.org/getpro/habr/post_images/bdc/cc3/f48/bdccc3f48716f0de1ad0e809eaf6a90a.png)
Значит, что-то не так с импортами. Ничего, восстановим их вручную.
Дампим процесс говорилки ещё раз, но на этот раз снимаем галочку с CheckBox'а «Rebuild Import». Скачиваем ImpREC, выбираем процесс говорилки из списка, вводим адрес 5A210 в поле для OEP — 0x0045A210 — image base (0x400000) = 0x5A210 и нажимаем на кнопку «IAT AutoSearch»:
![image](https://habrastorage.org/getpro/habr/post_images/2df/4d3/8ac/2df4d38ace48ab692f91dc471d58cad1.png)
Делаем, как сказали, и наблюдаем следующую картину:
![image](https://habrastorage.org/getpro/habr/post_images/53a/5a0/6ad/53a5a06ad251e5587f082ccd198b8832.png)
Очевидно, это ненормально. Пробуем указать RVA и size, которыми нам предлагала воспользоваться в случае неудачи сама программа, и нажать на кнопку «Get Imports» снова:
![image](https://habrastorage.org/getpro/habr/post_images/ae9/39d/057/ae939d05709c8a30ed8ba4fcc140e96e.png)
Что-то явно идёт не так, как ожидается. Давайте попробуем найти границы IAT вручную. Ищем в дизассемблированном листинге вызов любой WinAPI-функции. Например, вот этот:
![image](https://habrastorage.org/getpro/habr/post_images/d7f/549/13a/d7f54913a0ebc302644de15a09b45ca4.png)
Прыгаем на него (left click -> Enter) и видим, куда обращаются все JMP'ы:
![image](https://habrastorage.org/getpro/habr/post_images/a16/f9e/3ad/a16f9e3adc3fbf1d2b9eae9b99e8eca4.png)
Переходим по любому из указанных в данном месте адресов (right-click -> Follow in Dump -> Memory address), выбираем соответствующее представление (right-click по окну Memory Dump'а -> Long -> Address) и видим следующее:
![image](https://habrastorage.org/getpro/habr/post_images/6cf/71b/5d4/6cf71b5d43fe22497d117765ffc50380.png)
Пробегаемся глазами до начала списка адресов функций:
![image](https://habrastorage.org/getpro/habr/post_images/fe8/cf1/e00/fe8cf1e0038046733cd99fdbf8e916bc.png)
Нажимаем двойным кликом мыши на ntdll.RtlDeleteCriticalSection и ищем конец списка:
![image](https://habrastorage.org/getpro/habr/post_images/787/921/4f3/7879214f35d731bbc470def174a0e7ce.png)
Указываем адрес начала IAT (0005F168) и size (0000066C), снова нажимаем на кнопку «Get Imports» и получаем следующий результат:
![image](https://habrastorage.org/getpro/habr/post_images/40d/48c/abb/40d48cabbfb3e2d948702dbc08cf8b58.png)
Уже лучше, но всё равно остались невалидные импорты, причём самое странное, что мы вроде бы всё сделали правильно… Давайте воспользуемся другим приложением для восставления IAT — Scylla:
![image](https://habrastorage.org/getpro/habr/post_images/af9/39e/123/af939e123598711b3948d673e5a7b867.png)
Да что такое происходит? А давайте попробуем проделать то же самое в другой системе — например, в Windows 7 x32 (до этого эксперименты проводились в Windows 8 x64).
Чудесным образом мы получили валидные импорты:
![image](https://habrastorage.org/getpro/habr/post_images/94e/1ad/20c/94e1ad20c545faa4dff831affba70289.png)
Честно говоря, не уверен, что это — баг ImpRec'а и Scylla, особенность Windows 8 x64 или что-то ещё, но главное, что мы восстановили IAT. Точнее, для этого нам надо нажать на кнопку «Fix Dump», выбрать сделанный ранее дамп и попробовать запустить получившийся исполняемый файл. Да, при запуске говорилки без аргументов она работает, как и положено, а вот в случае передачи каких-либо фраз для озвучивания она падает, как и в запакованном варианте.
Итак, ASPack снят. Что дальше? А дальше, собственно, нам предстоит разобраться с возникающим на некоторых компьютерах багом.
Первым делом давайте посмотрим на стандартное окно Windows, сообщающее о краше приложения:
![image](https://habrastorage.org/getpro/habr/post_images/ed0/f43/4a8/ed0f434a8a2b8aa6a9381c77531a158e.png)
Вам должно быть заметно, что Exception Offset равен 0x000591D4. Загружаем говорилку в OllyDbg, ставим софтварный бряк на этот адрес (точнее, на image base + 0x000591D4 — в моём случае это 0x004591D4), нажимаем F9 и получаем следующее:
![image](https://habrastorage.org/getpro/habr/post_images/636/fd6/b47/636fd6b474999103f10ec0f9466655f0.png)
Как вы видите, значение регистра ESI на момент выполнения операции чтения по адресу ESI+38 равно нулю, что, разумеется, приводит к крашу приложения. Двумя инструкциями выше заметно, что в ESI значение попадает из регистра EAX. Значение регистра EAX до этого в данной процедуре не меняется, так что смотрим по Call Stack'у, откуда нас позвали:
![image](https://habrastorage.org/getpro/habr/post_images/fcc/936/b49/fcc936b49189048eeb357b5439f45011.png)
Прыгаем туда (right click -> Show Call) и видим, что прямо перед вызовом процедуры находится команда MOV EAX,DWORD PTR DS:[45EC5C]:
![image](https://habrastorage.org/getpro/habr/post_images/63a/822/60f/63a82260f589c47bc1f639e1a23d6094.png)
Как видно из предыдущего скриншота, по адресу 0x45EC5C также находится ноль. Ставим хардварный бряк на запись по этому адресу (right click -> Breakpoint -> Hardware, on write -> Dword), запускаем приложение ещё раз и обнаруживаем, что никакой записи по данномуадресу не происходит. Давайте проанализируем, как себя ведёт приложение в случае успешной работы в другой системе. Запись по адресу 0x45EC5C в этом случае действительно происходит:
![image](https://habrastorage.org/getpro/habr/post_images/2fb/1bc/5b3/2fb1bc5b373263fd36847b130a52de3d.png)
, в результате чего ESI+38 даёт какое-то осмысленное значение. Давайте узнаем, как мы попали в это место. Call Stack на момент выполнения данной инструкции пустой, так что у меня возникло впечатление, что это основная процедура программы. Чтобы понять, в каком именно месте приложение стало вести себя по-другому, давайте запустим Trace over (Ctrl-F12) на обеих системах (там, где приложение падает, и там, где оно работает).
В случае системы, где приложение постоянно падает, оно достигло бряка на доступе к ESI+38, где и упало. Последняя строчка кода в trace log'е была CALL'ом по адресу 0x004597C8:
![image](https://habrastorage.org/getpro/habr/post_images/fd7/af9/ad2/fd7af9ad288ccae42b639c70b0dd59eb.png)
, в то время как в системе, где приложение работает нормально, после вызова данной процедуры (0x004597C8) происходит выполнение других инструкций, на одной из которых и срабатывает хардварный бряк на запись по адресу 0x45EC5C:
![image](https://habrastorage.org/getpro/habr/post_images/6d4/89c/c28/6d489cc28c81b3ca4555444456c24a8c.png)
Возможно, что-то идёт не так в процедуре 0x004597C8. Ставим софтварный бряк на её вызов (0x0045AD5B) и обнаруживаем, что его установка ведёт к тому, что на той системе, где всё работало нормально, теперь не срабатывает хардварный бряк на запись по адресу 0x45EC5C, в результате чего приложение падает, как и в случае другой системы. С хардварными бряками на выполнение всё обстоит так же.
Экспериментальным путём было выяснено, что установка бряков на нескольких предшествующих 0x0045AD5B инструкциях ведёт к такому же результату. Установить бряк с последующей нормальной работой приложения получилось лишь на данной инструкции:
![image](https://habrastorage.org/getpro/habr/post_images/1f3/547/44a/1f354744a93f0ec9edd218234c846982.png)
Возможно, установкой бряков в данных местах мы мешали замерам времени, которые как раз и осуществляются при помощи вызовов функции 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 регистра флагов был выставлен на момент выполнения данной инструкции, в то время как в случае той системы, где приложение падает, данный флаг не был выставлен:
![image](https://habrastorage.org/getpro/habr/post_images/28d/a3f/77a/28da3f77a042e58b9733a3eac3a3cd65.png)
Давайте посмотрим, что же на это влияет. Итак, что у нас здесь происходит?
- Вызывается функция GetTickCount, результат её вызова записывается в регистр EAX
- Обнуляется содержимое регистра EDX
- Содержимое регистров EDX (0x0) и EAX (результат выполнения функции GetTickCount) заносятся на стек
- В регистр EAX помещается значение из адреса 0x45EC98
- К нему прибавляется 0x64
- Выполняется инструкция CDQ
- И, наконец, сравнивается содержимое регистра EDX и значение по адресу ESP-4, где хранится предыдущее значение данного регистра, т.е. 0x0
Поставив хардварный бряк на запись по адресу 0x45EC98, можно увидеть, что туда также помещается результат выполнения функции GetTickCount:
![image](https://habrastorage.org/getpro/habr/post_images/750/7bb/30d/7507bb30de5963d2865c8414c0147607.png)
Кстати, а почему 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
![image](https://habrastorage.org/getpro/habr/post_images/18f/79e/feb/18f79efebb83179826b9ca6cad0cb54a.png)
и заменяем адрес, на который прыгает JMP, на адрес начала нашего code cave'а (в моём случае это 0x0045BAAA):
![image](https://habrastorage.org/getpro/habr/post_images/848/bc0/866/848bc086621e65b2b2e954437449c1b0.png)
Сохраняем изменённый исполняемый файл (right-click по окну CPU -> Copy to executable -> All modifications -> Copy all -> right-click на появившемся окне -> Save file) и проверяем его работоспособность. Да, он действительно работает, однако при запуске говорилки теперь выдаётся знакомое практически всем пользователям Windows окно UAC. Почему именно это происходит можно почитать, например, тут, а для решения проблемы в нашем случае достаточно всего лишь дать нашему исполняемому файлу оригинальное название — «Govorilka_cp.exe».
Послесловие
Как видите, использовать реверс-инжиниринг можно не только для читерства или поиска уязвимостей, но и для вполне полезных целей. Старайтесь исправлять проблемы, а не забивать на них — вполне возможно, что в процессе исправления очередной ошибки вы откроете для себя что-то новое. А я в свою очередь снова надеюсь, что статья оказалась кому-нибудь полезной.
Также я выражаю огромную благодарность человеку с ником tihiy_grom — без него этой статьи просто не было бы.