Главная | Программы | Статьи | Заработай 100$ и более

Гостевая книга

 
Статьи: Отучаем Empire Earth 2 от CD (SecuRom) 26.08.2005

 

Здрасти всем, читающим этот документ. Я тут впал в детство, заигрался в игру EmpireEarth 2. Симпатичная графика, много юнитов, интересный геймплей, плохо что подтормаживает, но ничё не поделать – за качество надо платить, а не хочется. Ну вот значит, диск лицензионный (точнее два), и не мой. Пора бы отдавать, а игра не пашет без него. И тут меня понесло :) Со мной всё понятно – я должен пользоваться этой игрой, только если куплю лицензионный диск, а вот как же мой друг. У меня (да и у многих наверное) не раз диски рассыпались в приводе, ну или на худой конец царапались до невозможности чтения. На этот случай они скинуты на RW’хи. С этим я поступил также. Итог запуска заставляет задуматься – вставьте настоящий диск, а не копию. Это откровенная наглость. Вот так, из чисто демократических побуждений (хочу иметь выбор – с лицензии запускать или с копии, всё равно заплатил всю стоимость) появилась эта статья. Цель её сугубо научная – проверить, сможем ли мы играть в свою любимую игру после того, как настоящий диск с ней разлетится или испортится. Разлетать и портить диск мы не будем, а вот поисследовать можно, достаточно вытащить ЦД из привода и запустить файл с игрой :)

 

Инструменты:
OllyDebug
ImpRec
Microsoft Visual Studio 7.x

 

Ну ладно, я вроде успокоился, теперь приступим к исследованию. Сначала вытаскиваем диск и видим просьбу его вставить. Теперь вставляем копию и видим уже описанное сообщение, но обращаем внимание на последнюю строчку – 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
00C80A53   A1 781DE100      MOV EAX,DWORD PTR DS:[E11D78]
00C80A58   FFE0             JMP NEAR EAX

 

Это переход на OEP. Прыгаем туда и делаем дамп. Самое простое позади. Теперь посмотрим, что же делалось с импортом. Видим очень грустную картину – нормальных вызовов минимум и куча переходников. Их можно разделить на 2 группы – перенаправление в память, выделенную из кучи:

004022CF   50               PUSH EAX
004022D0   FF15 1098D100    CALL NEAR DWORD PTR DS:[D19810] ; 01C20000

 У меня это адреса с 1C000000 по 01CB0000.

 Или перенаправление в секцию SecuRom’a:

// Обычный вызов
00401EBF   E8 DCFE8A00      CALL EE2.00CB1DA0

// И по значению DWORD'a с адресом хххх (D3668C в данном случае)
004087B3   FF15 8C66D300    CALL NEAR DWORD PTR DS:[D3668C]          ; EE2.00CB1DA0

 

Какую именно апи функцию вызвать в обоих случаях решается из адреса возврата. Алгоритм получения достаточно громоздок и использует много данных, причём инициализируемых во время выполнения программы. Поэтому переписать алгоритм и выполнить его над сдампленным файлом не получится. Что мы можем сделать? Для начала посмотрим, как же работает этот механизм. Сперва обратим внимание на переходники в динамическую память. Ставим бряки на все их вызовы и отпускаем игру. Стопоримся здесь:

 00A01AE9   FF7424 04        PUSH DWORD PTR SS:[ESP+4]
00A01AED   FF15 3498D100    CALL NEAR DWORD PTR DS:[D19834]

 Теперь заходим и осторожно трейсим... 

01CB0695   8B85 68FFFFFF    MOV EAX,DWORD PTR SS:[EBP-98]
01CB069B  -FF2485 55F5C200  JMP NEAR DWORD PTR DS:[EAX*4+C2F555]     ; EE2.00C2ECF3

 Как видим, прыг возвращает нас в наш файл, точнее в секцию секурома.
 После продолжительных путешествий приходим сюда. Вот здесь мы и переходим на апи функцию.
 
Адрес АПИ-функции в таблице импорта ([EBP-34])

00C2F4AA   8B45 CC          MOV EAX,DWORD PTR SS:[EBP-34]
00C2F4AD   8BF0             MOV ESI,EAX

А потом берём значение по этому адресу. Видим, что за апи – onexit. Адрес апи в EAX

00C2F4AF   8B06             MOV EAX,DWORD PTR DS:[ESI]
00C2F4B1   5F               POP EDI
00C2F4B2   5E               POP ESI
00C2F4B3   5B               POP EBX
00C2F4B4   8BE5             MOV ESP,EBP
00C2F4B6   5D               POP EBP

И прыгаем туда...

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

Опять возвращаемся в программу.
И вот интересное место.
Адрес функции в таблице импорта программы в EAX

00C2F4AA   8B45 CC          MOV EAX,DWORD PTR SS:[EBP-34]
00C2F4AD   8BF0             MOV ESI,EAX

Адрес апи-функции

00C2F4AF   8B06             MOV EAX,DWORD PTR DS:[ESI]
00C2F4B1   5F               POP EDI
00C2F4B2   5E               POP ESI
00C2F4B3   5B               POP EBX
00C2F4B4   8BE5             MOV ESP,EBP
00C2F4B6   5D               POP EBP

И прыгаем туда...

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,             // адрес возврата в программу после вызова апи
ImportTableAddress,     // Адрес в таблице импорта, где хранится настоящее значение адреса АПИ-функции
OffsetInFile;

DWORD BeginFileInMemory = 0x401000,
EndFileInMemory = 0x0A6A000,
CurAddress = 0;

WORD   NearCall  = 0x15FF;// опкод near call'a
DWORD DestBytesInMemory  = 0x00D198FF;// В выделенную память типа 1C00000-1C80000

WORD   ReadBufferCall;// прочитанные байты из файла, проверять на опкоды прыгов
DWORD BytesReaded, ReadBufferBytesCall;
DWORD AddrOfFindCallFunc;
static    int i = BeginFileInMemory;

 BOOL APIENTRY DllMain( HANDLE hModule,
DWORD  ul_reason_for_call,
LPVOID lpReserved)
{

DUMP_Handle = CreateFile ("dmp.exe", GENERIC_WRITE | GENERIC_READ,FILE_SHARE_READ | FILE_SHARE_WRITE,NULL,OPEN_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL);
return TRUE;
}

__declspec(dllexport) void FindCall()
{
for ( ; i < EndFileInMemory;  )
{
i++;
 CurAddress = i - 0x400000;
SetFilePointer ( DUMP_Handle, CurAddress, 0, 0);
ReadFile ( DUMP_Handle, &ReadBufferCall, 2, &BytesReaded, 0 );
 

 // Проверка на nearCall
if ( ReadBufferCall == NearCall )
{
 CurAddress += 2;
// Проверка, первый ли тип переходника
SetFilePointer ( DUMP_Handle, CurAddress, 0, 0);
ReadFile ( DUMP_Handle, &ReadBufferBytesCall, 4, &BytesReaded, 0 );
// Сбрасываем младший байт и сравниваем с нашей константой
ReadBufferBytesCall |= 0x000000FF;
if ( ReadBufferBytesCall == DestBytesInMemory )
{
// Переходим туда
__asm jmp near i
}
}
}
}

Ну вот, поиск сделали, теперь стоит подумать о поправке данных в дампе. Просто берём адрес, ведущий в IAT (EBP-34) и пишем в дамп

// Исправление импорта типа CALL NEAR DWORD PTR DS:[D36664] в выделенную память

__declspec(dllexport) void FixImportCall ()
{
__asm
{
pop ebp; Инлайн-асм автоматом кладёт в стэк любой регистр, которым мы воспользуемся, поэтому восстанавливаем, чтоб ESP не испортить
mov eax, ebp                            ; Настоящие
sub eax,0x34                            ; байты
mov ImportTableAddress, eax
mov eax,  [esp+0x54]    ; откуда вызов апи в исходном файле
mov ReturnAddress, eax
sub eax, 0x400004                    ; смещение в файле байтов адреса вызова
push 0
push 0
push eax
push DUMP_Handle
call NEAR DWORD PTR DS:[SetFilePointer]

push 0
push offset ReturnAddress         ; Запишем сюда, сколько байтов записалос. Это нам не важно, но если не задать - упадём
push 4
push ImportTableAddress
push DUMP_Handle
call near DWORD PTR ds:[WriteFile]
jmp AddrOfFindCallFunc
}
}

Теперь надо загрузить нашу библиотеку. Где-нибудь в конце файла делаем изменения:

00A69764   75 6E            JNZ SHORT EE2.00A697D4
00A69766   2E:              PREFIX CS:                               ; Superfluous prefix
00A69767   64:6C            INS BYTE PTR ES:[EDI],DX                 ; I/O command
00A69769   6C               INS BYTE PTR ES:[EDI],DX                 ; I/O command
00A6976A   0000             ADD BYTE PTR DS:[EAX],AL
00A6976C   0000             ADD BYTE PTR DS:[EAX],AL
00A6976E   0000             ADD BYTE PTR DS:[EAX],AL
00A69770   0000             ADD BYTE PTR DS:[EAX],AL
00A69772   0000             ADD BYTE PTR DS:[EAX],AL
00A69774   0000             ADD BYTE PTR DS:[EAX],AL
00A69776   68 6497A600      PUSH EE2.00A69764                        ; ASCII "un.dll"
00A6977B   E8 E1414177      CALL kernel32.LoadLibraryA
00A69780  ^E9 7B85F9FF      JMP EE2.00A01D00
00A69785   90               NOP

И на переходе на ОЕП в EAX пишем 00A69776. Теперь мы можем пользоваться нашим кодом в библиотеке. На ОЕП переходим на поиск переходников

00A01D00  -E9 FBF22901      JMP un.?FindCall@@YAXXZ

Ну а дальше смотрим, что делает наша библиотека

01CA105E   0FB705 10ACCA01  MOVZX EAX,WORD PTR DS:[ReadBufferCall]
01CA1065   0FB70D 88A0CA01  MOVZX ECX,WORD PTR DS:[NearCall]
01CA106C > 3BC1             CMP EAX,ECX
01CA106E   75 5E            JNZ SHORT un.01CA10CE
01CA1070   A1 34ACCA01      MOV EAX,DWORD PTR DS:[CurAddress]
 

Ставим бряк за 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]
01C90449   ADD EAX,4
01C9044C   MOV DWORD PTR SS:[EBP-44],EAX
01C9044F   MOV ECX,DWORD PTR DS:[E14544]
01C90455   SHR ECX,2
01C90458   CMP DWORD PTR SS:[EBP-44],ECX
01C9045B   JGE SHORT 01C9047B
01C9045D   MOV EDX,DWORD PTR DS:[E172E4]
01C90463   MOV EAX,DWORD PTR DS:[EDX]
01C90465   MOV ECX,DWORD PTR SS:[EBP-44]
01C90468   MOV EDX,DWORD PTR DS:[E18564]            ; EE2.00C2C620
01C9046E   ADD EAX,DWORD PTR DS:[EDX+ECX*4]
01C90471   MOV ECX,DWORD PTR DS:[E172E4]
01C90477   MOV DWORD PTR DS:[ECX],EAX
01C90479  JMP SHORT 01C90446

Такие куски кода встречаются во всех выделенных участках памяти. Обходится эта штука элементарно:

01C904A1   SUB ECX,DWORD PTR DS:[EAX]
01C904A3   ADD ECX,0B1327A

Здесь после вычитания ECX и значения по адресу EAX должен быть ноль, то есть они должны быть равны, после к разности прибавляется константа. Теперь меняем

01C904A3   ADD ECX,0B1327A 

на

01C904A3   MOV ECX,0B1327A
01C904A8   NOP

И радуемся :)

Вот мы остановились на конце цикла. Жмём Alt+F2, фиксим импорт у дампа и грузим его в олю. Теперь открываем запакованную, доходим до ОЕП, ищем Intermodular Calls, сортируем по адресу местоположения и смотрим, где был первый переходник

Address=004016F0
Disassembly=CALL NEAR DWORD PTR DS:[D19830]
Destination=DS:[00D19830]=01C80000 

Теперь топаем на адрес 004016F0 в пофиксеном дампе

004016F0   FF15 DCA0A600    CALL NEAR DWORD PTR DS:[<&kernel32.GetPr>; kernel32.GetProcAddress

Хорошо, апи определилась. Значит с первым типом вроде разобрались. Давайте разбираться со вторым...

В дампе опять ищем все вызовы и видим наши цели:

00401432   FF15 5866D300    CALL NEAR DWORD PTR DS:[D36658]          ; dmp_.00CB1B90 
0040141F   E8 3C0B8B00      CALL dmp_.00CB1F60

Теперь сортируем по Destination и видим, что колы ведут сюда:

00CB1B90, 00CB1DA0, 00CB1F60 - то есть в 3 места, поэтому, видимо, придётся править 3 штуки. Теперь разбираемся, каким образом здесь происходит переход на апи. Ставим бряки на прыги туда в запакованной проге и смотрим.

0041ABA8   CALL NEAR DWORD PTR DS:[D36650]          ; EE2.00CB1B90
00CB1C3A   MOV EAX,DWORD PTR SS:[EBP-14]            ; EE2.00A6A1C0
00CB1C3D   POP EDI
00CB1C3E   POP ESI
00CB1C3F   POP EDX
00CB1C40   POP ECX
00CB1C41   POP EBX
00CB1C42   MOV ESP,EBP
00CB1C44   POP EBP
00CB1C45   MOV EAX,DWORD PTR DS:[EAX]
00CB1C47   JMP NEAR EAX

 Здесь адрес функции в IAT хранится в [EBP-14]. Переход на апи обычным джампом. Следующий.

004B7E9D   CALL EE2.00CB1F60
00CB2008   MOV DWORD PTR SS:[EBP-14],EAX
00CB200B   POP EDI
00CB200C   POP ESI
00CB200D   POP EDX
00CB200E   POP ECX
00CB200F   POP EBX
00CB2010   MOV ESP,EBP
00CB2012   POP EAX
00CB2013   XCHG EAX,EBP
00CB2014   MOV EAX,DWORD PTR DS:[EAX-14]
00CB2017   PUSH DWORD PTR DS:[EAX]
00CB2019   RETN

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

00CB1E6C   MOV DWORD PTR SS:[EBP-14],EAX
00CB1E6F   MOV ESI,DWORD PTR SS:[EBP-14]
00CB1E72   MOV EAX,DWORD PTR DS:[ESI]
00CB1E74   MOV DWORD PTR SS:[EBP+4],EAX
00CB1E77   POP EAX
00CB1E78   POP EDI
00CB1E79   POP ESI
00CB1E7A   POP EDX
00CB1E7B   POP ECX
00CB1E7C   POP EBX
00CB1E7D   POP EDI
00CB1E7E   POP ESI
00CB1E7F   POP EBX
00CB1E80   MOV ESP,EBP
00CB1E82   POP EBP
00CB1E83   RETN

Всё то же самое, только адрес возвращения в апи необычно устанавливается. Вывод: во всех случаях адрес функции в таблице импорта хранится в [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

// Адреса, куда перенаправлен импорт.
DWORD DestAddresses [] = {0x00CB1B90, 0x00CB1DA0, 0x00CB1F60};
DWORD DestBytesInFile = 0x00D366FF;              // В секцию SecuRom'a
// Мусорный код, маскирующийся перед или после call'a
BYTE     JunkCode [] = { 0x90, 0xf8, 0xf9, 0xf5, 0x40, 0x27 };
// Откуда именно начинается call
DWORD RealCallAddress;
WORD   Call       = 0xE8FF;                                // опкод обычного call'a

// Или в секцию SecuRom'a
SetFilePointer ( DUMP_Handle, CurAddress, 0, 0);
ReadFile ( DUMP_Handle, &ReadBufferBytesCall, 4, &BytesReaded, 0 );
ReadBufferBytesCall -= 0x400000;
SetFilePointer ( DUMP_Handle, ReadBufferBytesCall, 0, 0);
ReadFile ( DUMP_Handle, &ReadBufferBytesCall, 4, &BytesReaded, 0 );
if ( (ReadBufferBytesCall == DestAddresses[0]) || (ReadBufferBytesCall == DestAddresses[1]) || (ReadBufferBytesCall == DestAddresses[2]) )
{
RealCallAddress = i;
__asm jmp near i
}
}

// Если не near, а обычный call
ReadBufferCall |= 0xff;
if ( ReadBufferCall == Call )
{
CurAddress += 1;
SetFilePointer ( DUMP_Handle, CurAddress + 1, 0, 0);
ReadFile ( DUMP_Handle, &ReadBufferBytesCall, 4, &BytesReaded, 0 );
// Прочитано 2 байта, то есть перед E8 есть ещё байт, а смещение в файле на 1 меньше, поэтому +1
ReadBufferBytesCall += i + 1 + 5;
// Опять проверка, в секцию SecuRom'a или нет
if ( (ReadBufferBytesCall == DestAddresses[0]) || (ReadBufferBytesCall == DestAddresses[1]) || (ReadBufferBytesCall == DestAddresses[2]) )
{
AnalyzeCall ();
i++;
if ( RealCallAddress != -1 )
__asm jmp near i
}
}

Поиск сделали, теперь займёмся фиксингом. Чтобы починить переделанные колы нам для начала нужно найти настоящее начало, то есть мусорный байт стоит перед или после call'a, затем по этому смещению записать 2 байта - FF 15, чтобы превратить В CALL NEAR и дальше 4 байта адреса в IAT. Но для начала надо что-то типа анализатора :) сварганить. Ничё сложного вроде здесь нет.

void AnalyzeCall ()
{
 Junk_Before_Call = false;
Junk_After_Call = false;
SetFilePointer ( DUMP_Handle, CurAddress - 1, 0, 0);
ReadFile ( DUMP_Handle, &ByteBefore, 1, &BytesReaded, 0 );
for ( int i = 0; i < 6; i++ )
{
if ( ByteBefore == JunkCode  [ i ] )
Junk_Before_Call = true;
}
SetFilePointer ( DUMP_Handle, CurAddress + 5, 0, 0);
ReadFile ( DUMP_Handle, &ByteAfter, 1, &BytesReaded, 0 );
 
for ( int i = 0; i < 6; i++ )
{
if ( ByteAfter == JunkCode [ i ] )
Junk_After_Call = true;
}


if ( (Junk_Before_Call & Junk_After_Call) )
{
RealCallAddress = -1;
return;
}
 
if ( Junk_After_Call == true )
{
RealCallAddress = CurAddress;
RealCallAddress += 0x400000;
return;
}
           
if ( Junk_Before_Call == true )
{
RealCallAddress = CurAddress - 1;
RealCallAddress += 0x400000;
return;
}
}
 

И сам исправляющий код

// Исправление импорта типа CALL EE2.00CB1F60
__declspec(dllexport) void FixPolimorphImportCall ()
{
__asm
{         
 pop ebp
mov eax, ebp                            ; Настоящие
sub eax,0x14                            ; байты
mov ImportTableAddress, eax 
mov eax, RealCallAddress
mov ReturnAddress, eax
 

sub eax, 0x400000                    ; смещение в файле байтов call'a
push 0
push 0
push eax
push DUMP_Handle
call NEAR DWORD PTR DS:[SetFilePointer] 

push 0
push offset ReturnAddress
push 2
push offset NearCall
push DUMP_Handle
call near DWORD PTR ds:[WriteFile]

push 0
push offset ReturnAddress
push 4
push ImportTableAddress
push DUMP_Handle
call near DWORD PTR ds:[WriteFile]
 jmp AddrOfFindCallFunc
}
}

 Вот вроде и всё. Теперь тормозим на прыге на оеп, добавляем загрузку dll, переходим туда, дальше на оеп. Там делаем прыг в функцию FindCall (вобщем всё как в первый раз). Меняем 99 на адрес FindCall и добавляем переходы в нашу библиотеку перед переходами на апи

00CB1C3A  JMP un.?FixPolimorphImportCall@@YAXXZ

00CB1E6F  JMP un.?FixPolimorphImportCall@@YAXXZ

00CB200B  JMP un.?FixPolimorphImportCall@@YAXXZ

Опять ставим бряк за концом цикла... Остановились. Закрываем олю, восстанавливаем импорт (если надо), открываем в оле и смотрим на результат работы - все функции определились. Замечательно, теперь запускаем игру и радуемся :) Хотя один раз у меня неправильно определился вызов функции сохранения и через 5 минут игры мы упали на автосохранении, но, думаю, Вы с этим справитесь.

Как можно заметить, никаких особенных проблем с распаковкой у нас не возникло, хотя автор сего творения Sony corp. и ожидалось нечто большее.

Спасибо за интерес и пусть все игры идут у Вас без дисков :)