Статья ASM – Динамическое шифрование кода

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

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


PeTools.png



Оглавление:

1. Плюсы и минусы идеи;
2. Реализация в примитивах;
3. SEH – обработчик исключений;
4. Модуль расшифровки и шифрования процедур;
5. Сбор информации о шифровании;
6. Практика – пример программы;
7. Заключение.
-----------------------------------------------------------


1. Плюсы и минусы динамического шифрования

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

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

Ну а в остальном, динамическое шифрование превосходит статическое по всем фронтам, а что самое главное – это отличный объект для практики. Разрабатывая его алгоритм можно забрести в такие дебри, куда не каждый взломщик решится залезть. К примеру, ничто не мешает шифровать разные блоки разными ключами, связывать эти блоки (аля процедуры) между собой, как это делает тот-же AES в режиме
цепочки CBC (Cipher-Block-Chaining), и т.д.п. В общем направлений для самовыражения здесь предостаточно.


2. Реализация в примитивах

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

1. Служебная функция НЕ должна принимать никаких аргументов от основного кода, иначе взломщик сможет ухватиться за них и раскурить весь наш тайный план. Функцию нужно наделить как-минимум "байтом" собственного интеллекта, чтобы она на автомате вычисляла, к какой именно процедуре идёт обращение. Вариантов тут у нас всего два – это аппаратные "точки-останова" BreakPoint с использованием отладочных регистров DR0-DR7 (в этом случае мы сможем обслуживать не более четырёх процедур по кол-ву регистров DRx), или-же задействовать свой структурированный обработчик исключений SEH (Structured Exсeption Handler). Остальные варианты производные от них. Дабы не ограничивать себя в кол-ве рабочих процедур, я выбрал второй вариант SEH – ему без разницы, сколько клиентов тащить на себе. В следующей главе мы рассмотрим его детали.
2. Непосредственно крипт/декрипт исполняемых процедур. Это атомный реактор всей программы – именно на нём будет держаться вся конструкция, а потому нужно отнестись к данному модулю с особой внимательностью. Байки про то, как сложно организовать шифрование сильно преувеличены: тут главное чётко представлять себе конечную цель. Основное требование заключается лишь в определении начального адреса блока шифрования, и его длины. Можно было использовать в примере более серьёзный метод шифрования типа AES, позвав на помощь функцию BСryptEncrypt() из либы Bcrypt.dll, но для демонстрационного примера это громоздко, и я ограничился обычным XOR.

На рисунке ниже представлен алгоритм всей программы из части(6) данной статьи. Это секция-кода, которая начинается с выделенных синим трёх рабочих процедур А,В,С, хотя их может быть сколько угодно. Сразу за процедурами следует обёрнутая в модуль SEH служебная функция, и далее начинается непосредственно сам код с точкой-входа в программу start:. На модуль SEH возложена критически-важная часть работы – доступ к процедурам будет осуществляться только через него!

В самом хвосте адресного пространства валяется (в специально выделенной для этих целей секции) временный "криптор". Его задача – собрать информацию о процедурах типа: начало\размер\ключ блока шифрования, и вывести все эти данные на консоль. Это избавит нас от выносящего мозг ручного вычисления паспортов процедур. Модуль обязательно должен находиться в самом конце программы, чтобы после его удаления не съехали со-своих позиций все адреса. После того-как он выполнит свою задачу, нужно будет в HEX-редакторе вписать вместо него JMP на точку-входа в программу EntryPoint.


Algo.png


В данном случае из кода вызывается процедура(А) и поскольку модулю SEH система передаёт управление только в случае возникновения исключений "Exception", то вызов процедуры нужно будет предварить какой-нибудь исключительной ситуацией типа: деление на нуль, чтение с ядерного или иного недоступного прикладной задаче адреса, или-же специально предназначенной для этих целей инструкцией генерирования ошибок UD2 (UndefineD Instruction opcode). Однако UD2 явно раскрывает все наши карты, т.к. в легальных программах она практически не встречается и служит чисто для отладочных целей, поэтому лучше поступить так..

Создаём переменную со-значением нуль и делаем вид, что она предназначена для какого-нибудь указателя. На самом деле это утка и мы вообще не планируем ложить туда линк, а просто считываем этот нуль в любой регистр и используя его как адрес, пытаемся прочитать с него. В результате получаем нарушение прав доступа "Access Violation" с кодом исключения 0xC0000005. Это хоть какая-то маскировка, чем явный UD2 или деление на нуль.

На момент, когда SEH получит управление, диспетчер исключений мастдая положит в стек адрес глючной инструкции, и если прибавить к нему длину этой инструкции (в случае с UD2 и чтения с нулевого адреса это 2 байта), то в аккурат получим смещение к запрашиваемой процедуре, которая находится ниже аварийной:


C-подобный:
.data
zero     dd  0   ;//<--- переменная-утка
.code
;//----- Здесь решаются какие-нибудь задачи,
         mov     eax,1234h
         sub     eax,ebx
         nop
;//----- ....и пришло время вызвать процедуру "Mess".
         mov     ebx,[zero]     ;//<--- EBX=0 из переменной
         mov     eax,[ebx]      ;//<--- Внимание! Генерим исключение "Access Violation".
@mess:   jmp     Mess           ;//<--- SEH отловит исключение и вызовет эту процедуру
         xor     eax,15151515h  ;// продолжение кода..
         rol     eax,28
         nop

Обратите внимание, что вызывать процедуры из кода не нужно, т.е. можно вообще убрать инструкцию JMP Mess – в этом и вся фишка! Главное оставить метку @mess, чтобы указать функции SEH именно на процедуру "Mess". Алгоритм программы построен так, что имеется "таблица вызовов" процедур, где собраны все такие метки. Это позволяет запрашивать процедуры в любой последовательности просто перемещая метки в коде. Более того, если нам нужно добавить ещё процедур, мы тупо назначаем им метки и прописываем их в таблице. Позже мы ещё вернёмся к данному вопросу.


3.
SEH – обработчик исключений

Системному механизму SEH уже была посвящена
отдельная статья, где рассматривались все его детали. Поэтому повторяться не будем, а вспомним лишь основные моменты, которые пригодятся в динамическом шифровании кода.

Эта мощная фишка позволяет устанавливать (поверх системного) пользовательские обработчики исключений. На языках высокого уровня ЯВУ она имеет эквивалент в виде операторов "try-except". Создать цепочку обработчиков и вклиниться в неё можно всего тремя инструкциями ассемблера так:


C-подобный:
     push    mySeh             ;// указатель на наш обработчик
     push    dword[fs:0]       ;// сдвигаем системный (на него указывает регистр FS)
     mov     dword[fs:0],esp   ;// вклиниваемся в цепочку Chain!

SEH.png


С этого момента, все возникшие в системе исключения будут перенаправляться диспетчером на нашу функцию "mySeh", а что мы будем делать внутри неё – это уже зависит от фантазии и настроения. Она представляет собой документированную Callback-функцию обратного вызова с таким прототипом (имя может быть произвольным):


C-подобный:
mySeh(
   pRecord   dd  0    ;// линк на тех.информацию об исключении
   pFrame    dd  0    ;// линк на данный SEH-фрейм в стеке
   pContext  dd  0    ;// линк на контекст регистров
   pParam    dd  0    ;// резерв
);

Когда после исключения программа сколлапсирует в чёрную дыру и наш обработчик получит управление, эти 4 указателя окажутся на вершине стека. Суть в том, что в этот момент диспетчер сохраняет значения всех регистров процессора в специальную структуру под названием "Context", и предоставляет нам указатель на неё в третьем аргументе "pContext". Из всей своры этих регистров, нам нужен только регистр-указатель на инструкцию EIP (Instruction Pointer), который расположен по смещению 184 от начала структуры. Именно в нём будет храниться адрес инструкции исключения, и если (как было сказано выше) прибавим к нему 2, то получим адрес метки для вызова процедуры. Полный контекст регистров на который указывает аргумент "pContext" декларируется мелкомягкими так:


C-подобный:
struct CONTEXT              ;//<---- Size 204 (регистры CPU)
  ContextFlags         dd  0     ;// 00  (смещения от начала контекста)
  iDr0                 dd  0     ;// 04
  iDr1                 dd  0     ;// 08
  iDr2                 dd  0     ;// 12
  iDr3                 dd  0     ;// 16
  iDr6                 dd  0     ;// 20
  iDr7                 dd  0     ;// 24
  FloatSave            FLOATING_SAVE_AREA  ;// 28
  regGs                dd  0     ;// 140
  regFs                dd  0     ;// 144
  regEs                dd  0     ;// 148
  regDs                dd  0     ;// 152
  regEdi               dd  0     ;// 156
  regEsi               dd  0     ;// 160
  regEbx               dd  0     ;// 164
  regEdx               dd  0     ;// 168
  regEcx               dd  0     ;// 172
  regEax               dd  0     ;// 176
  regEbp               dd  0     ;// 180
  regEip               dd  0     ;// 184  <---- EIP наш клиент!
  regCs                dd  0     ;// 188
  regFlag              dd  0     ;// 192
  regEsp               dd  0     ;// 196
  regSs                dd  0     ;// 200
  ExtendedRegisters    rb 512    ;// 204
ends

struct FLOATING_SAVE_AREA   ;//<---- Size 112 (регистры FPU)
  ControlWord          dd  0
  StatusWord           dd  0
  TagWord              dd  0
  ErrorOffset          dd  0
  ErrorSelector        dd  0
  DataOffset           dd  0
  DataSelector         dd  0
  RegisterArea         db  80 dup(0)
  Cr0NpxState          dd  0
ends

Чтобы не подтягивать её инклудом в программу, в своём примере я явно указываю смещение на EIP=184, и взяв его значение прибавляю к нему 2:


C-подобный:
proc  mySeh  pRecord, pFrame, pContext, pParam
      mov     ebx,[pContext]  ;// линк на контекст
      mov     ebx,[ebx+184]   ;// EBX = значение регистра EIP
      add     ebx,2           ;// EBX = адрес метки процедуры

      call    ebx             ;// зовём её!

      mov     ebx,[pContext]  ;//
      mov     eax,[pRet]      ;// адрес возврата (предварительно сохранён в переменной pRet)
      mov     [ebx+184],eax   ;// заносим его в EIP
      mov     eax,0           ;// команда диспетчеру "Перезагрузить контекст и продолжить"
      ret              ;//<------ возвращаем управление диспетчеру исключений!

Значит, чтобы получить адрес процедуры, мы сначала считываем значение EIP на момент исключения, а в конце перезаписываем EIP новым значением, чтобы процессор прыгнул по адресу возврата на нужный нам участок кода, т.е. после процедуры.

Немаловажными являются тут и две последние инструкции. Поскольку всё-что происходит внутри нашего обработчика SEH находится под чутким надзором системного диспетчера исключений, по окончании в регистре EAX он ожидает получить от нас или нуль (что означает "в Багдаде всё спокойно" и мы обработали исключение), или-же единицу, которая говорит об обратном. Здесь я передаю нуль, и диспетчер восстанавливает значения всех регистров из сохранённого контекста, куда мы предварительно подсунули уже новый EIP. Это в очередной раз доказывает, что процессоры х86 не обладают собственным интеллектом и делают лишь то, на что мы им укажем явно.


4. Модуль расшифровки/шифрования процедур

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

Значит есть у меня три процедуры: "Mess" (тупо выводит сообщение), "Pass" (запрашивает пароль), и "Info" (выводит некоторую информацию). Теперь я должен предварительно их зашифровать, а когда придёт время, основная функция SEH должна будет: (1)расшифровать их, (2)передать на них управление, и после отработки (3)опять зашифровать. Все процедуры расположены в памяти плечом-к-плечу, поэтому чтобы вычислить размеры каждой из них, достаточно от адреса-начала следующей, отнимать адрес-начала предыдущей, т.е. получать разницу в байтах. В моём случае последовательность такая: Mess-->Pass-->Info-->mySeh.

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


C-подобный:
section '.inc' data readable writeable
procTable     dd  @mess, keyMess
              dd  @pass, keyPass
              dd  @info, keyInfo

keyMess       dd  Mess, (Pass -Mess-1)/4, 0xC9125877
keyPass       dd  Pass, (Info -Pass-1)/4, 0x65210833
keyInfo       dd  Info, (mySeh-Info-1)/4, 0xAA27E395

В первом поле каждого из массивов лежат адреса процедур, а дальше.. для вычисления размера первой процедуры "Mess", я беру её разницу в байтах со-следующей "Pass", и оставив в резерве байт(-1) делю на 4. Это потому, что в третьем поле ключ шифрования у меня 4-байтный, и планирую шифровать процедуры блоками такого-же размера. (для справки: 1-байтный ключ можно подобрать за 256 итераций, 2-байтный уже за 65536, а 4-байтный более чем за 4 млрд).

Когда внутри SEH из контекста регистров мы получим "адрес метки" вызываемой процедуры в лице значения EIP+2, нужно будет пройтись по таблице "procTable" где все они собраны, и получить указатель на соответствующий массив "keyMess" или другой (см.второй dword в procTable). Для расшифровки и последующего шифрования любой из процедур больше ничего и не требуется.


C-подобный:
;//-------------------------------------------------//
;//-------- Обработчик исключений SEH --------------//
;//-------------------------------------------------//
proc  mySeh  pRecord, pFrame, pContext, pParam
local    pRet    dd  0

         mov     ebx,[pContext]
         mov     ebx,[ebx+184]   ;// Context.EIP
         add     ebx,2           ;// EBX = адрес метки процедуры для вызова
         mov    [pRet],ebx       ;// ...(адрес возврата в лок.переменную)
         mov     esi,procTable   ;// ESI = таблица указателей
         mov     ecx,3           ;// ECX = число записей в ней
@@:      lodsd                   ;// EAX = очередное значение из таблицы
         cmp     eax,ebx         ;// ищем массив записей процедуры
         je      @procFound      ;// если нашли!
         add     esi,4           ;// иначе: переход к сл.записи
         loop    @b              ;// пройтись по всей таблице
@procFound:
         mov     ebx,[esi]       ;// EBX = указатель на массив данных
         mov     esi,[ebx]       ;// ESI = адрес процедуры (первый DWORD из массива)
         mov     ecx,[ebx+04]    ;// ECX = длина процедуры (второй DWORD)
         mov     eax,[ebx+08]    ;// EAX = ключ шифрования (третий DWORD)
         push    esi ecx eax esi ;// запомнить все данные для обратного шифрования
;//--- Декрипт
@@:      xor    [esi],eax
         add     esi,4
         loop    @b

         pop     esi
         call    esi        ;//<---- вызов процедуры!

;//--- обратный Крипт
         pop     eax ecx esi
@@:      xor    [esi],eax
         add     esi,4
         loop    @b

         mov     esi,[pContext]
         mov     eax,[pRet]     ;// адрес возврата из процедуры
         mov    [esi+184],eax   ;// загнать его в EIP
         xor     eax,eax        ;// EAX(0)= продолжить, 1 = прервать
         ret
endp

Рассмотрим, что здесь происходит..
Значит в коде мы генерим исключение, после которого стоит одна из трёх (привязанных к процедурам) меток. Обработчик SEH отлавливает исключение и получает контекст всех регистров, среди которых есть и указатель на глючную инструкцию EIP. Прибавив к нему двойку ADD EBX,2 мы получаем адрес метки, и обнаружив её в таблице "procTable" получаем указатель на связанный с процедурой массив данных (адрес процедуры, её размер и ключ). Остаётся расшифровать процедуру, запустить её на исполнение, после чего опять зашифровать.

Таким образом, без каких-либо аргументов, функция SEH сама на автомате вычисляет все необходимые данные, и без знания алгоритма пионерам-взломщикам трудно будет разобраться, что здесь к чему, тем более-что на Win7 и выше отладчики не заходят в пользовательские обработчики исключений (как это было на ХР), пока не поставишь бряк по F2. Такие дела..


5. Сбор информации о шифровании

Ну и на финишной-прямой рассмотрим метод начального шифрования процедур.
Поскольку получив управление SEH расшифровывает процедуры, значит нам нужно предварительно зашифровать их. Радует то, что в массивах "keyMess" и прочих все данные уже для этого есть, и остаётся просто организовать крипт. Для этого в самом хвосте программы я выделил специальную секцию (без имени, чтобы она не подталкивала на мысль), и собрал в ней 3 блока по такой схеме:


C-подобный:
section '' code readable executable writeable
@hideCrypt:
         mov     esi,Mess                   ;// адрес процедуры для шифрования
         mov     ecx,[keyMess+4]            ;// её длина из массива данных
         mov     ebx,[keyMess+8]            ;// ключ шифрования из массива
        cinvoke  printf,<10,' Addr: %08x',\
                         10,' Size: %d',\
                         10,' Key : %08x',10,0>,esi,ecx,ebx

         mov     esi,Pass
         mov     ecx,[keyPass+4]
         mov     ebx,[keyPass+8]
        cinvoke  printf,<10,' Addr: %08x',\
                         10,' Size: %d',\
                         10,' Key : %08x',10,0>,esi,ecx,ebx

         mov     esi,Info
         mov     ecx,[keyInfo+4]
         mov     ebx,[keyInfo+8]
        cinvoke  printf,<10,' Addr: %08x',\
                         10,' Size: %d',\
                         10,' Key : %08x',10,0>,esi,ecx,ebx
         jmp     @next
         push    start   ;//<----- опкоды перехода на EntryPoint,
         ret             ;// которые нужно будет вписать в самое начало секции.

cryptData.png


Теперь компилируем программу и открываем её дисковый образ в удобном HEX-редакторе – я по старинке предпочитаю HIEW, версия 8.66 которого поддерживает и 64-битный код. Значит загружаем в него образ и комбинацией F8-->F6 выбираем секцию кода. Далее манипулируя стрелками находим нужный адрес, который мы получили на предыдущем этапе (в данном случае 0x00403000, начало секции), и задаём ключ шифрования последовательностью F3-->F8 (Edit, XOR). По-умолчанию Hiew ксорит байтами и в этом окне нет возможности изменить длину ключа. Но поскольку ключ у нас 4-байтный, то придётся записывать его задом-наперёд, как показано на скрине ниже:


Hiew_0.png


После того-как определились с ключом, подтверждаем свои намерения клавишей Enter и всё той-же F8 приступаем к шифрованию первой процедуры. Как видно из логов, размер её 20 двойных слов DWORD, так-что жмём F8 ровно 20-раз (цвет зашифрованных байт должен меняться на жёлтый). По окончании сохраняем изменения по F9 и выходим из редактора по F10. Выйти надо для того, чтобы сменить значение ключа для шифрования следующей процедуры. Проделываем аналогичные операции с остальными двумя блоками кода.

Посмотрите на предыдущий фрагмент.. В самом его хвосте имеются строки push start --> ret.
Они вставлены для того, что получить опкоды перехода на EntryPoint в программу. Если посмотреть в отладчике, этим инструкциям (в данном случае) будет соответствовать последовательность байт 0x6846324000c3. На самом заключительном этапе, эту строку байт нужно будет вписать в самое начало безымянной секции "Крипт", а все временные блоки кода по сбору информации о процедурах можно забить нулями, чтобы они не бросались в глаза в дизассемблере и отладчиках. Так это будет выглядеть в редакторе:


Hiew_1.png



6. Практика – пример программы

Соберём теперь всё вышеизложенное под один капот.
Обратите внимание, что в шапке программы директива entry @hideCrypt определяет точку-входа, а данная метка прописана в самой последней секции "Крипт". Если не вставить туда JMP на метку start: по руководству выше, то основной код программы вообще не получит управления, со-всеми вытекающими последствиями.


C-подобный:
format  pe console
include 'win32ax.inc'
entry   @hideCrypt     ;//<--- точка-входа указывает на безымянную секцию "Крипт"
;//-------
section '.inc' data readable writeable
procTable          dd  @mess,keyMess
                   dd  @pass,keyPass
                   dd  @info,keyInfo

keyMess            dd  Mess, (Pass -Mess-1)/4, 0xC9125877
keyPass            dd  Pass, (Info -Pass-1)/4, 0x65210833
keyInfo            dd  Info, (mySeh-Info-1)/4, 0xAA27E395

struct PERF_INFORMATION
  cb               dd  sizeof.PERF_INFORMATION
  CommitTotal      dd  0
  CommitLimit      dd  0
  CommitPeak       dd  0
  PhyTotal         dd  0
  PhyAvailable     dd  0
  SystemCache      dd  0
  KernelTotal      dd  0
  KernelPaged      dd  0
  KernelNonpaged   dd  0
  PageSize         dd  0
  HandleCount      dd  0
  ProcessCount     dd  0
  ThreadCount      dd  0
ends

;//-------
.data
perfInfo    PERF_INFORMATION
zero        dd  0
buff        db  0

;//-------
section '.code' code readable executable writeable

;//-------- Рабочие процедуры = исходный материал -----------//
;//-------- Выводит путь и имя своей тушки ------------------
proc Mess
         invoke  GetModuleFileName,0,buff,256
        cinvoke  printf,<10,' Procedure #1  =============',\
                         10,'    File:  %s',10,0>,buff
         ret
endp
;//-------
;//-------- Запрашивает и проверяет пароль ------------------
proc  Pass
local    pKey    dd  'e'+'n'+'c'+'r'+'y'+'p'+'t'  ;//<--- хеш оригин.пароля

        cinvoke  printf,<10,' Procedure #2  =============',\
                         10,'    Type pass:  ',0>
         xor     eax,eax
         xor     ebx,ebx
@@:     cinvoke  _getch            ;// символ в AL
         cmp     al,13             ;// это Enter???
         je      @stop             ;// да - на выход
         add     ebx,eax           ;// иначе: считаем сумму в EBX
         push    eax ebx           ;//
        cinvoke  printf,<'*',0>    ;// [*] вместо введённого символа
         pop     ebx eax           ;//
         jmp     @b                ;// уйти на повтор..
@stop:   cmp     ebx,[pKey]        ;// сравнить хеши паролей!
         je      @ok
        cinvoke  printf,<'  <<----- Wrong PASS !!!',10,0>
         jmp     @retn
@ok:    cinvoke  printf,<'  <<----- Pass OK...',10,0>
@retn:   ret
endp
;//-------
;//-------- Выводит системную инфу (см.PERF_INFORMATION) -----------//
proc  Info
        cinvoke  printf,<10,' Procedure #3  =============',0>
         invoke  K32GetPerformanceInfo,perfInfo,14*4
        cinvoke  printf,<10,'    Mem Page size:  %d byte',\
                         10,'    Handle  count:  %d',\
                         10,'    Process count:  %d',\
                         10,'    Thread  count:  %d',10,0>,\
                         [perfInfo.PageSize],[perfInfo.HandleCount],\
                         [perfInfo.ProcessCount],[perfInfo.ThreadCount]
         ret
endp
;//-------
;//------------------------------------------------------------//
;//-------------- Обработчик исключений SEH -------------------//
;//------------------------------------------------------------//
proc  mySeh  pRecord, pFrame, pContext, pParam
local    pRet    dd  0

         mov     ebx,[pContext]
         mov     ebx,[ebx+184]   ;// Context.EIP
         add     ebx,2           ;// EBX = адрес процедуры для вызова
         mov    [pRet],ebx       ;// адрес возврата
         mov     esi,procTable   ;// ESI = таблица указателей
         mov     ecx,3           ;// ECX = число записей в ней
@@:      lodsd                   ;// EAX = очередное значение из таблицы
         cmp     eax,ebx         ;// ищем массив записей процедуры
         je      @procFound      ;// если нашли!
         add     esi,4           ;// иначе: переход к сл.записи
         loop    @b              ;// пройтись по всей таблице
@procFound:
         mov     ebx,[esi]       ;// EBX = указатель на массив данных
         mov     esi,[ebx]       ;// ESI = адрес процедуры (первый DWORD из массива)
         mov     ecx,[ebx+04]    ;// ECX = длина процедуры (второй DWORD)
         mov     eax,[ebx+08]    ;// EAX = ключ шифрования (третий DWORD)
         push    esi ecx eax esi ;// запомнить все данные для обратного шифрования
;//--- расшифровка
@@:      xor    [esi],eax
         add     esi,4
         loop    @b

         pop     esi
         call    esi             ;//<--------- Вызов процедуры!

;//--- обратное шифрование
         pop     eax ecx esi
@@:      xor    [esi],eax
         add     esi,4
         loop    @b

         mov     esi,[pContext]
         mov     eax,[pRet]      ;// адрес возврата
         mov    [esi+184],eax    ;// Context.EIP
         xor     eax,eax         ;// EAX(0)= продолжить, 1 = прервать
         ret
endp
;//*****************************************************************
;//********************   S T A R T   ******************************
;//*****************************************************************
start:   invoke  SetConsoleTitle,<'*** Dynamic Crypt Example ***',0>

         push    mySeh            ;//<--- Ставим свой SEH
         push    dword[fs:0]
         mov     dword[fs:0],esp

;//----- Здесь решаются какие-нибудь задачи,
         mov     eax,1234h
         sub     eax,ebx
         nop
;//----- ....и пришло время вызвать процедуру!
         mov     ebx,[zero]
         mov     eax,[ebx]      ;//<--- Внимание! Генерим исключение.
@mess:
         xor     eax,15151515h  ;// продолжение кода..
         rol     eax,28
         nop

;//----- Вызов очередных процедур!
         mov     ebx,[zero]
         mov     eax,[ebx]      ;//<--- Исключение!
@pass:
         mov     ebx,[zero]
         mov     eax,[ebx]      ;//<--- Исключение!
@info:
         nop
         nop
@next:  cinvoke  printf,<10,' Dynamic crypt OK...',0>
@exit:  cinvoke  _getch
        cinvoke  exit,0

;//----- Временная секция для сбора инфы о процедурах -----------------//
;//----- Сейчас это точка-входа в программу, поэтому нужно будет вставить сюда
;//----- инструкцию перехода на фактический EntryPoint в секцию-кода.
section '' code readable executable writeable
@hideCrypt:
         mov     esi,Mess         ;// адрес процедуры для шифрования
         mov     ecx,[keyMess+4]  ;// её длина из массива данных
         mov     ebx,[keyMess+8]  ;// ключ шифрования из массива
        cinvoke  printf,<10,' Mess  Addr: %08x',\
                         10,'       Size: %d',\
                         10,'       Key : %08x',10,0>,esi,ecx,ebx

         mov     esi,Pass
         mov     ecx,[keyPass+4]
         mov     ebx,[keyPass+8]
        cinvoke  printf,<10,' Pass  Addr: %08x',\
                         10,'       Size: %d',\
                         10,'       Key : %08x',10,0>,esi,ecx,ebx

         mov     esi,Info
         mov     ecx,[keyInfo+4]
         mov     ebx,[keyInfo+8]
        cinvoke  printf,<10,' Info  Addr: %08x',\
                         10,'       Size: %d',\
                         10,'       Key : %08x',10,0>,esi,ecx,ebx
         jmp     @exit
         push    start      ;//<---- опкоды перехода на ОЕР
         ret                ;//<---- ^^^^^^ вставить в начало данной секции!
;//-------------------
section '.idata' import data readable
library  msvcrt,'msvcrt.dll',kernel32,'kernel32.dll',user32,'user32.dll'
include  'api\msvcrt.inc'
include  'api\kernel32.inc'
include  'api\user32.inc'

Result.png


Кстати если вскормить этот код аверам, то из 13-ти всего двое посчитали его малварью, хотя на самом деле в нём нет ничего особого, а только крипт. Так-что слепо доверять антивирусам тоже не есть гуд, хотя у каждого своё право.


Aver.png



7. Заключение

Здесь был рассмотрен классический вариант динамического шифрования, ..как говорят фундаментальные основы. В реальных-же программах имеет смысл воспользоваться более продвинутыми методами шифрования, с использованием специальных API-функций из библиотек Advapi32.dll, Bcrypt.dll и прочих. Потренировавшись "на кошках" и освоив эту технику можно смело двигаться дальше и придумать алгоритм, при котором шифруемые процедуры связывались-бы между собой в цепочку так, что одна без другой просто не функционировала-бы. Хорошие результаты даёт и периодический пересчёт контрольной суммы отдельных процедур и всего кода в целом. Это позволит предотвратить всякого рода мод двоичного кода, если вдруг кто-то захочет изменить JZ на JNZ при проверке паролей.

В скрепку кладу исполняемый файл данного исходника – попробуйте погонять его в отладчике и найти пароль. Для тех-кто захочет попрактиковаться в шифровании и собрать исходник своими руками, в скрепке имеется и HEX-редактор "Hiew 8.66", способный редактировать как 32, так и 64-бит приложения. Всем удачи, пока!
 

Вложения

  • DynamicEncrypt.zip
    1,8 МБ · Просмотры: 248

Hardreversengineer

Green Team
20.08.2019
48
26
Судя по скриншотам с результатами детекта антивирусов - они обиделись на инструкцию XOR. Возможно, её тоже можно замаскировать.
 

DragonFly

Green Team
01.06.2020
222
59
Статья, как всегда, на высоте! спасибо!

куда мы предварительно подсунули уже новый EIP. Это в очередной раз доказывает, что процессоры х86 не обладают собственным интеллектом и делают лишь то, на что мы им укажем явно.
на x64 это было бы по-другому? имею в виду реализация
 

Marylin

Mod.Assembler
Red Team
05.06.2019
291
1 267
они обиделись на инструкцию XOR
..думаю не только XOR здесь палится.
Антивирусы гоняют код на своих вирт.машинах и по своим шаблонам (штам) смотрят, что он делает. К примеру вот тот-же XOR, но авер детектит его уже по-иному:

C-подобный:
include 'win32ax.inc'
;//----------
.data
szText  db  'Maintained by Stephen J. Gowdy <[email protected]'
        db  'If you have any new entries, please submit them via'
        db  'http://www.linux-usb.org/usb-ids.html'
txtLen  dd  $ - szText
;//----------
.code
start:  mov     esi,szText
        mov     ecx,[txtLen]
        shr     ecx,2
@@:     xor     dword[esi],0x12345678
        add     esi,4
        loop    @b

        invoke  ExitProcess,0
.end start

xor.png


на x64 это было бы по-другому?

Мой пример для х32, и на машинах х64 он запускается из-под оболочки WOW64 (Windows on Windows), поэтому должен работать исправно. Если-же писать полноценное х64 приложение, то размеры регистров и всех полей в структурах нужно расширять до 8-байт QWORD.
 
Последнее редактирование:

rulet1337

One Level
14.05.2020
3
3
На моей Win10 x64 код исправно отрабатывает (через WOW64).

Посмотреть вложение 54163
Я имел ввиду на бинаре 64 разрядном этого не сделать уже, для safeseh заранее прописываются же все обработчики в структуре заголовка, свой обработчик не добавит насколько я понимаю.
ps: Marylin спасибо огромное тебе за твои статьи, тот случай, когда один человек вывозит весь форум (не в обиду остальным авторам)
 
  • Нравится
Реакции: Marylin
Мы в соцсетях: