Оригинальный DVD-ROM: eXeL@B DVD !
eXeL@B ВИДЕОКУРС !

ВИДЕОКУРС ВЗЛОМ
выпущен 12 ноября!


УЗНАТЬ БОЛЬШЕ >>
Домой | Статьи | RAR-cтатьи | FAQ | Форум | Скачать | Видеокурс
Новичку | Ссылки | Программирование | Интервью | Архив | Связь

Экстримальная защита в виде XtremeProtector 1.07

Обсудить статью на форуме

Массу крэкерских инструментов, видеоуроков и статей вы сможете найти на видеокурсе от нашего сайта. Подробнее здесь.

Автор: dragon <c_dragon@mail.ru>

1. Аналитика(начало)

        Протекторы как и многое другое можно разделить на три категории по уровню защиты программы - low end, middle end и high end. К low end можно отнести все простые упаковщики типа UPX, ASPack, а также некоторые протекторы. К middle end несомненно относятся такие вещи, как ASProtect, Armadillo, Obsidum и др. А среди high end, т.е. наиболее мощных протекторов известно только два - XtremeProtector и StarForce. Их отличает очень хорошая антиотладка(особенно это касается XProtector'а, попробуйте его запустить, если в системе стоит softice), довольно неплохая защита импорта, сокрытие программного кода, который не получить так просто, как это можно сделать с copymem II в Armadillo, и самое главное - использование Ring0 кода, который наиболее сильно осложняет исследование.

        Можно выделить два способа снятия протекторов. Первый - исследование в лоб от точки входа до OEP и написание автоматического распаковщика. Второй - ручная распаковка с помощью нахождения и использования "чёрных ходов" в коде протектора, с целью снятия дампа, нахождения OEP, получения импорта и. т. д. При распаковке XProtector'а мы пойдём естесственно вторым путём, так как это в любом случае проще и быстрее.

2. Небольшое описание защиты

        В качестве цели будет использоваться XtremeProtector 1.07 DEMO, который доступен на официальном сайте производителя. На момент написания статьи доступна уже версия 1.08, но она защищена хуже, поэтому в статье и будет описываться 1.07.

        Перед прочтением рекомендуется немного разбираться в защищённом режиме и устройстве Windows NT, в частности в ядре. Без этого трудно будет понять, как же работает защита. Что касается инструментов, то основным будет IDA версии не древнее 4.50. Также нужен будет редактор PE файлов(PE Tools recommended), ImpRec, шестнадцатеричный редактор(WinHex), а также пакет MASM32. Также стоит установить плагин-эмулятор x86 для IDA(доступен на wasm.ru) и плагин для очистки от scrambled кода(есть в сопровождающем статью архиве). Ладно, теперь можно перейти непосредственно к описанию.

        Итак, защита состоит из двух частей, самой упакованной программы и драйвера под именем xprotector.sys, который лежит в каталоге \System32\Drivers. В определённый момент работы код протектора вызывает функцию DeviceIoControl с кодом 1800h, при обработке которого драйвер проводит некоторую инициализацию. Смотрим, что же он там делает:

  • Определяет число процессоров в системе
  • Находит адрес IDT для каждого процессора и разрешает доступ к IDT из пользовательского режима на уровне страниц
  • Открывает доступ к своей первой странице с данными, где и находится массив с адресами IDT
  • Открывает доступ ко всем портам ввода-вывода

В большинстве случаев IDT будет конечно одна, только в случае многопроцессорной машины или Pentium IV HT таблиц прерываний будет больше одной, хотя на распаковке это никак не сказывается. А сказывается то, что код протектора получает прямой доступ в ring0 и может делать что хочет. А хочет он много чего, например то, чтобы его не отлаживали и не дампили. Ведь действительно, к запущенной программе не подступится, попытка сдампить из PE Tools приводит к сообщению о невозможности чтения памяти процесса. Если запустить softice, то приложение закрывается, а если приаттачится к процессу Ollydbg, то будет вообще перезагрузка. Первым делом надо ликвидировать антидамповую защиту и антиотладку, а потом уже думать, что делать дальше

3. Снятие антидамповой и антиотладочной защиты

        Очевидно, что если память доступна для чтения из одного процесса и недоступна из другого, то перехвачены API функции для работы с памятью. Начинаем смотреть код функции ReadProcessMemory из kernel32.dll. Как видно перехватов не наблюдается. Также их не наблюдается и в native api функции NtReadVirtualMemory. Далее уже ядро. Как управление попадает на соответствующую функцию в ядро WinNT хорошо описано во второй статье об упаковщиках на wasm.ru. Первое, куда стоит посмотреть, так это в system service table или короче sst - таблицу адресов функций ntoskrnl.exe. Для этого в архиве лежит утилита R0cmd.exe и драйвер. С её помощью можно дампить и загружать IDT, SST, SST shadow для ntoskrnl.exe и win32k.sys и вообще произвольные участки памяти в области ядра. Для определения перехвата можно сравнить два дампа sst - до запуска XProtector'а и после. Таблицы отличаются, значит XProtector контроллирует некоторые функции ntoskrnl. Чтобы выяснить, какие это функции, надо смещение отличающися элементов в дампе поделить на 4 и найти число в массиве Zw-функций из библиотеки ntdll.dll. В результате имеем список перехваченных функций:

  • NtAllocateVirtualMemory
  • NtCreateThread
  • NtQueryVirtualMemory
  • NtReadVirtualMemory
  • ZwTerminateProcess
  • NtWriteVirtualMemory

Содержание этих функций приводить нет смысла, там при попытке доступа из другого процесса к защищённой программе возвращается код ошибки. Также блокируется дамп с помощью удалённого треда(CreateRemoteThread). Но самое удивительное это то, что после в обработчике ZwTerminateProcess не восстанавливается ни sst ни idt. Вероятно протектор залез куда-то ещё, вероятно в планировщик потоков. Предоставляю проверить это тем, кто хорошо разбирается в ядре WinNT. Нас же здесь больше интересует снятие антидампа. Очевидно, чтобы его снять, достаточно подгрузить на место нормальную таблицу sst при помощи той же утилиты R0cmd. И теперь можно использовать такие утилиты, как PE Tools и ImpRec. Но вот нельзя пока использовать отладчик.

        R0cmd /viewidt:??? - позволяет посмотреть IDT в текстовом виде. Сразу привлекают внимание вырубленные отладочные прерывания int1 и int3(их адрес стал 0FFFFFFFFh). Казалось бы всё просто, восстановил IDT да отлаживай, но ничего не выходит. При попытке восстановить IDT программа либо вырубается, либо IDT возвращается к исходному состоянию. Виной тому лишние потоки, порождённые протектором, которые и проверяют IDT. Но хотя саму защищённую программу отлаживать нельзя, но можно отлаживать другие программы. Как же это возможно? А вот как, при вызове int1 или int3 управление попадает на адрес 0FFFFFFFFh, памяти по этому адресу обычно нет, поэтому происходит исключение Page fault (#PF), обработчик которого тоже перехвачен. Понятно, что в нём и происходит обработка всяких отладочных действий пользователя. Стоит привести код его обработчика:

 seg000:F8AB8000                 pushf
 seg000:F8AB8001                 pusha
 seg000:F8AB8002                 call    $+5
 seg000:F8AB8007                 pop     ebp
 seg000:F8AB8008                 sub     ebp, 1E09229h
 seg000:F8AB800E                 inc     dword ptr [ebp+1E0925Fh]
 seg000:F8AB8014                 cmp     dword ptr [ebp+1E0925Fh], 63h
 
  Переход выполнится при переполнении счётчика, в результате
  чего на место адресов int1/int3 в IDT запишутся 0FFFFFFFFh, это не особо интересно
 seg000:F8AB801B                 ja      loc_F8AB80B0
 
 seg000:F8AB8021
 seg000:F8AB8021 loc_F8AB8021:                           ; CODE XREF: sub_F8AB8000+128
 
  А вот то, что надо, сравнение EIP с 0FFFFFFFFh, значит идём по пореходу на F8AB8041
 seg000:F8AB8021                 mov     eax, [esp+28h]
 seg000:F8AB8025                 cmp     eax, 0FFFFFFFFh
 seg000:F8AB8028                 jz      short loc_F8AB8041
 

 seg000:F8AB8041 loc_F8AB8041:                           ; CODE XREF: sub_F8AB8000+28
 seg000:F8AB8041                 mov     eax, 1
 seg000:F8AB8046                 mov     ecx, eax
 seg000:F8AB8048                 or      eax, eax
 
  Ну нет, чтоб просто jmp написать ..
 seg000:F8AB804A                 jnz     loc_F8AB8138
 
 

 seg000:F8AB8138                 push    fs
 seg000:F8AB813A                 mov     eax, 30h ;селектор для регистра fs в ядре - 30h
 seg000:F8AB813F                 db      66h
 seg000:F8AB813F                 mov     fs, ax
 
  ;Получаем указатель на структуру KTHREAD(или ETHREAD)
 seg000:F8AB8142                 mov     eax, large fs:KPRCB.CurrentThread
 
  ;А это указатель на KPROCESS, он же указатель на объект процесса
 seg000:F8AB8148                 mov     eax, [eax+KTHREAD.ApcState.Process]
 seg000:F8AB814B                 pop     fs
 seg000:F8AB814D                 mov     ebx, eax
 seg000:F8AB814F                 and     ebx, 7FFFFFFFh
 
  ;Видимо по этому адресу или чуть дальше записывается указатель на объект
   защищённого XProtector'ом процесса, если он текущий, или константа 47616420h, если нет.
   Всё это вероятно происходит в коде переключения потоков
 seg000:F8AB8155                 mov     esi, 0F8AB6000h
 seg000:F8AB815A                 cmp     dword ptr [esi], 0
 seg000:F8AB815D                 jz      loc_F8AB8075
 seg000:F8AB8163
 seg000:F8AB8163 loc_F8AB8163:                           ; CODE XREF: sub_F8AB8000+17A
 seg000:F8AB8163                 add     esi, 4
 seg000:F8AB8166                 cmp     dword ptr [esi], 47616420h
 
  Ага, переход на вызов старого обработчика прерывания, если процесс не защищён.
 seg000:F8AB816C                 jz      short loc_F8AB8187
 seg000:F8AB816E                 cmp     [esi], eax
  А далее переходы не туда, куда надо, причём они выполняются, если
  #PF произошло в защищённом процессе
 seg000:F8AB8170                 jz      short loc_F8AB817C
 seg000:F8AB8172                 cmp     [esi], ebx
 seg000:F8AB8174                 jz      loc_F8AB8075
 seg000:F8AB817A                 jmp     short loc_F8AB8163
 

        Очевидно, что если пропатчить переход по адресу F8AB816C(Вообще то он может быть и другим, главное, что этот адрес высчитывается как #PF+16Ch. В программах, защищённых XProtector'ом 1.08 смещение будет 17A). Патчить ядро без специальных инструментов нельзя, но можно опять воспользоваться R0cmd. Для этого надо создать файл размером 1 байт и записать в него EB, а потом загрузить, используя команду /load. После восстановления sst и патча обработчика #PF становится возможна отладка с помощью Ring3 отладчиков, например Ollydbg. На softice рассчитывать не приходится, слишком уж много внимания уделили ему разработчики.

4. Восстановление импорта и нахождение OEP

        Теперь приступаем к достаточно традиционным вещам в распаковке - импорту и OEP. Для нахождения OEP будет описано целых два способа, но для одного из них требуется распалогать некоторой информацией, а именно адресом процедуры расшифровки блоков кода, который узнать пока не представляется возможным. Поэтому для начала способ такой: находим таблицу с переходниками на импортируемые функции, и вместо настоящих переходов записать туда переходы на функцию, которая скажем показывает в MessageBox'е адрес своего вызова. Очевидно, что этот адрес будет находится близко к OEP. Осталось только найти таблицу этих переходов и пропатчить её. С тем, чтобы найти таблицу, проблем не возникает, просто стоит просматривать код на предмет вызовов процедур и смотреть, куда он ведёт. Видно, что таблица переходов начинается по адресу 439850h и заканчивается на адресе 439AF0h А вот с тем, чтобы пропатчить, проблем гораздо больше. Способ такой, создаём процесс с замороженным потоком(флаг CREATE_SUSPENDED в соотв. параметре), затем используя API CreateRemoteThread с адресом удалённого потока равного адресу функции LoadLibraryA и параметром, равным имени внедряемой DLL загружаем эту самую внедряемую DLL в адресной пространство защищённой программы. Имя естесственно надо заранее прописать с помощью пары функций VirtualAllocEx и WriteProcessMemory. Всё это показано в исходнике. Теперь DLL будет получать уведомления о загрузке в адресное пространство процесса, создания потока, и.т.д. Самый такой очевидный способ - использовать многопоточность кода протектора, т.е. дождаться создания последнего потока, и во время обработки уведомления переделать всю эту таблицу переходов под свои нужды, но XProtector не так прост, он этого не позволяет(Если в таблице будут заметны изменения, то протектор изменит переходы ещё раз, одного изменения ему оказывается мало). Выход в том, чтобы всё это проделать сразу после заполнения этой таблицы, а ещё лучше во время её создания. Теперь этот код необходимо как-то найти. Это очень пригодится и при восстановлении импорта, т.к. другие возможности ну никак не подходят. Приведение кода переходников в порядок и поочерёдное сравнение с экспортируемыми функциями - слишком сложно, а вариант подсунуть вместо функций переходники вида push XXXXXXXX/ret не проходит, протектор это замечает и вырубается.

        Если присмотрется к адресам переходников, то можно заметить, что они находятся в выделенной виртуальной памяти, которую можно выделить функцией VirtualAlloc. Теперь дальше, в зависимости от версии Windows размер кода API функций разный, значит выделение памяти происходит непосредственно в процедуре создания импорта. Значит выход в том, чтобы отловить вызовы этой API. В принципе у протектора всё схвачено, патч API непосредственно kernel32.dll не поможет, т.к. она читается непосредственно с диска в память, обрабатывается, и уж оттуда вызываются функции. В ядро лезть слишком муторно, значит самое идеальное место для перехвата - Zw-функции. Вот как выглядит нужная нам функция ZwAllocateVirtualMemory(Надеюсь в Microsoft не обидятся, ведь запрещено исходники Windows выкладывать):

 .text:77F75832                 public ZwAllocateVirtualMemory
 .text:77F75832 ZwAllocateVirtualMemory proc near
 .text:77F75832                 mov     eax, 11h
 .text:77F75837                 mov     edx, 7FFE0300h
 .text:77F7583C                 call    edx
 .text:77F7583E                 retn    18h
 .text:77F7583E ZwAllocateVirtualMemory endp
 
Просто меняем инструкцию по адресу 77F75837 так, чтобы в регистре edx был адрес обработчика перехваченной процедуры, которая, скажем, собирает все адреса вызовы VirtualAlloc в массив. Получить адрес вызова VirtualAlloc можно, прочитав его из стека. Смещение относительно esp можно выяснить, отладив программу, вызывающую VirtualAlloc. В Windows XP SP1 это смещение равно 68h. При выгрузке DLL содержимое этого массива можно скинуть в файл, как и сделано в исходнике по восстановлению импорта.

        Адрес, который должен привлечь внимание - 7E0B0Eh, т.к. именно при возврате на этот адрес выделяется память, адреса которой очень напоминают адреса переходников. Именно от него и стоит копать всю процедуру. Здесь я приведу только самые интересные моменты в этой процедуре. Вот например небольшой кусок кода, который сохраняет смещение после опкода 0E9(long jmp):

 XPROT___:007E0C21 lStoreJumpToImportFunction:             ; CODE XREF: XPROT___:007E0C05
 XPROT___:007E0C21                                         ; XPROT___:007E0C10
 
   ;edi - адрес, по которому находится это самое смещение
 XPROT___:007E0C21                 mov     eax, [ebp+AddressOfImportFunction]
 XPROT___:007E0C27                 sub     eax, edi
 XPROT___:007E0C29                 sub     eax, 4
 XPROT___:007E0C2C                 stosd
 
Для начала нам этого хватит, меняем инструкцию mov eax, [ebp+AddressOfImportFunction] на что-то вроде mov eax, offset import_accepted. Менять надо после первого перехвата ZwAllocateVirtualMemory с искомым адресом возврата из VirtualAlloc. И опять нас ждёт сюрприз, а именно один нехороший MessageBox с сообщением "An Error has ocurred while loading imports". Конечно никакой ошибки быть не может, а вот проверка целостности кода процедуры очень даже может. Просматривая код создания процедуры дальше, меня привлёк следующий код:
 XPROT___:007E0737                 pusha
 XPROT___:007E0738                 lea     esi, [ebp+BeginProtectedArea] ;007E046F - начало процедуры
 XPROT___:007E073E                 lea     edi, [ebp+EndProtectedArea] ;007E12F9 - и конец процедуры
 XPROT___:007E0744                 sub     edi, esi
 XPROT___:007E0746                 mov     edx, edi ; edx - длина проверяемого кода
 XPROT___:007E0748                 mov     edi, [ebp+1D70619h]
 XPROT___:007E074E                 or      ecx, 0FFFFFFFFh
 XPROT___:007E0751
 XPROT___:007E0751 hash_loop:                             ; CODE XREF: XPROT___:007E0761
 XPROT___:007E0751                 xor     eax, eax
 XPROT___:007E0753                 mov     al, [esi]
 XPROT___:007E0755                 xor     al, cl
 XPROT___:007E0757                 inc     esi
 XPROT___:007E0758                 mov     eax, [edi+eax*4]
 XPROT___:007E075B                 shr     ecx, 8
 XPROT___:007E075E                 xor     ecx, eax
 XPROT___:007E0760                 dec     edx
 XPROT___:007E0761                 jnz     hash_loop
 XPROT___:007E0767                 mov     eax, ecx
 XPROT___:007E0769                 not     eax
 XPROT___:007E076B                 cmp     [ebp+ValidHashValue], eax ;Вот и сравнение..
 XPROT___:007E0771                 jz      loc_7E0781 ; ну а вот и условный переход.
 XPROT___:007E0777                 mov     dword ptr [ebp+1D71831h], 1
 XPROT___:007E0781
 XPROT___:007E0781 loc_7E0781:                             ; CODE XREF: XPROT___:007E0771
 XPROT___:007E0781                 popa
 
 
Сделанный безусловным переход по адресу 007E0771h разрешит любые модификации в процедуре создания импорта, что очень даже хорошо. Например это позволяет программе-примеру вывести на экран весьма издевательское окошко с надписью "dump me!!!" и заголовком OEP near 42A37Ah. И действительно OEP лежит совсем рядом, по адресу 42A373h. Но адрес, по которому передаётся управления после отработки кода протектора находится немного выше, это связано с защитой кода около OEP, подробности будут рассмотрены ниже, когда будет рассказываться о защите кода.

        Теперь займёмся получением адресов импортируемых функций, для того, чтобы можно было используя imprec, восстановить директорию импорта. Т.к. защищаются только функции kernel32, user32 и advapi32, то должна быть группа сравнений и переходов, которые надо найти и подправить. Думаю для этогол сойдёт такая группа:

 XPROT___:007E08A9 loc_7E08A9:                             ; CODE XREF: XPROT___:007E0828j
  ;Как на ладони лежат четыре перехода, правка которых приведёт к чистой таблице
  ;импорта и таблице переходов(вернее почти чистой, одну функцию всё же придёться распознать вручную,
  ;в данном случае эта функция - GetStartupInfoA
 XPROT___:007E08A9                 cmp     dword ptr [ebp+1D70A05h], 1
 XPROT___:007E08B0                 jz      lScrambled0
 XPROT___:007E08B6                 cmp     ecx, [ebp+BaseOfKernel32]
 XPROT___:007E08BC                 jz      lScrambled0
 XPROT___:007E08C2                 cmp     ecx, [ebp+BaseOfUser32]
 XPROT___:007E08C8                 jz      lScrambled0
 XPROT___:007E08CE                 cmp     ecx, [ebp+BaseOfAdvapi32]
 XPROT___:007E08D4                 jz      lScrambled0
 XPROT___:007E08DA
 XPROT___:007E08DA lNoScrambled:                           ; CODE XREF: XPROT___:007E08FD
 XPROT___:007E08DA                                         ; XPROT___:007E0922 ...
 XPROT___:007E08DA                 lea     ebx, [ebp+GetProcAddressX0]
 XPROT___:007E08E0                 call    ebx
 XPROT___:007E08E2                 mov     edi, eax
  ;Здесь всё просто, записывается правильный адрес функции и передаётся куда-то
  ;дальше - на создание перехода и элемента IAT. Управление должно попасть сюда
 XPROT___:007E08E4                 mov     [ebp+AddressOfImportFunction], eax
 XPROT___:007E08EA                 jmp     loc_7E0B4A
 
Заменив каждый переход шестью инструкциями nop мы добиваемся создания чистой таблицы импорта и переходов. Найти IAT здесь не трудно, стоит лишь посмотреть поле Base Of Data в PE заголовке, обычно таблица импорта там и лежит. Теперь наконец-то можно восстановить импорт, но неободимо также переориентировать переходы в таблице, чтобы они указывали на элементы IAT а не в пустоту. Для этого в архиве также лежит небольшая утилита. После всего этого можно сказать, что завершён первый этап распаковки, но XtremeProtector также позаботился о защите своего кода. Дальше придётся работать над тем, чтобы получить весь код в открытом виде.

5. Защита кода протектора, scrambled код

        Так как дольше пойдёт активное исследование кода протектора, то стоит немного рассказать о том, что мешает его исследованию. Прежде всего это scrambled-код, т.е. код, который сильно разбавлен мусором. Ниже привожу некоторые виды мусора:

 
push eXX push eXX ... sidt [esp - 2] ;Сохранённый адрес IDT не используется ... pop eXX ... pop eXX
push eax push edx ... rdtsc ;Считывание счётчика тактов процессора, также не используется ... pop edx ... pop eax
pusha ... popa
Также встречаются бессмысленные условные и безусловные переходы, инструкции вида mov eax, eax. Кроме scrambled-кода также имеются конструкции следующего вида:
 
push eXX call proc0 db 0XXh ;Мусорный байт jmp [esp - 0Ch] proc0: pop eXX mov [esp-8], eXX ;Здесь прибавляется смещение относительно "мусорного" байта, туда потом передаётся управление add [esp-8], 20h inc eXX ;Обход мусорного байта push eXX retn
Попробуйте потрассировать по F8 такой код в Softice и увидите кое-что странное, дальше инструкции jmp [esp-0Ch] пройти не получится, может кто сможет это объяснить?

Присутствует также и такой код(расшифровывающий впереди двойное слово):
 push    ebx
 push    edx
 push    edx ;Регистры естесственно могут быть и другие
 sidt    [esp - 2]
 
  ;Обычно в ebx получается 0Ch или 1Ch, что соответствует смещению параметров int1 или int3 в IDT
 mov     ebx, XXXXXXXX
 xor     ebx, XXXXXXXX ;Вместо xor может быть другая арифметическая инструкция
 pop     edx
 add     edx, ebx
  
  ;Ключ пишется прямо в IDT, всё равно отладочные прерывания вырублены
 mov     dword ptr [edx], XXXXXXXX
 mov     edx, [edx]
 call    $+5
 pop     ebx
 add     ebx, XX
 xor     [ebx], edx ;Расшифровка части кода впереди
  ...
 xor     [ebx], edx ;А вот и восстановление для возможности повторного исполнения кода
 push    0FFFFFFFFh
 
  ;Там адреса обработчиков int1 и int3 становятся равными передаваемому параметру
 call    proc0
 pop     edx
 pop     ebx
 
Этот код также разбавлен значительным количеством мусорных инструкций.

Также стоит упомянуть о двух видах ring0-дишифровщиков кода. Они слишком сложны, поэтому их код приводить не буду. Плагин для IDA в архиве позволяет заменять наиболее распространённый scrambled-код группой nop'ов, и поддерживает один из ring0-дешифровщиков. Он сильно облегчает исследование, ведь scrambled код встречается в процедурах, отвечающих за расшифровку кода, драйвере xprotector.sys, и много где ещё.

6. Полное восстановление кода программы

        Полученный в предыдущих шагах дамп даже не запускается, сразу выдаёт ошибку, причина которой - слишком много отсутствующего кода, который сейчас и будем восстанавливать. Первое, чем стоит занятся, так это подгрузить отсутсвующий код, участки которого явно видны в PE Tools в опции dump region. Что-то похожее на CopyMem II в Armadillo, но лишь с той разницей, что процесс один, и исключения обрабатывает код протектора. Ещё одно отличие от CopyMem II - участки кода расшифровываются при попытке выполнения, а не при попытке любого доступа к памяти. Способ расшифровки достаточно лёгок, передаём управление на такой участок памяти, а протектор после обработки исключения должен в любом случае вызвать функцию ZwContinue для продолженя выполнения. Первый и единственный параметр у неё - указатель на структуру CONTEXT. Чтобы управление не передалось дальше, стоит её просто перехватить тем же способом, что и ZwAllocateVirtualMemory при создании импорта и переориентировать EIP для последующего исполнения цикла. Полученные куски кода(все размером 2000h, т.е. две страницы) складывать в отведённую область памяти, и после окончания цикла просто разрешить доступ к памяти и скопировать их на место. Именно такой способ и используется в исходнике. Всё это проделывать надо, потому что XProtector при открытии одной пары страниц, может закрыть другую.

        Есть и ещё три конструкции для защиты кода, рассмотрим сначала сами конструкции, а потом способы получения кода.

 call    XXXXXXXX ;Адрес процедуры дешифровки, в данном случае - 7DC1BBh
 dd      0XXh ;Идентификатор потока, который всё это будет расшифровывать
 dd      0    ;0 - дешифровка, 1 - шифровка
 dd      0XXh ;длина кода
 db      06Fh ;мусорный байт
  ... - ;Далее зашифрованный код.
 
Завершающее звено может отличатся. Если зашифрованный код выполняется только один раз, то он затирается нулями:
 pusha
 call    $+5
 pop     edi
 sub     edi, 0XXh
 mov     ecx, 0XXh ;Здесь и выше 0XXh - длина зашифрованного кода, увеличенного на 1
 xor     eax, eax
 rep stosb
 popa
 
А вот если код исполняется многократно, то из такого затирания ничего хорошего не выйдет, поэтому завершение кода такое:
 call    XXXXXXXX ;Адрес процедуры шифрования кода - другой, здесь он равен 007C0D1Bh
 dd      0XXh
 dd      1
 dd      0XXh
 db      06Fh
 
Принцип действия этих процедур заключается в заполнении переменных данными о адресе, длине, и.т.д. Потом процедура вызывает функцию SetEvent и пускается в бесконечный цикл jmp eax. Расшифровывающий поток отрабатывает и передаёт управление на расшифрованный код посредством переходника. Теоритически опять же, чтобы изменить EIP в другом потоке необходим вызов SetThreadContext. Всё снова решает перехват native api, на этот раз ZwSetContextThread. Достаточно подменить EIP на свой переходник и продолжить цикл восстановления кода, т.к. он остаётся в открытом виде. Конструкции такие надо находить прямым поиском, лучше и не придумать. Также стоит предусмотреть чистку кода от вызовов и параметров шифровки/дешифровки кода и замещения его нулями. Теперь переходим далее.

        При рассмотрении процедуры создания импорта можно было заметить, что вместо функции wsprintfA ставится не реальный её адрес, и даже не scrambled-переходник, а какой-то подозрительный код. А в самом коде программы встречаются не менее подозрительные вызовы wsprintfA со странными параметрами. Работу этой функции можно понаблюдать под отладчиком, или, что легче, под эмулятором x86 в IDA. Приводить здесь подробности нет смысла, т.к. расшифровать участки кода, открываемые этой функцией просто элементарно - передаёшь управление на такой вызов и меняешь адрес возврата. Вызовы эти также необходимо подчистить, как открывающий вызов:

 push    78263845h
  ;Все XX - параметры расшифровки
 push    XX
 push    0 ;0 - открывающий вызов, расшифровка кода
 push    XXXXXXXX
 push    XXXXXXXX
  ;78263845h - это константа('x&8E' или 'E8&x'), по ней
  ;обработчик wsprintfA узнаёт, что надо делать - переходить на реальный wsprintfA
  ;или расшифровывать код
 push    78263845h
 call    XXXXXXXX ;Адрес обработчика wsprintfA(точнее адрес перехода jmp на него)
 
так и закрывающий:
 push    78263845h
 push    XX
 push    1 ;1 - закрывающий вызов, шифрование кода
 push    XXXXXXXX
 push    XXXXXXXX
 push    78263845h
 call    XXXXXXXX
 
И наконец последний самый сложный этап - восстановление краденого кода, причём его крадут не на OEP, а по всей секции кода. Делается такое при помощи sdk XProtectora для защиты каких-нибудь важных участков кода, скажем проверки введённого пароля.

        Все эти защиты кода обнаруживаются при попытке запуска распакованной программы с отрезанной секцией XPROT. Вот например попытаемся открыть файл для защиты, программа вылетит на адресе 435569h на неправильном опкоде. Вот общая структура такой защиты кода:

 jmp     invalid_opcode
 db      "xpro" Это просто сигнатура
 dd      0 0 - начало, 1 - окончание.
 dd      0
 db      "xpro"
  invalid_opcode:
 ... Любой опкод, вызывающий шестое исключение(invalid opcode)
 
В конце этого участка стоит то же самое, только с единицей вместо нуля. Для получения кода придёться посмотреть, куда же попадает управление после обработки исключения. Оказывается, что EIP меняется, и управление передаётся куда-то далеко в виртуальную память. Посмотрим, что же там такое. Начало не представляет собой ничего интересного, просто ожидание появления двойки по адресу 2F103CCh. Далее начинается расшифровка лежащего впереди кода(приведённый ниже код обработан плагином из архива):
 seg007:02F10039                 call    loc_2F1004F ;Начало расшифровки
 seg007:02F10039 ; ---------------------------------------------------------------------------
 seg007:02F1003E                 db 75h, 0Ah, 7Bh, 98h, 0F1h, 0D6h, 57h, 44h, 2Dh, 62h
 seg007:02F1003E                 db 0F3h, 0B0h, 29h, 0AEh, 4Fh, 0DCh, 0E5h
 seg007:02F1004F ; ---------------------------------------------------------------------------
 seg007:02F1004F
 seg007:02F1004F loc_2F1004F:                            ; CODE XREF: seg007:02F10039
 seg007:02F1004F                 push    edi
 seg007:02F10050                 jmp     loc_2F1005C
 seg007:02F10050 ; ---------------------------------------------------------------------------
 seg007:02F10055                 db 0E3h, 0E0h, 99h, 5Eh, 3Fh, 0Ch, 55h
 seg007:02F1005C ; ---------------------------------------------------------------------------
 seg007:02F1005C
 seg007:02F1005C loc_2F1005C:                            ; CODE XREF: seg007:02F10050
 seg007:02F1005C                 pop     ebx
 seg007:02F1005D                 pop     edx
 seg007:02F1005E                 jmp     loc_2F10069
 seg007:02F1005E ; ---------------------------------------------------------------------------
 seg007:02F10063                 db 0E6h, 27h, 0D4h, 7Dh, 72h, 0C3h
 seg007:02F10069 ; ---------------------------------------------------------------------------
 seg007:02F10069
 seg007:02F10069 loc_2F10069:                            ; CODE XREF: seg007:02F1005E
 
  ;В стеке лежит адрес возврата после выполнения инструкции call loc_2F1004F
 seg007:02F10069                 add     edx, 1F1h
 seg007:02F10069 ; ---------------------------------------------------------------------------
 seg007:02F1006F                 db 13h dup(90h)
 seg007:02F10082 ; ---------------------------------------------------------------------------
 seg007:02F10082                 mov     esi, 2Fh ; '/'
 seg007:02F10087                 mov     bl, dh
 seg007:02F10089
 seg007:02F10089 loc_2F10089:                            ; CODE XREF: seg007:02F1016Bj
 seg007:02F10089                 push    dword ptr [edx]
 seg007:02F1008B                 and     ebx, 1B9E6815h
 seg007:02F10091                 pop     ecx ;В ecx - зашифрованное двойное слово
 seg007:02F10092                 mov     ebx, 67DD3A64h
 seg007:02F10097                 add     ecx, 99117BDh ;Действие 1
 seg007:02F1009D                 sbb     bx, 8085h
 seg007:02F100A2                 push    edi
 seg007:02F100A3                 sub     bh, 83h ; 'Г'
 seg007:02F100A6                 pop     ebx
 seg007:02F100A7                 add     ecx, 2130E1B2h ;Действие 2
 seg007:02F100AD                 mov     ebx, 3EADBF30h
 seg007:02F100B2                 add     ecx, 48408503h ;Действие 3
 seg007:02F100B8                 jmp     loc_2F100D1
 seg007:02F100B8 ; ---------------------------------------------------------------------------
 seg007:02F100BD                 db 5Ch, 65h, 3Ah, 0EBh, 48h, 0E1h, 6, 0C7h, 0F4h, 1Dh
 seg007:02F100BD                 db 92h, 63h, 60h, 19h, 0DEh, 0BFh, 8Ch, 0D5h, 0EAh, 0DBh
 seg007:02F100D1 ; ---------------------------------------------------------------------------
 seg007:02F100D1
 seg007:02F100D1 loc_2F100D1:                            ; CODE XREF: seg007:02F100B8j
 seg007:02F100D1                 mov     ebx, 40853042h
 seg007:02F100D6                 push    ecx
 seg007:02F100D7                 jmp     loc_2F100EF
 seg007:02F100D7 ; ---------------------------------------------------------------------------
 seg007:02F100DC                 db 0AFh, 0BCh, 45h, 9Ah, 0CBh, 0A8h, 0C1h, 66h, 0A7h, 54h
 seg007:02F100DC                 db 0FDh, 0F2h, 43h, 0C0h, 0F9h, 3Eh, 9Fh, 0ECh, 0B5h
 seg007:02F100EF ; ---------------------------------------------------------------------------
 seg007:02F100EF
 seg007:02F100EF loc_2F100EF:                            ; CODE XREF: seg007:02F100D7j
 seg007:02F100EF                 pop     dword ptr [edx] ;Запись на место 4-х расшифрованных байт
 seg007:02F100F1                 sbb     bx, 1D97h
 seg007:02F100F6                 sub     edx, 2
 seg007:02F100F9                 pusha
 seg007:02F100F9 ; ---------------------------------------------------------------------------
 seg007:02F100FA byte_2F100FA    db 16h dup(90h)
 seg007:02F10110 ; ---------------------------------------------------------------------------
 seg007:02F10110                 mov     bh, 31h ; '1'
 seg007:02F10112                 jmp     loc_2F1011F
 seg007:02F10112 ; ---------------------------------------------------------------------------
 seg007:02F10117                 db 50h, 49h, 4Eh, 6Fh, 7Ch, 5, 5Ah, 8Bh
 seg007:02F1011F ; ---------------------------------------------------------------------------
 seg007:02F1011F
 seg007:02F1011F loc_2F1011F:                            ; CODE XREF: seg007:02F10112j
 seg007:02F1011F                 popa
 seg007:02F10120                 dec     edx
 seg007:02F10121                 dec     edx ;Уменьшение адреса на 4(sub edx, 2;dec edx; dec edx)
 seg007:02F10122                 js      loc_2F1012E
 seg007:02F10128
 seg007:02F10128 loc_2F10128:
 seg007:02F10128                 xor     ebx, 72129B9h
 seg007:02F1012E
 seg007:02F1012E loc_2F1012E:                            ; CODE XREF: seg007:02F10122j
 seg007:02F1012E                 sub     esi, 1
 seg007:02F10131                 jnz     loc_2F1014D ;esi - счётчик
 seg007:02F10137
 seg007:02F10137 loc_2F10137:
 seg007:02F10137                 jmp     loc_2F10177 ;Переход на расшифрованный код
 
Такие расшифровки кода встречаются очень часто в самом коде протектора. Чтобы расшифровать, очень удобно использовать эмулятор x86. Ставим его на начало(02F10039), а на адрес 02F10137 Можно поставить брэйкпоинт и запустить эмулятор командой run, или же поставить на этот адрес курсор и дать команду run to cursor. Код впереди расшифруется без проблем. Теперь можно посмотреть его дальше. Очень интересно выглядит команда по адресу 02F101C5 - вызов подпроцедуры почти в начале страницы. Посмотрим на эту процедуру:
 seg007:02F10004                 pop     ebp
 seg007:02F10005                 call    $+5
 seg007:02F1000A                 pop     eax
 seg007:02F1000B                 push    eax
 seg007:02F1000C                 sub     eax, 0Ah
 
  ;Читаем смещение украденного кода от адреса возврата из данной процедуры
 seg007:02F10011                 mov     eax, [eax]
 seg007:02F10013                 add     ebp, eax
 seg007:02F10015                 pop     eax
 seg007:02F10016                 add     eax, 16h
  ;И записываем адрес как аргумент команды push по адресу 02F10020
 seg007:02F1001B                 mov     [eax+1], ebp
 seg007:02F1001E                 popf
 seg007:02F1001F                 popa
 seg007:02F10020                 push    2F101CAh
 seg007:02F10025                 retn ;Собственно переход на краденный код
 
Теперь значит так, все процедуры такого типа одинаковы, то можно проходить по коду, вносить изменения в процедуру в начале страницы. Только так просто тут не внести, адрес перехода(аргумент команды push) каждый раз перезаписывается. Но можно воспользоваться другим обстоятельством, после инструкции ret мы имеем корректный контекст потока, т.е. все значения регистров и стека такие же, как до возникновения исключения, иначе краденный код выполнялся бы неправильно. Если убрать команду push по адресу 02F10020, то команда retn передаст управление на адрес, который находится в стеке при возникновении исключения. Используя эти сведения, просто меняем опкод команды push imm32 на mov eax, imm32. А при обработке перехваченной ZwContinue добавим в стек адрес возврата, который возвратит нас на цикл, перебирающий такие защитные конструкции, причём после возврата в регистре EAX получим адрес краденного кода! Вот таким простым изменением всё решается, и не надо ничего расшифровывать и восстанавливать краденный код вручную. Осталось только выяснить его длину. Это просто, т.к. после кода следуют такие инструкции:
 seg007:02F10225                 push    eax
 seg007:02F10226                 push    eax
 seg007:02F10227                 mov     eax, 355C4h
  ;Смещение относительно ImageBase. Далее формируется адрес, куда
  подадает управление после отработки краденного кода
 seg007:02F1022C                 mov     [esp+4], eax
 seg007:02F10230                 pop     eax
 
Дальше код, который зашифровывался ранее зашифровывается обратно, это для повторного использования кода. А ещё дальше видим вот это:
 seg007:02F10388                 call    near ptr byte_2F10394
 seg007:02F10388 ; ---------------------------------------------------------------------------
 seg007:02F1038D                 db 83h, 0, 39h, 7Eh, 0DFh, 2Ch, 0F5h
 seg007:02F10394 byte_2F10394    db 1Ah dup(90h)         ; CODE XREF: seg007:02F10388p
 seg007:02F103AE ; ---------------------------------------------------------------------------
 seg007:02F103AE                 pop     esi
 seg007:02F103AF                 call    $+5
 seg007:02F103B4                 pop     ebp
 seg007:02F103B5                 add     ebp, 18h ;ebp = 2F103CCh
   ;Запись единицы по адресу 2F103CCh - элемент синхронизации
 seg007:02F103BB                 mov     dword ptr [ebp+0], 1
 seg007:02F103C2                 mov     eax, [ebp+4]
   ;Запись в стек адреса возврата
 seg007:02F103C5                 add     [esp+24h], eax
 seg007:02F103C9                 popf
 seg007:02F103CA                 popa
    ;Ну и собственно возврат
 seg007:02F103CB                 retn
 seg007:02F103CB ; ---------------------------------------------------------------------------
 seg007:02F103CC                 dd 2
 seg007:02F103D0                 dd 400000h
 seg007:02F103D4                 db 38h ; 8
 
Проблема в том, что придётся повторить действия этого кода(записать 1 по адресу 2F103CCh), иначе цикл обламается из-за отсутствия синхронизации.Также во всём этом присутсвует одна странность, последний такой краденный блок восстанавливается некорректно(адрес 00436086), придётся его восстановить отдельно, например при следующем запуске, и заменить кусок в дампе. Специально для этого в исходнике предусмотрена возможность записи в log-файл всех адресов, откуда спёрли код. Нужно просмотреть все адреса, и если где-то код выглядит неправильно, то нужно начать поиск не сначала секции кода, а с того адреса из файла. При распаковке XProtector'а как 1.07, так и 1.08 неправильно восстанавливается только последний краденный блок. Если наконец все блоки восстановлены правильно, та распаковка завершена.

7. Определение присутствия XProtector'а в памяти и его устранение

        Теперь с виду программа функционирует нормально, но стоит запустить справку, как она вылетит. Причина этому - не защита кода, а прямой call на переходник в обход IAT и таблицы переходов. К счастью таких всего 4, все они сосредоточены в одном месте. Их адреса - 439B59h, 439BA5h, 439BC7h и 439BD7h. Чтобы посмотреть, что это за функции, достаточно снять дамп, где была исправлена таблица импорта, там они будут направлены непосредственно на API функции. Можно открыть в IDA библиотеки kernel32.dll и advapi32.dll и посмотреть, что это за функции. А можно это сделать в отладчике. Кстати, можно заметить, что перед каждым call'ом или после стоит инструкция nop, это значит, что на этом месте стояла 6-байтовая команда, т.е. call mem32. Итак, функции следующие:

439B59h - GetProcAddress
439BA5h - RegOpenKeyExA
439BC7h - RegQueryValueExA
439BD7h - RegCloseKey

Теперь находя эти функции в директории импорта правим инструкции. Справка стала открываться, да и вообще программа функционирует полностью, только вот неправильно. Выражается это в том, что упакованные распакованным XProtector'ом программы не работают. Эта защита от распаковки оказывается на самом деле едва ли не самой мощной, т.к. приходится очень активно тестировать распакованную программу, и если найдена ошибка, то приходится реверсить уже не протектор и не защиту программы, а основной её код. Мало того, здесь ошибка проявляется даже не в самой программе, что ещё более усложняет дело. Исследование в данном случае слишком муторно, поэтому я приведу его в сильно укороченном варианте.

        Упакованная программа вылетает на интсрукции, использующей регистр ebp в качестве смещения. Причём смещение не изменилось. Взгляните на адрес 401A6Eh в распакованной программе, вы увидете код XProtecor'а без scrambled кода, и главное со смещениями относительно того, где код сейчас и лежит. Т.е. при упаковке смещения должны изменятся, а они не меняются. При упаковке XProtector поддерживает специальную таблицу, где хранится адрес инструкции, длина инструкции, и.т.д. В какой-то момент таблица эта должна обрабатываться и заполняться правильными смещениями. Начало таблицы находится по адресу 45295Eh. С помощью xref'ов можно просмотреть всё, что делается с таблицей, и процедура по адресу 42BB7Fh как раз то, что надо. Там происходит проверка элемента таблицы, вычисление смещения и запись его не место(в готовую инструкцию, которая помещается в упакованный exe). Причём запись выглядит очень интересно:

 CODE____:0042BC16 loc_42BC16:                             ; CODE XREF: accept_relocations+78
 CODE____:0042BC16                 lea     esi, ds:4511CEh
 CODE____:0042BC1C                 sub     esi, 0Eh ;esi = 4511C0h
  
   ;А по этому адресу лежит адрес, принадлежащий стеку, очень интересно..
 CODE____:0042BC1F                 mov     esi, [esi]
 CODE____:0042BC21                 cmp     dword ptr [esi+4], 1
 
   ;И если там в стеке не единица, то изменения смещений не сохраняются!
 CODE____:0042BC25                 jnz     short loc_42BC2B
 CODE____:0042BC27                 mov     [ecx+eax+1], ebx
 CODE____:0042BC2B
 CODE____:0042BC2B loc_42BC2B:                             ; CODE XREF: accept_relocations+A6
   ;Далее идёт затирание элемента таблицы
 CODE____:0042BC2B                 mov     eax, 0FFFFFFFFh
 CODE____:0042BC30                 mov     ecx, 5
 CODE____:0042BC35                 rep stosd
 CODE____:0042BC37                 jmp     loc_42BBAF
 
Во как, значит XProtector засунул в стек единицу, записал в секцию данных адрес и проверяет его. Проверка xref'ов на 4511C0h даёт ещё несколько похожих ситуаций. Правка переходов тут не поможет, поэтому делаем так: пишем по адресу 4511C0h значение 4511C0h, а по следующему DWORD'у записываем единицу. Т.к. следующий DWORD тоже отличается по время выполнения(значение 80000002h) то его тоже дописываем после единицы для подстраховки. И наконец получаем полностью работоспособную распакованную программу, работа закончена!

        Конечно пересказанное выше исследование малопонятно и трудоёмко, поэтому предлагаю другой способ: остановить упакованную программу на OEP и посмотреть, чем отличается стек. Если отличия явные, как в данном случае, то стоит поискать по exe-файлу адрес стека, где содержатся различия. После этого можно подправить распакованный файл. Также в дополнение ко всему этому можно сказать, что в XProtector 1.08 эту лазейку с таблицей смещений вообще убрали и ставнение стеков и поиск адреса по файлу, где найдены различия остаётся единственным.

8. Аналитика(окончание)

        Итак, после преодоления защит всех уровней, можно сказать, что XtremeProtector буквально во всём превосходит другие протекторы. Трудно найти такую защиту, где настолько сложно было снять дамп и восстановить секцию кода(Starforce не в счёт), восстановить импорт, а особенно найти обнаружение упаковщика в памяти. У XProtector'а очень большие перспективы развития, интересно, как будет выглядеть следующая версия протектора? Остануться ли там хоть какие-нибудь лазейки? Ведь именно на поиске лазеек и строися вся распаковка. Представьте, как бы пришлось мучаться с импортом, если бы нельзя было найти процедуру его создания простым перехватом API. Аналогично и с восстановлением кода. И если бы не было такой возможности, то основным и единственным был метод исследования защиты в лоб. Это всё можно сравнить с возможностями подбора ключа в криптографии. Его можно подобрать перебором всех вариантов, или что лучше и быстрее, использованием какой-либо уязвимостью в алгоритме. Спасает только то, что защитить программу, не оставив какой-либо возможности для быстрого взлома практически невозможно. Авторы XtremeProtector'а попытались это сделать и им это почти удалось.

        И в заключение хочется сказать, что на такую защиту, как XtremeProtector не жалко тратить время, ведь можно узнать много нового, как про новые способы защиты, так и про внутреннее устройство Windows, а это может очень пригодится.

Архив с инструментами и исходниками здесь
dragon, c_dragon@mail.ru
14 августа 2004 г.

Обсуждение статьи: Экстримальная защита в виде XtremeProtector 1.07 >>>


Комментарии к статье: Экстримальная защита в виде XtremeProtector 1.07

gloom 15.08.2004 10:58:15
Это просто невероятно! Нет слов... Dragon - Вы бог распаковки. Я в шоке. :|
З.Ы. Оценка: 10!
---
sanniassin 15.08.2004 11:41:44
Как найти адресс #PF ?
ЗЫ: присоединяюсь к отзывам gloom`а :-)
---
MoonShiner 15.08.2004 16:15:09
Молодец, dragon, я в шоке! Молодчина.
---
ilya[CRACKL@B] 15.08.2004 17:30:51
молодец dragon,интересная статья
оценка 5+

---
The Monk of Mind 15.08.2004 17:58:02
Да... Молодец! Это самое лучше из того что я когда-либо видел. Ты гордость русской краксцены... Думаю другие поддержат моё мнение.
---
newborn 16.08.2004 10:50:59
Просто нет слов : однозначно оценка 5
---
Demode 16.08.2004 11:34:11
Ну что сказать?! Все уже сказано ...
Пора сворачить компы. . и искать другие развлечения .!
Одно слово, молодец!

Оценка - в шкале не предусмотрена!!!
---
DFC 16.08.2004 16:27:33
Dragon, класс! :) Так держать.
---
Nothing 16.08.2004 17:02:32
Как всегда без рабочего примера
---
sunman 24.12.2004 16:46:19
Клево написано, без излишнего разжевывания но и без пальцовок...
---
QIce 29.12.2004 15:21:42
Applauses. The best.
---
Ruptor 10.01.2005 08:21:08
Nu vsio, mnie teper tochno mozhno na pensiyu uhodit. Yest komu prodolzhat nashe delo. Tak derzhat, dragon! Zahodi v gosti na #ucl\’00.
---
The Force 22.07.2005 01:30:53
Да, я согласен - сейчас очень развита crack-сцена, а такие люди как Вы - ее гордость.
---

Материалы находятся на сайте https://exelab.ru



Оригинальный DVD-ROM: eXeL@B DVD !


Вы находитесь на EXELAB.rU
Проект ReactOS