Статьи: Отучаем Empire Earth 2 от CD (SecuRom) | 26.08.2005 |
Здрасти всем, читающим этот документ. Я тут впал в детство, заигрался в игру EmpireEarth 2. Симпатичная графика, много юнитов, интересный геймплей, плохо что подтормаживает, но ничё не поделать – за качество надо платить, а не хочется. Ну вот значит, диск лицензионный (точнее два), и не мой. Пора бы отдавать, а игра не пашет без него. И тут меня понесло :) Со мной всё понятно – я должен пользоваться этой игрой, только если куплю лицензионный диск, а вот как же мой друг. У меня (да и у многих наверное) не раз диски рассыпались в приводе, ну или на худой конец царапались до невозможности чтения. На этот случай они скинуты на RW’хи. С этим я поступил также. Итог запуска заставляет задуматься – вставьте настоящий диск, а не копию. Это откровенная наглость. Вот так, из чисто демократических побуждений (хочу иметь выбор – с лицензии запускать или с копии, всё равно заплатил всю стоимость) появилась эта статья. Цель её сугубо научная – проверить, сможем ли мы играть в свою любимую игру после того, как настоящий диск с ней разлетится или испортится. Разлетать и портить диск мы не будем, а вот поисследовать можно, достаточно вытащить ЦД из привода и запустить файл с игрой :)
Инструменты:
Ну ладно, я вроде успокоился, теперь приступим к исследованию. Сначала вытаскиваем диск и видим просьбу его вставить. Теперь вставляем копию и видим уже описанное сообщение, но обращаем внимание на последнюю строчку – www.securom.com/copy. Это означает, что игра запротекчена SecuRom’ом. В принципе, диск можно скопировать с помощью Alcohol’a, правда для его запуска тоже нужен Alcohol. Но раз уж тут SecuRom, то почему бы не попытаться его снять, тогда игра будет запускаться вообще без диска. Приступим. Сначала общие данные - этот протектор пионерит часть кода, шифрует его данными с диска, и если у нас в сидюке оригинал (или точная копия), то восстанавливает код правильно, иначе всё без толку. Поэтому вставляем лицензию и смотрим в олю. Пишет расшифрованный код он с помощью функции WriteProcessMemory, поэтому бряка на неё и смотрим. Первый раз пишем в секцию игры, второй в SFX, третий тоже в SFX, теперь в секцию игры, а дальше много пишется в rdata, то есть импорт меняется. После n-ого вызова игра запускается. Можно повторить на n-1 вызов, но я Вам подскажу, что проще поставить бряк на CreateEventA, и по параметру EventName = “EV_APPL_STARTED" можно догадаться, что вот-вот игра запустится. Пройдя ещё чуть-чуть видим:
00C80A52 58 POP EAX
Это переход на OEP. Прыгаем туда и делаем дамп. Самое простое позади. Теперь посмотрим, что же делалось с импортом. Видим очень грустную картину – нормальных вызовов минимум и куча переходников. Их можно разделить на 2 группы – перенаправление в память, выделенную из кучи:
004022CF 50 PUSH EAX У меня это адреса с 1C000000 по 01CB0000. Или перенаправление в секцию SecuRom’a: //
Обычный вызов // И по значению DWORD'a с
адресом хххх (D3668C в данном случае)
Какую именно апи функцию вызвать в обоих случаях решается из адреса возврата. Алгоритм получения достаточно громоздок и использует много данных, причём инициализируемых во время выполнения программы. Поэтому переписать алгоритм и выполнить его над сдампленным файлом не получится. Что мы можем сделать? Для начала посмотрим, как же работает этот механизм. Сперва обратим внимание на переходники в динамическую память. Ставим бряки на все их вызовы и отпускаем игру. Стопоримся здесь:
00A01AE9 FF7424 04 PUSH DWORD PTR
SS:[ESP+4] Теперь заходим и осторожно трейсим...
01CB0695 8B85 68FFFFFF MOV EAX,DWORD PTR
SS:[EBP-98]
Как видим, прыг
возвращает нас в наш файл, точнее в секцию секурома.
00C2F4AA 8B45 CC MOV EAX,DWORD PTR
SS:[EBP-34] А потом берём значение по этому адресу. Видим, что за апи – onexit. Адрес апи в EAX 00C2F4AF 8B06
MOV EAX,DWORD PTR DS:[ESI] И прыгаем туда... 00C2F4B7 FFE0 JMP NEAR EAX
Теперь жмём F9. Опять переход в ту же память (01CB0000). Мы всё про неё посмотрели. Жмём F9. Жмём, ещё. Короче, в другой участок переходим отсюда 00421CC9 FF15 1498D100 CALL NEAR DWORD PTR DS:[D19814] Это прыг в 01C30000 01C3069B -FF2485 55F5C200 JMP NEAR DWORD PTR DS:[EAX*4+C2F555] ; EE2.00C2EDBE
Опять возвращаемся
в программу.
00C2F4AA 8B45 CC MOV EAX,DWORD PTR
SS:[EBP-34] Адрес апи-функции
00C2F4AF 8B06 MOV EAX,DWORD PTR
DS:[ESI] И прыгаем туда... 00C2F4B7 FFE0 JMP NEAR EAX Как Вы, надеюсь, заметили, переход на апи осуществляется в том же месте. Это, несомненно, хорошо. Мы можем написать код, который будет перехватывать управление, брать значение по адресу EBP-34 (адрес вызываемой апи в таблице импорта) и писать его куда-нибудь, лучше всего прямо в файл с дампом и править вызов типа call near 01C00000 на вызов апи. Теперь осталось решить, как мы вычислим, куда в дампе писать. Это просто, останавливаемся на месте, где мы планируем сделать переход на наш исправляющий код (00C2F4AA) и глядим в стэк: 00328A94 00421CCF RETURN to EE2.00421CCF from 01C30000 Это значит, что по адресу 00328A94 (а точнее ESP-54) хранится адрес, куда вернётся управление после вызова апи, а так как длина команды вызова 6 байт (call near – FF15, плюс 4 байта адреса в IAT), то мы легко найдём, куда надо писать новые байты (адрес возврата - 4). Для разнообразия исправляющий код будем писать в библиотеке, а её подгружать перед переходом на OEP.
В первоначальном варианте библиотека лишь перехватывала прыг на апи, переписывала в дамп правильный адрес в таблице импорта и возвращала управление в игру (поэтому мы и вычисляли адрес возврата, читая его из стэка), т.е. играя в игру библиотека по мере вызова функций исправляла их и в дампе, но потом всё-таки решил изменить план работы, т.к. некоторые функции могли не вызваться и соответственно не исправиться. Можно и в ручную потом исправить, но раз уж идея автоматизировать процесс распаковки, надо автоматизировать его полностью. Следующий способ, который пришёл мне в голову - открываем дамп и ищем все вызовы переходников, делаем их вызов, а потом опять возвращаемся в поиск по файлу. Таким образом от нас не уйдёт ни одна функция. Теперь осталось подумать, как нам искать вызов переходника. Возьмём 2 вызова в разные участки памяти: 004016F0 FF15 3098D100 CALL NEAR DWORD PTR DS:[D19830] и 004018F1 FF15 2498D100 CALL NEAR DWORD PTR DS:[D19824] Вызов состоит из 2 байт FF 15 - это сам вызов, и 3098D100 - по этому адресу берётся значение и на него происходит прыжок. Для начала в файле будем искать FF 15, а потом смотреть следующие 4 байта. Видно, что эти 4 байта различаются всего одним байтом, последним, так что делаем из трёх константу DWORD DestBytesInMemory = 0x00D198FF; Последний байт мы сделали FF для упрощения проверки, т.к. бинарной операцией ИЛИ мы обЭФим младший прочитанный из дампа байт вызова.
DWORD ReturnAddress, // адрес возврата в
программу после вызова апи
DWORD
BeginFileInMemory = 0x401000,
WORD NearCall = 0x15FF;// опкод
near call'a
WORD
ReadBufferCall;// прочитанные байты из
файла, проверять на опкоды прыгов
BOOL
APIENTRY DllMain( HANDLE hModule,
DUMP_Handle = CreateFile ("dmp.exe",
GENERIC_WRITE | GENERIC_READ,FILE_SHARE_READ |
FILE_SHARE_WRITE,NULL,OPEN_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL);
__declspec(dllexport)
void FindCall()
//
Проверка
на
nearCall Ну вот, поиск сделали, теперь стоит подумать о поправке данных в дампе. Просто берём адрес, ведущий в IAT (EBP-34) и пишем в дамп // Исправление импорта типа CALL NEAR DWORD PTR DS:[D36664] в выделенную память
__declspec(dllexport)
void
FixImportCall
()
push
0 Теперь надо загрузить нашу библиотеку. Где-нибудь в конце файла делаем изменения: 00A69764
75 6E
JNZ SHORT EE2.00A697D4 И на переходе на ОЕП в EAX пишем 00A69776. Теперь мы можем пользоваться нашим кодом в библиотеке. На ОЕП переходим на поиск переходников 00A01D00 -E9 FBF22901 JMP un.?FindCall@@YAXXZ Ну а дальше смотрим, что делает наша библиотека
01CA105E 0FB705 10ACCA01 MOVZX EAX,WORD PTR
DS:[ReadBufferCall] Ставим бряк за JNZ, чтобы остановиться на первом коле. Как видим, это не тот тип, тогда ставим бряк на 01CA10C8 -FF25 A4A0CA01 JMP NEAR DWORD PTR DS:[ i ]; EE2.00401432 Отлично. Теперь ставим бряк 00C2F4AA (выше это место уже нашли). Здесь меняем на 00C2F4AA -E9 921C0701 JMP un.?FixImportCall@@YAXXZ Пока всё отлично, адрес в таблице импорта 00A6A0DC. Теперь дошли сюда. Нам нельзя возвращать управление обратно в игру, поэтому я сделал прыг обратно на поиск, но как получить адрес функции перед компиляцией я не понял и сделал прыжок на 99. Здесь нам надо поставить правильное значение адреса функции FindCall 01CA118B -FF25 04ACCA01 JMP NEAR DWORD PTR DS:[AddrOfFindCallFun>] После этого мы возвращаемся в поиск. Вот в принципе и всё. Теперь ставим бряк на конце цикла и ждём финиша... До этого у меня были попытки восстановить эти переходники, в итоге выяснилось, что SecuRom проверяет целостность своего кода. Чтобы в этом удостовериться достаточно поставить мемори бряк на изменённый фрагмент памяти. Я делал прыг немного ниже, в итоге раскручивая вызовы обнаружил вот такое интересное место:
01C90446 MOV EAX,DWORD PTR
SS:[EBP-44] Такие куски кода встречаются во всех выделенных участках памяти. Обходится эта штука элементарно:
01C904A1 SUB ECX,DWORD PTR DS:[EAX] Здесь после вычитания ECX и значения по адресу EAX должен быть ноль, то есть они должны быть равны, после к разности прибавляется константа. Теперь меняем 01C904A3 ADD ECX,0B1327A на
01C904A3 MOV ECX,0B1327A И радуемся :) Вот мы остановились на конце цикла. Жмём Alt+F2, фиксим импорт у дампа и грузим его в олю. Теперь открываем запакованную, доходим до ОЕП, ищем Intermodular Calls, сортируем по адресу местоположения и смотрим, где был первый переходник
Address=004016F0 Теперь топаем на адрес 004016F0 в пофиксеном дампе 004016F0 FF15 DCA0A600 CALL NEAR DWORD PTR DS:[<&kernel32.GetPr>; kernel32.GetProcAddress Хорошо, апи определилась. Значит с первым типом вроде разобрались. Давайте разбираться со вторым... В дампе опять ищем все вызовы и видим наши цели:
00401432 FF15 5866D300 CALL NEAR DWORD PTR
DS:[D36658] ; dmp_.00CB1B90 Теперь сортируем по Destination и видим, что колы ведут сюда: 00CB1B90, 00CB1DA0, 00CB1F60 - то есть в 3 места, поэтому, видимо, придётся править 3 штуки. Теперь разбираемся, каким образом здесь происходит переход на апи. Ставим бряки на прыги туда в запакованной проге и смотрим.
0041ABA8 CALL NEAR DWORD PTR
DS:[D36650] ; EE2.00CB1B90 Здесь адрес функции в IAT хранится в [EBP-14]. Переход на апи обычным джампом. Следующий.
004B7E9D CALL EE2.00CB1F60 Адрес функции всё там же, но переход осуществляется ретом. Остался последний. Отпускать игру не стоит, ибо похоже, что он вызывается после запуска DirectX, а там это может плохо кончиться, поэтому ручками смотрим
00CB1E6C MOV DWORD PTR
SS:[EBP-14],EAX Всё то же самое, только адрес возвращения в апи необычно устанавливается. Вывод: во всех случаях адрес функции в таблице импорта хранится в [EBP-14]. Что ж, ещё одна халява. Теперь смотрим, как делать реализацию. 004C0171 CALL NEAR DWORD PTR DS:[D366A4] ; EE2.00CB1B90 Тут всё просто. Делаем как в первом случае - меняем байты адреса на правильные, прочитанные из EBP-14. 004C7329 E8 62A87E00 CALL EE2.00CB1B90 А вот тут нас подловили. Вообще вызов апи функций осуществляется так: CALL NEAR DWORD PTR DS:[АДРЕС_В_ТАБЛИЦЕ_ИМПОРТА_ГДЕ_ХРАНИТСЯ_АДРЕС_ВЫЗЫВАЕМОЙ_АПИ]. Это вроде как разыменовывание указателя и переход на адрес, который мы получили после разыменовывания. Занимает это 6 байт. А теперь посмотрите, сколько занимает обычный call... 5, а 1 байт они не просто сделали нопом и поставили за или перед вызовом, они сделали что-то типа полиморфа - байт может быть как перед вызовом, так и после, да в добавок байт этот разный. Вот с этим бороться придётся по-новому. Для начала посмотрим, какие варианты байта-мутанта используются - CMC (F5), NOP (90), CLC (F8), INC EAX (40), DAA (27), STC (F9). Теперь как с этим бороться - для начала надо удостовериться, что этот call ведёт именно в секцию секурома, а точнее на любой из тех 3 адресов. У near'ов я решил посмотреть, куда именно ведёт прыг. Для этого надо прочиать 4 байта за опкодами кола, вычесть 0x400000, прочитать значение по полученному адресу и проверить с 3 нашими константами. Как я уже где-то писал, переход обычного call'a осуществляется не просто подстановкой следующих 4 байт за опкодом E8 в EIP (то есть 4 байта это не адрес, по которому будет передано управление), а сложением EIP с этими байтами и переходом на результат этого сложения. Алгоритм - ищем байт E8, прибавляем к смещению, где нашли четыре следующих байта, + ещё 5 (длина команды). Если получаем 00CB1B90, 00CB1DA0, 00CB1F60, то это оно. Нижеприведённый код вставляется после jmp near i в проверке на NearCall
// Адреса, куда
перенаправлен импорт.
// Или
в
секцию
SecuRom'a
// Если не near, а
обычный call Поиск сделали, теперь займёмся фиксингом. Чтобы починить переделанные колы нам для начала нужно найти настоящее начало, то есть мусорный байт стоит перед или после call'a, затем по этому смещению записать 2 байта - FF 15, чтобы превратить В CALL NEAR и дальше 4 байта адреса в IAT. Но для начала надо что-то типа анализатора :) сварганить. Ничё сложного вроде здесь нет.
void
AnalyzeCall ()
И сам исправляющий код
// Исправление
импорта типа CALL EE2.00CB1F60
push 0
push 0 Вот вроде и всё. Теперь тормозим на прыге на оеп, добавляем загрузку dll, переходим туда, дальше на оеп. Там делаем прыг в функцию FindCall (вобщем всё как в первый раз). Меняем 99 на адрес FindCall и добавляем переходы в нашу библиотеку перед переходами на апи 00CB1C3A JMP un.?FixPolimorphImportCall@@YAXXZ 00CB1E6F JMP un.?FixPolimorphImportCall@@YAXXZ 00CB200B JMP un.?FixPolimorphImportCall@@YAXXZ Опять ставим бряк за концом цикла... Остановились. Закрываем олю, восстанавливаем импорт (если надо), открываем в оле и смотрим на результат работы - все функции определились. Замечательно, теперь запускаем игру и радуемся :) Хотя один раз у меня неправильно определился вызов функции сохранения и через 5 минут игры мы упали на автосохранении, но, думаю, Вы с этим справитесь. Как можно заметить, никаких особенных проблем с распаковкой у нас не возникло, хотя автор сего творения Sony corp. и ожидалось нечто большее. Спасибо за интерес и пусть все игры идут у Вас без дисков :) |