MetaMorphing?

Aug 10, 2008 03:31

Кое-что о метаморфинге.

DISCLAIMER.
Скажу сразу, что я не пытался создать самый лучший метаморфер, который спасет любую malware от сигнатурного сканирования. Если угодно, это -- "проба пера". Просто мне пришла довольно забавная (на мой взгляд) мысль, как можно изменить код программы не изменяя ее алгоритма. О чем и пойдет речь далее. Отмечу заранее: это не готовый метаморф. Более того, его сложно (или даже невозможно) применить к уже существующему коду, но можно использовать при написании кода с нуля. По этой же причине я избегаю слов "морфить", "морфинг" или "морфер", т.к., во-первых, до реального морфинга моему коду еще далеко, во-вторых мне просто не очень нравятся эти слова. Впрочем, местами я это правило нарушаю. Отмечу, также, что от вас требуется хотя бы базовое знание формата инструкций x86.

Цимус.
Смысл идеи очень прост: взять целевой код, дизассемблировать его и однозначно переставить используемые в программе регистры. Так, например, eax станет ecx, ecx -- edx и т.д. Основное правило такого изменения -- регистры должны переставляться один к одному, т.е. нельзя взять регистры eax и ecx и превратить оба в edx. Если представить перестановки в виде графа, где вершинами будут регистры, а путями показано на какой регистр изменится вершина-источник, то ни в одну вершину графа не должны вести два или более путей из разных вершин. Таким образом, для восьми регистров количество всех вариантов перестановок будет (8!), что довольно много. Однако, по разным причинам переставлять все 8 регистров нельзя, что существенно сокращает количество перестановок.

Проблемы.
Даже такой простой морфинг имеет несколько неприятных проблем:
  1. Многие команды не кодируют в себе регистры, которые они используют, а подразумевают их своим кодом операции. Например, цепочечные команды (movs, etc) работают с регистрами esi/edi/ecx, команды mul/div с eax/edx и изменить такое поведение невозможно. Выход из такой ситуации -- либо вообще их не использовать, либо написать их аналоги используя другие команды.

Регистры и привязанные к ним инструкции:
eax: mul, div, imul, idiv, lods.
ecx: loop, shl, shr, movs, cmps, scas, stos, lods.
edx: mul, div, imul, idiv.
ebx: святой регистр. Его не использует неявно ни одна команда.
esp: push, pop, pushad, popad, pushf, popf, call, ret, iret.
ebp: еще один святой регистр, но часто используется в качестве указателя на стековый кадр. Нам, однако, это не мешает.
esi: movs, cmps, scas, lods.
edi: movs, cmps, stos.

2. К сожалению нельзя переставлять регистры по принципу "любой в любой". Это ограничение связано с кодированием регистров ah/ch/dh/bh: регистры esp, ebp, esi и edi не имеют байтовых регистров bpl, spl и т.д. Поэтому al/ch/dh/bh используют коды esp/ebp/esi/edi. Например, ah имеет код esp, ch -- ebp и т.д. В результате, если мы будем переставлять регистры в произвольном порядке, то можем столкнуться с такой ситуацией:

исходный код:
mov edx, very_very_important_value
mov al, [ebx]

Допустим, что регистр eax становится регистром esi. Тогда исходный код примет вид:

mov edx, very_very_important_value
mov dh, [ebx]

что безнадежно испортит значение регистра edx. Единственный, на мой взгляд, выход -- разделить регистры на две группы, первая включает в себя регистры al(eax), cl(ecx), dl(edx) и bl(ebx), вторая -- ah(esp), ch(ebp), dh(esi) и bh(edi). Перестановка регистров может происходить только в пределах своей группы, что, к сожалению, уменьшает число вариантов с 8! до 4! * 4!

Помимо этого есть еще несколько мелочей, которые могут сильно испортить нам жизнь:
  1. Вызов внешней ф-ии. Т.к. внешняя ф-ия (например, вызов API) всегда возвращает значение в регистре eax, то необходимо позаботиться о том, чтобы правильно передать возвращенное в eax значение в регистр, который заместил eax в нашей программе. Кроме того, нельзя полагаться на сохранение вызываемой ф-ией регистров ebp, ebx, esi и edi, т.к. то, что было регистром ebx может стать, например, регистром ecx и будет испорчено вызовом внешней функции.
  2. Регистр eax очень любим фирмой Intel. В результате некоторые команды, например, 'and', имеют сокращенный код инструкции когда получателем является al/ax/eax. Так, инструкция and al, 0x1 может иметь две формы: короткую '0x24 0x01' и длинную '0x80 0xE0 0x1'. Для начала мы решим эту проблему с помощью макросов, а потом и более радикально -- модификацией ассемблера.
  3. Регистр esp указывает на вершину стека и заменять его невозможно, если мы хотим работать со стеком с помощью команд push/pop или просто вызывать процедуру. Кроме того, esp не может быть индексным регистром в инструкциях, использующих SIB. В результате, количество перестановок снижается до 4! * 3!
  4. Инструкции, использующие регистр ebp в качестве адресного регистра кодируются несколько иначе чем остальные инструкции. Если поле MOD байта MODRM имеет значение 00b и поле RM имеет значение 101b (код ebp), то такая комбинация обозначает адресацию [disp32]. Когда значение ebp используется как адрес, то оно кодируется формой [ebp + 0x0], т.е. поле MOD имеет значение 01b, а следом за инструкцией должен идти байт disp8. Значит, чтобы свободно менять адресные регистры на ebp необходимо, чтобы все инструкции типа 'command [reg], ...' имели форму 'command [reg + 0x0], ...'. Это же правило касается и инструкций, имеющих байт SIB. Единственный в этом случае выход -- модификация ассемблера так, чтобы он генерировал нужные нам формы инструкций.

Ко всему этому хотелось бы добавить, что морфер не умеет отличать код от данных. Это придется учитывать при написании программ.

Немного об алгоритме.
Алгоритм, реализующий перестановки довольно прост. Он скорее показывает, как это работает, чем преследует какие-то реальные цели. Первым делом он подготавливает данные, используемые для перестановок. Затем выделяет память и копирует себя в выделенный участок. После этого выполняет перестановку в новой копии и передает ей управление. Процесс повторяется до тех пор, пока не закончится память. Разбор и модификация инструкций осуществляется в файле m.asm. Подготовкой перестановок занимается permutate.asm (нам он практически не интересен). Для первичного дизассемблирования используется дизассемблер длин от z0mbie. Затем выполняется дополнительный разбор и модификация инструкций.
Алгоритм разбора и модификации показан на блок-схеме:

Теперь, получив общие сведения о том, как это работает, рассмотрим конкретную реализацию. Для начала мы будем переставлять только пять регистров из семи возможных. Три регистра из "младшей" (eax, ecx, edx, ebx) группы и два из "старшей" (esp, ebp, esi, edi). Элементы массива allowed_regs содержат битовую маску для каждой группы. Битовая маска разрешает или запрещает изменение конкретного регистра в группе. Маска кодирует регистры в порядке их следования. Например, eax имеет код 000b, соответственно, запрещает или разрешает его изменение нулевой бит нулевого элемента массива, ecx -- первый бит нулевого элемента и т.д. Остальные данные используются перестановщиком и особо нас не интересуют. В первой версии разрешены перестановки только (ecx, edx, ebx) и (esi, edi). Для запсука исполняемого необходимо скомпилировать дизассемблер длин как dll с именем lde32. lde32.dll должна импортировать символы: '_disasm@4', 'disasm_len', 'disasm_flag', 'disasm_opcode' и 'disasm_opcode2'.
Итак, код:
format PE GUI 4.0 include 'win32a.inc' entry start C_66 =0x00000001 ; 66-prefix C_67 =0x00000002 ; 67-prefix C_LOCK =0x00000004 ; lock C_REP =0x00000008 ; repz/repnz C_SEG =0x00000010 ; seg-prefix C_OPCODE2 =0x00000020 ; 2nd opcode present (1st==0F) C_MODRM =0x00000040 ; modrm present C_SIB =0x00000080 ; sib present rounds: db 0xFF db 0xFF fucks: db 0x6 db 0x2 allowed_regs: db 00001110b db 00001100b curr_grp: db 0x0 prev_map: db 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7 curr_map: db 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7 start: mov esi, polymorph polymorph: push esi call create_map push PAGE_EXECUTE_READWRITE push MEM_RESERVE or MEM_COMMIT push 0x1000 push 0x0 call [VirtualAlloc] pop esi mov ebx, eax add ebx, CODE_SIZE mov edi, eax mov ecx, CODE_SIZE @@: mov dl, [esi] mov [edi], dl inc esi inc edi dec ecx jnz @b mov esi, eax .morph: push esi push ebx push esi call [disasm] or eax, eax jnz @f call err_disasm @@: pop ebx pop esi ;Here we have to decide, have we MODRM byte, or not. ; If not -- just check if the instruction defines 'reg' ; field in its opcode. mov eax, [disasm_flag] mov eax, [eax] test eax, C_MODRM jnz .modrm_present ;There is no MODRM. Well, let's check if the instruction ; contains 'reg' field in its opcode. test eax, C_OPCODE2 jnz @f mov ecx, [disasm_opcode] mov cl, [ecx] jmp .e_opcode2 @@: mov ecx, [disasm_opcode2] mov cl, [ecx] .e_opcode2: cmp cl, 0x40 jb .next cmp cl, 0x60 jb .reg_present cmp cl, 0x91 jb .next cmp cl, 0x98 jb .reg_present cmp cl, 0xB0 jb .next cmp cl, 0xC0 jb .reg_present cmp cl, 0xC8 jb .next cmp cl, 0xD0 jae .next .reg_present: ;Here we have to patch the instruction mov eax, esi call get_opcode_offset mov edi, eax movzx eax, byte [edi] and al, 0x7 push eax call map_reg and byte [edi], 11111000b or byte [edi], al jmp .next .modrm_present: ;MODRM present. Let's deal with it. Some instructions may contain opcode extension in 'reg' field of MODRM byte. ; If 'reg' extends opcode, we do not morph it. mov eax, esi call get_opcode_offset mov edi, eax mov eax, [disasm_flag] test byte [eax], C_OPCODE2 jnz .opcode2 cmp byte [edi], 0x80 jb .reg_is_reg cmp byte [edi], 0x83 jbe .check_sib cmp byte [edi], 0x8F jz .check_sib cmp byte [edi], 0xC0 jz .check_sib cmp byte [edi], 0xC1 jz .check_sib cmp byte [edi], 0xC6 jz .check_sib cmp byte [edi], 0xC7 jz .check_sib cmp byte [edi], 0xD0 jb .reg_is_reg cmp byte [edi], 0xD4 jbe .check_sib cmp byte [edi], 0xF6 jz .check_sib cmp byte [edi], 0xF7 jz .check_sib cmp byte [edi], 0xFE jz .check_sib cmp byte [edi], 0xFF jz .check_sib jmp .reg_is_reg .opcode2: cmp byte [edi], 0x0 jz .check_sib cmp byte [edi], 0x1 jz .check_sib cmp byte [edi], 0x18 jz .check_sib cmp byte [edi], 0x71 jb .reg_is_reg cmp byte [edi], 0x73 jbe .check_sib cmp byte [edi], 0xAE jz .check_sib cmp byte [edi], 0xB9 jz .check_sib cmp byte [edi], 0xBA jz .check_sib cmp byte [edi], 0xC7 jz .check_sib .reg_is_reg: movzx eax, byte [edi + 0x1] shr al, 0x3 and al, 0x7 push eax call map_reg shl al, 0x3 and byte [edi + 0x1], 11000111b or [edi + 0x1], al .check_sib: ;If MODRM present, it is possible that SIB is used. Let's check it. If SIB is present, ; we leave R/M field unchanged. mov eax, [disasm_flag] test byte [eax], C_SIB jz .no_sib ;SIB is here. Let's deal with it. movzx eax, byte [edi + 0x2] and al, 0x7 push eax call map_reg and byte [edi + 0x2], 11111000b or byte [edi + 0x2], al movzx eax, byte [edi + 0x2] shr al, 0x3 and al, 0x7 push eax call map_reg shl al, 0x3 and byte [edi + 0x2], 11000111b or byte [edi + 0x2], al jmp .next .no_sib: ;There is no SIB. Let's change the R/M field. movzx eax, byte [edi + 0x1] and al, 0x7 push eax call map_reg and byte [edi + 0x1], 11111000b or byte [edi + 0x1], al .next: mov eax, [disasm_len] mov eax, [eax] add esi, eax cmp esi, ebx jb .morph .switch: sub esi, CODE_SIZE mov edi, esi jmp esi get_opcode_offset: mov edx, [disasm_flag] mov edx, [edx] test edx, C_66 jz @f inc eax @@: test edx, C_66 jz @f inc eax @@: test edx, C_67 jz @f inc eax @@: test edx, C_LOCK jz @f inc eax @@: test edx, C_REP jz @f inc eax @@: test edx, C_SEG jz @f inc eax @@: test edx, C_OPCODE2 jz @f inc eax @@: ret err_disasm: push 0x0 push 0x0 push str_disasm_err push 0x0 call [MessageBox] push 0x1 call [ExitProcess] ret include 'permutate.asm' CODE_SIZE = $ - polymorph str_disasm_err: db 'Disassembling error.', 0x0 data import library kernel32, 'KERNEL32.DLL',\ user32, 'USER32.DLL',\ lde32, 'LDE32.DLL' import kernel32, ExitProcess, 'ExitProcess',\ VirtualAlloc, 'VirtualAlloc' import user32, MessageBox, 'MessageBoxA' import lde32, disasm, '_disasm@4',\ disasm_len, 'disasm_len',\ disasm_flag, 'disasm_flag',\ disasm_opcode, 'disasm_opcode',\ disasm_opcode2, 'disasm_opcode2' end data Подключаемый файл permutate.asm выглядит так:
;void create_map() create_map: push ebp mov ebp, esp sub esp, 0x4 call copy_map call get_next_round call fill_map movzx ecx, byte [curr_grp] push 0x0 lea eax, [ebp - 0x4] push eax lea eax, [curr_map + ecx * 4] push eax call prepare_map movzx ecx, byte [curr_grp] movzx edx, byte [rounds + ecx] push edx push eax lea eax, [ebp - 0x4] push eax call permutate movzx ecx, byte [curr_grp] push 0x1 lea eax, [ebp - 0x4] push eax lea eax, [curr_map + ecx * 4] push eax call prepare_map add esp, 0x4 pop ebp ret ;void copy_map() copy_map: mov esi, curr_map mov edi, prev_map mov ecx, 0x8 .l00p: mov al, [esi] mov [edi], al inc esi inc edi dec ecx jnz .l00p ret ;void fill_map() fill_map: mov esi, curr_map movzx ecx, byte [curr_grp] lea eax, [ecx * 4] mov dl, al add dl, 0x4 .l00p: cmp al, dl jae @f mov [esi + eax], al add al, 0x1 jmp .l00p @@: ret ;byte map_reg(byte reg) map_reg: mov ecx, prev_map mov dl, [esp + 0x4] xor eax, eax .l00p: cmp eax, 0x8 jae .e_l00p cmp [ecx + eax], dl jz .e_l00p add eax, 0x1 jmp .l00p .e_l00p: mov ecx, curr_map mov al, [ecx + eax] ret 0x4 ;void prepare_map(byte *curr_map, byte *temp_map, bool direction) prepare_map: xor eax, eax mov ecx, 0x8 movzx ebx, byte [curr_grp] .l00p: test ecx, [allowed_regs + ebx] jz @f bsr ecx, ecx test dword [esp + 0xC], 0x1 jnz .to_map mov esi, [esp + 0x4] mov edi, [esp + 0x8] mov dl, [esi + ecx] mov [edi + eax], dl jmp .fi .to_map: mov esi, [esp + 0x8] mov edi, [esp + 0x4] mov dl, [esi + eax] mov [edi + ecx], dl .fi: xor edx, edx bts edx, ecx mov ecx, edx add eax, 0x1 @@: shr ecx, 0x1 jnz .l00p .locret: ret 0xC ;void get_next_round() get_next_round: mov byte [curr_grp], 0x0 .l00p: movzx ecx, byte [curr_grp] cmp ecx, 0x2 jb @f mov ecx, 0x1 mov [curr_grp], cl @@: add byte [rounds + ecx], 0x1 mov dl, [rounds + ecx] cmp dl, [fucks + ecx] jb .locret mov byte [rounds + ecx], 0xFF add byte [curr_grp], 0x1 jmp .l00p .locret: ret ;void permutate(byte *map, dword len, dword round) permutate: push ebp mov ebp, esp sub esp, 0x10 mov esi, [ebp + 0x8] mov edi, [ebp + 0xC] mov eax, [ebp + 0x10] mov [ebp - 0x8], eax mov ecx, 0x1 .l00p: cmp ecx, edi jae .e_l00p push esi ; push edi ;store used registers on stack push ecx ; push ebp ; push ecx lea eax, [ebp - 0x8] push eax call divide pop ebp ;restore used registers pop ecx push ecx ;and push them back on stack push ebp add ecx, 0x1 push ecx mov eax, [ebp - 0x8] mov [ebp - 0x10], eax lea eax, [ebp - 0x10] push eax call divide pop ebp pop ecx pop edi pop esi mov edx, [ebp - 0xC] mov al, [esi + edx] xchg al, [esi + ecx] mov [esi + edx], al add ecx, 0x1 jmp .l00p .e_l00p: add esp, 0x10 pop ebp ret 0xC ;Well, this is a VERY simple algorithm! ;void divide(qword quotient, dword divisor) divide: mov esi, [esp + 0x4] mov eax, [esi] mov edx, [esp + 0x8] or edx, edx jnz @f ;raise division by zero exception: xor eax, eax div eax @@: cmp edx, 0x1 jnz @f mov dword [esi + 0x4], 0x0 jmp .locret @@: xor ecx, ecx .l00p: cmp eax, edx jb .e_l00p sub eax, edx add ecx, 0x1 jmp .l00p .e_l00p: cmp dword [esi], 0x0 jz @f mov [esi + 0x4], eax jmp .fi @@: mov dword [esi + 0x4], 0x0 .fi: mov [esi], ecx .locret: ret 0x8 Замечу, что исходник специально написан немного "неоднородно", например, иногда используется 'add eax, 0x1', иногда 'inc eax'. Я старался использовать как можно больше разных форм инструкций для тестирования дизассемблера.

Перестановка eax.
Как я уже говорил выше, перестановка eax требует дополнительных усилий. Вызов внешней ф-ии помещает результат в al/ax/eax, а это не совсем то, что нам нужно. Обойти это препятствие можно таким маневром:

pushad
push arg
...
call [AnyAPIFunction]
push eax
popad
mov eax, [esp - 0x24]

Регистр eax в инструкции 'mov eax, [esp - 0x24]' будет заменен на соответствующий в текущей перестановке регистр, что нам и надо. Кроме того, pushad спасет все регистры от вызываемой внешней ф-ии. Однако, инструкцию 'push eax' необходимо как-то защитить от изменения, т.к. нас интересует значение, находящееся именно в eax. Для этого можно создать список исключений -- инструкции, изменять которые нельзя. Адреса таких инструкций хранятся в массиве. Когда дизассемблер движется по коду, первым условием он проверяет, не находится ли инструкция в списке исключений. В случае, если инструкция -- исключение, то она пропускается. Иначе выполняется обычный алгоритм перестановки. В результате, код вызова внешней ф-ии будет выглядеть так:

pushad
push arg
...
call [AnyAPIFunction]
..ex0:
push eax
popad
mov eax, [esp - 0x24]

Метка ..ex0 должна быть помещена в массив исключений.
Помимо возвращаемых значений мы должны позаботиться об инструкциях, имеющих короткую форму при использовании al/ax/eax. Естественно, использовать такие инструкции нельзя, т.к. в них невозможна перестановка. Как временное решение, я написал несколько макросов, которые заменяют короткие инструкции их длинными формами. Позже мы решим этот вопрос более радикально.
Код теперь выглядит так:
format PE GUI 4.0 include 'win32a.inc' entry start C_66 =0x00000001 ; 66-prefix C_67 =0x00000002 ; 67-prefix C_LOCK =0x00000004 ; lock C_REP =0x00000008 ; repz/repnz C_SEG =0x00000010 ; seg-prefix C_OPCODE2 =0x00000020 ; 2nd opcode present (1st==0F) C_MODRM =0x00000040 ; modrm present C_SIB =0x00000080 ; sib present macro and_al imm* { db 0x82 db 0xE0 db imm } macro mov_al offset* { db 0x8A db 0x5 dd offset } macro mov_eax offset* { db 0x8B db 0x5 dd offset } macro test_eax imm* { db 0xF7 db 11000000b dd imm } macro test_al imm* { db 0xF6 db 11000000b db imm } macro add_al imm* { db 0x80 db 11000000b db imm } rounds: db 0xFF db 0xFF fucks: db 0x18 db 0x2 allowed_regs: db 00001111b db 00001100b curr_grp: db 0x0 prev_map: db 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7 curr_map: db 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7 start: mov esi, polymorph polymorph: push esi call create_map pushad push PAGE_EXECUTE_READWRITE push MEM_RESERVE or MEM_COMMIT push 0x1000 push 0x0 call [VirtualAlloc] ..ex0: push eax add esp, 0x4 popad mov eax, [esp - 0x24] push eax call create_adapters_addrs pop esi mov ebx, eax add ebx, CODE_SIZE mov edi, eax mov ecx, CODE_SIZE @@: mov dl, [esi] mov [edi], dl inc esi inc edi dec ecx jnz @b mov esi, eax .morph: push esi push ebx pushad push esi call [disasm] ..ex1: push eax add esp, 0x4 popad mov eax, [esp - 0x24] or eax, eax jnz @f call err_disasm @@: pop ebx pop esi ;First of all we have to check, does the instruction is an 'adapter' between ; the morphed call and external function. push dword [excludes_size] push excludes push esi call check_adapter test eax, eax jnz .next ;The instruction is not an exception. Then ; here we have to decide, have we MODRM byte, or not. ; If not -- just check if the instruction defines 'reg' ; field in its opcode. ;mov eax, [disasm_flag] mov_eax disasm_flag mov eax, [eax] ;test eax, C_MODRM test_eax C_MODRM jnz .modrm_present ;There is no MODRM. Well, let's check if the instruction ; contains 'reg' field in its opcode. ;test eax, C_OPCODE2 test_eax C_OPCODE2 jnz @f mov ecx, [disasm_opcode] mov cl, [ecx] jmp .e_opcode2 @@: mov ecx, [disasm_opcode2] mov cl, [ecx] .e_opcode2: cmp cl, 0x40 jb .next cmp cl, 0x60 jb .reg_present cmp cl, 0x91 jb .next cmp cl, 0x98 jb .reg_present cmp cl, 0xB0 jb .next cmp cl, 0xC0 jb .reg_present cmp cl, 0xC8 jb .next cmp cl, 0xD0 jae .next .reg_present: ;Here we patch the instruction mov eax, esi call get_opcode_offset mov edi, eax movzx eax, byte [edi] ;and al, 0x7 and_al 0x7 push eax call map_reg and byte [edi], 11111000b or byte [edi], al jmp .next .modrm_present: ;MODRM present. Let's deal with it. Some instructions may contain opcode extension in 'reg' field of MODRM byte. ; If 'reg' extends opcode, we do not morph it. mov eax, esi call get_opcode_offset mov edi, eax ;mov eax, [disasm_flag] mov_eax disasm_flag test byte [eax], C_OPCODE2 jnz .opcode2 cmp byte [edi], 0x80 jb .reg_is_reg cmp byte [edi], 0x83 jbe .check_sib cmp byte [edi], 0x8F jz .check_sib cmp byte [edi], 0xC0 jz .check_sib cmp byte [edi], 0xC1 jz .check_sib cmp byte [edi], 0xC6 jz .check_sib cmp byte [edi], 0xC7 jz .check_sib cmp byte [edi], 0xD0 jb .reg_is_reg cmp byte [edi], 0xD4 jbe .check_sib cmp byte [edi], 0xF6 jz .check_sib cmp byte [edi], 0xF7 jz .check_sib cmp byte [edi], 0xFE jz .check_sib cmp byte [edi], 0xFF jz .check_sib jmp .reg_is_reg .opcode2: cmp byte [edi], 0x0 jz .check_sib cmp byte [edi], 0x1 jz .check_sib cmp byte [edi], 0x18 jz .check_sib cmp byte [edi], 0x71 jb .reg_is_reg cmp byte [edi], 0x73 jbe .check_sib cmp byte [edi], 0xAE jz .check_sib cmp byte [edi], 0xB9 jz .check_sib cmp byte [edi], 0xBA jz .check_sib cmp byte [edi], 0xC7 jz .check_sib .reg_is_reg: movzx eax, byte [edi + 0x1] shr al, 0x3 ;and al, 0x7 and_al 0x7 push eax call map_reg shl al, 0x3 and byte [edi + 0x1], 11000111b or [edi + 0x1], al .check_sib: ;If MODRM present, it is possible that SIB is used. Let's check it. If SIB is present, ; we leave R/M field unchanged. ;mov eax, [disasm_flag] mov_eax disasm_flag test byte [eax], C_SIB jz .no_sib ;SIB is here. Let's deal with it. movzx eax, byte [edi + 0x2] ;and al, 0x7 and_al 0x7 push eax call map_reg and byte [edi + 0x2], 11111000b or byte [edi + 0x2], al movzx eax, byte [edi + 0x2] shr al, 0x3 ;and al, 0x7 and_al 0x7 push eax call map_reg shl al, 0x3 and byte [edi + 0x2], 11000111b or byte [edi + 0x2], al jmp .next .no_sib: ;There is no SIB. Let's change the R/M field. movzx eax, byte [edi + 0x1] ;and al, 0x7 and_al 0x7 push eax call map_reg and byte [edi + 0x1], 11111000b or byte [edi + 0x1], al .next: ;mov eax, [disasm_len] mov_eax disasm_len mov eax, [eax] add esi, eax cmp esi, ebx jb .morph .switch: sub esi, CODE_SIZE mov edi, esi jmp esi get_opcode_offset: mov edx, [disasm_flag] mov edx, [edx] test edx, C_66 jz @f inc eax @@: test edx, C_66 jz @f inc eax @@: test edx, C_67 jz @f inc eax @@: test edx, C_LOCK jz @f inc eax @@: test edx, C_REP jz @f inc eax @@: test edx, C_SEG jz @f inc eax @@: test edx, C_OPCODE2 jz @f inc eax @@: ret ;dword check_adapter(dword addr, dword *array, dword size) check_adapter: mov eax, [esp + 0x4] mov edi, [esp + 0x8] mov ecx, [esp + 0xC] lea edi, [edi + ecx * 4 - 0x4] .l00p: cmp edi, [esp + 0x8] jb .not_found cmp eax, [edi] jnz @f jmp .found @@: sub edi, 0x4 jmp .l00p .found: mov eax, 0x1 jmp .locret .not_found: xor eax, eax .locret: ret 0xC ;void create_adapters_addrs(void *base) create_adapters_addrs: mov edx, [esp + 0x4] add edx, ..ex0 - polymorph mov [excludes], edx mov edx, [esp + 0x4] add edx, ..ex1 - polymorph mov [excludes + 0x4], edx ret 0x4 err_disasm: push 0x0 push 0x0 push str_disasm_err push 0x0 call [MessageBox] push 0x1 call [ExitProcess] ret include 'permutate3.asm' CODE_SIZE = $ - polymorph excludes_size: dd 0x2 excludes: dd 0x0 dd 0x0 str_disasm_err: db 'Disassembling error.', 0x0 data import library kernel32, 'KERNEL32.DLL',\ user32, 'USER32.DLL',\ lde32, 'LDE32.DLL' import kernel32, ExitProcess, 'ExitProcess',\ VirtualAlloc, 'VirtualAlloc' import user32, MessageBox, 'MessageBoxA' import lde32, disasm, '_disasm@4',\ disasm_len, 'disasm_len',\ disasm_flag, 'disasm_flag',\ disasm_opcode, 'disasm_opcode',\ disasm_opcode2, 'disasm_opcode2' end data В файле permutate.asm изменилась только одна строка:
64:
;add al, 0x1
add_al 0x1

Перестановка ebp.
Как ни странно, перестановка ebp требует, пожалуй, самых больших усилий. Чтобы иметь возможность заменить инструкцию типа 'command [reg], ...' на инструкцию 'command [ebp], ...' необходимо, чтобы все инструкции 'command [reg], ...' имели вид 'command [reg + 0x0], ...'. Т.е. значение поля MOD в них должно быть 01b, и после байта MODRM должен идти байт disp8. Конечно, это можно сделать с помощью макросов, но тогда придется переписать на них половину ассемблера. Гораздо проще изменить сам ассемблер, чтобы он генерировал нужные нам формы инструкций. К счастью, FASM довольно прост и нужные нам модификации достигаются всего-лишь комменитрованием нескольких строк (ну и добавлением одной). И раз уж мы модифицируем ассемблер, то неплохо было бы запретить генерацию коротких форм инструкций, использующих al/ax/eax. В коде FASM особый интерес для нас представляет файл x86_64.inc -- именно в нем происходит генерация инструкций. Я взял самую последнюю версию FASM'а на тот момент -- 1.6.27. Генерация инструкции происходит в ф-ии 'store_instruction'. Чтобы FASM всегда генерировал инструкции типа 'command [reg + 0x0], ...' необходимо закомментировать строки 6519 и 6520:
; or edx,edx
; jz simple_address
именно здесь происходит решение, имеет ли инструкция адресацию [reg] или же требует адресации [reg + dispX]. Значение disp находится в edx, и если он равен 0, то происходит переход на метку simple_address. Закомментировав проверку и переход, мы заставляем FASM всегда генерировать инструкцию с адресацией [reg + dispX], допуская форму [reg + 0x0]. Аналогичное поведение требуется и от инструкций, использующих байт SIB. Для этого необходимо закомментировать строки 6439 и 6440:
; cmp bh,5
; je address_value
jmp address_value
добавив сразу после них безусловный переход на метку 'address_value'.
Теперь займемся инструкциями, имеющими короткую форму при использовании регистра eax. Наличие короткой формы замечено за инструкциями: add, adc, and, xor, xchg, mov, or, sbb, sub, cmp, test. Генерацию add, adc, and, xor, or, sbb, sub и cmp контролирует ф-ия basic_instruction. Чтобы отучить ее выбирать короткие формы этих инструкций надо закомментировать строки:
297:
; jz basic_al_imm
и 365:
; jz basic_eax_imm
Теперь разберемся с инструкцией test. Ее генерацию контролирует ф-ия 'test_instruction'. Комментируем строки:
1127:
; jz test_al_imm
1146:
; jz test_ax_imm
и 1167:
; jz test_eax_imm
Инструкция mov тоже обслуживается отдельной функцией -- 'mov_instruction'. Комментируем строки:
464:
; jz mov_mem_ax
471:
; jz mov_mem_al
719:
; jz mov_ax_mem
и 727:
; jz mov_al_mem
И, наконец, инструкция xchg и ее функция xchg_instruction:
1237:
; cmp [postbyte_register],0
1238:
; je xchg_ax_reg
После перекомпиляции с измененным x86_64.inc FASM будет генерировать нужный нам код и мы можем избавиться от надоедливых макросов. Исходный код также претерпел некоторые изменения: добавилась проверка при перестановке ebp -- мы должны проверять поле MOD, чтобы не спутать адресации [ebp] и [disp32].
Код:
format PE GUI 4.0 include 'win32a.inc' entry start C_66 =0x00000001 ; 66-prefix C_67 =0x00000002 ; 67-prefix C_LOCK =0x00000004 ; lock C_REP =0x00000008 ; repz/repnz C_SEG =0x00000010 ; seg-prefix C_OPCODE2 =0x00000020 ; 2nd opcode present (1st==0F) C_MODRM =0x00000040 ; modrm present C_SIB =0x00000080 ; sib present rounds: db 0xFF db 0xFF fucks: db 0x18 db 0x6 allowed_regs: db 00001111b db 00001110b curr_grp: db 0x0 prev_map: db 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7 curr_map: db 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7 start: mov esi, polymorph polymorph: push esi call create_map pushad push PAGE_EXECUTE_READWRITE push MEM_RESERVE or MEM_COMMIT push 0x1000 push 0x0 call [VirtualAlloc] ..ex0: push eax add esp, 0x4 popad mov eax, [esp - 0x24] push eax call create_adapters_addrs pop esi mov ebx, eax add ebx, CODE_SIZE mov edi, eax mov ecx, CODE_SIZE @@: mov dl, [esi] mov [edi], dl inc esi inc edi dec ecx jnz @b mov esi, eax .morph: push esi push ebx pushad push esi call [disasm] ..ex1: push eax add esp, 0x4 popad mov eax, [esp - 0x24] or eax, eax jnz @f call err_disasm @@: pop ebx pop esi ;First of all we have to check, does the instruction is an 'adapter' between ; the morphed call and external function. push dword [excludes_size] push excludes push esi call check_adapter test eax, eax jnz .next ;The instruction is not an exception. Then ; here we have to decide, have we MODRM byte, or not. ; If not -- just check if the instruction defines 'reg' ; field in its opcode. mov eax, [disasm_flag] ;mov_eax disasm_flag mov eax, [eax] test eax, C_MODRM jnz .modrm_present ;There is no MODRM. Well, let's check if the instruction ; contains 'reg' field in its opcode. test eax, C_OPCODE2 jnz @f mov ecx, [disasm_opcode] mov cl, [ecx] jmp .e_opcode2 @@: mov ecx, [disasm_opcode2] mov cl, [ecx] .e_opcode2: cmp cl, 0x40 jb .next cmp cl, 0x60 jb .reg_present cmp cl, 0x91 jb .next cmp cl, 0x98 jb .reg_present cmp cl, 0xB0 jb .next cmp cl, 0xC0 jb .reg_present cmp cl, 0xC8 jb .next cmp cl, 0xD0 jae .next .reg_present: ;Here we patch the instruction mov eax, esi call get_opcode_offset mov edi, eax movzx eax, byte [edi] and al, 0x7 push eax call map_reg and byte [edi], 11111000b or byte [edi], al jmp .next .modrm_present: ;MODRM present. Let's deal with it. Some instructions may contain opcode extension in 'reg' field of MODRM byte. ; If 'reg' extends opcode, we do not morph it. mov eax, esi call get_opcode_offset mov edi, eax mov eax, [disasm_flag] test byte [eax], C_OPCODE2 jnz .opcode2 cmp byte [edi], 0x80 jb .reg_is_reg cmp byte [edi], 0x83 jbe .check_sib cmp byte [edi], 0x8F jz .check_sib cmp byte [edi], 0xC0 jz .check_sib cmp byte [edi], 0xC1 jz .check_sib cmp byte [edi], 0xC6 jz .check_sib cmp byte [edi], 0xC7 jz .check_sib cmp byte [edi], 0xD0 jb .reg_is_reg cmp byte [edi], 0xD4 jbe .check_sib cmp byte [edi], 0xF6 jz .check_sib cmp byte [edi], 0xF7 jz .check_sib cmp byte [edi], 0xFE jz .check_sib cmp byte [edi], 0xFF jz .check_sib jmp .reg_is_reg .opcode2: cmp byte [edi], 0x0 jz .check_sib cmp byte [edi], 0x1 jz .check_sib cmp byte [edi], 0x18 jz .check_sib cmp byte [edi], 0x71 jb .reg_is_reg cmp byte [edi], 0x73 jbe .check_sib cmp byte [edi], 0xAE jz .check_sib cmp byte [edi], 0xB9 jz .check_sib cmp byte [edi], 0xBA jz .check_sib cmp byte [edi], 0xC7 jz .check_sib .reg_is_reg: movzx eax, byte [edi + 0x1] shr al, 0x3 and al, 0x7 push eax call map_reg shl al, 0x3 and byte [edi + 0x1], 11000111b or [edi + 0x1], al .check_sib: ;If MODRM present, it is possible that SIB is used. Let's check it. If SIB is present, ; we leave R/M field unchanged. mov eax, [disasm_flag] ;mov_eax disasm_flag test byte [eax], C_SIB jz .no_sib ;SIB is here. Let's deal with it. movzx eax, byte [edi + 0x2] and al, 0x7 cmp al, 0x5 jnz .no_ebp_sib test byte [edi + 0x1], 0xC0 jz .morph_index .no_ebp_sib: push eax call map_reg and byte [edi + 0x2], 11111000b or byte [edi + 0x2], al .morph_index: movzx eax, byte [edi + 0x2] shr al, 0x3 and al, 0x7 push eax call map_reg shl al, 0x3 and byte [edi + 0x2], 11000111b or byte [edi + 0x2], al jmp .next .no_sib: ;There is no SIB. Let's change the R/M field. movzx eax, byte [edi + 0x1] and al, 0x7 cmp al, 0x5 jnz .no_ebp_rm test byte [edi + 0x1], 0xC0 jz .next .no_ebp_rm: push eax call map_reg and byte [edi + 0x1], 11111000b or byte [edi + 0x1], al .next: mov eax, [disasm_len] mov eax, [eax] add esi, eax cmp esi, ebx jb .morph .switch: sub esi, CODE_SIZE mov edi, esi mov ebp, edi jmp esi get_opcode_offset: mov edx, [disasm_flag] mov edx, [edx] test edx, C_66 jz @f inc eax @@: test edx, C_66 jz @f inc eax @@: test edx, C_67 jz @f inc eax @@: test edx, C_LOCK jz @f inc eax @@: test edx, C_REP jz @f inc eax @@: test edx, C_SEG jz @f inc eax @@: test edx, C_OPCODE2 jz @f inc eax @@: ret ;dword check_adapter(dword addr, dword *array, dword size) check_adapter: mov eax, [esp + 0x4] mov edi, [esp + 0x8] mov ecx, [esp + 0xC] lea edi, [edi + ecx * 4 - 0x4] .l00p: cmp edi, [esp + 0x8] jb .not_found cmp eax, [edi] jnz @f jmp .found @@: sub edi, 0x4 jmp .l00p .found: mov eax, 0x1 jmp .locret .not_found: xor eax, eax .locret: ret 0xC ;void create_adapters_addrs(void *base) create_adapters_addrs: mov edx, [esp + 0x4] add edx, ..ex0 - polymorph mov [excludes], edx mov edx, [esp + 0x4] add edx, ..ex1 - polymorph mov [excludes + 0x4], edx ret 0x4 err_disasm: push 0x0 push 0x0 push str_disasm_err push 0x0 call [MessageBox] push 0x1 call [ExitProcess] ret include 'permutate4.asm' CODE_SIZE = $ - polymorph excludes_size: dd 0x2 excludes: dd 0x0 dd 0x0 str_disasm_err: db 'Disassembling error.', 0x0 data import library kernel32, 'KERNEL32.DLL',\ user32, 'USER32.DLL',\ lde32, 'LDE32.DLL' import kernel32, ExitProcess, 'ExitProcess',\ VirtualAlloc, 'VirtualAlloc' import user32, MessageBox, 'MessageBoxA' import lde32, disasm, '_disasm@4',\ disasm_len, 'disasm_len',\ disasm_flag, 'disasm_flag',\ disasm_opcode, 'disasm_opcode',\ disasm_opcode2, 'disasm_opcode2' end data Макрос в permutate.asm можно убрать, оставив первоначальную версию.

Заключение.
В общем-то, это все, что я хотел сказать. Повторюсь, я сильно сомневаюсь в качестве такого метаморфинга, но надеюсь, что вам было интересно. Если у вас есть какие-то замечания или предложения -- пишите на почту/ICQ.

Исполняемый модифицированного FASM'а: FASM.
Исходные коды всех версий и модифицированный X86_64.inc: src.
lde32.dll: lde32.dll (я использовал отладочную версию, на случай ошибок. Перекомпилировать сейчас нет возможности. Если кого-то смущает размер .dll -- скажите, я скомпилирую release).
Previous post Next post
Up