Как всё начиналось…
Приветствую всех, кто благополучно добрался до второй части. В этой части мы, наконец, закончим с философией и перейдём к практическим исследованиям. Конечно, для практики нам понадобятся некоторые знания и умения. О некоторых из них я говорил в конце первой части, но тут хотелось бы добавить, что, по-моему, само собой разумеется, но всё же – вы должны знать ассемблер. Нет, не наизусть – такого знания от вас никто не требует, но уметь читать листинги дизассемблера вы обязаны. Т.к. протекторы по своей природе являются весьма изощрёнными штуками, мы вполне можем ожидать использования в их коде редких ассемблерных инструкций. Поэтому весьма может быть, что рано или поздно вы столкнётесь с незнакомой вам инструкцией, вот тут-то и понадобятся мануалы интела:
http://www.intel.com/products/processor/manuals/
Также следует скачать себе прекрасную утилиту Import Reconstructor (ImpRec) и прочитать не менее чудесную документацию к ней. Поверьте, чтение документации в данном случае действительно оправдано, тем более, что мы ещё вернёмся к обсуждению этой программы, примерно, в четвёртой части нашего цикла статей. Там мы подробно обсудим всё, что с ней связано, а пока качайте и изучайте.
http://www.cracklab.ru/download.php?action=get&n=MjI1
Далёкий 2000 год. В свет на паблик вышла первая версия Asprotect с многообещающим индексом 1.0. По утверждениям автора этого именно протектора:
ASProtect – the system of software protection of applications, designed for quick implementation of application protection functions, especially targeted for shareware authors. ASProtect is designed for such specific tasks as working with registration keys and creation of evaluation and trial application versions.
Я тогда ещё не знал, что такое компьютер, а Аспр уже жил и развивался. Конечно, первая версия не сравнится с поделками нашего времени, но начнём разбирать механизм этого прота с простого, постепенно переходя к более сложным и изощрённым версиям Аспра.
В первой практической статье я хотел бы обратить ваше внимание на общую структуру защитного кода, позже - на восстановление важных таблиц РЕ-файла, на удаление из него более ненужного кода самого протектора. Техника ключей и удаление триальных окон пока останется в стороне. Но, дорогу осилит идущий, так что давайте начнём уже топать.
Наша цель
Понятно, что нашей целью является аспр, но он не интересует нас сам по себе, т.е. как самостоятельная программа. Здесь мы рассматриваем его как навесную систему защиты. И, по логике вещей, она должна быть на что-то навешана. Т.к. сам ехе-файл прота, который у меня есть, уже давно пропатчен и взломан, мы не будем его трогать, а запакуем им (Аспром) какую-нибудь программку, после чего будем распаковывать. На том и порешили.
В качестве цели я выбрал небезызвестную Resource Haker, изначально написанную на Delphi. Почему именно эту программку? Хороший, а главное – правильный вопрос. Дело в том, что эта версия 1.0. очень нестабильна – некоторые программы просто отказывались работать после того, как я их упаковывал. Т.к. сам ASProtect написан тоже на Delphi, я предположил, что он совместим хотя бы с тем же Delphi, и не ошибся. Но, чего вы хотели? Это ведь первая версия – далее Солодовников доработает своё детище, в чём мы ещё не раз убедимся.
Опции защиты… Тут, собственно, пока и говорить нечего. Все доступные опции в этой версии аспра:
- Anti-debugger protection.
- Checksum protection.
Это и всё, что мы можем выбрать. Ну и ладно, выбираем эти опции и получаем защищённое приложение. Как я и предупреждал – триальные опции и ключи шифрования ждут нас где-то впереди.
Как это работает…
In general, application protection mechanism is based on the “envelope” principle, in which the application is put. The application is packed (using ASPack compression engine), i.e. all application sections (code, data, import tables, resources) are processed and then the protection code is appended to the end of the file. The size of the protection code is about 60K bytes packed. First the control is gained by the protection code, which checks the application integrity, debugger presence, the registration key, processes trial version limitations, then decrypts and unpacks the application data, initializes the application data needed (processes the import functions and relocation tables) and passes the control to the application. Unlike other protection schemes ASProtect has the API (Application Programming Interface) to interact with the application being protected, and this greatly increases the counteraction to the attempts to remove the protection.
Солодовников в общем написал всё за нас. Давайте добавим сюда подробностей. Загружаем приложение в отладчик (т.е. в Olly).
Вот сейчас сюда добавим немного теории. Как именно вы загружаете приложение в отладчик? Не именно это, защищённое аспром, а любое другое, защищённое неизвестным вам протектором?
Изначально, давайте обсудим вопрос касательно Ольки – нашего отладчика. Что он (отладчик) из себя представляет? Все, наверное, слышали, что это – отладчик режима пользователя, предназначенный для отладки таких же пользовательских приложений, но что это значит? Это значит, что код ядра ОС для этого отладчика закрыт, поэтому с его помощью мы можем исследовать код, работающий только в Ring-3, а также мы не можем патчить (изменять) код за пределами всё того же Ring-3 с помощью Ольки. Также следует сказать, что Олька работает, используя Debug API. Что это значит? Это значит, что при её работе используются только экспортируемые отладочные функции, разработанные Microsoft и являющиеся частью ОС.
BOOL CreateProcess(
LPCWSTR pszImageName,
LPCWSTR pszCmdLine,
LPSECURITY_ATTRIBUTES psaProcess,
LPSECURITY_ATTRIBUTES psaThread,
BOOL fInheritHandles,
DWORD fdwCreate,
LPVOID pvEnvironment,
LPWSTR pszCurDir,
LPSTARTUPINFOW psiStartInfo,
LPPROCESS_INFORMATION pProcInfo
);
Этой функцией создаётся процесс для отладки, но чтоб его можно было отлаживать, необходимо создать процесс с флагами DEBUG_PROCESS и DEBUG_ONLY_THIS_PROCESS для того, чтоб в случае, если отлаживаемый процесс создаст ещё один процесс, наш отладчик не реагировал на события отладки, которые бы в этом процессе возникали. Далее функция WaitForDebugEvent получает уведомления обо всем отладочных событиях. Подробнее – MSDN.
Так вот, всё, что мы можем настроить в Ольке (касательно начала отладки) находится в Options -> Debugging options -> Events -> Make first pause at рекомендую поставить там System breakpoint. Что это за системная точка останова?
typedef struct _EXCEPTION_DEBUG_INFO {
EXCEPTION_RECORD ExceptionRecord;
DWORD dwFirstChance;
} EXCEPTION_DEBUG_INFO;
В этой чудесной структуре dwFirstChance как раз сделана для детекта этой системной точки останова.Таким образом, в случае, если приложение отлаживается, в его недрах происходит отладочное исключение. А конкретнее – происходит оно по адресу экспортируемой функции внутри модуля ntdll.dll, имя которой DbgBreakPoint. Некоторые могут подумать, будто это исключение происходит всегда, но это не так – оно имеет место быть только в отлаживаемых процессах. Не верите? Смотрите на листинг дизассемблера:If the dwFirstChance member is nonzero, this is the first time the debugger has encountered the exception.
На этот адрес может попасть только отсюда:Code:loc_7C93E615: ; DbgBreakPoint() call _DbgBreakPoint@0 - внутри этой функции происходит останов по System breakpoint mov eax, [ebx+68h] shr eax, 1 and al, 1 mov _ShowSnaps, al jmp loc_7C9210BC
Таким образом, если byte ptr [ebx+2] не равен нулю, тогда и только тогда происходит переход на функцию DbgBreakPoint. Осталось только понять, что же за значение находится в ebx. А в ebx находится адрес чудесной структуры PEB (вы ведь читали рекомендованную литературу к первой части цикла статей про распаковку асприка и знаете, что такое РЕВ).Code:loc_7C9210B2: cmp byte ptr [ebx+2], 0 jnz loc_7C93E615
Остался ещё последний, да и то незначительный вопрос – что же из себя представляет функция DbgBreakPoint? Нет ничего проще:
Пока не забивайте себе голову, что такое Trap to Debugger, и почему именно Trap, просто поймите, что здесь расположена инструкция int 3, которая является точкой останова, и первый останов происходит именно благодаря её присутствию.Code:; int __stdcall DbgBreakPoint() public _DbgBreakPoint@0 _DbgBreakPoint@0 proc near int 3 ; Trap to Debugger retn _DbgBreakPoint@0 endp
Всё это очень хорошо, но зачем я вам это рассказываю? Да потому, что это – азы антиотладки. Ведь сам по себе отладчик не скрывает своего присутствия от протектора. Сейчас я не буду подробно описывать всю антиотладку аспра – этим мы займёмся, наверное, в третьей части, но пару слов об этих трюках скажу – пока воспринимайте их на веру.
Так вот, вполне возможно, что вы уже слышали хоть что-то про плагины, скрывающие Ольгу от разного рода способов её обнаружения. Так вот для того, чтоб спрятать её от аспра, достаточно самых хилых плагинов типа IsDebugPresent, а самый продвинутый – Phant0m – тоже прекрасно справляется с этой задачей.
Но как происходит процесс сокрытия отладчика, и от чего в данном случае мы его скрываем? Как мы уже убедились выше, некий байт, находящийся по смещению 2 от начала РЕВ, будучи установленным в 1 сообщает приложению, что оно отлаживается. Тогда задачей этих плагинов будет установка его в нуль. Но в какой момент его можно приравнять нулю, чтоб не нарушить взаимосвязь отладчика и отлаживаемого приложения, но, в то же время, не дать API функциям типа IsDebuggerPresent сдетектить отладчик? Да как раз в тот момент, когда срабатывает эта системная точка останова! Это – идеальный момент ещё и потому, что код протектора, будучи расположенным лишь в третьем кольце защиты, никак не сможет выполниться до этой System breakpoint.
Начало кода протектора и полиморфизм
Надеюсь, всё вышесказанное убедило вас:
- Поставить Phant0m и выбрать опцию Hide from PEB.
- В Options -> Debugging options -> Events -> Make first pause at ставим System breakpoint.
Второе действие здесь выполняется больше на перспективу, т.к. думаю, что вам придётся исследовать не только аспр, но и более коварные протекторы и даже вирусы, поэтому здесь нужно всё учитывать.
Теперь жмём F9 и останавливаемся на ЕР (Entry Point) упакованной программы.
И т.д. Что будем делать дальше? А дальше нужно трассировать код последовательными нажатиями F7, иначе мы рискуем пропустить какой-то важный элемент кода, который мог бы в дальнейшем пролить свет на механизм упаковки. Я знаю, что по распаковке аспра таких версий существует масса литературы, но везде его пытаются снять разными трюками (стойка на ушах, прыжки через пятку и т.д.), а мы должны проанализировать код протектора, чтоб что-то узнать о механизмах его защиты. Итак, начнём…Code:004ED001 > 60 PUSHAD 004ED002 E8 01000000 CALL ResHacke.004ED008 004ED007 90 NOP 004ED008 5D POP EBP 004ED009 81ED BFAF4500 SUB EBP,ResHacke.0045AFBF
Пока ничего особенного…Code:004ED001 > 60 PUSHAD – регистры затолкались в стек 004ED002 E8 01000000 CALL ResHacke.004ED008 переход на указанный адрес, причём адрес 004ED007 кладётся на вершину стека. 004ED007 90 NOP – вообще не выполниться никогда 004ED008 5D POP EBP – ЕВР=004ED007 004ED009 81ED BFAF4500 SUB EBP,ResHacke.0045AFBF – ЕВР=004ED007 – 0045AFBF=00092048 004ED00F BB B8AF4500 MOV EBX,ResHacke.0045AFB8 – ЕВХ=0045AFB8 004ED014 03DD ADD EBX,EBP - ЕВХ=0045AFB8+00092048=004ED000 004ED016 2B9D 91C34500 SUB EBX,DWORD PTR SS:[EBP+45C391] – из ЕВХ вычитается 000ED000, и в нём остаётся ImageBase 004ED01C 83BD 8CC24500 0>CMP DWORD PTR SS:[EBP+45C28C],0 – Какое-то значение сравнивается с нулём 004ED023 899D F5BF4500 MOV DWORD PTR SS:[EBP+45BFF5],EBX – ImageBase записывается по какому-то адресу 004ED029 0F85 CE100000 JNZ ResHacke.004EE0FD – переход не выполняется
Теперь у нас есть база загрузки kernel32.dll. Мчимся дальше…Code:004ED02F 8D85 94C24500 LEA EAX,DWORD PTR SS:[EBP+45C294] – определяется адрес строки, где содержится имя библиотеки kernel32.dll 004ED035 50 PUSH EAX – этот адрес кладётся на стек 004ED036 FF95 D0C34500 CALL NEAR DWORD PTR SS:[EBP+45C3D0] – вызывается функция GetModuleHandleA
Получили адрес чудесной функции LocalAlloc:Code:004ED03C 8985 90C24500 MOV DWORD PTR SS:[EBP+45C290],EAX ; kernel32.7C800000 – записываем её по какому-то адресу. 004ED042 8BF8 MOV EDI,EAX – и её же – в EDI 004ED044 8D9D A1C24500 LEA EBX,DWORD PTR SS:[EBP+45C2A1] – загружаем в EBX адрес строки с именем LocalAlloc 004ED04A 53 PUSH EBX – кладём в стек этот адрес 004ED04B 50 PUSH EAX – кладём в стек базу загрузки kernel32.dll 004ED04C FF95 CCC34500 CALL NEAR DWORD PTR SS:[EBP+45C3CC] – вызываем GetProcAddress.
В общем-то, пока ничего необычного. Да, код базонезависим, но пока никаких сложностей в его исследовании не наблюдается. Идём дальше.Code:004ED052 8985 99C34500 MOV DWORD PTR SS:[EBP+45C399],EAX ; kernel32.LocalAlloc – записали полученное значение 004ED058 8D9D ACC24500 LEA EBX,DWORD PTR SS:[EBP+45C2AC] – загружаем в EBX адрес строки с именем LocalFree 004ED05E 53 PUSH EBX – кладём в стек этот адрес 004ED05F 57 PUSH EDI – кладём в стек базу загрузки kernel32.dll 004ED060 FF95 CCC34500 CALL NEAR DWORD PTR SS:[EBP+45C3CC] – вызываем GetProcAddress. 004ED066 8985 9DC34500 MOV DWORD PTR SS:[EBP+45C39D],EAX – адрес полученной функции сохраняем 004ED06C 8B85 F5BF4500 MOV EAX,DWORD PTR SS:[EBP+45BFF5] – извлекаем ImageBase в ЕАХ 004ED072 8985 8CC24500 MOV DWORD PTR SS:[EBP+45C28C],EAX – сохраняем по другому адресу 004ED078 EB 4B JMP SHORT ResHacke.004ED0C5 - переходим на 004ED0C5
Вот, наконец-то первый трюк, который призван защитить код от дизассемблеров. А именно – короткий переход в середину следующей инструкции дизассемблируется неправильно. Так, к слову – это значение (значение регистра ESP, которое записалось в стек) в результате команды по адресу 004ED0C6 записывается именно это значение вместо того дальнего перехода.Code:004ED0C5 54 PUSH ESP – сохраняем предыдущее значение вершины стека 004ED0C6 8F85 86B04500 POP DWORD PTR SS:[EBP+45B086] – записываем это значение по какому-то адресу. 004ED0CC EB 04 JMP SHORT ResHacke.004ED0D2 – делаем переход в середину следующего дальнего перехода 004ED0CE - E9 000000FF JMP FF4ED0D3 – а вот этого перехода на самом деле нету.
Вот на самом деле какие там инструкции. К слову сказать, этот трюк с прыжком в середину следующей команды аспр повторяет довольно часто. У меня сложилось впечатление, что всё двигло полиморфа основано на этом трюке. По крайней мере, на данном этапе это так. Конечно, можно тупо пройти все эти переходы по F7, но согласитесь – они немного отвлекают и становится сложнее уловить суть алгоритма. Я, вначале, для интереса, конечно, трассировал. Но понял, что ничего из-за этих джампов не понял, поэтому позже я с помощью своей нехитрой тулзы очистил трассу от этих тупых переходов.Code:004ED0D2 FFB5 86B04500 PUSH DWORD PTR SS:[EBP+45B086] 004ED0D8 8F85 2BBC4500 POP DWORD PTR SS:[EBP+45BC2B] 004ED0DE FFB5 8CC24500 PUSH DWORD PTR SS:[EBP+45C28C] 004ED0E4 8F85 A4B04500 POP DWORD PTR SS:[EBP+45B0A4]
Так выглядит очищенная от следов морфинга трасса. После последнего RETN происходит переход на GetProcAddress. Для получения этого участочка трассы я поставил memory breakpoint on access на секцию кода библиотеки kernel32.dll. Почему я так сделал? Всё очень просто – так как прот работает в третьем кольце защиты, то для тех или иных полезных действий ему всё равно придётся обращаться к системных библиотекам, поскольку непосредственного доступа в ядро он не имеет. Конечно, он может эмулировать многие функции, но без доступа к Native API через ntdll.dll ему не обойтись. Поэтому в крайнем случае можно ставить такие же точки останова на секцию кода ntdll.dll.Code:004ED0D8 Main POP DWORD PTR SS:[EBP+45BC2B] 004ED0DE Main PUSH DWORD PTR SS:[EBP+45C28C] 004ED0E4 Main POP DWORD PTR SS:[EBP+45B0A4] 004ED0F0 Main PUSH DWORD PTR SS:[EBP+45B0A4] 004ED0F6 Main POP EBX ; EBX=00400000 004ED0F7 Main PUSH EBX 004ED0F8 Main POP DWORD PTR SS:[EBP+45B0B8] 004ED104 Main PUSH DWORD PTR SS:[EBP+45B0B8] 004ED10A Main POP DWORD PTR SS:[EBP+45BC0F] 004ED110 Main PUSH DWORD PTR SS:[EBP+45C290] 004ED116 Main POP DWORD PTR SS:[EBP+45B0D6] 004ED122 Main PUSH DWORD PTR SS:[EBP+45B0D6] 004ED128 Main POP EAX ; EAX=7C800000 004ED129 Main LEA EBX,DWORD PTR SS:[EBP+45BBE5] ; EBX=004EDC2D 004ED138 Main PUSH EAX 004ED142 Main CALL ResHacke.004ED14A 004ED14A Main POP EAX ; EAX=004ED147 004ED154 Main POP EAX ; EAX=7C800000 004ED15E Main PUSH EBX 004ED168 Main PUSH EBX 004ED172 Main CALL ResHacke.004ED17A 004ED17A Main POP EBX ; EBX=004ED177 004ED184 Main POP EBX ; EBX=004EDC2D 004ED197 Main PUSH EAX 004ED1A1 Main CALL ResHacke.004ED1A9 004ED1A9 Main POP EAX ; EAX=004ED1A6 004ED1B3 Main POP EAX ; EAX=7C800000 004ED1BD Main PUSH EAX 004ED1C7 Main PUSH EBX 004ED1D1 Main CALL ResHacke.004ED1D9 004ED1D9 Main POP EBX ; EBX=004ED1D6 004ED1E3 Main POP EBX ; EBX=004EDC2D 004ED1ED Main LEA EAX,DWORD PTR SS:[EBP+45B1B3] ; EAX=004ED1FB 004ED1F3 Main INC EAX ; EAX=004ED1FC 004ED1F4 Main PUSH EAX 004ED1F5 Main PUSH DWORD PTR SS:[EBP+45C3CC] 004ED1FB Main RETN
Что, вы уже собрались анализировать вышеприведенный код? Погодите, давайте ещё раз на него внимательно посмотрим.
Тут даже без отладчика очевидно, что это – мусорная конструкция. Первая инструкция сохраняет EAX в стеке, далее CALL переходит на 004ED14A, при этом на стек кладётся адрес 004ED147, что соответствует значению адреса команды, идущей непосредственно за CALL. И тут же этот адрес выталкивается из стека в EAX. После чего в EAX снова выталкивается значение, которое в из него же заталкивалось в стек по адресу 004ED138. Короче говоря, этот участок кода ничего не меняет – всё остаётся как было. Поэтому от таких участков можно смело избавляться.Code:004ED138 Main PUSH EAX 004ED142 Main CALL ResHacke.004ED14A 004ED14A Main POP EAX ; EAX=004ED147 004ED154 Main POP EAX ; EAX=7C800000
Вот и весь полезный код из тех инструкций. Да и разбирать тут особо нечего – последняя связка PUSH+ RETN – аналогично переходу JMP на GetProcAddress. Чуть выше PUSH EAX – инструкция кладёт на вершину стека адрес возврата, на который переместится программа после выполнения функции GetProcAddress.Code:004ED0F6 Main POP EBX ; EBX=00400000 004ED0F7 Main PUSH EBX 004ED0F8 Main POP DWORD PTR SS:[EBP+45B0B8] 004ED104 Main PUSH DWORD PTR SS:[EBP+45B0B8] 004ED10A Main POP DWORD PTR SS:[EBP+45BC0F] 004ED110 Main PUSH DWORD PTR SS:[EBP+45C290] 004ED116 Main POP DWORD PTR SS:[EBP+45B0D6] 004ED122 Main PUSH DWORD PTR SS:[EBP+45B0D6] 004ED128 Main POP EAX ; EAX=7C800000 004ED129 Main LEA EBX,DWORD PTR SS:[EBP+45BBE5] ; EBX=004EDC2D 004ED15E Main PUSH EBX 004ED1BD Main PUSH EAX 004ED1ED Main LEA EAX,DWORD PTR SS:[EBP+45B1B3] ; EAX=004ED1FB 004ED1F3 Main INC EAX ; EAX=004ED1FC 004ED1F4 Main PUSH EAX 004ED1F5 Main PUSH DWORD PTR SS:[EBP+45C3CC] 004ED1FB Main RETN
Вот эта пара инструкций – подготовка того самого адреса. Ещё выше PUSH EAX помещает в стек адрес базы kernel32.dll.Code:004ED1ED Main LEA EAX,DWORD PTR SS:[EBP+45B1B3] ; EAX=004ED1FB 004ED1F3 Main INC EAX ; EAX=004ED1FC
А вот здесь получается указатель на имя функции GetTickCount и заталкивается в стек.Code:004ED129 Main LEA EBX,DWORD PTR SS:[EBP+45BBE5] ; EBX=004EDC2D 004ED15E Main PUSH EBX
FARPROC GetProcAddress(
HMODULE hModule,
LPCWSTR lpProcName
);
Вот, как и в документации, имеем два параметра для вышеупомянутой функции.
Далее вызывается сама GetTickCount, после – полученное значение подвергается воздействию AND EAX,1FFFh. Я не привожу здесь подробный анализ дальнейшего кода, т.к. выше уже было показано, как чистить его от мусора. Далее, если мы вычистим мусор, то увидим, что дальше вызывается функция LocalAlloc.
HLOCAL LocalAlloc(This function allocates the specified number of bytes from the heap.
In the linear Microsoft® Windows® CE API environment, there is no difference between the local heap and the global heap.
UINT uFlags,
UINT uBytes
);
Т.е. – эта чудесная функция просто выделяет память. Остальные подробности нас не интересуют. Далее, продираясь через дебри полиморфа, видим, что функция эта вызывается дважды. Теперь у нас есть два выделенных блока памяти, давайте выясним, зачем они нужны протектору. Если опустить все подробности, до которых вы теперь и сами можете дойти, то становится видно, что в выделенную память записывается некий исполняемый код, на который позже передаётся управление. Далее некоторые участки памяти освобождаются, и мы стоим здесь:
Расшифрованный код, на первый взгляд, не имеет полиморфных конструкций. Кодес этот тоже планировался быть базонезависимым. Об этом свидетельствуют инструкции до JNZ 001A7C1F. Что же происходит после этого перехода?Code:001A76BD BB 08294400 MOV EBX,442908 001A76C2 03DD ADD EBX,EBP 001A76C4 2B9D 35294400 SUB EBX,DWORD PTR SS:[EBP+442935] 001A76CA 83BD A0304400 0>CMP DWORD PTR SS:[EBP+4430A0],0 001A76D1 899D F72D4400 MOV DWORD PTR SS:[EBP+442DF7],EBX 001A76D7 0F85 42050000 JNZ 001A7C1F 001A76DD 8D85 A8304400 LEA EAX,DWORD PTR SS:[EBP+4430A8] 001A76E3 50 PUSH EAX 001A76E4 FF95 B4314400 CALL NEAR DWORD PTR SS:[EBP+4431B4] 001A76EA 8985 A4304400 MOV DWORD PTR SS:[EBP+4430A4],EAX 001A76F0 8BF8 MOV EDI,EAX
Здесь – снова определение адреса загрузки kernel32.dll.Code:001A76E3 50 PUSH EAX 001A76E4 FF95 B4314400 CALL NEAR DWORD PTR SS:[EBP+4431B4] ; kernel32.GetModuleHandleA
А вот здесь GetProcAddress уже определяет адрес функции VirtualAlloc. VirtualAlloc также, как и LocalAlloc, используется протектором для выделения памяти под собственные нужды, но имеет принципиальные отличия по функционалу. Впрочем, в данном случае нам это не важно.Code:001A76F8 53 PUSH EBX 001A76F9 50 PUSH EAX 001A76FA FF95 B0314400 CALL NEAR DWORD PTR SS:[EBP+4431B0] ; kernel32.GetProcAddress
Соответственно, чуть ниже происходит получение адреса VirtualFree.Code:001A770C 53 PUSH EBX 001A770D 57 PUSH EDI 001A770E FF95 B0314400 CALL NEAR DWORD PTR SS:[EBP+4431B0] ; kernel32.GetProcAddress
А вот и первое выделение памяти не заставило себя долго ждать. Далее становится ясно, что память эта была выделена не просто так.Code:001A7726 6A 04 PUSH 4 001A7728 68 00100000 PUSH 1000 001A772D 68 4A050000 PUSH 54A 001A7732 6A 00 PUSH 0 001A7734 FF95 3D294400 CALL NEAR DWORD PTR SS:[EBP+44293D] ; kernel32.VirtualAlloc
Данный код осуществляет расжатие данных, и перемещение уже расжатых данных в регион памяти, только что выделенный функцией VirtualAlloc. Любители статиков могут рипать эту функцию безбоязненно – я не нашёл в ней ни одной базозависимой инструкции.Code:001A7746 50 PUSH EAX 001A7747 53 PUSH EBX 001A7748 E8 78050000 CALL 001A7CC5
Вот это – тоже интересный момент. Здесь из только что расжатых и перемещённых байтиков некая область копируется непосредственно в ту область кода, где мы сейчас находимся.Code:001A774F 8DBD 092A4400 LEA EDI,DWORD PTR SS:[EBP+442A09] 001A7755 8BB5 39294400 MOV ESI,DWORD PTR SS:[EBP+442939] 001A775B F3:A4 REP MOVS BYTE PTR ES:[EDI],BYTE PTR DS:[>
Здесь вызывается VirtualFree и освобождает область памяти, выделенную немногим ранее. Получается, что вся эта процедура выполнялась лишь для того, чтоб модифицировать немного кода в районе EIP? Ну, немного – понятие растяжимое, но в общем – да, так и получается. Далее мы попадаем на RETN, который переносит нас на вновь расшифрованный код.Code:001A7763 68 00800000 PUSH 8000 001A7768 6A 00 PUSH 0 001A776A 50 PUSH EAX 001A776B FF95 41294400 CALL NEAR DWORD PTR SS:[EBP+442941]
Здесь происходит второй вызов VirtualAlloc.Code:001A79A9 6A 04 PUSH 4 001A79AB 68 00100000 PUSH 1000 001A79B0 50 PUSH EAX 001A79B1 6A 00 PUSH 0 001A79B3 FF95 3D294400 CALL NEAR DWORD PTR SS:[EBP+44293D] ; kernel32.VirtualAlloc
Уже знакомая нам функция. Догадаетесь, куда будет перемещён результат декодирования?Code:001A79C8 50 PUSH EAX 001A79C9 53 PUSH EBX 001A79CA E8 F6020000 CALL 001A7CC5
Смысл этого кода мне несильно понятен: ну, копируется по какому-то адресу опкод инструкции RETN, ну идёт переход на эту инструкцию, после чего она возвращает нас опять назад, на следующую за CALL NEAR EDI инструкцию.Code:001A79E8 C607 C3 MOV BYTE PTR DS:[EDI],0C3 001A79EB FFD7 CALL NEAR EDI
Ну и что? (Может, вы знаете, зачем это сделано?).
Этот достаточно большой кусок кода нам вообще не интересен – он выполняет фикс базозависимых инструкций в коде. Т.е. инструкции типа ближних пятибайтовых переходов (NEAR Jump) и инструкции Call NEAR, как вы знаете, базозависимы. Так вот здесь проводится их настройка на правильные адреса. НО! Есть у этого кода одно важное предназначение – благодаря ему мы можем задать себе вопрос: «А что это за код такой, в котором правятся эти инструкции?». Это – код за пределами данного учатска памяти, т.е. того участка памяти, в котором мы сейчас находимся. Это пока всё, что мы можем с уверенностью утверждать. Если будем упорно трассировать код, то попадём на этот участок памяти, а переход на него находится вот здесь:Code:001A79EF 50 PUSH EAX 001A79F0 51 PUSH ECX 001A79F1 56 PUSH ESI 001A79F2 53 PUSH EBX 001A79F3 8BC8 MOV ECX,EAX 001A79F5 83E9 06 SUB ECX,6 001A79F8 8BB5 39294400 MOV ESI,DWORD PTR SS:[EBP+442939] 001A79FE 33DB XOR EBX,EBX 001A7A00 0BC9 OR ECX,ECX 001A7A02 74 1C JE SHORT 001A7A20 001A7A04 78 1A JS SHORT 001A7A20 001A7A06 AC LODS BYTE PTR DS:[ESI] 001A7A07 3C E8 CMP AL,0E8 001A7A09 74 08 JE SHORT 001A7A13 001A7A0B 3C E9 CMP AL,0E9 001A7A0D 74 04 JE SHORT 001A7A13 001A7A0F 43 INC EBX 001A7A10 49 DEC ECX 001A7A11 ^ EB ED JMP SHORT 001A7A00 001A7A13 291E SUB DWORD PTR DS:[ESI],EBX 001A7A15 83C3 05 ADD EBX,5 001A7A18 83C6 04 ADD ESI,4 001A7A1B 83E9 05 SUB ECX,5 001A7A1E ^ EB E0 JMP SHORT 001A7A00 001A7A20 5B POP EBX 001A7A21 5E POP ESI 001A7A22 59 POP ECX 001A7A23 58 POP EAX
Инструкция по адресу 001A7C40 как раз и заталкивает в стек адрес, на который потом осуществляется переход. А куда же мы переходим? А вот куда:Code:001A7C35 61 POPAD 001A7C36 75 08 JNZ SHORT 001A7C40 001A7C38 B8 01000000 MOV EAX,1 001A7C3D C2 0C00 RETN 0C 001A7C40 68 00000000 PUSH 0 001A7C45 C3 RETN
Ниже этого кода идут нули. А это – код так называемой ASProtect.dll. Что это за библиотека, зачем она здесь и прочие подробности про это программное чудо вы узнаете чуть позже, а пока давайте подытожим полученную информацию, а также составим простой скрипт для прохода на точку входа в эту библиотеку.Code:00199CAC 55 PUSH EBP 00199CAD 8BEC MOV EBP,ESP 00199CAF 83C4 F4 ADD ESP,-0C 00199CB2 E8 6DFBFEFF CALL 00189824 00199CB7 ^ 0F85 8F09FFFF JNZ 0018A64C 00199CBD E8 460EFFFF CALL 0018AB08 00199CC2 E8 4537FFFF CALL 0018D40C 00199CC7 E8 1C55FFFF CALL 0018F1E8 00199CCC E8 A3AAFFFF CALL 00194774 00199CD1 E8 7609FFFF CALL 0018A64C 00199CD6 8BE5 MOV ESP,EBP 00199CD8 5D POP EBP 00199CD9 C2 0C00 RETN 0C
Только что мы узнали, что аспр имеет многослойную структуру, т.е. на пути к распаковке нам встречаются расшифровщики, которые расшифровывают другие расшифровщики и т.д. Код расшифровщиков перемешан с полиморфным мусором, который, тем не менее, однотипен и достаточно просто отделяется от основного полезного кода. На данном этапе нашей конечной целью является останов на точке входа в ASProtect.dll. Скрипт будет выглядеть достаточно просто – после второго срабатывания брейкпоинта на начале функции VirtualAlloc мы выходим из этой функции и ищем по маске инструкции (надеюсь, вы читали справку по написанию скриптов и знаете, как это делается):
Далее ставим брейкпоинт на последний RETN, а когда он сработает – делаем Single Step. Скрипт прилагается к статье.Code:001A7C35 61 POPAD 001A7C36 75 08 JNZ SHORT 001A7C40 001A7C38 B8 01000000 MOV EAX,1 001A7C3D C2 0C00 RETN 0C 001A7C40 68 00000000 PUSH 0 001A7C45 C3 RETN
Знакомство с ASProtect.dll и техникой использования Dll-injection
Вот мы и добрались до некоего загадочного кода, который я именую как ASProtect.dll. На самом деле, так его именую не я – его так назвали ещё задолго до меня, я лишь повторяю это название. А сейчас я постараюсь вам объяснить, что эта библиотека из себя представляет.
Этот код – код обычной динамической библиотеки, написанной на том же языке программирования, что и сам Аспр – на Delphi. Об этом красноречиво говорит точка входа – ЕР данного кода. Хотя, правильнее было бы сказать, что этот код был обычным, пока его не исказил Аспр. Не верите? Тогда посмотрите – на месте заголовка у этой библиотеки нули. А где вы видели такую библиотеку, у которой нет заголовка? Её же просто не загрузит загрузчик операционной системы. Так как же всё это работает? И главное – зачем?
На первый вопрос – как – мы ответили бы, если бы более тщательно изучали код с момента исправлений базозависимых инструкций (помните, такой был чуть выше?). Всё, на самом деле, очень просто – раз загрузчик не работает с такими библиотеками, то протектор будет выполнять его работу на стадии инициализации библиотеки. Т.е. – протектор расшифровывает код этой библиотеки, располагая его по адресу в памяти, выделенной функцией VirtualAlloc. Далее он должен заполнить таблицу импорта (если поставить брейкпоинт на GetProcAddress, увидите, как протектор это делает), исправить базозависимые инструкции – пересчитать их соответственно новому адресу загрузки, а потом передать управление на точку входа в библиотеку. Вот и весь общий алгоритм работы по извлечению кода этой библиотеки.
Далее вопрос – почему именно ASProtect.dll? Откуда взялось это название? На самом деле, название взялось от балды, т.е. просто кто-то так захотел её назвать. Доподлинно неизвестно, как её называет сам Солодовников, поэтому мы остановимся на таком названии.
Теперь следующий вопрос – зачем эта библиотека тут нужна? Здесь всё ещё проще – представьте себя на месте автора какого-нибудь пакера. Хотя бы пакера. Для того, чтоб ваше детище успешно паковало различные программы, оно должно добавлять в них свой код, который должен быть базонезависимым. «Ну и что?» - спросите вы. А то, что это 10 строчек базонезависимого кода написать легко и приятно, а что будет, если таких строк нужно будет наваять, эдак, тысяч 5? Тогда окажется, что занятие это очень неприятное, но всё же – если кому-то не лень, то скрипя зубами, этого можно добиться. Но это – если вы пишете на С, а если вы, как Алексей Солодовников, выбрали Delphi? Тогда ситуация усложняется тем, что встроенный в этот компилятор Борландовский ассемблер напрочь отказывается понимать такие конструкции, как:
Вот вам и подвох для базонезависимости от Борланда. Поэтому чтоб не извращаться, Солодовников решил забить на базонезависимость (везде, кроме начала) и весь основной код защиты сделать самым обычным кодом в виде динамической библиотеки. Удобно, и со вкусом. И теперь нам с вами придётся исследовать, что же происходит внутри этой библиотеки.Code:Call dword ptr [ebp + offset MyCall]
Наш скрипт для прохода на ОЕР этой библиотеки успешно отработал, и мы переходим на вот этот последний CALL, т.к. именно внутри него начинается работа кода протектора.
Кстати, адреса могут меняться, просто учтите, что это – последний CALL от начала ЕР библиотеки. Если хотите – потрассируйте предыдущие, чтобы убедиться, что там нет ничего интересного. Далее внутри немного пошагав по F7, попадаем на вот такое место (попасть на него можно очень просто, нажав Ctrl-F9, а потом F7 – это обусловлено тем, что переход делается с помощью команды RETN):Code:001A7E81 E8 7609FFFF CALL 001987FC
Перед тем, как продолжить читать дальше, настоятельно рекомендую вам ознакомиться с циклом статей о структурной обработке исключений.Code:001A7384 55 PUSH EBP 001A7385 8BEC MOV EBP,ESP 001A7387 53 PUSH EBX 001A7388 56 PUSH ESI 001A7389 57 PUSH EDI 001A738A 33C0 XOR EAX,EAX 001A738C 55 PUSH EBP 001A738D 68 AA731A00 PUSH 1A73AA 001A7392 64:FF30 PUSH DWORD PTR FS:[EAX] 001A7395 64:8920 MOV DWORD PTR FS:[EAX],ESP 001A7398 8B45 08 MOV EAX,DWORD PTR SS:[EBP+8]
http://www.wasm.ru/series.php?sid=7
Здесь я, конечно, буду объяснять, что к чему, но поверьте – эти знания не будут лишними. По прочтении вышеупомянутого цикла вы будете знать о SЕH всё, что нужно, и даже больше.
Теперь давайте новыми, поумневшими после чтения Питрека, глазами посмотрим на этот код:
Первые четыре инструкции добавили ещё одну структуру EXCEPTION_REGISTRATION в односвязный список.Code:001A08AC 55 PUSH EBP 001A08AD 68 CA081A00 PUSH 1A08CA 001A08B2 64:FF30 PUSH DWORD PTR FS:[EAX] 001A08B5 64:8920 MOV DWORD PTR FS:[EAX],ESP 001A08B8 8B45 08 MOV EAX,DWORD PTR SS:[EBP+8] 001A08BB E8 28FAFFFF CALL 001A02E8 001A08C0 33C0 XOR EAX,EAX 001A08C2 5A POP EDX 001A08C3 59 POP ECX 001A08C4 59 POP ECX 001A08C5 64:8910 MOV DWORD PTR FS:[EAX],EDX 001A08C8 EB 11 JMP SHORT 001A08DB 001A08CA ^ E9 E900FFFF JMP 001909B8
Здесь подготавливается аргумент и вызывается функция с соглашением о вызовах fastcall. Т.е. единственный параметр передается через регистр EAX. Работу этой функции мы рассматривать не будем, но скажу, что пока в ней нет ничего для нас интересного, но она очень большая, чтоб приводить здесь её описание.Code:001A08B8 8B45 08 MOV EAX,DWORD PTR SS:[EBP+8] 001A08BB E8 28FAFFFF CALL 001A02E8
Тут даже без комментариев. Идём дальше. Дальше идёт короткий переход, ведущий нас к участку кода, весьма похожему по своей структуре на предыдущий. Обратите внимание, что EXCEPTION_REGISTRATION пока ни разу нам не пригодились – ошибок в коде не возникает, обработчики не выполняются. И тут после очередного короткого перехода мы попадаем на участок кода, который не начинается с добавления структуры EXCEPTION_REGISTRATION в односвязный список этих структур. А попадаем мы на такое вот место:Code:001A08C0 33C0 XOR EAX,EAX 001A08C2 5A POP EDX 001A08C3 59 POP ECX 001A08C4 59 POP ECX 001A08C5 64:8910 MOV DWORD PTR FS:[EAX],EDX
Даже не исследуя дальше код, такой поворот событий наводит на мысль, что где-то рядом нас ждёт работа с импортом. И действительно – ближайший CALL действительно подготавливает импортируемые функции для дальнейшего использования в приложении. Не верите? А и не надо, но по F7 туда идти придётся, потому что если попробуете нажать F8 – программа просто запустится. Там, внутри этой функции, очень много кода, но весь он относится к расшифровке участков зашифрованной программы, поэтому смело трассируем до ближайшего RETN, т.е. опять жмём в Ольке Ctrl-F9, далее попадаем на код расжатия каких-то данных, но нам это неинтересно – поэтому идём внутрь ближайшего CALL. Дальше – генерация импорта и знаменитых аспровских переходников. А пока пишем скрипт, который будет каждый раз помогать нам доходить до этого места. В скрипте всё просто – вначале с помощью предыдущего скрипта доходим до точки входа в аспровскую библиотеку, оттуда доходим до последнего CALL, заходим внутрь, доходим до RETN, Single Step, по маске ищем CALL, перед которым в стек заталкиваются в качестве аргументов адреса, заходим внутрь этого CALL, снова доходим до RETN, далее ищем ближайший CALL и заходим в него. Как-то так… Если вдруг вам что-то неясно, то этот скрипт в моём исполнении прилагается к статье.Code:001A0994 B8 5B231900 MOV EAX,19235B 001A0999 40 INC EAX – адрес перехода на LoadLibraryA 001A099A 8905 40741A00 MOV DWORD PTR DS:[1A7440],EAX 001A09A0 B8 3B231900 MOV EAX,19233B 001A09A5 8905 44741A00 MOV DWORD PTR DS:[1A7444],EAX 001A09AB FF05 44741A00 INC DWORD PTR DS:[1A7444] – адрес перехода на GetProcAddress.
API Redirectors и методы борьбы с ними
Вот мы и добрались до самой занимательной части статьи – восстановления переходников на импортируемые функции в защищённом приложении. Страшно звучит, не правда ли? Но страшного тут ничего нет. Давайте сейчас разберём теорию, а потом уж перейдём к практическим исследованиям.
Но перед дальнейшим повествованием хотел бы отвлечь ваше внимание на ещё одну мелочь – я тут попропускал при исследовании кучу кода, мотивируя это тем, что нам он неинтересен или попросту не нужен. Но как я определил, что он нам не нужен? Всё очень просто – вначале я трассирую код, как только на моём пути попадается некая инструкция CALL, я стараюсь пройти её, не заходя внутрь подпрограммы, на которую она ссылается. Если при этом получилось так, что программа запустилась (или того хуже – аварийно завершилась), то тогда приходится заглядывать внутрь этой подпрограммы, если нет – то это уже ваш выбор. И так я стараюсь добраться до диапазона адресов, который бы лежал в пределах между ImageBase (Или Base of Image – как её только не называли) и ImageBase+ImageSize, т.к. по понятным причинам этот код будет либо самим ОЕР, либо должен находиться в непосредственной от него близости. Тогда вы спросите: «почему нельзя воспользоваться автоматической трассировкой с заходом в подпрограммы, и найти ОЕР вообще без напряга?» Вопрос очень правильный, но так делать НЕЛЬЗЯ по двум причинам:
Сейчас, как и обещал, немного теории. Если вы читали статьи «Об упаковщиках в последний раз», о которых я упоминал в первой теоретической части, то уже и так всё знаете про таблицу импорта. Здесь я буду говорить только о её малой части. В документации по РЕ-формату можно найти упоминание о некоей FirstThunk, которая заполняется загрузчиком. По сути – это указатель на начало массива адресов импортируемых функций. У каждой отдельной структуры, именуемой IMAGE_IMPORT_DESCRIPTOR, своя FirstThunk. Пока не будем углубляться дальше, и из всего сказанного вам следует сделать такие выводы:
- Зачастую это очень долго.
- Существуют методы, мешающие такой трассировке, но о них речь пойдёт чуть позже.
- Таблица импорта существует для обеспечения возможности запуска ехе-файлов на разных версиях ОС Windows (другие ОС мы не рассматриваем).
- Она состоит из последовательно расположенных друг за другом структур IMAGE_IMPORT_DESCRIPTOR.
- Одним из полей структуры IMAGE_IMPORT_DESCRIPTOR является поле, именуемое как FirstThunk.
- Это поле – указатель на массив адресов импортируемых функций.
- По массивам адресов импортируемых функций можно воссоздать таблицу импорта.
Пока нам больше знать и не нужно. К слову, пунктом под номером 5 занимается ImpRec почти без нашего участия. В дальнейшем мы рассмотрим этот вопрос более подробно, а пока идём дальше.
Таким образом, становится очевидным, что для противодействия распаковке протектор должен каким-либо образом исказить значения этих самых адресов импортируемых функций. Но сделать это таким образом, чтоб программа при этом осталась рабочей. И такой способ есть – в литературе его именуют как «создание переходников» на импортируемые функции. Т.е. вместо перехода на код импортируемой функции мы переходим на некий код протектора, в недрах которого эта самая импортируемая функция и будет выполняться. Как именно выполняется этот переход – уже дело десятое. Но принцип защиты таким способом от распаковки у большинства протекторов (не только у Аспра) один и тот же.
И что же нам с этим делать? На самом деле, здесь, как в старом армейском анекдоте – есть два мнения: одно – моё, а другое – ошибочное. Моё мнение состоит в том, что нам с вами во что бы то ни стало необходимо из этих переходников получить настоящие адреса импортируемых функций, которые потом можно было бы скормить ImpRec’у. Иногда, правда, сделать это весьма затруднительно, но стремиться нужно именно к этому.
На этой оптимистической ноте мы заканчиваем теоретическую часть и переходим к практическим исследованиям генерации переходников в ASProtect.dll.
Переходники первого типа
Сейчас мы с вами в отладчике выполнили два вышеописанных скрипта и стоим на начале функции генерации переходников.
Вначале идёт стандартный пролог функции, потом выделяется память под локальные переменные, далее значения регистров сохраняются в стеке, после чего аргументы функции, переданные ей через стек, извлекаются в регистры EDI и ESI. Сразу замечу, что данная функция выполняется в цикле, и после каждого выполнения мы имеем в качестве результата её работы один готовый переходник. Идём дальше.Code:00192984 55 PUSH EBP 00192985 8BEC MOV EBP,ESP 00192987 81C4 E0FEFFFF ADD ESP,-120 0019298D 53 PUSH EBX 0019298E 56 PUSH ESI 0019298F 57 PUSH EDI 00192990 8B7D 0C MOV EDI,DWORD PTR SS:[EBP+C] 00192993 8B75 08 MOV ESI,DWORD PTR SS:[EBP+8]
После этого в BL содержится номер, по которому аспр определит тип переходника. Нашему первому типу переходника соответствует номер 2. Почему так? Почему я не рассматриваю этот переходник вторым согласно его номеру? Потому что этот номер соответствует самому простому типу переходника, и именно с него мне хотелось бы начать описание.Code:00192996 8A1E MOV BL,BYTE PTR DS:[ESI]
Этот код обнуляет память начиная с адреса в EAX, и заканчивая через 100h двойных слов + 1 байт.Code:00192998 8D85 E3FEFFFF LEA EAX,DWORD PTR SS:[EBP-11D] 0019299E 33C9 XOR ECX,ECX 001929A0 BA 00010000 MOV EDX,100 001929A5 E8 F206FFFF CALL 0018309C
Далее – обычная case – конструкция. Вы ещё не забыли, что у нас находится в BL? А теперь, когда мы отнимем оттуда 2, следующий переход, конечно же, будет выполнен.Code:001929AA 80EB 02 SUB BL,2 001929AD 74 0E JE SHORT 001929BD
Помните, чуть выше в EDI загрузился один из параметров исследуемой функции? Так вот теперь мы видим, что это – некий адрес чего-то. И первый байт, находящийся по этому адресу, сравнивается с константой по адресу (в моём случае) 198F6C. В моём случае в AL находится 1, а в 198F6C – 2. Поэтому переход не выполняется. Далее для нас идёт не очень интересный код, и за неимением времени я его здесь не привожу, но вы можете самостоятельно его исследовать. А мы с помощью трассировки переходим вот сюда:Code:001929BD 8A07 MOV AL,BYTE PTR DS:[EDI] 001929BF 3A05 6C8F1900 CMP AL,BYTE PTR DS:[198F6C] 001929C5 74 43 JE SHORT 00192A0A
Code:001929DF E8 2825FFFF CALL 00184F0C – здесь расшифровывается имя библиотеки, из которой импортируется функция 001929E4 8D5E 01 LEA EBX,DWORD PTR DS:[ESI+1] 001929E7 8BC3 MOV EAX,EBX 001929E9 E8 AA2FFFFF CALL 00185998 001929EE 48 DEC EAX 001929EF 50 PUSH EAX 001929F0 8BCB MOV ECX,EBX 001929F2 33D2 XOR EDX,EDX 001929F4 8A55 E3 MOV DL,BYTE PTR SS:[EBP-1D] 001929F7 8D85 E3FEFFFF LEA EAX,DWORD PTR SS:[EBP-11D] 001929FD E8 7EE3FFFF CALL 00190D80 – а здесь – имя самой импортируемой функцииА вот внутри этой функции мы определяем настоящий адрес импортируемой функции. Как вы, возможно, уже догадались – за это внутри вызова CALL 00192698 отвечает известная нам GetProcAddress. Тогда вы, без сомнения, сумеете догадаться, какие параметры передаются внутрь CALL 00192698.Code:00192A42 53 PUSH EBX 00192A43 46 INC ESI 00192A44 56 PUSH ESI 00192A45 E8 4EFCFFFF CALL 00192698
А вот здесь хитроумным способом генерируется переходник. Функция по адресу 00192948 принимает всего один аргумент – настоящий адрес, и формирует переходник на него. Таким образом, чтоб противостоять формированию переходника мы просто должны не выполнять этот вызов. Переходник же имеет достаточно убогий вид – из себя он представляет простой переход на оригинальный адрес функции. С такими переходниками может справиться даже ImpRec (опция Trace Level1 (Disasm)), именно поэтому во многих статьях по распаковке аспра говориться про какие-то «режимы трассировки» и «трассировочные плагины». Но поверьте – на самом деле гораздо проще, удобнее и правильнее избегать этих плагинов, а проанализировав структуру генерации переходников, раз и навсегда от них избавиться. Если вы обратили внимание, после выполнения функции, генерирующей переходники, идёт безусловный прыжок на, фактически, конец функции:Code:00192A4A 50 PUSH EAX ; ntdll.RtlDeleteCriticalSection 00192A4B E8 F8FEFFFF CALL 00192948
Для избавления от переходника нам следует просто переместить этот переход на пару инструкций вверх. И всё – с первым типом переходников мы разобрались.Code:00192A50 /E9 A9000000 JMP 00192AFE
Второй тип переходников
Этот тип переходников гораздо более сложен и громоздок как в своей работе, так и в восстановлении, но не расстраивайтесь – в более новых версиях аспра Солодовников почему-то отказался от данного типа переходников. Хотя кто знает – может быть когда-то они нам ещё попадутся, а сейчас – поехали.
Стоим снова здесь, но теперь в BL находится 4.Code:00192996 8A1E MOV BL,BYTE PTR DS:[ESI]
Теперь выполняется последний переход.Code:001929AA 80EB 02 SUB BL,2 001929AD 74 0E JE SHORT 001929BD 001929AF 80EB 02 SUB BL,2 001929B2 0F84 9D000000 JE 00192A55
Вкратце прокомментирую этот код. В стеке создаётся структура, которая из себя представляет опкоды двух команд – например:Code:00192A55 B8 16000000 MOV EAX,16 00192A5A E8 C503FFFF CALL 00182E24 00192A5F 8BD8 MOV EBX,EAX 00192A61 B8 F2FFFFFF MOV EAX,-0E 00192A66 83E8 E8 SUB EAX,-18 00192A69 8945 E4 MOV DWORD PTR SS:[EBP-1C],EAX 00192A6C A0 8C8F1900 MOV AL,BYTE PTR DS:[198F8C] 00192A71 8845 E8 MOV BYTE PTR SS:[EBP-18],AL 00192A74 8BC3 MOV EAX,EBX 00192A76 0345 E4 ADD EAX,DWORD PTR SS:[EBP-1C] 00192A79 8945 E9 MOV DWORD PTR SS:[EBP-17],EAX 00192A7C 8A15 908F1900 MOV DL,BYTE PTR DS:[198F90] 00192A82 8855 ED MOV BYTE PTR SS:[EBP-13],DL 00192A85 BA A8281900 MOV EDX,1928A8 00192A8A 2BD0 SUB EDX,EAX 00192A8C 8955 EE MOV DWORD PTR SS:[EBP-12],EDX 00192A8F 46 INC ESI 00192A90 8975 F2 MOV DWORD PTR SS:[EBP-E],ESI 00192A93 897D F6 MOV DWORD PTR SS:[EBP-A],EDI 00192A96 8BD3 MOV EDX,EBX 00192A98 8D45 E8 LEA EAX,DWORD PTR SS:[EBP-18] 00192A9B B9 16000000 MOV ECX,16 00192AA0 E8 6F04FFFF CALL 00182F14 00192AA5 8BC3 MOV EAX,EBX
Далее второй CALL 00182F14 перемещает эту структуру по некоему адресу переходника, который потом и будет фигурировать в массиве адресов импортируемых функций и не будет распознан ImpRec’ом. Генерация переходника на этом заканчивается и далее идёт уже знакомый нам переход, свидетельствующий о конце функции:Code:00A7436C 68 7643A700 PUSH 0A74376 00A74371 E8 32E571FF CALL 001928A8
Ситуация с переходником несколько другая, чем в первом случае. Возникает вопрос – где же вычисляется настоящий адрес? А он вычисляется по ходу вызова этой зашифрованной функции, т.е. когда из программы вызывается 2 тип переходников, управление передаётся на адрес этого переходника – в нашем случае управление передалось бы на адрес 00A7436C. Давайте вручную перейдём на этот адрес, и зайдём внутрь вызова CALL 001928A8. Да, кстати, переходников такого типа может быть много, но все они неизбежно сводятся к вызову подпрограммы по адресу 001928A8. Отсюда вывод – если мы сможем из этой подпрограммы выудить настоящие адреса, то это и будет победой над этим типом переходников. Я не буду вас долго мучить детальным рассмотрением кода этого типа переходников, скажу только, что с помощью банальной трассировки мы вскоре окажемся на вызове всё той же GetProcAddress:Code:00192AA7 /EB 55 JMP SHORT 00192AFE
Теперь в ЕАХ у нас имеется правильный адрес, но как нам получать эти правильные адреса не во время выполнения программы, а ещё до начала её выполнения при генерации переходников? Всё достаточно просто: для этого после генерации этого типа переходников нам нужно попасть на код переходника, потом подправить эпилог таким образом, чтоб вновь подправленная подпрограмма не переходила на адрес функции, полученный через GetProcAddress, а просто возвращала его в ЕАХ. Я не заморачивался здесь со скриптовыми командами, а написал скрипт, который патчит код ASProtect.dll чтоб получить вышеописанный результат. Как именно я это сделал – можете посмотреть в скрипте, который также приложен к статье.Code:00192932 FF75 FC PUSH DWORD PTR SS:[EBP-4] 00192935 FF75 F8 PUSH DWORD PTR SS:[EBP-8] 00192938 E8 5BFDFFFF CALL 00192698
Третий тип переходников
Третий вид переходников являет из себя действительно нечто особенное, но пока не особо опасное. Все вы, наверное, знаете о существовании неких плагинов для ImpRec, которые позволяют определять нераспознанные функции в программах, запакованных аспром. Так вот знайте – эти плагины борются как раз с третьим видом переходников. Этот вид я поначалу даже и не заметил. Но потом оказалось, что когда почти всё уже было пофиксено и программа стояла на ОЕР в ожидании прикручивания импорта к готовому дампу – я увидел один нераспознанный переходник. Но это был переходник, по своей работе весьма напоминающий функцию GetProcAddress. Собственно, в оригинале там и должна была быть эта функция. Как я узнал об этом? Да очень просто – в окне дизассемблера перешёл на этот адрес и посмотрел на листинг. Так вот – пока в коде аспра таким образом защищены только две функции: LoadLibraryA и GetProcAddress.
Вот так, собственно, выглядит установка переходника на GetProcAddress.Code:00196729 B8 88521900 MOV EAX,195288 0019672E E9 DB000000 JMP 0019680E
А это – код самого переходника. Естественно, восстановить их очень просто – нужно вместо адресов переходников вбить в код настоящие адреса функций. Вот и всё. Теперь мы запускаем скрипт по восстановлению переходников и получаем чистую таблицу адресов импортируемых функций, которую теперь можно скормить ImpRec’у.Code:00195288 55 PUSH EBP 00195289 8BEC MOV EBP,ESP 0019528B 8B55 0C MOV EDX,DWORD PTR SS:[EBP+C] 0019528E 8B45 08 MOV EAX,DWORD PTR SS:[EBP+8] 00195291 3B05 B0CB1900 CMP EAX,DWORD PTR DS:[19CBB0] 00195297 75 09 JNZ SHORT 001952A2 00195299 8B0495 10CC1900 MOV EAX,DWORD PTR DS:[EDX*4+19CC10] 001952A0 EB 07 JMP SHORT 001952A9 001952A2 52 PUSH EDX 001952A3 50 PUSH EAX 001952A4 E8 FB38FFFF CALL 00188BA4 ; JMP to kernel32.GetProcAddress 001952A9 5D POP EBP 001952AA C2 0800 RETN 8
Подведём небольшой итог. Мы узнали, что в аспре существует 3 типа переходников, мешающих нормально восстановить таблицу импорта. К каждому из этих типов нужен свой уникальный подход для успешной нейтрализации его защитных функций. Здесь нельзя тупо переправить переходы так, чтоб аспр считал, будто все переходники – одного типа, поскольку в ходе их генерации используются разные таблицы для декриптовки переходников и разные алгоритмы. Отсюда вывод – каждый переходник в зависимости от своего типа декриптуется по-своему, что и необходимо учитывать при распаковке программы, защищённой аспром.
(Помните, в первой части я говорил, что Magic Jump – фуфло дешёвое? )
Усвоив это, идём дальше. Осталось уже совсем недолго – нас ждут впереди проход на ОЕР, дамп и его подготовка к эксплуатации.
Поиск ОЕР
Знаете, про ОЕР сказано уже так много, что и добавить-то особо нечего. Если вы хотя бы немного в теме «что такое распаковка», то про ОЕР вам и так известно даже больше, чем нужно для поиска этой самой ОЕР. А что же нам нужно для её поиска? На самом деле, немного – нужно знать, где искать и что искать. Давайте подумаем, что мы с вами ищем. Наверное, некую последовательность байт, с которой бы начиналась наша программа. Вот тут-то и первая проблема – мы никак не можем определить такую последовательность байт, поскольку её попросту не существует. Ведь, по сути, каждая программа может начинаться практически как угодно. В общем, при поиске ОЕР мы никогда не знаем заранее, что мы ищем. Запомните это важное утверждение, иначе всю оставшуюся жизнь не сможете научиться снимать пакеры. Помните – при распаковке нужно исходить из самой сложной ситуации – когда вы распаковываете некую очень вредоносную программу, и выполнение кода, функционал которого вам неизвестен, смерти подобно! (Ну, из такой ситуации нужно исходить не всегда, мы рассмотрим и другие подходы, но позже).
Ладно, с этим как-то определились, но где же искать ОЕР? Да где угодно! (Вот это расклады ). И что же делать, чтоб её найти? А сделать тут можно немногое – учитывая современные методы её сокрытия, нужно из кода пакера дотопать до места, расположенного где-то между ImageBase и ImageBase+ImageSize, и если ОЕР не там, то топать назад и искать, с какой инструкции, относящейся к коду пакера, программа нормально запускается. Этот приём прекрасно работает против Stolen Bytes, но отвратительно – против виртуальных машин. Однако, и с тем, и с другим мы пока не имеем дел, но пробовать будем именно такую последовательность действий.
Переходим к практике… Пролистайте статью немного назад и найдите такие строчки:
И тут после очередного короткого перехода мы попадаем на участок кода, который не начинается с добавления структуры EXCEPTION_REGISTRATION в односвязный список этих структур
Вот, помните, как с него начиналось наше знакомство с аспровскими переходниками? Так вот – теперь после уничтожения всех переходников мы спокойно можем выйти из этой функции и пройти почти до нижеследующей за ней инструкцией RETN. Но перед этой самой RETN будет последний CALL, и если мы на нём не остановимся, то программа запустится, поэтому останавливаемся на таком вот коде:
И заходим внутрь CALL 0019EE24. Здесь – внутри этой подпрограммы – трюки достаточно однообразны. Помните, я говорил вам, что знания о структурной обработке исключений нам ещё пригодятся? Так вот самое время вспомнить, как происходит обработка исключений с флагом EXCEPTION_NONCONTINUABLE. Также следует учесть, что RaiseException и только он могут генерировать исключения с произвольными ExceptionCode. Впрочем, здесь нет ничего нового, за исключением того, как это мешает трассировке. В архиве к статье я прилагаю подробную информацию о том, как это не даёт нам трассировать код и тупо дотопать до ОЕР на F7. Но, по сути, нам важен лишь вот этот абзац:Code:0019F78D 8B45 08 MOV EAX,DWORD PTR SS:[EBP+8] 0019F790 8B10 MOV EDX,DWORD PTR DS:[EAX] 0019F792 8B45 08 MOV EAX,DWORD PTR SS:[EBP+8] 0019F795 8B40 1C MOV EAX,DWORD PTR DS:[EAX+1C] 0019F798 E8 87F6FFFF CALL 0019EE24
Если возникает исключение #DB(STATUS_SINGLE_STEP), то в контексте потока находящемся в стеке и
текущем сбрасывается TF(Trap Flag). Для иного исключения, отличного от #DB(STATUS_SINGLE_STEP) на момент возникновения которого был взведён TF, вход в диспетчер исключений выполняется с взведённым TF. После чего генерируется трассировочное исключение(#DB) и TF сбрасывается.
К статье также прилагается скрипт, который приведёт вас на ОЕР. Но, я думаю, что после прочитанного вам и самим захочется поисследовать код и узнать, как же аспр проходит на ОЕР без чьей-либо помощи. Я же порядком устал от этой части статьи, потому что мне кажется, будто она немного затянулась.
Сейчас вы уже стоите на ОЕР, а в ImpRec у вас светится полностью правильная будущая таблица импорта. Вы уже можете тупо прикрутить импорт к дампу (который, кстати, тоже можете тупо сделать уже сейчас), и поверьте, худо-бедно, но это будет работать, а если вы имеете дело с малварью, то на этом можете даже остановиться и начинать исследования вредоносного кода. Но! Это же не наши методы! Поэтому в следующих частях мы рассмотрим более подробно такие вопросы, как дампинг файлов, восстановление TLS и релоков, восстановление ресурсов, антиотладку аспра и многое другое.
Архив
Архив ко второй части
Послесловие
Настоятельно рекомендую всем понаходить файлы, работающие после упаковки этой версией аспра и тренироваться проделывать вышеописанные действия. До встречи в третьей части...
ARCHANGEL © AHTeam, r0 Crew



Reply With Quote
Thanks
