Вирусы или программное управление. Часть 3

Вирусы или программное управление? Часть 3

«Мутанты наступают». Метаморфизм

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

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

Если вспомнить, что мы генерируем код декриптора, который максимально независим от того, где и когда он исполняется (не содержит системные вызовы, не обращается к сохраненному состоянию, не содержит сложные объекты), и работает с уже готовыми данными в памяти по известным смещениям, то задача кажется вполне себе разрешимой. На входе у генератора три основных параметра — адрес зашифрованного буфера, его длина и ключ. Ну, пусть будет еще seed для псевдослучайной генерации всяких констант, будущих ключей и т.п. Также декриптор содержит условные переходы, но только в пределах своего тела, что также немного упрощает задачу.

Генерация мусора

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

— следить за смещениями характерных точек (адресов переходов, выходов из цикла и т.п.);
— следить, чтобы мусорный код не испортил необходимые регистры и регистр флагов.

Очень заманчивыми кандидатами на звание мусорных инструкций являются всякие MMX, SSE, floating-point инструкции, их можно легко сгенерировать сколько надо, главное — не трогать стек, не писать в регистры общего назначения и не ломать флаги, необходимые декриптору, и первый метаморфный код выглядит вот так:

        mov ecx, 100h;                 ; декриптор

lbl0:   mov eax, [esi + ecx]                  ; декриптор

        xor eax, edi                          ; декриптор   

        mov [ebx], eax                        ; декриптор

        add ebx, 4h                           ; декриптор

 

        movd mm0,edx                  ; мусор

        movd mm1,eax                   ; мусор

        psubw mm1,mm0                  ; мусор

 

lbl1:   jcxz lbl2                                     ; декриптор, выход из цикла

 

        psubw mm1,mm0                  ; мусор

        movd mm3,ecx                   ; мусор

 

        jmp lbl0                                      ; декриптор, продолжение цикла

lbl2:   sub ebx, 100h                         ; декриптор


Авер не сильно волнуется, т.к. эвристик всё-таки продолжает ругаться на заражённые файлы (работая над генератором, вирмейкеру неохота возиться с серьёзным инфектором), но точно идентифицировать конкретный вирус уже не может. Поэтому тёмной ночью аверу снится инфектор, который не поддается эвристику, и его навязчивой идеей становится необходимость задетектить гада со 100% точностью. Чтобы точно идентифицировать вирус, детектор надо дорабатывать — теперь необходимо, начав с точки входа, шагать по инструкциям, пропускать все мусорные и добавлять в анализируемые только значимые, а это означает, что дизассемблер в детекторе начинает расти. Если вы помните про NOP зоны в абзаце про пермутацию, то пропуск NOP-ов при набивании буфера для сравнения по сигнатуре, фактически, и есть первый подход к сняряду — детектор пропускает NOP-ы, как мусорные инструкции. Теперь авер вместо сравнения с 0x90 (опкод NOP) использует дизассемблер (чем быстрее, тем лучше), который:

  1. Сдвигает указатель на начало следующей инструкции (дизассемблер длин).
  2. Говорит, является ли данная инструкция мусорной (NOP, MMX, SSE и т.п.).
  3. Значимые инструкции добавляет в анализируемый буфер.
  4. В случае безусловного перехода помечает адрес перехода, как следующий анализируемый.
  5. В случае условного перехода помечает обе возможных ветки кода для дальнейшего анализа.


Таким образом авер собирает буфер из инструкций, которые составляют основной код декриптора, и уже в нём может провести сравнение по сигнатуре. Это пока еще довольно быстрая процедура, но, программируя её, авер все больше волнуется: «всегда ли я смогу отличить мусорную инструкцию от значимой?» Вирмейкер, чувствуя это, дорабатывает свой генератор мусора. Теперь он зовет на помощь инструкции сохранения контекста: pushad/popad (положить или достать со стека все регистры общего назначения) и pushfd/popfd (то же самое для регистра флагов).

<pre>

        mov ecx, 100h;         ; декриптор

lbl0:   mov eax, [esi + ecx]           ; декриптор

        xor eax, edi                   ; декриптор   

        mov [ebx], eax                 ; декриптор

        add ebx, 4h                    ; декриптор

 

        pushad                         ; сохраняем регистры

        pushfd                         ; сохраняем флаги

        mov eax, 12321h        ; мусор

 

        xor edx,edx                    ; делаем что хотим

        sub eax, esi                 ; продолжаем мусорить

        popfd                          ; восстанавливаем флаги

        popad                          ; восстанавливаем регистры

 

lbl1:   jcxz lbl2                             ; декриптор, выход из цикла

 

        pushad                         ; сохраняем регистры

        pushfd                         ; сохраняем флаги

        shr ebx, 4                            ; мусор

        popfd                          ; восстанавливаем флаги

        popad                          ; восстанавливаем регистры

 

        jmp lbl0                              ; декриптор, продолжение цикла

lbl2:   sub ebx, 100h                  ; декриптор

</pre>



Теперь дизассемблер анализатора должен следить не только за тем какие инструкции он анализирует, но и находятся ли они в области «делаем что хотим». А это означает, что у дизассемблера появляются глобальные переменные, хранящие информацию о том, в каком месте программы мы находимся. Все становится еще интереснее. Ну а вообще, инструкции сохранения контекста для любого реверс-инженера как красная тряпка для быка — при анализе исполняемых файлов любая встреча с такой инструкцией означает «скорее ставь сюда breakpoint!».

Следующей итерацией в развитии метаморфного кода является генерация необходимого действия различными способами при помощи различных арифметических операций и всяких ассемблерных хитростей. Типа того:

«базовая инструкция»

сгенерированный код 1

сгенерированный код 2

virt_mov eax, 10h

mov eax, 20h;
sub eax, 10h;

mov edx, 10h;
mov eax, edx;

virt_mov ecx, 08h

xor ecx,ecx;
add ecx, 08h;;

mov ecx, 04h;
add ecx, 04h;

virt_sub eax, ecx

neg ecx;
add eax, ecx;

mov edx, ecx;
sub eax, edx;

Например, так можно работать со всеми константами: предположим в «базовом коде» лежат две инструкции «virt_mov edx, 10h» и «virt_mov ecx, 100h». Тогда генерируя новый код, движок выбирает случайную константу, например, «50h», и использует ее для работы со всеми абсолютными значениями, и «virt_mov edx, 10h» мутирует в «mov edx, 50h; sub edx, 40h;», a «virt_mov ecx, 100h» в «mov ecx, 50h; add ecx, B0h». Различные константы порождают различные байт-паттерны, что вынуждает авера добавлять в дизассемблер всё больше логики, реализовывать wildcards в сигнатурах по инструкциям, делая для инструкций что-то наподобие «mov eax, <wildcard-константа>; <skip мусор>; mov ecx, <wildcard-константа>». Это уже не очень просто, и уже не очень быстро, и вообще пахнет жареным…

После анализа кода детектора, помимо констант, для модификации данных в инструкциях, вирмейкер теперь хочет менять и весь набор регистров, используемый в декрипторе. Чтобы позволить себе такое, необходимо использовать разделение регистров — часть регистров являются рабочими для декриптора, а остальные — для генератора мусора. В этом случае генератор мусора не трогает рабочие регистры, а также не портит регистр флагов. Например, весь декриптор может работать только с eax, edx и esi. Тогда все порождённые генератором инструкции должны работать только с ebx,ecx, edi и не менять флаги. При этом, набор регистров должен изменяться в каждом новом поколении вируса.

. . .

mov eax, 10h   ; декриптор

 

mov ebx, 20h   ; декриптор, ebx - мусорный регистр, его можно испортить командой xchg

xchg    edx, ebx       ; для того, чтобы загрузить 20h в регистр edx 

 

xor ecx,ecx            ; мусор

inc ebx        ; мусор

add ecx,ebx            ; мусор

add eax, edx   ; декриптор

mov edx, [esi] ; декриптор

xchg edi,ebx   ; мусор

cmp edx, 0             ; декриптор

. . .                  ;


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

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

«базовая инструкция»

сгенерированный код 1

сгенерированный код 2

virt_push eax

sub esp, 04h;
mov [esp], eax;

mov edx, esp;
sub edx, 04h;
mov [edx], eax;

virt_mov eax, ebx

lea eax,[ebx];

push ebx;
xchng eax,ebx;
pop ebx;

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

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

 

«Наш ответ Чемберлену». Эмуляция

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

Вспомним, как крекеры снимают навесную защиту исполняемых файлов, остановив ее на первой инструкции в момент, когда работоспособный распакованный код лежит в памяти (на Original Entry Point). Для понимания того, как эмулятор может помочь детектору добраться до вкусного, зашифрованного payload кода я коротко опишу один простой, но действенный способ остановить программу на OEP. Основан он на том, что в момент старта основной программы указатель стека должен быть установлен в своё изначальное значение, т.к. на стеке лежат данные, относящиеся к окружению программы, агрументы, переменные среды и т.п. Поэтому, можно быть уверенным, что после того, как защита отрботает, esp вернётся к тому значению, которое было установлено в начале. Крекер останавливает программу прямо в точке входа, запоминает значение указателя стека esp и ставит условный breakpoint, который остановит программу в тот момент, когда esp станет равным тому самому значению, которое было зафиксировано на момент старта. С большой вероятностью именно в этот момент он будет находиться на OEP (ну или в корне декриптора с точки зрения вложенности функций). Декриптор вируса (если он использует стек, конечно) также должен вернуть указатель стека на место, и код детектора, бегущий по инструкциям, может следить за этим указателем, заведя переменную cur_esp и изменяя её каждый раз, когда встречает инструкции, меняющие esp.

. . .                  ; base_esp = cur_esp; 

push eax               ; cur_esp -= 4;

mov eax, 1h            ; -

push edx               ; cur_esp -= 4;

. . .                  ; -

pop edx                ; cur_esp += 4;

pop eax                ; cur_esp += 4; (cur_esp == base_esp) !!!

. . .                  ; здесь возможно отработал декриптор или весь вирус


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

Остался пустячок — автоматизировать этот процесс. Как я уже упоминал, эмулятор — это софтовая модель процессора, исполняющего код нашего файла. Причем процессора-халявщика, потому что ему не надо писать в память, делать ввод-вывод и вообще, ему интересно только то, что позволит остановить программу в нужном месте. Он не умеет MMX, SSE и вообще, чем меньше он умеет (при этом выполняя свою функцию), тем лучше (т.к. халявщик он условный, и весьма тяжеловесен). Предположим, в какой-то момент декриптор кладет на стек строку «BANANAS», зная это авер может исполнять код на виртуальном процессоре постоянно проверяя верхушку стека на наличие этой строки.
Движок эмулятора имеет в себе переменные, соответствующие регистрам, флагам, память под эмуляцию стека и т.п. Я намеренно оставил блоки между pushad/popad, чтобы продемонстрировать, что эмулятор может пропускать в том числе и блоки кода, а не отдельные инструкции, т.к. эмуляция — процедура не из простых. Вот как оно примерно работает (пусть по адресу в ESI лежит этот самый «BANANAS\0»).

        mov ecx, 0h;                  ; ecx_var = 0;

lbl0:   mov eax, [esi + ecx]           ; esi и eax мы знаем (из прошлой эмуляции),

                                              ; поэтому загрузим из правильного места в памяти

                                              ; указатель на "BANANAS"

        xor eax, edi                   ; eax_var = eax_var XOR edi_var;     

        push eax                              ; esp_var -= 4; *esp_var = eax_var; 

        pushad                         ; включаем режим безделья

        pushfd                         ; skip

        mov eax, 12321h        ; skip

 

 

        xor edx,edx                    ; skip

        sub eax, esi                 ; skip

        popfd                          ; skip

        popad                          ; выключаем режим безделья

 

                                                             ; "качественные" мусорные инструкции

                                                             ; эмулятор не знает об этом

                                                             ; и вынужден исполнять их виртуально

        mov edx, 23h                   ; edx_var = 23h;                     

        or edx, eax;                   ; edx_var = edx_var OR eax_var;

 

lbl1:   inc ecx                        ; ecx_var++;

        cmp ecx, 8h;                   ; if (ecx_var == 8) { goto lbl2; }:

 

        pushad                        ; включаем режим безделья

        pushfd                         ; skip

        shr ebx, 4                            ; skip

        popfd                          ; skip

        popad                          ; выключаем режим безделья

 

        jmp lbl0                              ; goto lbl0

lbl2:   sub ebx, 100h                  ; на стеке лежит "BANANAS" - попался!


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

Философские вопросы

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

Рассматривая шифрование буфера с основным кодом вируса мы говорили об изменении вируса с точки зрения байт — т.е. менялся набор байт, составляющих вирус. Это самый примитивный уровень, отдельный байт несет немного информации о свойствах вируса и изменениями на этом уровне не добиться вариативности поколений. Если проводить аналогию с развитием жизни, это напоминает многообразие простых химических соединений. Множество простых соединений, в различных комбинациях, вода, аммиак, углекислый газ, кислотные остатки и гидроксильные группы — в течение миллионов лет этот коктейль не мог породить ничего сложного. Но в конце концов удачные комбинации привели к появлению сложных органических молекул — основы жизни.

Рассматривая мутацию декриптора с помощью метаморфного генератора мы в общем рассматривали вирус уже как набор инструкций, а не байт. Это важный факт, означающий, что теперь мы работаем с информационными элементами следующего порядка. Теперь мы работаем с «обнуляем eax» вместо того, чтобы вдаваться в подробности, как мы это делаем (xor eax,eax или sub eax, eax) и все наши дизассемблеры-детекторы — это замена детектирования по байтам детектированием по цепочке инструкций. Заменяя одну инструкцию другой, мы меняем в том числе и набор байт, т.е. этот уровень включает в себя предыдущий, и, с точки зрения эволюции видов, он куда более продвинут. Изменением набора инструкций можно добиться более целенаправленной вариативности, нежели тупым псевдо-случайным «перемешиванием» байт. Биологическим аналогом, наверное, могли бы выступить аминокислоты, каждая из которых уже сама по себе способна порождать некоторое биологическое действие, при этом они целостны, умеют комбинироваться в более сложные структуры и несут в себе больший квант информации, нежели простейшие соединения.

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

Ну а выше только мутация на уровне общего алгоритма программы. Например вирус, который сейчас детектируется антивирусом в следующем поколении становится вообще другой программой, хорошей и пушистой, и антивирусу его просто не нужно детектировать, хехе. Хотелось бы растечься мыслью, как в прошлом абзаце, но… Это фантастика.

Бабло побеждает зло

Ну и где это всё, спросите вы? Где тысячи страшных метаморфных генераторов, заполонивших компы пользователей, отправившие в психиатрические клиники сотни программистов антивирусных компаний, где жуткие вирусные эпидемии кладущие сеть на недели, где это всё? По моему мнению причин тут несколько.

Первая причина, техническая — это ограничения среды. До NT-шного ядра, NTFS и Linux на домашних PC коду вирусов жилось очень привольно — пиши куда хочешь, исполняйся где хочешь. Сейчас использовать зараженную систему намного сложней и не так уж интересно. Нет, я не хочу сказать, что всё потеряно, но о былом могуществе осталось только мечтать и с каждым годом ситуация всё хуже — права на файлы и процессы, подписи файлов, online-валидация запускаемого софта — все это практически убило «чистокровные» компьютерные вирусы. Но кто знает, по отчетам рынок мобильных зараз растет весьма активно, не означает ли это, что мобильным разработчикам придется пройти по тем же граблям, что и разработчикам больших ОС? Очень надеюсь, что это не так, и технологии защиты в полной мере перекочевали на мобильные устройства.

Ну а главная, как мне кажется, причина — это квалификация программиста. Если вы в состоянии написать хороший метаморфный генератор, с которым специалисты провозятся хотя бы несколько дней, или эмулятор детектирующий сигнатуру внутри качественного вирусного движка, или сделать качественный crackme, который публично зауважают, то… просто напишите мне. Я не рекрутер, но если вас будет много — сменю профессию, просто представляя вас компаниям, которые занимаются безопасностью. Будьте уверены, ваши доходы и стабильность по жизни во много раз перевесят то, что можно получить распространяя вирусы или взламывая софт. В этом и есть главная причина — продвинутые движки пишут очень немногие энтузиасты с очень хорошей подготовкой, и, в большинстве случаев, находят в жизни куда более привлекательные применения своему таланту. Ну а еще, по моему мнению, коммерческая часть современной заразы откровенно скучна, поэтому те, кто поддался соблазну получать ворованные деньги, надолго зависают в сугубо прикладных аспектах и бросают заниматься совершенствованием внутренних алгоритмов.

Эпилог

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

Категория: Вирусы, банеры | Добавил: masterov (13.09.2016) E W
Просмотров: 321 | Теги: тело вируса, исполняемый код, компьютер, вирус | Рейтинг: 0.0/0
Всего комментариев: 0
Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]