Используемый инструментарий:
SoftICE & APIMon |
для отладки и исследования |
ProcDump |
для снятия дампа |
Revirgin |
для восстановления импорта |
PEIDentifier |
для распознавания большинства упаковщиков, протекторов и компиляторов для PE файлов |
Воспользовавшись программой PEIdentifier, я убедился, что она запакована “ASProtect 1.2x [New Strain] -> Alexey Solodovnikov”. Работа будет нелегкой.
Мало того, что ASProtect пакует программу и мне придется искать то место, где начинает работать программа, так он еще затрудняет ее поиск, применяя всяческие антиотладочные приемы.
Вообще ASProtect работает таким образом, что сначала начальный распаковщик распаковывает тело протектора, затем передает ему управление. Протектор распаковывает программу, выполняет необходимые действия и передает управление программе.
Ну, что же, когда-то надо начинать. У меня есть небольшой опыт в написании программ под WIN32, и я знаю, что при создании диалоговых окон, нужно значение, которое возвращает функция GetModuleHandleA. К тому же, это одна из первых функций, вызываемых программой, что мне несравненно играет на руку. Т.е. если мы найдем, где эта функция вызывается первый раз из NetVampire (далее программа), то мы будем рядом с OEP (Original Entry Point – оригинальная точка входа в программу; если бы не было ASProtect, то программа начиналась выполняться именно отсюда). Я стал отлавливать в программе то место, когда её вызовет не ASProtect, а NetVampire, адрес должен был начинаться с 4ххххх, но не как не с 9ххххх или с Аххххх. Для этого я поставил бряк на функцию GetModuleHandleA (bpx GetModuleHandleA). Но прерываний было слишком много, я устал жать клавишу F5 L . Поэтому, я пошел другим путем: запустил APIMon. В меню выбрал Monitor\spy\Add API…, ModuleName = kernel32.dll, APIName = GetModuleHandleA. Однако, иногда APIMon вел себя странно и не отлавливал все вызовы GetModuleHandleA, а только несколько десятков (должно быть 2-3 тысячи). В этом случае не пишите название библиотеки kernel32.dll и название функции GetModuleHandleA, а нажмите Export и выберите библиотеку kernel32.dll вручную, затем в появившемся списке выберите GetModuleHandleA, нажмите Add и затем OK. Для мониторинга, я нажал F5 и выбрал vampire.exe. Дождавшись появления окна, я закрыл программу NetVampire, чтобы больше не появлялись события, а то их и так больше 3 тысяч. Теперь моя задача – найти первый вызов функции GetModuleHandleA из модуля vampire.exe. Первый вызов функции GetModuleHandleA из программы должен принимать параметр 0 и возвращать значение ImageBase, т.е. в нашем случае 400000. Это справедливо для более чем 90% Windows-программ. Поэтому, используя функцию поиска, я нашел такое событие, в котором в стеке лежали бы адреса модуля Vampire.exe и возвращаемое значение было бы 400000. Я нажал на кнопку поиска и ввел 400000. И только четвертое вхождение увенчалось успехом. Логично, что найдя это место, где первый раз вызывается функция GetModuleHandleA из программы, а не из тела протектора, то я окажусь рядом с OEP.
Вот как выглядит найденный вызов в APIMon:
После найденного вызова я переписал все адреса в стеке, которые относились к модулю vampire.exe.
Затем в SoftICE поставил бряки на эти адреса, использовав команду bpm “addr” x (где “addr” это то, что закрашено серым цветом). BPX отпадает, т.к. физически SoftICE ставит команду INT3 по текущему адресу, что нарушает целостность программы. Об этом нам напоминает ASProtect, выдавая ошибку №15 при запуске. А команда bpm x целостности программы не нарушает и ASProtect не догадывается, что мы поставили бряк. Ключ Х означает, что бряк сработает, когда должна будет выполниться команда по указанному адресу. Для начала мне надо попасть в контекст программы. Ставлю бряк на функцию GetModuleHandleA (bpx GetModuleHandleA), и жму F5, пока в нижнем правом углу не появится имя модуля (Vampire). Все теперь можно ставить бряки на два адреса, которые были получены из APIMon (bpm 403774 x, bpm 4037CA x).
Запустил заново, не забыв отключить бряк на GetModuleHandleA, чтобы он мне не мешал. Сработал бряк по адресу 004037СА. Ниже была пара команд, а затем RET. Поэтому я решил протрейсить (идти пошагово, с помощью F10; можно также и F12 для выхода из функции, но не всегда она работает) программу дальше, и найти её "верхушку" – самую верхнюю процедуру, выше которой нельзя подняться. И в итоге после двух выходов из функций (например после двух нажатий F12) я оказался на адресе 4AD349. Ниже идет куча вызовов (CALL'ов), а по адресу:
4AD538 INVALID
Обычно так можно определить что это самая главная процедура, на которую передает управление загрузчик (в моем случаи ASProtect) и единственный выход из нее, это завершения процесса, т.к. никаких RET’ов в конце нет.
004AD333: |
0078CE |
add [eax][-0032],bh |
004AD336: |
4A |
dec edx |
004AD337: |
00558B |
add [ebp][-0075],dl |
004AD33A: |
EC |
in al,dx |
004AD33B: |
83C4F4 |
add esp,-00C |
004AD33E: |
53 |
push ebx |
004AD33F: |
B8A0CE4A00 |
mov eax,0004ACEA0 |
004AD344: |
E84B88F5FF |
call .000405B94 |
004AD349: |
E86A6AFCFF |
call .000473DB8 |
Где-то тут должно быть начало (OEP), т.к. работа с портом (команда in) не к месту. К тому же большинство программ начинаются с push ebp (байт 55h), по которому можно ориентироваться, но push ebx смотрится правильной командой, и чтобы узнать какие адреса выполнялись до нее (для нахождения OEP), отключаю все старые бряки, ставлю новый по адресу 04AD33E (bpm 04AD33E x) и перезапускаю программу. Когда происходит прерывание по этому бряку, SoftICE пишет, на каком адресе мы были до того, как попали на этот адрес. Это можно видеть из следующих строчек в окне СофтАйса:
MSR LastBranchFromIp=0A36306
MSR LastBranchToIp=04AD338
Значит по адресу 04AD338 тоже инструкция, опять удаляю старый бряк, добавляю новый на адрес 04AD338 и перезапускаю программу. Получаю сообщение:
MSR LastBranchFromIp=0A3493E
; этот адрес может быть другим!
Гляну что там по адресу 0А3493Е (u 0А3493Е).
0A3493E |
jmp eax |
; переход на Original Entry Point (OEP) |
Этот адрес не принадлежит модулю (иначе было бы написано над окном кода в айсе имя секции + смещение), значит, его создал распаковщик, и это явный переход от распаковщика к программе. Таким образом OEP = 4AD338 !
Для снятия дампа надо иметь девственную программу, где переменные не только не заданы, но и не инициализированные. Для этого её надо зациклить на адресе 0A3493E (этот адрес, как я уже сказал, меняется и может быть другим). Чтобы зациклить программу, в тот момент, когда мы прервались по бряку и находимся на адресе 4AD338, вводим в СофтАйсе следующие команды:
R eip A3493E |
; вы вводите свой адрес! |
A eip |
; начать ввод опкодов |
Jmp eip |
; зацикливаем на себя |
<Enter> |
|
<F5> |
Затем запускаю ProcDump для снятия дампа. Нахожу процесс vampire.exe в списке, далее кликаю правой кнопкой на его пути, потом в меню – Dump(Full). Ввожу любое имя файла для сохранения дампа. И убиваю процесс (в ProcDump’e правой кнопкой мыши (далее ПКМ) по процессу и выбираем kill task), т.к. зацикленный процесс занимает все свободное процессорное время :( .
Теперь надо восстановить таблицу адресов импорта. Так как таблица импорта уже инициализирована, то все ссылки из нее жестко прописаны на конкретные адреса функций, и на другой машине могут и не работать, если библиотека имеет другой build. Тем более таблицу импорта необходимо восстановить еще и потому, что протектор заменил адреса некоторых API-функции на адреса внутри себя, где эмулируются эти функции. Когда же мы распаковали программу, то адреса в тело протектора нужно заменить адресами исходных API-функций, т.к. протектор уже отключен и эти адреса не существуют, память не инициализирована. В ЕХЕ файле вместо реальных адресов, стоят ссылки на текстовые имена функций, которые потом в процессе инициализации заменяются адресами на начало функции. Поэтому надо вместо адреса на начало функции, поменять адрес имени функции. Можно это делать вручную, но это долго и муторно, да и незачем - лучше использовать специальные утилиты.
Отрубаю все бряки в айсе, пока они мне не понадобятся, и запускаю vampire.exe и программу Revirgin. Выбираю процесс vampire.exe. Программа ругается, что таблица импорта плохая и просит ввести ОЕР. Если ввести тот, что мы получили, она почему-то зависает L , поэтому я оставил тот, что по умолчанию - 401000. Жму Fetch IAT, она находит импорт и его длину. Потом кнопку IAT Resolved, немного подумав, программа выплевывает список всех импортируемых процессом функций. Потом нажал Resolve again, чтобы функции модуля kernel32, которые перенаправлены, определились (в принципе если непонятно, то надо практически всегда потом нажимать Resolve again). Осталось 8 неопределенных функций, которые ссылаются на адреса распаковщика. Вот эти адреса: A20DBC, A21210, A2122C, A2125C, A21264, A21270, A21280, A21290. Неплохо бы узнать, что эти функции возвращают, чтобы заменить их на реально существующие. Параметры функции передаются через стек, а ответ функция возвращает в регистре ЕАХ. Идем дальше.
Листинг можно получить в айсе (команда u). Но сначала надо сделать наш процесс текущим (для этого запускаем программу, затем в СофтАйсе вводим (addr vampire), и затем “u xxxxxx”, где xxxxxx – один из вышеуказанных адресов распаковщика (протектора))
0A20DBC: |
55 |
push ebp |
|
0A20DBD: |
8BEC |
mov ebp,esp |
|
0A20DBF: |
8B550C |
mov edx,[ebp][0000C] |
; второй параметр, переданный функции |
0A20DC2: |
8B4508 |
mov eax,[ebp][00008] |
; первый параметр, переданный функции |
0A20DC5: |
8B0D2454A200 |
mov ecx,[000A25424] |
|
0A20DCB: |
8B09 |
mov ecx,[ecx] |
|
0A20DCD: |
3BC8 |
cmp ecx,eax |
|
0A20DCF: |
7509 |
jne 000A20DDA |
; переход |
0A20DD1: |
8B04955053A200 |
mov eax,[000A25350][edx]*4 |
|
0A20DD8: |
EB07 |
jmps 000A20DE1 |
|
0A20DDA: |
52 |
push edx |
|
0A20DDB: |
50 |
push eax |
|
0A20DDC: |
E87F43FFFF |
call kernel32!GetProcAddress |
; <= |
0A20DE1: |
5D |
pop ebp |
|
0A20DE2: |
C20800 |
retn 00008 |
Понять, что это за эмулируемая функция, можно, если прерваться на адресе A20DBC и проследить, как будет выполняться программа. В конце концов, я приду на адрес A20DDC и выполнится функция GetProcAddress, которая вернет адрес запрашиваемой функции в регистре eax и с тех пор, eax больше меняться не будет. Значит это так и есть функция GetProcAddress.
0A21210: |
55 |
push ebp |
|
0A21211: |
8BEC |
mov ebp,esp |
|
0A21213: |
8B4508 |
mov eax,[ebp][00008] |
; первый параметр, переданный функции |
0A21216: |
85C0 |
test eax,eax |
; проверка на неравенство нулю |
0A21218: |
7507 |
jne 000A21221 |
; если не равен 0, то прыгаем |
0A2121A: |
A17469A200 |
mov eax,[000A26974] |
; eax=400000 |
0A2121F: |
EB06 |
jmps 000A21227 |
; переход |
0A21221: |
50 |
push eax |
|
0A21222: |
E8313FFFFF |
call kernel32!GetModuleHandleA |
; <= |
0A21227: |
5D |
pop ebp |
|
0A21228: |
C20400 |
retn 00004 |
GetModuleHandleA(0)=400000. Значение по адресу А26974 было получено, где-то ранее, и по ходу выполнения программы оно не меняется, поэтому вызов функции не требуется. А если параметр не равен 0, то вызывается функция GetModuleHandleA. Так пусть она будет вызываться всегда.
Это замаскированная функция GetModuleHandleA.
0A2122C: |
6A00 |
push 000 |
|
0A2122E: |
E8253FFFFF |
call 000A15158 |
; не важно что за функция |
0A21233: |
FF35E06CA200 |
push d,[000A26CE0] |
; еах все равно изменит след. команда |
0A21239: |
58 |
pop eax |
; eax=[000A26CE0]=0A280105=GetVersion() |
0A2123A: |
C3 |
retn |
Это замаскированная функция GetVersion. Как я это узнал, да очень просто ;)
Ставлю бряк на GetModuleHandleA (bpx GetModuleHandleA), что бы попасть в контекст программы. Стираю бряк и ставлю новый на первый адрес программы - 401000 (bpm 401000 x). Жму F5, и после того, как программа запустилась, я ее перезапускаю. Сработает бряк на адресе 401000. Теперь я всегда буду останавливаться в этом месте и могу добавлять новые бряки. Дело в том, что все бряки, которые я ставлю на адреса А2хххх, после выхода из программы пропадают L . Теперь надо найти то место когда заполняют ячейку по адресу А26СЕ0.
Ставлю бряк на чтение/запись с этого адреса (bpm A26CE0) и когда он срабатывает вижу следующие строки:
MSR LastBranchFromIp=0A210E5
MSR LastBranchToIp=0A210E7
0A210E5 |
ret |
||
0A210E7 |
mov [ebx-0A], eax |
; eax=0A280105 |
Удаляю все бряки, ставлю новый на адрес 401000 (bpm 401000 x) и перезапускаю программу. Когда прерываюсь, удаляю либо просто отключаю (потом можно включить) бряк, по которому только что прервались и ставлю бряк на адрес 0A210E5 (bpm 0A210E5 x). Первый раз меня выкинуло, когда ЕАХ=400000, не то число, должно быть 0А280105, жму (F5) и вижу:
MSR LastBranchFromIp=0A21072
MSR LastBranchToIp=0A210DE
0A21072 |
jmp 0A210DE |
|
… |
… |
|
0A210DE |
push 0A210E7 |
Опять делаю, что надо чтобы прерваться на адресе 401000, после чего ставлю бряк на адрес 0A21072 (bpm 0A21072 х). И когда бряк срабатывает, вижу:
MSR LastBranchFromIp=77E7C486
MSR LastBranchToIp=0A2106D
77E7С486 |
ret |
; kernel32!GetVersion |
|
… |
… |
||
0A2106D |
push 0A21075 |
По адресу 77E7C486 находится последняя команда функции GetVersion, с тех пор значение ЕАХ не менялось и попало в ячейку по адресу А26СЕ0. Значит это функция GetVersion.
Смотрим дальше:
0A2125C: |
A1E46CA200 |
mov eax,[000A26CE4] |
; eax=[000A26CE4]=FFFFFFFF |
0A21261: |
C3 |
retn |
Это замаскированная функция GetCurrentProccess=FFFFFFFF. Аналогично тому, как мы находили GetVersion, ставим бряк на адрес 0А26СЕ4 (bpm 0А26СЕ4). Вылезает айс по адресу А210ЕА. Айс пишет, с какого адреса перешли на этот – а именно …ToIP=0A210E7. Ставим бряк туда (bpm 0A210E7 x). Оттуда на 0A210E5, затем на 0A210DE, 0A210D9, 0A210D4. При последнем возврате …ToIP=последняя команда функции GetCurrentProccess.
0A21264: |
E8073FFFFF |
call kernel32!GetVersion |
; камуфляж |
0A21269: |
A1EC6CA200 |
mov eax,[000A26CEC] |
; eax=[000A26CEC]=ProccessID |
0A2126E: |
C3 |
retn |
Видно, что хоть и вызывается функция GetVersion, которая возвращает значение в eax, но все равно, следующей командой значение в eax перезаписывается. Число подозрительно маленькое. Вообще из опыта известно, и вы запомните, что ASProtect часто в числе функций, которые он маскирует, маскирует и GetCurrentProcessID а такое небольшое относительно число, которое записалось в eax, напоминает ID процесса. Набираем команду “addr” и в списке имен процессов ищем vampire. Находим и видим, что в колонке PID, напротив vampire, как раз находится то же число, которое и записывается в регистр eax в рассматриваемой нами функции (или ? PID, находясь в контексте программы). Значит эта функция возвращает в eax ID процесса, а этим занимается функция GetCurrentProcessID, хотя конечно никто не мешал мне опять делать рутинную работу по отлову места сохранения этой переменной как в случае с GetVersion.
0A21270: |
6A00 |
push 000 |
|
0A21272: |
E8E13EFFFF |
call 000A15158 |
|
0A21277: |
FF35F06CA200 |
push d,[000A26CF0] |
|
0A2127D: |
58 |
pop eax |
; eax=[000A26CF0]=142360=GetCommandLineA |
0A2127E: |
C3 |
retn |
Хоть и вызывается какая-то функция call A155158 , но значение eax потом все равно меняется. Сначала в стек ложится какое-то значение, хранящееся по адресу А26CF0, и потом это значение восстанавливается из стека в регистр eax. Смотрю, что у нас в eax, и вижу некое значение. Скорее всего это какой-то адрес в памяти. Смотрю, что лежит по адресу eax (“d eax”), и вижу, что это путь к файлу vampire.exe, а точнее командная строка. Адрес командной строки возвращает функция GetCommandLineA , значит это и есть замаскированная функция GetCommandLineA. Если вас что-то смущает – используйте предыдущий метод =))
0A21280: |
55 |
push ebp |
|
0A21281: |
8BEC |
mov ebp,esp |
|
0A21283: |
E8983EFFFF |
call kernel32!GetCurrentProcess |
|
0A21288: |
8B4508 |
mov eax,[ebp][00008] |
; первый параметр, переданный функции |
0A2128B: |
5D |
pop ebp |
|
0A2128C: |
C20400 |
retn 00004 |
Эта функция возвращает то же самое, что и принимает. Т.е. это как бы пустышка. Почему? Давайте разберемся. Перед вызовом данной функции в стек помещается передаваемый параметр. Когда эта функция вызывается с помощью команды call, то в стек еще помещается адрес возврата, потом уже в функции по адресу 0A21280 в стек еще помещается ebp.
Теперь, с учетом смещения указателя стека, в стеке по адресу esp будет лежать ebp, по адресу esp+4 будет адрес возврата, и по адресу esp+8 – передаваемый параметр. Mov eax, [ebp][0008] это то же самое, что и mov eax, [ebp+8], а так как выше мы приравняли ebp к esp , то это тоже самое что и mov eax, [esp+8], значит в eax помещается передаваемый параметр. А так как в eax помещается значение, возвращаемое функцией, то получается, что функция возвращает тот же параметр, который ей и передали. Т.е., как я уже сказал, это функция-пустышка. Такие функции заменяются функцией LockResource.
0A21290: |
55 |
push ebp |
0A21291: |
8BEC |
mov ebp,esp |
0A21293: |
E8D83EFFFF |
call kernel32!GetVersion |
0A21298: |
5D |
pop ebp |
0A21299: |
C20400 |
retn 00004 |
Это замаскированная функция GetVersion.
В итоге я разгадал все функции. Осталось в Revirgin’е вписать адреса реальных функций, вместо тех, что не определились. Для получения реальных адресов достаточно написать в айсе exp <ExportFunct>. Например:
exp GetVer |
|
KERNEL32 |
|
001B:77E7C486 |
GetVersion |
001B:77E7C657 |
GetVersionExA |
001B:77E7C61c |
GetVersionExW |
77Е7С4
86 и надо будет вписывать.Для внесения изменений нажал ПКМ и выбрал меню EDIT, а в колонке адрес, где до этого были адреса Аххххх, вписал адреса распознанных API-функций. Например, вместо A21290 я вписал 77E7C486 – адрес функции GetVersion, и так со всеми адресами. Затем жму кнопку generate и выбираю дамп (файл, который создали, когда делали dump в ProcDump’е). Осталось сменить ЕР на ОЕР. В ProcDump’e жму кнопку PE Editor, выбираю дамп, и в поле Entry Point, ввожу новое значение, а именно 0AD338 = (4AD338-400000).
С радостью запускаю и … облом, ошибка чтения по адресу 0А13861. Дело в том, что раньше эти адреса занимал распаковщик, а сейчас там его нет. Запускаю оригинал с целью посмотреть что там есть (делаю процесс текущим, как я объяснял выше и пишу команду “d A13861”), а там 0 L . Ну тут опять мне помог предыдущий опыт использования и исследования ASProtect, в особенности его примеры; ASProtect предоставляет кроме защиты ЕХЕ, еще 4 функции: ASProtect Key, Keygen, Trial, User Key, с примерами. В нашем случае, в программе NetVampire защита построена с помощью функции ASProtect’a – Trial. В этой функции защита построена на ненадежном способе (если бы не запакованный файл), когда проверка на регистрацию продукта сводится к проверке длины строки, которая возвращает функция GetRegistrationInformation. За эту функцию отвечает сам ASProtect. Если строка пусто (т.е. первый байт = 0), то продукт не зарегистрирован. Вся регистрация сводилась к тому, что вместо адреса 0А13861, я подсунул другой, в теле программы, и по тому адресу написал свое имя :) . Этим я сделал 2 дела: теперь программа запускалась, т.к. обращение по адресу A13861 уже не происходило, и ошибки не возникало, и программа думала, что она зарегистрирована. Вообще надо найти такое место в программе, где было бы много нулей, и если туда, что-то вписать, то это не повлияет на работу программу. Такие места обычно бывают в конце секций, в конце файла. Надо найти такое место, вписать там свое имя и указать этот адрес вместо A13861. Я использовал в качестве такого места – участок по адресу 4AD55B (AD55B в дампе). Приведу листинг:
4A4FD2: |
89903C060000 |
mov [eax][00000063C],edx |
|
4A4FD8: |
A1B8F94A00 |
mov eax,[0004AF9B8] |
; еах = 4AEDF0 |
4A4FDD |
8B00 |
mov eax,[eax] |
; еах = адрес, по которому будем проверять длину строки |
4A4FDF: |
E8BC2FF6FF |
call .000407FA0 |
|
4A4FE4: |
85C0 |
test eax,eax |
407FA0: |
89FA |
mov edx,edi |
|
407FA2: |
89C7 |
mov edi,eax |
|
407FA4: |
B9FFFFFFFF |
mov ecx,0FFFFFFFF |
|
407FA9: |
30C0 |
xor al,al |
|
407FAB: |
F2AE |
repne scasb |
; здесь и происходит ошибка |
407FAD: |
B8FEFFFFFF |
mov eax,0FFFFFFFE |
|
407FB2: |
29C8 |
sub eax,ecx |
|
407FB4: |
89D7 |
mov edi,edx |
|
407FB6: |
C3 |
retn |
По адресу 4AEDF0 лежит 0A13861, вот его я и заменил на своё - 4AD55B. В НЕХ-редакторе по адресу AEDF0 в файле вписал 5B D5 4A 00, именно в таком перевернутом порядке. А по адресу AD55B вписал своё имя на веки :) .
Автор freeExec благодарит MozgC <MozgCstopSpam @ avtograd.ru> за то, что согласился проверить статью и
исправить все недочеты
04.07.2003 – 13.07.2003 – by freeExec
Материалы находятся на сайте http://cracklab.narod.ru/doc/