Кое-что о метаморфинге.
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).