R0 CREW

Борьба с ASProtect - версия 1.0. (Часть 2)

Как всё начиналось…

Приветствую всех, кто благополучно добрался до второй части. В этой части мы, наконец, закончим с философией и перейдём к практическим исследованиям. Конечно, для практики нам понадобятся некоторые знания и умения. О некоторых из них я говорил в конце первой части, но тут хотелось бы добавить, что, по-моему, само собой разумеется, но всё же – вы должны знать ассемблер. Нет, не наизусть – такого знания от вас никто не требует, но уметь читать листинги дизассемблера вы обязаны. Т.к. протекторы по своей природе являются весьма изощрёнными штуками, мы вполне можем ожидать использования в их коде редких ассемблерных инструкций. Поэтому весьма может быть, что рано или поздно вы столкнётесь с незнакомой вам инструкцией, вот тут-то и понадобятся мануалы интела:

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, и не ошибся. Но, чего вы хотели? Это ведь первая версия – далее Солодовников доработает своё детище, в чём мы ещё не раз убедимся.

Опции защиты… Тут, собственно, пока и говорить нечего. Все доступные опции в этой версии аспра:

  1. Anti-debugger protection.
  2. 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. Некоторые могут подумать, будто это исключение происходит всегда, но это не так – оно имеет место быть только в отлаживаемых процессах. Не верите? Смотрите на листинг дизассемблера:

loc_7C93E615:           ; DbgBreakPoint()
call    _DbgBreakPoint@0        - внутри этой функции происходит останов по System breakpoint
mov     eax, [ebx+68h]
shr     eax, 1
and     al, 1
mov     _ShowSnaps, al
jmp     loc_7C9210BC

На этот адрес может попасть только отсюда:

loc_7C9210B2:
cmp     byte ptr [ebx+2], 0
jnz     loc_7C93E615

Таким образом, если byte ptr [ebx+2] не равен нулю, тогда и только тогда происходит переход на функцию DbgBreakPoint. Осталось только понять, что же за значение находится в ebx. А в ebx находится адрес чудесной структуры PEB (вы ведь читали рекомендованную литературу к первой части цикла статей про распаковку асприка и знаете, что такое РЕВ).

Остался ещё последний, да и то незначительный вопрос – что же из себя представляет функция DbgBreakPoint? Нет ничего проще:

; int __stdcall DbgBreakPoint()
public _DbgBreakPoint@0
_DbgBreakPoint@0 proc near
int     3               ; Trap to Debugger
retn
_DbgBreakPoint@0 endp

Пока не забивайте себе голову, что такое Trap to Debugger, и почему именно Trap, просто поймите, что здесь расположена инструкция int 3, которая является точкой останова, и первый останов происходит именно благодаря её присутствию.

Всё это очень хорошо, но зачем я вам это рассказываю? Да потому, что это – азы антиотладки. Ведь сам по себе отладчик не скрывает своего присутствия от протектора. Сейчас я не буду подробно описывать всю антиотладку аспра – этим мы займёмся, наверное, в третьей части, но пару слов об этих трюках скажу – пока воспринимайте их на веру.

Так вот, вполне возможно, что вы уже слышали хоть что-то про плагины, скрывающие Ольгу от разного рода способов её обнаружения. Так вот для того, чтоб спрятать её от аспра, достаточно самых хилых плагинов типа IsDebugPresent, а самый продвинутый – Phant0m – тоже прекрасно справляется с этой задачей.

Но как происходит процесс сокрытия отладчика, и от чего в данном случае мы его скрываем? Как мы уже убедились выше, некий байт, находящийся по смещению 2 от начала РЕВ, будучи установленным в 1 сообщает приложению, что оно отлаживается. Тогда задачей этих плагинов будет установка его в нуль. Но в какой момент его можно приравнять нулю, чтоб не нарушить взаимосвязь отладчика и отлаживаемого приложения, но, в то же время, не дать API функциям типа IsDebuggerPresent сдетектить отладчик? Да как раз в тот момент, когда срабатывает эта системная точка останова! Это – идеальный момент ещё и потому, что код протектора, будучи расположенным лишь в третьем кольце защиты, никак не сможет выполниться до этой System breakpoint.

Начало кода протектора и полиморфизм

Надеюсь, всё вышесказанное убедило вас:

  1. Поставить Phant0m и выбрать опцию Hide from PEB.
  2. В Options -> Debugging options -> Events -> Make first pause at ставим System breakpoint.

Второе действие здесь выполняется больше на перспективу, т.к. думаю, что вам придётся исследовать не только аспр, но и более коварные протекторы и даже вирусы, поэтому здесь нужно всё учитывать.

Теперь жмём F9 и останавливаемся на ЕР (Entry Point) упакованной программы.

004ED001 >  60              PUSHAD
004ED002    E8 01000000     CALL ResHacke.004ED008
004ED007    90              NOP
004ED008    5D              POP EBP
004ED009    81ED BFAF4500   SUB EBP,ResHacke.0045AFBF

И т.д. Что будем делать дальше? А дальше нужно трассировать код последовательными нажатиями F7, иначе мы рискуем пропустить какой-то важный элемент кода, который мог бы в дальнейшем пролить свет на механизм упаковки. Я знаю, что по распаковке аспра таких версий существует масса литературы, но везде его пытаются снять разными трюками (стойка на ушах, прыжки через пятку и т.д.), а мы должны проанализировать код протектора, чтоб что-то узнать о механизмах его защиты. Итак, начнём…

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 – переход не выполняется

Пока ничего особенного…

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

Теперь у нас есть база загрузки kernel32.dll. Мчимся дальше…

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.

Получили адрес чудесной функции LocalAlloc:

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

В общем-то, пока ничего необычного. Да, код базонезависим, но пока никаких сложностей в его исследовании не наблюдается. Идём дальше.

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 – а вот этого перехода на самом деле нету.

Вот, наконец-то первый трюк, который призван защитить код от дизассемблеров. А именно – короткий переход в середину следующей инструкции дизассемблируется неправильно. Так, к слову – это значение (значение регистра ESP, которое записалось в стек) в результате команды по адресу 004ED0C6 записывается именно это значение вместо того дальнего перехода.

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]

Вот на самом деле какие там инструкции. К слову сказать, этот трюк с прыжком в середину следующей команды аспр повторяет довольно часто. У меня сложилось впечатление, что всё двигло полиморфа основано на этом трюке. По крайней мере, на данном этапе это так. Конечно, можно тупо пройти все эти переходы по F7, но согласитесь – они немного отвлекают и становится сложнее уловить суть алгоритма. Я, вначале, для интереса, конечно, трассировал. Но понял, что ничего из-за этих джампов не понял, поэтому позже я с помощью своей нехитрой тулзы очистил трассу от этих тупых переходов.

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

Так выглядит очищенная от следов морфинга трасса. После последнего RETN происходит переход на GetProcAddress. Для получения этого участочка трассы я поставил memory breakpoint on access на секцию кода библиотеки kernel32.dll. Почему я так сделал? Всё очень просто – так как прот работает в третьем кольце защиты, то для тех или иных полезных действий ему всё равно придётся обращаться к системных библиотекам, поскольку непосредственного доступа в ядро он не имеет. Конечно, он может эмулировать многие функции, но без доступа к Native API через ntdll.dll ему не обойтись. Поэтому в крайнем случае можно ставить такие же точки останова на секцию кода ntdll.dll.

Что, вы уже собрались анализировать вышеприведенный код? Погодите, давайте ещё раз на него внимательно посмотрим.

004ED138 Main     PUSH EAX
004ED142 Main     CALL ResHacke.004ED14A
004ED14A Main     POP EAX                                   ; EAX=004ED147
004ED154 Main     POP EAX                                   ; EAX=7C800000

Тут даже без отладчика очевидно, что это – мусорная конструкция. Первая инструкция сохраняет EAX в стеке, далее CALL переходит на 004ED14A, при этом на стек кладётся адрес 004ED147, что соответствует значению адреса команды, идущей непосредственно за CALL. И тут же этот адрес выталкивается из стека в EAX. После чего в EAX снова выталкивается значение, которое в из него же заталкивалось в стек по адресу 004ED138. Короче говоря, этот участок кода ничего не меняет – всё остаётся как было. Поэтому от таких участков можно смело избавляться.

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+ RETN – аналогично переходу JMP на GetProcAddress. Чуть выше PUSH EAX – инструкция кладёт на вершину стека адрес возврата, на который переместится программа после выполнения функции GetProcAddress.

004ED1ED Main     LEA EAX,DWORD PTR SS:[EBP+45B1B3]         ; EAX=004ED1FB
004ED1F3 Main     INC EAX                                   ; EAX=004ED1FC

Вот эта пара инструкций – подготовка того самого адреса. Ещё выше PUSH EAX помещает в стек адрес базы kernel32.dll.

004ED129 Main     LEA EBX,DWORD PTR SS:[EBP+45BBE5]         ; EBX=004EDC2D
004ED15E Main     PUSH EBX

А вот здесь получается указатель на имя функции GetTickCount и заталкивается в стек.

FARPROC GetProcAddress(
HMODULE hModule,
LPCWSTR lpProcName
);

Вот, как и в документации, имеем два параметра для вышеупомянутой функции.

Далее вызывается сама GetTickCount, после – полученное значение подвергается воздействию AND EAX,1FFFh. Я не привожу здесь подробный анализ дальнейшего кода, т.к. выше уже было показано, как чистить его от мусора. Далее, если мы вычистим мусор, то увидим, что дальше вызывается функция LocalAlloc.

HLOCAL LocalAlloc(
UINT uFlags,
UINT uBytes
);

Т.е. – эта чудесная функция просто выделяет память. Остальные подробности нас не интересуют. Далее, продираясь через дебри полиморфа, видим, что функция эта вызывается дважды. Теперь у нас есть два выделенных блока памяти, давайте выясним, зачем они нужны протектору. Если опустить все подробности, до которых вы теперь и сами можете дойти, то становится видно, что в выделенную память записывается некий исполняемый код, на который позже передаётся управление. Далее некоторые участки памяти освобождаются, и мы стоим здесь:

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

Расшифрованный код, на первый взгляд, не имеет полиморфных конструкций. Кодес этот тоже планировался быть базонезависимым. Об этом свидетельствуют инструкции до JNZ 001A7C1F. Что же происходит после этого перехода?

001A76E3    50              PUSH EAX
001A76E4    FF95 B4314400   CALL NEAR DWORD PTR SS:[EBP+4431B4]      ; kernel32.GetModuleHandleA

Здесь – снова определение адреса загрузки kernel32.dll.

001A76F8    53              PUSH EBX
001A76F9    50              PUSH EAX
001A76FA    FF95 B0314400   CALL NEAR DWORD PTR SS:[EBP+4431B0]      ; kernel32.GetProcAddress

А вот здесь GetProcAddress уже определяет адрес функции VirtualAlloc. VirtualAlloc также, как и LocalAlloc, используется протектором для выделения памяти под собственные нужды, но имеет принципиальные отличия по функционалу. Впрочем, в данном случае нам это не важно.

001A770C    53              PUSH EBX
001A770D    57              PUSH EDI
001A770E    FF95 B0314400   CALL NEAR DWORD PTR SS:[EBP+4431B0]      ; kernel32.GetProcAddress

Соответственно, чуть ниже происходит получение адреса VirtualFree.

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

А вот и первое выделение памяти не заставило себя долго ждать. Далее становится ясно, что память эта была выделена не просто так.

001A7746    50              PUSH EAX
001A7747    53              PUSH EBX
001A7748    E8 78050000     CALL 001A7CC5

Данный код осуществляет расжатие данных, и перемещение уже расжатых данных в регион памяти, только что выделенный функцией VirtualAlloc. Любители статиков могут рипать эту функцию безбоязненно – я не нашёл в ней ни одной базозависимой инструкции.

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:[>

Вот это – тоже интересный момент. Здесь из только что расжатых и перемещённых байтиков некая область копируется непосредственно в ту область кода, где мы сейчас находимся.

001A7763    68 00800000     PUSH 8000
001A7768    6A 00           PUSH 0
001A776A    50              PUSH EAX
001A776B    FF95 41294400   CALL NEAR DWORD PTR SS:[EBP+442941]

Здесь вызывается VirtualFree и освобождает область памяти, выделенную немногим ранее. Получается, что вся эта процедура выполнялась лишь для того, чтоб модифицировать немного кода в районе EIP? Ну, немного – понятие растяжимое, но в общем – да, так и получается. Далее мы попадаем на RETN, который переносит нас на вновь расшифрованный код.

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

Здесь происходит второй вызов VirtualAlloc.

001A79C8    50              PUSH EAX
001A79C9    53              PUSH EBX
001A79CA    E8 F6020000     CALL 001A7CC5

Уже знакомая нам функция. Догадаетесь, куда будет перемещён результат декодирования?

001A79E8    C607 C3         MOV BYTE PTR DS:[EDI],0C3
001A79EB    FFD7            CALL NEAR EDI

Смысл этого кода мне несильно понятен: ну, копируется по какому-то адресу опкод инструкции RETN, ну идёт переход на эту инструкцию, после чего она возвращает нас опять назад, на следующую за CALL NEAR EDI инструкцию.
Ну и что? (Может, вы знаете, зачем это сделано?).

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

Этот достаточно большой кусок кода нам вообще не интересен – он выполняет фикс базозависимых инструкций в коде. Т.е. инструкции типа ближних пятибайтовых переходов (NEAR Jump) и инструкции Call NEAR, как вы знаете, базозависимы. Так вот здесь проводится их настройка на правильные адреса. НО! Есть у этого кода одно важное предназначение – благодаря ему мы можем задать себе вопрос: «А что это за код такой, в котором правятся эти инструкции?». Это – код за пределами данного учатска памяти, т.е. того участка памяти, в котором мы сейчас находимся. Это пока всё, что мы можем с уверенностью утверждать. Если будем упорно трассировать код, то попадём на этот участок памяти, а переход на него находится вот здесь:

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

Инструкция по адресу 001A7C40 как раз и заталкивает в стек адрес, на который потом осуществляется переход. А куда же мы переходим? А вот куда:

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. Что это за библиотека, зачем она здесь и прочие подробности про это программное чудо вы узнаете чуть позже, а пока давайте подытожим полученную информацию, а также составим простой скрипт для прохода на точку входа в эту библиотеку.

Только что мы узнали, что аспр имеет многослойную структуру, т.е. на пути к распаковке нам встречаются расшифровщики, которые расшифровывают другие расшифровщики и т.д. Код расшифровщиков перемешан с полиморфным мусором, который, тем не менее, однотипен и достаточно просто отделяется от основного полезного кода. На данном этапе нашей конечной целью является останов на точке входа в ASProtect.dll. Скрипт будет выглядеть достаточно просто – после второго срабатывания брейкпоинта на начале функции VirtualAlloc мы выходим из этой функции и ищем по маске инструкции (надеюсь, вы читали справку по написанию скриптов и знаете, как это делается):

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

Далее ставим брейкпоинт на последний RETN, а когда он сработает – делаем Single Step. Скрипт прилагается к статье.

Знакомство с ASProtect.dll и техникой использования Dll-injection

Вот мы и добрались до некоего загадочного кода, который я именую как ASProtect.dll. На самом деле, так его именую не я – его так назвали ещё задолго до меня, я лишь повторяю это название. А сейчас я постараюсь вам объяснить, что эта библиотека из себя представляет.

Этот код – код обычной динамической библиотеки, написанной на том же языке программирования, что и сам Аспр – на Delphi. Об этом красноречиво говорит точка входа – ЕР данного кода. Хотя, правильнее было бы сказать, что этот код был обычным, пока его не исказил Аспр. Не верите? Тогда посмотрите – на месте заголовка у этой библиотеки нули. А где вы видели такую библиотеку, у которой нет заголовка? Её же просто не загрузит загрузчик операционной системы. Так как же всё это работает? И главное – зачем?

На первый вопрос – как – мы ответили бы, если бы более тщательно изучали код с момента исправлений базозависимых инструкций (помните, такой был чуть выше?). Всё, на самом деле, очень просто – раз загрузчик не работает с такими библиотеками, то протектор будет выполнять его работу на стадии инициализации библиотеки. Т.е. – протектор расшифровывает код этой библиотеки, располагая его по адресу в памяти, выделенной функцией VirtualAlloc. Далее он должен заполнить таблицу импорта (если поставить брейкпоинт на GetProcAddress, увидите, как протектор это делает), исправить базозависимые инструкции – пересчитать их соответственно новому адресу загрузки, а потом передать управление на точку входа в библиотеку. Вот и весь общий алгоритм работы по извлечению кода этой библиотеки.

Далее вопрос – почему именно ASProtect.dll? Откуда взялось это название? На самом деле, название взялось от балды, т.е. просто кто-то так захотел её назвать. Доподлинно неизвестно, как её называет сам Солодовников, поэтому мы остановимся на таком названии.

Теперь следующий вопрос – зачем эта библиотека тут нужна? Здесь всё ещё проще – представьте себя на месте автора какого-нибудь пакера. Хотя бы пакера. Для того, чтоб ваше детище успешно паковало различные программы, оно должно добавлять в них свой код, который должен быть базонезависимым. «Ну и что?» - спросите вы. А то, что это 10 строчек базонезависимого кода написать легко и приятно, а что будет, если таких строк нужно будет наваять, эдак, тысяч 5? Тогда окажется, что занятие это очень неприятное, но всё же – если кому-то не лень, то скрипя зубами, этого можно добиться. Но это – если вы пишете на С, а если вы, как Алексей Солодовников, выбрали Delphi? Тогда ситуация усложняется тем, что встроенный в этот компилятор Борландовский ассемблер напрочь отказывается понимать такие конструкции, как:

Call dword ptr [ebp + offset MyCall]

Вот вам и подвох для базонезависимости от Борланда. Поэтому чтоб не извращаться, Солодовников решил забить на базонезависимость (везде, кроме начала) и весь основной код защиты сделать самым обычным кодом в виде динамической библиотеки. Удобно, и со вкусом. И теперь нам с вами придётся исследовать, что же происходит внутри этой библиотеки.

Наш скрипт для прохода на ОЕР этой библиотеки успешно отработал, и мы переходим на вот этот последний CALL, т.к. именно внутри него начинается работа кода протектора.

001A7E81    E8 7609FFFF     CALL 001987FC

Кстати, адреса могут меняться, просто учтите, что это – последний CALL от начала ЕР библиотеки. Если хотите – потрассируйте предыдущие, чтобы убедиться, что там нет ничего интересного. Далее внутри немного пошагав по F7, попадаем на вот такое место (попасть на него можно очень просто, нажав Ctrl-F9, а потом F7 – это обусловлено тем, что переход делается с помощью команды RETN):

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 всё, что нужно, и даже больше.

Теперь давайте новыми, поумневшими после чтения Питрека, глазами посмотрим на этот код:

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

Первые четыре инструкции добавили ещё одну структуру EXCEPTION_REGISTRATION в односвязный список.

001A08B8    8B45 08         MOV EAX,DWORD PTR SS:[EBP+8]
001A08BB    E8 28FAFFFF     CALL 001A02E8

Здесь подготавливается аргумент и вызывается функция с соглашением о вызовах fastcall. Т.е. единственный параметр передается через регистр EAX. Работу этой функции мы рассматривать не будем, но скажу, что пока в ней нет ничего для нас интересного, но она очень большая, чтоб приводить здесь её описание.

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

Тут даже без комментариев. Идём дальше. Дальше идёт короткий переход, ведущий нас к участку кода, весьма похожему по своей структуре на предыдущий. Обратите внимание, что EXCEPTION_REGISTRATION пока ни разу нам не пригодились – ошибок в коде не возникает, обработчики не выполняются. И тут после очередного короткого перехода мы попадаем на участок кода, который не начинается с добавления структуры EXCEPTION_REGISTRATION в односвязный список этих структур. А попадаем мы на такое вот место:

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.

Даже не исследуя дальше код, такой поворот событий наводит на мысль, что где-то рядом нас ждёт работа с импортом. И действительно – ближайший CALL действительно подготавливает импортируемые функции для дальнейшего использования в приложении. Не верите? А и не надо, но по F7 туда идти придётся, потому что если попробуете нажать F8 – программа просто запустится. Там, внутри этой функции, очень много кода, но весь он относится к расшифровке участков зашифрованной программы, поэтому смело трассируем до ближайшего RETN, т.е. опять жмём в Ольке Ctrl-F9, далее попадаем на код расжатия каких-то данных, но нам это неинтересно – поэтому идём внутрь ближайшего CALL. Дальше – генерация импорта и знаменитых аспровских переходников. А пока пишем скрипт, который будет каждый раз помогать нам доходить до этого места. В скрипте всё просто – вначале с помощью предыдущего скрипта доходим до точки входа в аспровскую библиотеку, оттуда доходим до последнего CALL, заходим внутрь, доходим до RETN, Single Step, по маске ищем CALL, перед которым в стек заталкиваются в качестве аргументов адреса, заходим внутрь этого CALL, снова доходим до RETN, далее ищем ближайший CALL и заходим в него. Как-то так… Если вдруг вам что-то неясно, то этот скрипт в моём исполнении прилагается к статье.

API Redirectors и методы борьбы с ними

Вот мы и добрались до самой занимательной части статьи – восстановления переходников на импортируемые функции в защищённом приложении. Страшно звучит, не правда ли? Но страшного тут ничего нет. Давайте сейчас разберём теорию, а потом уж перейдём к практическим исследованиям.

Но перед дальнейшим повествованием хотел бы отвлечь ваше внимание на ещё одну мелочь – я тут попропускал при исследовании кучу кода, мотивируя это тем, что нам он неинтересен или попросту не нужен. Но как я определил, что он нам не нужен? Всё очень просто – вначале я трассирую код, как только на моём пути попадается некая инструкция CALL, я стараюсь пройти её, не заходя внутрь подпрограммы, на которую она ссылается. Если при этом получилось так, что программа запустилась (или того хуже – аварийно завершилась), то тогда приходится заглядывать внутрь этой подпрограммы, если нет – то это уже ваш выбор. И так я стараюсь добраться до диапазона адресов, который бы лежал в пределах между ImageBase (Или Base of Image – как её только не называли) и ImageBase+ImageSize, т.к. по понятным причинам этот код будет либо самим ОЕР, либо должен находиться в непосредственной от него близости. Тогда вы спросите: «почему нельзя воспользоваться автоматической трассировкой с заходом в подпрограммы, и найти ОЕР вообще без напряга?» Вопрос очень правильный, но так делать НЕЛЬЗЯ по двум причинам:

  1. Зачастую это очень долго.
  2. Существуют методы, мешающие такой трассировке, но о них речь пойдёт чуть позже.

Сейчас, как и обещал, немного теории. Если вы читали статьи «Об упаковщиках в последний раз», о которых я упоминал в первой теоретической части, то уже и так всё знаете про таблицу импорта. Здесь я буду говорить только о её малой части. В документации по РЕ-формату можно найти упоминание о некоей FirstThunk, которая заполняется загрузчиком. По сути – это указатель на начало массива адресов импортируемых функций. У каждой отдельной структуры, именуемой IMAGE_IMPORT_DESCRIPTOR, своя FirstThunk. Пока не будем углубляться дальше, и из всего сказанного вам следует сделать такие выводы:

  1. Таблица импорта существует для обеспечения возможности запуска ехе-файлов на разных версиях ОС Windows (другие ОС мы не рассматриваем).
  2. Она состоит из последовательно расположенных друг за другом структур IMAGE_IMPORT_DESCRIPTOR.
  3. Одним из полей структуры IMAGE_IMPORT_DESCRIPTOR является поле, именуемое как FirstThunk.
  4. Это поле – указатель на массив адресов импортируемых функций.
  5. По массивам адресов импортируемых функций можно воссоздать таблицу импорта.

Пока нам больше знать и не нужно. К слову, пунктом под номером 5 занимается ImpRec почти без нашего участия. В дальнейшем мы рассмотрим этот вопрос более подробно, а пока идём дальше.

Таким образом, становится очевидным, что для противодействия распаковке протектор должен каким-либо образом исказить значения этих самых адресов импортируемых функций. Но сделать это таким образом, чтоб программа при этом осталась рабочей. И такой способ есть – в литературе его именуют как «создание переходников» на импортируемые функции. Т.е. вместо перехода на код импортируемой функции мы переходим на некий код протектора, в недрах которого эта самая импортируемая функция и будет выполняться. Как именно выполняется этот переход – уже дело десятое. Но принцип защиты таким способом от распаковки у большинства протекторов (не только у Аспра) один и тот же.

И что же нам с этим делать? На самом деле, здесь, как в старом армейском анекдоте – есть два мнения: одно – моё, а другое – ошибочное. Моё мнение состоит в том, что нам с вами во что бы то ни стало необходимо из этих переходников получить настоящие адреса импортируемых функций, которые потом можно было бы скормить ImpRec’у. Иногда, правда, сделать это весьма затруднительно, но стремиться нужно именно к этому.

На этой оптимистической ноте мы заканчиваем теоретическую часть и переходим к практическим исследованиям генерации переходников в ASProtect.dll.

Переходники первого типа

Сейчас мы с вами в отладчике выполнили два вышеописанных скрипта и стоим на начале функции генерации переходников.

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]

Вначале идёт стандартный пролог функции, потом выделяется память под локальные переменные, далее значения регистров сохраняются в стеке, после чего аргументы функции, переданные ей через стек, извлекаются в регистры EDI и ESI. Сразу замечу, что данная функция выполняется в цикле, и после каждого выполнения мы имеем в качестве результата её работы один готовый переходник. Идём дальше.

00192996    8A1E            MOV BL,BYTE PTR DS:[ESI]

После этого в BL содержится номер, по которому аспр определит тип переходника. Нашему первому типу переходника соответствует номер 2. Почему так? Почему я не рассматриваю этот переходник вторым согласно его номеру? Потому что этот номер соответствует самому простому типу переходника, и именно с него мне хотелось бы начать описание.

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

Этот код обнуляет память начиная с адреса в EAX, и заканчивая через 100h двойных слов + 1 байт.

001929AA    80EB 02         SUB BL,2
001929AD    74 0E           JE SHORT 001929BD

Далее – обычная case – конструкция. Вы ещё не забыли, что у нас находится в BL? А теперь, когда мы отнимем оттуда 2, следующий переход, конечно же, будет выполнен.

001929BD    8A07            MOV AL,BYTE PTR DS:[EDI]
001929BF    3A05 6C8F1900   CMP AL,BYTE PTR DS:[198F6C]
001929C5    74 43           JE SHORT 00192A0A

Помните, чуть выше в EDI загрузился один из параметров исследуемой функции? Так вот теперь мы видим, что это – некий адрес чего-то. И первый байт, находящийся по этому адресу, сравнивается с константой по адресу (в моём случае) 198F6C. В моём случае в AL находится 1, а в 198F6C – 2. Поэтому переход не выполняется. Далее для нас идёт не очень интересный код, и за неимением времени я его здесь не привожу, но вы можете самостоятельно его исследовать. А мы с помощью трассировки переходим вот сюда:

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 – а здесь – имя самой импортируемой функции
00192A42    53              PUSH EBX
00192A43    46              INC ESI
00192A44    56              PUSH ESI
00192A45    E8 4EFCFFFF     CALL 00192698

А вот внутри этой функции мы определяем настоящий адрес импортируемой функции. Как вы, возможно, уже догадались – за это внутри вызова CALL 00192698 отвечает известная нам GetProcAddress. Тогда вы, без сомнения, сумеете догадаться, какие параметры передаются внутрь CALL 00192698.

00192A4A    50              PUSH EAX                                 ; ntdll.RtlDeleteCriticalSection
00192A4B    E8 F8FEFFFF     CALL 00192948

А вот здесь хитроумным способом генерируется переходник. Функция по адресу 00192948 принимает всего один аргумент – настоящий адрес, и формирует переходник на него. Таким образом, чтоб противостоять формированию переходника мы просто должны не выполнять этот вызов. Переходник же имеет достаточно убогий вид – из себя он представляет простой переход на оригинальный адрес функции. С такими переходниками может справиться даже ImpRec (опция Trace Level1 (Disasm)), именно поэтому во многих статьях по распаковке аспра говориться про какие-то «режимы трассировки» и «трассировочные плагины». Но поверьте – на самом деле гораздо проще, удобнее и правильнее избегать этих плагинов, а проанализировав структуру генерации переходников, раз и навсегда от них избавиться. Если вы обратили внимание, после выполнения функции, генерирующей переходники, идёт безусловный прыжок на, фактически, конец функции:

00192A50   /E9 A9000000     JMP 00192AFE

Для избавления от переходника нам следует просто переместить этот переход на пару инструкций вверх. И всё – с первым типом переходников мы разобрались.

Второй тип переходников

Этот тип переходников гораздо более сложен и громоздок как в своей работе, так и в восстановлении, но не расстраивайтесь – в более новых версиях аспра Солодовников почему-то отказался от данного типа переходников. Хотя кто знает – может быть когда-то они нам ещё попадутся, а сейчас – поехали.

00192996    8A1E            MOV BL,BYTE PTR DS:[ESI]

Стоим снова здесь, но теперь в BL находится 4.

001929AA    80EB 02         SUB BL,2
001929AD    74 0E           JE SHORT 001929BD
001929AF    80EB 02         SUB BL,2
001929B2    0F84 9D000000   JE 00192A55

Теперь выполняется последний переход.

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

Вкратце прокомментирую этот код. В стеке создаётся структура, которая из себя представляет опкоды двух команд – например:

00A7436C    68 7643A700     PUSH 0A74376
00A74371    E8 32E571FF     CALL 001928A8

Далее второй CALL 00182F14 перемещает эту структуру по некоему адресу переходника, который потом и будет фигурировать в массиве адресов импортируемых функций и не будет распознан ImpRec’ом. Генерация переходника на этом заканчивается и далее идёт уже знакомый нам переход, свидетельствующий о конце функции:

00192AA7   /EB 55           JMP SHORT 00192AFE

Ситуация с переходником несколько другая, чем в первом случае. Возникает вопрос – где же вычисляется настоящий адрес? А он вычисляется по ходу вызова этой зашифрованной функции, т.е. когда из программы вызывается 2 тип переходников, управление передаётся на адрес этого переходника – в нашем случае управление передалось бы на адрес 00A7436C. Давайте вручную перейдём на этот адрес, и зайдём внутрь вызова CALL 001928A8. Да, кстати, переходников такого типа может быть много, но все они неизбежно сводятся к вызову подпрограммы по адресу 001928A8. Отсюда вывод – если мы сможем из этой подпрограммы выудить настоящие адреса, то это и будет победой над этим типом переходников. Я не буду вас долго мучить детальным рассмотрением кода этого типа переходников, скажу только, что с помощью банальной трассировки мы вскоре окажемся на вызове всё той же GetProcAddress:

00192932    FF75 FC         PUSH DWORD PTR SS:[EBP-4]
00192935    FF75 F8         PUSH DWORD PTR SS:[EBP-8]
00192938    E8 5BFDFFFF     CALL 00192698

Теперь в ЕАХ у нас имеется правильный адрес, но как нам получать эти правильные адреса не во время выполнения программы, а ещё до начала её выполнения при генерации переходников? Всё достаточно просто: для этого после генерации этого типа переходников нам нужно попасть на код переходника, потом подправить эпилог таким образом, чтоб вновь подправленная подпрограмма не переходила на адрес функции, полученный через GetProcAddress, а просто возвращала его в ЕАХ. Я не заморачивался здесь со скриптовыми командами, а написал скрипт, который патчит код ASProtect.dll чтоб получить вышеописанный результат. Как именно я это сделал – можете посмотреть в скрипте, который также приложен к статье.

Третий тип переходников

Третий вид переходников являет из себя действительно нечто особенное, но пока не особо опасное. Все вы, наверное, знаете о существовании неких плагинов для ImpRec, которые позволяют определять нераспознанные функции в программах, запакованных аспром. Так вот знайте – эти плагины борются как раз с третьим видом переходников. Этот вид я поначалу даже и не заметил. Но потом оказалось, что когда почти всё уже было пофиксено и программа стояла на ОЕР в ожидании прикручивания импорта к готовому дампу – я увидел один нераспознанный переходник. Но это был переходник, по своей работе весьма напоминающий функцию GetProcAddress. Собственно, в оригинале там и должна была быть эта функция. Как я узнал об этом? Да очень просто – в окне дизассемблера перешёл на этот адрес и посмотрел на листинг. Так вот – пока в коде аспра таким образом защищены только две функции: LoadLibraryA и GetProcAddress.

00196729    B8 88521900     MOV EAX,195288
0019672E    E9 DB000000     JMP 0019680E

Вот так, собственно, выглядит установка переходника на GetProcAddress.

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

А это – код самого переходника. Естественно, восстановить их очень просто – нужно вместо адресов переходников вбить в код настоящие адреса функций. Вот и всё. Теперь мы запускаем скрипт по восстановлению переходников и получаем чистую таблицу адресов импортируемых функций, которую теперь можно скормить ImpRec’у.

Подведём небольшой итог. Мы узнали, что в аспре существует 3 типа переходников, мешающих нормально восстановить таблицу импорта. К каждому из этих типов нужен свой уникальный подход для успешной нейтрализации его защитных функций. Здесь нельзя тупо переправить переходы так, чтоб аспр считал, будто все переходники – одного типа, поскольку в ходе их генерации используются разные таблицы для декриптовки переходников и разные алгоритмы. Отсюда вывод – каждый переходник в зависимости от своего типа декриптуется по-своему, что и необходимо учитывать при распаковке программы, защищённой аспром.
(Помните, в первой части я говорил, что Magic Jump – фуфло дешёвое? )

Усвоив это, идём дальше. Осталось уже совсем недолго – нас ждут впереди проход на ОЕР, дамп и его подготовка к эксплуатации.

Поиск ОЕР

Знаете, про ОЕР сказано уже так много, что и добавить-то особо нечего. Если вы хотя бы немного в теме «что такое распаковка», то про ОЕР вам и так известно даже больше, чем нужно для поиска этой самой ОЕР. А что же нам нужно для её поиска? На самом деле, немного – нужно знать, где искать и что искать. Давайте подумаем, что мы с вами ищем. Наверное, некую последовательность байт, с которой бы начиналась наша программа. Вот тут-то и первая проблема – мы никак не можем определить такую последовательность байт, поскольку её попросту не существует. Ведь, по сути, каждая программа может начинаться практически как угодно. В общем, при поиске ОЕР мы никогда не знаем заранее, что мы ищем. Запомните это важное утверждение, иначе всю оставшуюся жизнь не сможете научиться снимать пакеры. Помните – при распаковке нужно исходить из самой сложной ситуации – когда вы распаковываете некую очень вредоносную программу, и выполнение кода, функционал которого вам неизвестен, смерти подобно! (Ну, из такой ситуации нужно исходить не всегда, мы рассмотрим и другие подходы, но позже).

Ладно, с этим как-то определились, но где же искать ОЕР? Да где угодно! (Вот это расклады ). И что же делать, чтоб её найти? А сделать тут можно немногое – учитывая современные методы её сокрытия, нужно из кода пакера дотопать до места, расположенного где-то между ImageBase и ImageBase+ImageSize, и если ОЕР не там, то топать назад и искать, с какой инструкции, относящейся к коду пакера, программа нормально запускается. Этот приём прекрасно работает против Stolen Bytes, но отвратительно – против виртуальных машин. Однако, и с тем, и с другим мы пока не имеем дел, но пробовать будем именно такую последовательность действий.

Переходим к практике… Пролистайте статью немного назад и найдите такие строчки:

И тут после очередного короткого перехода мы попадаем на участок кода, который не начинается с добавления структуры EXCEPTION_REGISTRATION в односвязный список этих структур

Вот, помните, как с него начиналось наше знакомство с аспровскими переходниками? Так вот – теперь после уничтожения всех переходников мы спокойно можем выйти из этой функции и пройти почти до нижеследующей за ней инструкцией RETN. Но перед этой самой RETN будет последний CALL, и если мы на нём не остановимся, то программа запустится, поэтому останавливаемся на таком вот коде:

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

И заходим внутрь CALL 0019EE24. Здесь – внутри этой подпрограммы – трюки достаточно однообразны. Помните, я говорил вам, что знания о структурной обработке исключений нам ещё пригодятся? Так вот самое время вспомнить, как происходит обработка исключений с флагом EXCEPTION_NONCONTINUABLE. Также следует учесть, что RaiseException и только он могут генерировать исключения с произвольными ExceptionCode. Впрочем, здесь нет ничего нового, за исключением того, как это мешает трассировке. В архиве к статье я прилагаю подробную информацию о том, как это не даёт нам трассировать код и тупо дотопать до ОЕР на F7. Но, по сути, нам важен лишь вот этот абзац:

Если возникает исключение #DB(STATUS_SINGLE_STEP), то в контексте потока находящемся в стеке и
текущем сбрасывается TF(Trap Flag). Для иного исключения, отличного от #DB(STATUS_SINGLE_STEP) на момент возникновения которого был взведён TF, вход в диспетчер исключений выполняется с взведённым TF. После чего генерируется трассировочное исключение(#DB) и TF сбрасывается.

К статье также прилагается скрипт, который приведёт вас на ОЕР. Но, я думаю, что после прочитанного вам и самим захочется поисследовать код и узнать, как же аспр проходит на ОЕР без чьей-либо помощи. Я же порядком устал от этой части статьи, потому что мне кажется, будто она немного затянулась.

Сейчас вы уже стоите на ОЕР, а в ImpRec у вас светится полностью правильная будущая таблица импорта. Вы уже можете тупо прикрутить импорт к дампу (который, кстати, тоже можете тупо сделать уже сейчас), и поверьте, худо-бедно, но это будет работать, а если вы имеете дело с малварью, то на этом можете даже остановиться и начинать исследования вредоносного кода. Но! Это же не наши методы! Поэтому в следующих частях мы рассмотрим более подробно такие вопросы, как дампинг файлов, восстановление TLS и релоков, восстановление ресурсов, антиотладку аспра и многое другое.

Архив

Архив ко второй части

Послесловие

Настоятельно рекомендую всем понаходить файлы, работающие после упаковки этой версией аспра и тренироваться проделывать вышеописанные действия. До встречи в третьей части…

ARCHANGEL © AHTeam, r0 Crew

Спасибо, статья отличная!! Огромный труд вложен! Ждем продолжения…