Статья Entry Point – дело о пропавшем индексе (часть 2)

Marylin

Marylin

Mod.Assembler
Red Team
05.06.2019
131
325
Вернёмся к первой части и вспомним, о чём тут вообще идёт речь..
Статья рассматривает, как при запуске приложения передать бразды фиктивной точке-входа EntryPoint. При этом РЕ-заголовок дискового файла не модифицируется, что пускает по ложному следу софт реверс-инженеринга типа анализаторы, дизассемблеры и отладчики. Акцент сделан на то, чтобы TLS-callback процедурой вмешаться в работу загрузчика LDR и перехватить точку-входа именно в стеке, а не в РЕ-заголовке файла, как это делают многие.

Загрузчик использует стек программы в качестве пусковой установки. Координаты цели в виде точки-входа, он берёт из нашего заголовка по смещению PE.28h, и дальше функцией ZwContinue() передаёт на неё управление. Наша задача как диверсантов выбрать подходящий момент и пока функция ZwContinue() ждёт от загрузчика команды "пуск", подставить в стек левый EntryPoint, захватив таким образом цель в свою пользу. Что примечательно – мастдай не может этому противостоять, поскольку стек всегда доступен на запись.

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


В этой части..

Будем считать, что изменили точку-входа в программу, что дальше?
Теперь нужно придумать, на какой адрес и зачем передавать управление. Здесь самое время подключить фантазию, которую ограничивает только наш опыт в программировании скилл. Например, если хотим защитить прожку от отладчиков типа OllyDbg, нужно передать управление на участок кода, который до оригинальной точки будет искать Олю и одним из способов признаваться ей в "любви". Если-же мы пытаемся скрыть свою тушку от дизассемблеров (что гораздо трудней), то выход один – шифровать всё, или только критически-важный участок тела.

Чтобы не разделять читателей на два этих лагеря, мы устроим файлу полный тюннинг, припарковав к нему защиту и от отладки, и от дизасма. Программа будет запрашивать пароль, и по результатам проверки выводить или "OK", или посылать нас.. нет не туда, а читать книжки по реверсу. Условимся, что точку-входа будем сбрасывать в нуль, в результате чего управление примет ImageBase. Тогда алгоритм программы может быть примерно таким:
  1. Зашифровать процедуру проверки пароля
  2. Поместить декриптор процедуры (1) в ImageBase
  3. Создать TLS-callback функцию в коде
  4. Перехватить в стеке оригинальную точку-входа Callback'ом
  5. Фиктивной точкой обозначить ImageBase
  6. Проверить этим-же Callback'ом наличие отладчика
  7. Если обнаружим отладчик, передаём управление сразу на Original ePoint (OEP)
  8. Если отладчика нет, то расшифровываем процедуру (1) и передаём на неё управление.
Заслуживающим внимание здесь можно считать пункт 7.
Значит если мы обнаруживаем факт отладки, то прямо из коллбэка передаём управление на оригинальную точку-входа, пропуская процесс расшифровки критически-важного участка кода с проверкой пароля. В результате отладчик нарвётся на зашифрованный код и будет пыхтель над ним, вплоть до какого-нибудь исключения. Зато если отладчика нет, то по фиктивной точке-входа, управление примет декриптор в ImageBase, который расшифрует процедуру проверки пароля, и она сделает своё благое дело.

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


ImageBase как EntryPoint

Откроем наш crackme в любом хек-редакторе (у меня HxD) и посмотрим на его облик..
Зайдя в будуар базы, сразу натыкаемся на 40h-байтный DOS-заголовок, за которым следует заглушка Stub, озадаченая выводом мессаги "Этот файл не для MS-DOS". Всё это досовское барахло можно смело затереть нулями и файл от этого никак не пострадает. Освободившееся пространство будем использовать для своей фиктивной точки-входа.

Правда нужно оставить два важных поля, без которых исполняемый файл просто не запустится – это сигнатура Марка Збиковски "MZ" по-смещению Base+00, и указатель на РЕ-заголовок по-смещению Base+3Ch. Рисунок ниже демонстрирует сказанное:

Entry Point – дело о пропавшем индексе (часть 2)


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

Если управление принимает ImageBase, то процессор сразу наткнётся на пару байт 4D5Ah (см.рис.выше), после которых ровным строем идёт болото нулей. Можно-ли трактовать значение 4D5Ah как инструкцию процессора, или эта пара вызовет исключение? Чтобы разобраться, загружаем файл-кастрат в отладчик и комбинацией Ctrl+G переходим на адрес ImageBase, который в данном случае равен 0х00400000 (дефолтная база пользовательских программ):

Entry Point – дело о пропавшем индексе (часть 2)


Так-так.. в верхнем окне видим вполне нормальные инструкции DEC_EBP и POP_EDX, которые на самом деле являются сигнатурой "MZ". По умолчанию это данные, но мы сделали из них код. Ещё один важный момент – это сбитый на входе стек. Чтобы выровнить его после POP_EDX, "запушим" этот EDX обратно с инкрементом EBP, и тогда наша совесть будет чиста.

Теперь, в диапазон от сигнатуры MZ и до указателя на РЕ-заголовок нужно повесить декриптор, который будет расшифровывать процедуру проверки пароля в секции-кода нашей программы. Значит нужно написать сначала саму процедуру пароля, и к её адресу потом цеплять декриптор. Здесь есть несколько вариантов и для наглядности я выбрал самый простой – вот пример:

C-подобный:
format   pe console
include  'win32ax.inc'
entry    start
;//------
.data
capt     db   13,10,' Demo crackme v0.1'
         db   13,10,' *********************************'
         db   13,10,' Type password..: ',0
frmt     db   '%s',0
pass     rb   64                       ;// резерв под юзерский пароль
;//------
.code
start:
        cinvoke   printf,capt          ;// запрос на ввод..
        cinvoke   scanf,frmt,pass      ;// принимаем пасс от юзера

;//*** Критический участок кода, который будем шифровать ********************
;//**************************************************************************
@crypt:  call     @check                ;// пропускаем строку ниже          *
         db      'codeby.net'           ;// валидный пароль                 *
mOk      db      ' PASS OK! thanks.',0  ;// строки валидации                *
mWrong   db      ' WRONG password!' ,0  ;//   ..^^^                         *
@check:  pop      edi                   ;// EDI = адрес валидного пароля    *
         mov      esi,pass              ;// ESI = адрес пароля юзера        *
         mov      ecx,10                ;// длина валидного пассворда       *
         repe     cmpsb                 ;// сравнить их!                    *
         jcxz     @pass_OK              ;// если ECX = 0 (jump cx zero)     *
        cinvoke   printf,mWrong         ;// иначе: выводим WRONG            *
         jmp      @exit                 ;//   ..и на выход.                 *
@pass_OK:                               ;// пароль совпал по всей длине ECX *
        cinvoke   printf,mOk            ;// ОК!                             *
@end_crypt:                             ;//==== шифровать до этой метки     *
;//*** Конец критичекого блока **********************************************
;//**************************************************************************

@exit:  cinvoke   scanf,frmt,capt      ;// ждём клаву..
         invoke   exit,0               ;

;//=== Секция импорта =======================
data     import
library  msvcrt, 'msvcrt.dll'
import   msvcrt, printf,'printf',scanf,'scanf',exit,'exit'
end      data
Entry Point – дело о пропавшем индексе (часть 2)


Здесь видно, что правильный пасс и выводимые строки хранятся внутри зашифрованного участка кода, что позволило скрыть их от посторонних глаз. Адрес валидного пароля мы получаем через инструкцию CALL, которая перед тем как передать управление на свой операнд, сохраняет в стеке адрес-возврата для заканчивающей вызов, инструкции RET.

Так сложились звёзды, что адрес-возврата будет указывать как-раз на начало строки с валидным пассвордом, поэтому мы снимает его со-стека в регитр EDI и используем как неявный второй операнд для инструкции сравнения строк CMPSB (она сравнивает esi с edi по длинне ecx). Такую технику вычисления адресов назвали "получением дельта-смещения" и она широко используется малварью всех поколений:

C-подобный:
@crypt:  call     @check                ;// пропускаем строку ниже
         db      'codeby.net'           ;// валидный пароль
mOk      db      ' PASS OK! thanks.',0  ;// строки валидации
mWrong   db      ' WRONG password!' ,0  ;//   ..^^^
@check:  pop      edi                   ;// EDI = адрес валидного пароля
         mov      esi,pass              ;// ESI = адрес пароля юзера
         mov      ecx,10                ;// длина валидного пассворда
         repe     cmpsb                 ;// сравнить их!
Теперь у нас есть "процедура проверки пароля" и нужно прицепить к ней декриптор (саму проверку шифровать будем позже). Поскольку декриптор придётся переносить в область ImageBase, оформим его как shell. Для этого просто поместим код декриптора в секцию-данных нашей программы и компилятор ассемблера сам выстроит из него цепочку байт с опкодами инструкций. Дальше хек-редактором вырежем эту цепочку из .data-секции, и поместим её по адресу ImageBase.

Здесь нужно учитывать, что в секции-данных есть и полезные переменные со-строками, для которых трассер компилятора уже сформировал адреса. Поэтому код декриптора кидаем в самый конец секции, что позволит безболезненно для окружающих, потом изъять его от туда. Значит добавляем в секцию-данных предыдущего кода такие строки:

C-подобный:
format   pe console
include  'win32ax.inc'
entry    start
;//------
.data
capt     db   13,10,' Demo crackme v0.1'
         db   13,10,' *********************************'
         db   13,10,' Type password..: '   ,0
frmt     db   '%s',0
pass     rb   64

align    16                         ;// выравнивание на 16-байт (параграф)
         db   16 dup(0xff)          ;// маркер начала блока
;//~~~ Шелл-код декриптора ~~~~~~~~~8<~~~~~~~~~~~~~~~8<~~~~~~~~~~~~~~~~~~~~~
;//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
         xchg     eax,edx                    ;// восстанавливаем стек,
         inc      ebp                        ;// ..и регистр EBP,
         push     eax                        ;//   ..от глюков сигнатуры "MZ"

         mov      ecx,@end_crypt - @crypt    ;// ECX = длина шифруемого блока
         shr      ecx,1                      ;// разделить её на 2 (в словах)
         mov      esi,@crypt                 ;// ESI = адрес начала блока
decrypt: xor      word[esi],3e97h            ;// ксорим слово из ESI ключом
         add      esi,2                      ;// следующее слово..
         loop     decrypt                    ;// промотать цикл ECX-раз!
         push     start                      ;// куда передавать управление
         ret                                 ;// перейти по адресу в стеке!!!
;//~~~ Вырезать будем до сюда ~~~~~~>8~~~~~~~~~~~~~~>8~~~~~~~~~~~~~~~~~~~~~~
         db   16 dup(0xff)          ;// маркер хвоста блока
;//------
.code
start:
        cinvoke   printf,capt          ;// шапка
        cinvoke   scanf,frmt,pass      ;// принимаем пасс от юзера

;//*** Критический участок кода, который будем шифровать ********************
;//**************************************************************************
@crypt:  call     @check                ;// пропускаем строку ниже          *
         db      'codeby.net'           ;// валидный пароль                 *
mOk      db      ' PASS OK! thanks.',0  ;// строки валидации                *
mWrong   db      ' WRONG password!' ,0  ;//   ..^^^                         *
@check:  pop      edi                   ;// EDI = адрес валидного пароля    *
         mov      esi,pass              ;// ESI = адрес пароля юзера        *
         mov      ecx,10                ;// длина валидного пассворда       *
         repe     cmpsb                 ;// сравнить их!                    *
         jcxz     @pass_OK              ;// если ECX = 0 (jump cx zero)     *
        cinvoke   printf,mWrong         ;// иначе: выводим WRONG            *
         jmp      @exit                 ;//   ..и на выход.                 *
@pass_OK:                               ;// пароль совпал по всей длине ECX *
        cinvoke   printf,mOk            ;// ОК!                             *
@end_crypt:                             ;//==== шифровать до этой метки     *
;//*** Конец критичекого блока **********************************************
;//**************************************************************************

@exit:  cinvoke   scanf,frmt,capt     ;// ждём нажатие клавиши..
         invoke   exit,0               ;

;//=== Секция импорта =======================
data     import
library  msvcrt, 'msvcrt.dll'
import   msvcrt, printf,'printf',scanf,'scanf',exit,'exit'
end      data
Entry Point – дело о пропавшем индексе (часть 2)


Разберём детали этого кода...
Значит в нижнем окне секции-данных, среди текстовых строк видим маркеры начала и конца шелла, в виде цепочки байт со-значением 0хFF – это чтобы визуально выделить полезную нагрузку Payload. Сам шелл в красном блоке занимает всего 1Fh байт и вполне влезет в пустую область ImageBase, где мы приготовили для него место. Можно было оптимизировать код, например убрав из четвёртой инструкции 0хВ960000000 три байта нулей в хвосте, но в демонстрационном примере это не важно – у нас в ImageBase места предостаточно.

Интересным моментом являются опкоды переходов, например той-же инструкции цикла LOOP (см.верхнее окно). На этапе компиляции, вычисляющий адреса трассировщик ассемблера подставляет в код не абсолютный, а относительный адрес – в данном случае это опкод 0xE2F6. Второй байт со-значением 0xF6 это и есть адрес относительно текущего – он кодируется одним знаковым байтом. Диапазон этого байта от нуля и до 0x7F означает переход "вперёд", а верхняя половина от 0xFF и до 0х80 – это переход "назад" (на указанное кол-во байт). Здесь мы видим значение 0xF6, значит это FF-F6=9 байт назад от текущего адреса.

Другими словами, если шелл-код использует переходы в пределах 127 байт (0х80), то компилятор вставит относительный адрес – значит после компиляции шелл можно вырезать и переместить в любое место. В противном случае (переход за пределы 127 байт), компилятор вставит уже абсолютный адрес, и двигать такой шелл со-своей позиции нельзя – вся конструкция рухнет. Если нужны дальние FAR-переходы, то можно использовать стек с последующим RET, что я и сделал в своём шелл-коде на выходе, указавав ему переход на метку "start" (оригинальная точка-входа). Это нужно учитывать при зачатии шелл-кодов.

Ну и в первых трёх инструкциях, шелл жонглирует регистрами – что это и зачем?
Выше упоминалось, что сигнатура "MZ" в виде слова 0х4D5A мутирует в инструкции DEC_EBP и POP_EDX. Поэтому для отвода глаз сначала я меняю местами EDX и EAX, после чего восстанавливаю EBP и отправляю EAX (в лице регистра edx) на своё место. На этом этапе стек держится на "честном слове", и чтобы не засветиться сторожам, необходимо соблюдать его баланс.


Инкапсуляция шелл-кода в ImageBase

У нас есть готовый шелл и теперь нужно переместить его из секции-данных в ImageBase, т.е. в заголовок DOS-Header. Для этого открываем "crackme " в хек-редакторе HxD и ищем в нём шелл-код по маркерам начала и конца с цепочкой байт 0хFF. Упс, есть такой – выделяем и копируем его в буфер, а оригинал забиваем нулями. Теперь переходим в самое начало файла, и выделив после сигнатуры "MZ" 1Fh-байт (размер в опкодах), вставляем шелл из буфера по Ctrl+V. Обязательно нужно выделить блок и потом вставить из буфа, иначе указатель на РЕ по смещению Base+0х3С съедет со-своей позиции. Должно получиться примерно так:

Entry Point – дело о пропавшем индексе (часть 2)


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


Основная Callback-процедура для перехвата EntryPoint

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

Во-первых, на входе в Callback нужно обязательно сохранять все регистры инструкцией PUSHA, и восстановив их на выходе через глупый POPA, возвратить загрузчику в регистре EAX значение 1. Только в этом случае наш план по перехвату оригинальной EnrtyPoint возымеет успех, и своей функцией NtContinue() загрузчик проглотит наживку. Ситуацию нагнетает и то, что на момент работы Callback-процедуры обработчик структурных исключений SEH ещё не оформлен, поэтому если мы сделаем что-то неправильно, то даже не узнаем об этом – наша прожка тупо провалится в чёрную дыру, без каких-либо намёков на исключение.

Смысл процедуры Callback в том, чтобы получив адрес оригинальной точки-входа, найти его на дне стека и изменить на ImageBase. Для этого берём из поля РЕ.28h относительный RVA адрес точки, и отнимаем его от найденного значения в стеке. В результате получим базу образа, которую и подхватит функия NtContinue(), передавая управление на наш декриптор. После того как декриптор расшифрует уязвимый участок кода с проверкой пароля, он сам передаст управление на Original EntryPoint (OEP) и программа продолжит уже работу по заданному алго.

Тут есть одна проблема – запрет на запись в секцию-кода! Дело в том, что в процессе расшифровки декриптор должен будет ксорить и перезаписывать каждое слово кода, значит нам нужно внутри процедуры Callback снять эту защиту функцией VirtualProtect(), с аргументом "PAGE_EXECUTE_READWRITE" равным 0х40. Кроме этого аргумента, функция требует ещё и адреса блока (в нашем случае секция-кода), её размер 0х1000 или одна страница, и указатель на переменную для текущих флагов. Эта функция BOOL, так-что возвращает нуль в случае ошибки.

В примере ниже, коллбэк ищет ещё и отладчик по системному флагу NtGlobalFlag, и если обнаружит его, то сразу выходит из Callback'a. В результате, точка-входа в стеке оказывается не перехваченной и декриптор управление не получает. Тогда в отладчике мы пароль-то введём, а вот его проверку осуществить не сможем, т.к. блок останется не расшифрованным. Вот сама процедура, как рождественский гусь напичканная комментариями:

C-подобный:
;//=================================================
;//=== CallBack процедура для подмены EP в стеке   |
;//=================================================
proc  fixStackEpoint hinst,reason,reserved   ;// загрузчик передаёт в коллбэк три аргумента
         pusha                        ;// сохранить все регистры!
         mov      esi,[ fs:0x30]      ;// ESI = PEB address
         mov      eax,[esi+0x68]      ;// EAX = PEB.68h NtGlobalFlag
         cmp      eax,0x70            ;// EAX = 70h - процесс отлаживается!
         je       @stopCallback       ;// выйти из коллбэка..
                                      ;// иначе..
         mov      ebx,[esi+0х08]      ;// EBX = PEB.ImageBase
         mov      ecx,[ebx+0х3C]      ;// ECX = РЕ.Offset
         add      ecx,ebx             ;// ECX = РЕ VirtualAddr
         mov      ecx,[ecx+0x28]      ;// ECX = PE.EntryPoint_RVA

         mov      edx,ecx             ;// EDX = EntryPoint RVA
         add      edx,ebx             ;// EDX = оригинал eРoint - сигнатура!

;//=== Подмена EntryPoint в стеке ==================================
         mov      esi,[fs:4]          ;// ESI = TEB.04 StackBase
         std                          ;// DF=1 - обратный шаг для ESI
@find_Signature:
         lodsd                        ;// EAX = dword из ESI
         cmp      eax,edx             ;// сравнить его с оригинал ePoint
         jne      @find_Signature     ;// повторить, если не равно..
         cld                          ;// иначе: сбросить DF – прямой шаг
         add      esi,4               ;// коррекция указателя ESI в стеке
         sub     [esi],ecx            ;// подмена точки-входа в стеке!!!

;//=== Открываем секции-кода доступ на запись ======================
         invoke   VirtualProtect,edx,0x1000,0x40,flags     ;// PAGE_EXECUTE_READWRITE
@stopCallback:
         popa                ;// восстановить все регистры
         mov      eax,1      ;// вернуть загрузчику TRUE   
         ret                 ;// возвратить ему управление.
endp
Обратитте внимание, что Callback не передаёт управление на фиктивную точку по адресу ImageBase. Мы только поменяли ОЕР в стеке инструкцией sub [esi],ecx (отнять RVA), а дальше функция NtContinue() снимет наше значение со-стека, и сама вызовет декриптор.


Шифрование процедуры проверки пароля

Теперь наш декриптор принимает управление и ксорит все слова уязвимого блока ключом 3e97h в надежде, что эта процедура зашифрована. Однако на данном этапе мы ещё не шифровали её, и декриптор вместо расшифровки наоборот шифрует блок. Ручное шифрование интересно, но требует особой внимательности. Так-что вынимаем мозги из штанов и приступаем к шифрованию критически-важного участка кода.

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

C-подобный:
;//~~~ Шелл-код декриптора ~~~~~~~~~
         mov      ecx,@end_crypt - @crypt    ;// ECX = длина шифруемого блока
         shr      ecx,1                      ;// разделить её на 2 (в словах)
         mov      esi,@crypt                 ;// ESI = адрес начала блока
decrypt: xor      word[esi],3e97h            ;// ксорим слово из ESI ключом
         add      esi,2                      ;// следующее слово..
         loop     decrypt                    ;// промотать цикл ECX-раз!
         push     start                      ;// адрес оригинальной точки-входа
         ret                                 ;// отдать ей управление!!!
Здесь видно, что длинна блока в байтах заносится в регистр ECX, но поскольку мы собираемся ксорить словами по 2 байта, то операцией сдвига вправо, ECX делится на 2. Остаток от деления нас не интересует, в результате чего кол-во итераций цикла в любом случае будет чётным. Теперь загрузим crackme в отладчик, и посмотрим, что лежит в регистре ECX нашего шелл-кода:

Entry Point – дело о пропавшем индексе (часть 2)


Красным по белому мы видим здесь значение 0х30, что в десятичном будет 48. Адрес начала блока равен 0х402021 – запишем эти два значения на бумажке. Теперь вскармливаем crackme чудо-редактору Hiew и по энтеру выбрав режим дизассемблера, приступаем к шифрованию блока "проверки пароля".

Для начала жмём Goto (F5) и предварив точкой, вводим адрес начала блока .402021 – у меня это выглядит так:

Entry Point – дело о пропавшем индексе (часть 2)


Теперь жмём Edit (F3) и в меню появляется Crypt (F7), выбрав который видим окно, в где нам предлагают определиться с размером ксора на каждом шаге (1,2,4,8 байт) и задать алгоритм шифрования. Поскольку наш декриптор расшифровывает словами, то по F2 обязательно задаём размер Word:

В независимости от операнда инструкции XOR в нашем коде, для Hiew'a это всегда регистр EAX, причём переходы LOOP, JMP и прочии он привязывает не к адресу, а к номеру строки в своём окне. Двигать указатель на следующее слово не ;нужно – LOOP во второй строке сам прибавит два. Скрин ниже показывает детали:

Entry Point – дело о пропавшем индексе (часть 2)


Что интересно, тулза не может отменять предыдущую операцию и если вы сделали что-то не так, то придётся перезагрузить Hiew. Если ошибок нет, то жмём Esc и приступаем непосредственно к шифрованию, для чего тискаем всё ту-же клавишу Crypt (F7). Каждое зашифрованное слово выделяется жёлтым цветом, и нам нужно повторить операцию 48 раз (длинна блока в словах = 30h или 48).

В демо-версии Hiew'a есть ограничение (а может и не только в демке) – как только мы дойдём до конца окна, ксор встаёт как вкопанный. Нужно будет запомнить кол-во уже проксоренных слов, и сохранить текущее состояние клавишей Update (F9). Теперь прокручиваем окно вверх и продолжаем шифровать комбинацией F3->F7 опять до видимой части окна. Алгоритм каждый раз задавать не нужно – просто обновляем по F9->F3 и едем дальше по F7, пока не простучим по ней (в данном случае) 48 раз. Это напрягает, но другого выхода вроде нет. Пример представлен ниже:

Entry Point – дело о пропавшем индексе (часть 2)



Эпилог..

После всех манипуляций, наш фрегат может отправиться в свободное плавание, и никакие препятствия в виде отладчиков или дизассемблеров ему не страшны. Ручное шифрование требует терпения, которое с лихвой оправдывается результатом. Ясно, что это лишь учебный пример который ни на что не претендует. Но идея была в том, чтобы показать технику перехвата точки-входа в приложения так, чтобы никто не заметил подвоха.

У анализаторов исполняемых файлов есть такой параметр как "энтропия кода". Этот параметр по 10-бальной шкале определяет меру беспорядка в коде. Так, если код зашифрован весь, то его энтропия может достигать значения (7) – у накрытых протектором она ещё больше. Поскольку я зашифровал только малую часть своего кода, то этот показатель у меня 4.72, а у секции-данных вообще около двух. Таким способом удалось обвести вокруг пальца поле EntryPoint, в котором гордо красуется RVA 0x2000:

Entry Point – дело о пропавшем индексе (часть 2)


Готовый исходник и скомпилированный к употреблению файл цепляю в скрепке. После компиляции его в fasm'e, нужно будет только по указанной методике перенести декриптор в заголовок DOS-Header, и зашифровать уязвимый блок "проверки пароля" ключом, которым расшифровывает декриптор. Чтобы проверить свои силы в реверсе, можете вскормить отладчику готовый экхешник и посмотреть на его реакцию – уверяю, будет интересно. Из всех , один только F-Secure что-то там бормочит, а остальные молчат как партизаны.

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

Вложения

Последнее редактирование:
B

bot039193

New member
20.10.2019
2
1
Хороший метод, но можно самому настроить контекст и восстановить его через апи/обработчик исключений. Так будет правильнее или я ошибаюсь?
 
  • Нравится
Реакции: Marylin
Marylin

Marylin

Mod.Assembler
Red Team
05.06.2019
131
325
настроить контекст и восстановить его через апи/обработчик исключений.
да, это тоже вариант..
только здесь ЕР перехватывается внутри загрузчика, и в момент вызова цепочки TLS-callback цепочки SEH ещё не сформирован. Я пробовал вызывать в нём исключения - они игнорируются., хотя лазейку найти можно всегда.
 
  • Нравится
Реакции: bot039193
B

bot039193

New member
20.10.2019
2
1
да, это тоже вариант..
только здесь ЕР перехватывается внутри загрузчика, и в момент вызова цепочки TLS-callback цепочки SEH ещё не сформирован. Я пробовал вызывать в нём исключения - они игнорируются., хотя лазейку найти можно всегда.
В самом TLS даже не пытался, нужно будет посмотреть. VEH пробовали?
Моя задумка: зарегистрировать обработчик исключений в TLS, повесить на липовую entry point non-executable/guard защиту и передать управление загрузчику. Во время выполнения липовой точки входа, будет вызван наш обработчик. В обработчике почистить/настроить контекст и восстановить выполнение. Еще можно попробовать перехват Zw/NtContinue. Минусы метода: без вызовов апи не обойтись, как в вашем случае, плюсы: переносимость, своего рода гарантия.
 
Aleks Binakril

Aleks Binakril

Happy New Year
04.12.2019
25
4
целая детективная история - очень интересно читать такую развернутую детализацию цифрового кода программ
 
Мы в соцсетях: