Подписывайтесь на наш Telegram и не пропускайте важные новости! Перейти

Гайд [ASM] Ассемблер - векторная математика | Полиморфизм через SIMD.

EXCLUSIVE
EXCLUSIVE
Статус
Оффлайн
Регистрация
21 Июн 2025
Сообщения
148
Реакции
49
Технологии полиморфизма можно встретить не только в вирусах, но и, например, в защитных механизмах. это передовой путь мысли программиста, притягивающий новичков как магнитом.
Продолжение вот этой статьи, в этой больше работы с SIMD на практике.
Хотел уложить полный движок со стабом и шифровщиком, но не успел т.к нужно доделывать проект, поэтому решил резко перенести это на 3 часть.
Полиморфизм - это свойство программы выполнять несколько разных алгоритмов с одним и тем же результатом. Обрабатываемый код делится на блоки постоянного или переменного размера, которые в каждом поколении вируса переставляются в случайном порядке. Это еще не настоящая полиморфия, но и обычным такой код уже не назовешь. Он легко программируется, но и легко обнаруживается, ведь содержимое блоков остается неизменным, поэтому с ними справляется даже сигнатурный поиск. Данная техника может использоваться как для защиты своего ПО, так и вирусописателями с целью усложнить сигнатурный анализ антивирусными защитными механизмами.
Злоумышленник создает несколько алгоритмов с одним и тем же результатом в каждом алгоритме, к примеру, зануление регистра EAX. Вместо классического производительного xor eax, eax можно впихнуть sub eax, eax, and eax, 0 или вообще извратиться через push 0 / pop eax. Результат один и тот же - в регистре ноль, но байт-код в памяти выглядит абсолютно иначе. Статический сканер антивируса ищет знакомую последовательность байт, а её там тупо нет, хотя логика выполнения осталась прежней.
На одном другом форуме, мой знакомый уже выкладывал подобную статью, я повзаимстовал у него код с обнулением регистра, давайте проанализируем его:

ASM:
Expand Collapse Copy
MutateXorEax:
    cmp rcx, 1
    je .case_xor
    cmp rcx, 2
    je .case_sub
    cmp rcx, 3
    je .case_mov
    cmp rcx, 4
    je .case_and
    cmp rcx, 5
    je .case_push
    cmp rcx, 6
    je .case_imul
    ret

.case_xor:
    mov word [rdi], 0xC031
    add rdi, 2
    ret

.case_sub:
    mov word [rdi], 0xC029
    add rdi, 2
    ret

.case_mov:
    mov byte [rdi], 0xB8
    mov dword [rdi+1], 0
    add rdi, 5
    ret

.case_and:
    mov dword [rdi], 0x00E083
    add rdi, 3
    ret

.case_push:
    mov dword [rdi], 0x58006A
    add rdi, 3
    ret

.case_imul:
    mov dword [rdi], 0x00C06B
    add rdi, 3
    ret
 
randomYzer: ;// рандомайзер для выбора алгоритма XORSHIFT
    rdtsc
    shl rdx, 32
    or rax, rdx
    mov r8, rax
    shr r8, 12
    xor rax, r8
    mov r8, rax
    shl r8, 25
    xor rax, r8
    mov r8, rax
    shr r8, 27
    xor rax, r8
    xor rdx, rdx
    mov r9, 6
    div r9
    inc rdx
    mov rax, rdx
    ret
В коде передается в регистр RDI указатель буфера, куда будет записываться готовый код, состоящий из байтов. Суть проста: в зависимости от рандомного числа в RCX, мы прыгаем на нужный алгоритм и копируем в память конкретные опкоды.
Например, если рандомайзер выбрал xor eax, eax, в буфер записывается 0x31 0xC0, а если mov eax, 0, то целых 5 байт, начиная с 0xB8.
Самый важный момент в данном коде - это смещение указателя RDI после каждой записи. Если бы мы записали 5 байт для mov, но при этом не подвинули бы адрес, то следующая инструкция просто записалась бы поверх предыдущей, и на выходе получилась бы нерабочая каша. Поэтому после каждой вставки важно делать add rdi, (размер байтов). Это позволяет выстраивать цепочку команд одну за другой, формируя цельный и каждый раз уникальный поток инструкций.
Сам выбор кейса ложится на плечи randomYzer. Он генерирует псевдорандомное число через rdtsc и математические сдвиги(алгоритм xorshift вроде), после чего мы через div получаем остаток от деления в диапазоне от 1 до 6. Этот индекс и летит в RCX. В итоге, сколько бы раз мы ни вызывали этот генератор, последовательность байт в буфере всегда будет отличаться от предыдущей, хотя по факту программа просто продолжает обнулять регистры.

1. Новая эра полиморфизма через SIMD.
В мире существует тысячи готовых полиморфных движков, вобравших в себя множество блестящих идей и чтобы переплюнуть их придется очень сильно постараться.
Использование SIMD (SSE/AVX) переносит логику расшифровки в XMM/YMM регистры, которые могут игнорироватся многими эвристическими движками из-за высокой зашумленности легитимным мультимедиа-трафиком.
Использование инструкций PXOR, PADD, PSUB позволяет обрабатывать блоки данных по 128/256 бит за одну итерацию. Это не только ускоряет расшифровку основного тела малвари, но и создает аномальный для сигнатурного анализатора профиль кода.
Вместо цикличного xor [ebx], al реверсер видит векторные операции.
В главе 0 был пример с занулением регистра, вместо банального xor eax, eax - сделаем:
ASM:
Expand Collapse Copy
pxor xmm0, xmm0        ;// самые быстрое обнуление xmm регистра
movq eax, xmm0         ;// eax = 0
Полиморфные движки хранят в себе шифрофщики/расшифровщики(стабы), в продвинутом движке можно заменить операцию XOR на циклическое сложение или вычитание с переполнением, используя PADDB/PADDW. Для аналитика это выглядит как сложная математическая трансформация массива, хотя по факту это обычный симметричный шифр:
ASM:
Expand Collapse Copy
movdqu xmm1, [data]    ;// 16 байт encrypt code
movdqu xmm2, [key]     ;// векторный ключ в xmm1
psubb xmm1, xmm2       ;// побайтовое вычитание ключа
movdqu [data], xmm1
Вместо cmp/jne в стабах применяются инструкции сравнения PCMPEQB. Они генерируют битовую маску прямо в XMM-регистре:
ASM:
Expand Collapse Copy
pcmpeqb xmm0, xmm1 ;// сравнение блоков. результат - маска в xmm0
pmovmskb eax, xmm0 ;// перенос маски в gpr для проверки валидности ключа
Вместо того чтобы пушить по 1-4-8 байт в стек для вызова API как это делают неопытные программисты с целью того, чтобы не объявлять в секции .data строку к примеру "NtOpenProcess" после предварительной загрузив kernel32.dll/GetProcAdress-а из PEB программа просто собирает ее на стеке после чего вызывает передав значение в rcx:
1771671186953.png

более умные закидывают всю строку целиком через XMM. Это убивает возможность найти строки типа GetProcAddress простым поиском в памяти процесса.
ASM:
Expand Collapse Copy
movdqu xmm0, [rel str] ;// 16 байт зашифрованной строки
pxor xmm0, xmm1  ;// декрипт в регистре
movdqu [rsp-16], xmm0   ;// строка на стеке, готова к вызову
Самую базу я думаю рассказал, думаю можно переходить к написанию базового мини-полиморфного движка с SIMD.

2. Практическая реализация базового полиморфного движка
ASM:
Expand Collapse Copy
macro .code {
    section '.text' code readable executable
}

macro .data {
    section '.data' data readable writeable
}

macro .idata {
    section '.idata' import data readable writeable
}
1771672219886.png

ASM:
Expand Collapse Copy
format PE64 console
entry start

include 'C:\FLAT\INCLUDE\win64a.inc'
include 'C:\FLAT\section.inc'

.data
information db "~~~~ Polymorf Generation for Yougame SIMD v0.01 ~~~~~", 10, 0
lin db "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~", 10, 0
author db "Author: KVANTOR815", 10, 0
description db "This is Polimorf Engine", 10, 0
msg_gen db "generating SIMD polymorf shellcode...", 10, 0
msg_buf db "shell bytes: ", 0
fmt_hex db "%02X ", 0
msg_exec db 10, "executing generated stub. RAX check...", 10, 0
msg_done db "shell executed successfully.", 10, 0

align 16
    shell rb 512
    seed dq ?
    old_prot dd ?

.code
start:
                        sub rsp, 28h
                        lea rcx, [information]
                        call [printf]
                        lea rcx, [author]
                        call [printf]
                        lea rcx, [description]
                        call [printf]
                        lea rcx, [lin]
                        call [printf]

                        lea rcx, [msg_gen]
                        call [printf]

                        rdtsc
                        mov [seed], rax

                        lea rdi, [shell]
                        mov rbx, 4

.gen_loop:
                        call randomYzer
                        mov rcx, rax
                        call MutateSimdXor
              
                        call randomYzer
                        mov rcx, rax
                        call InsertJunk
              
                        dec rbx
                        jnz .gen_loop

                        mov byte [rdi], 0xC3

                        lea rcx, [msg_buf]
                        call [printf]
              
                        lea rsi, [shell]
.print_loop:
                        cmp rsi, rdi
                        jae .print_end
                        lea rcx, [fmt_hex]
                        movzx rdx, byte [rsi]
                        call [printf]
                        inc rsi
                        jmp .print_loop
.print_end:
                        lea rcx, [shell]
                        mov rdx, 512
                        mov r8, 0x40
                        lea r9, [old_prot]
                        call [VirtualProtect]

                        lea rcx, [msg_exec]
                        call [printf]

                        call shell

                        lea rcx, [msg_done]
                        call [printf]
                        call [_getch]

                        xor ecx, ecx
                        call [ExitProcess]

MutateSimdXor:
                        cmp rcx, 1
                        je .case_pxor
                        cmp rcx, 2
                        je .case_xorps
                        mov dword [rdi], 0xC0760F66
                        add rdi, 4
                        mov dword [rdi], 0xC0FA0F66
                        add rdi, 4
                        jmp .finalize
.case_pxor:
                        mov dword [rdi], 0xC0EF0F66
                        add rdi, 4
                        jmp .finalize
.case_xorps:
                        mov dword [rdi], 0xC0570F
                        add rdi, 3
.finalize:
                        mov word [rdi], 0x4866
                        mov dword [rdi+2], 0xC07E0F
                        add rdi, 5
                        ret

InsertJunk:
                        cmp rcx, 1
                        je .junk_nop
                        cmp rcx, 2
                        je .junk_mov
                        mov dword [rdi], 0x00180F
                        add rdi, 3
                        ret
.junk_nop:
                        mov byte [rdi], 0x90
                        inc rdi
                        ret
.junk_mov:
                        mov dword [rdi], 0xC9280F
                        add rdi, 3
                        ret

randomYzer:
                        mov rax, [seed]
                        mov rdx, 0x41C64E6D
                        mul rdx
                        add rax, 0x3039
                        mov [seed], rax
                        xor rdx, rdx
                        mov r9, 3
                        div r9   
                        inc rdx   
                        mov rax, rdx
                        ret

.idata
    library kernel32, 'kernel32.dll', \
            msvcrt, 'msvcrt.dll'

    import kernel32,\
           VirtualProtect, 'VirtualProtect', \
           ExitProcess, 'ExitProcess'

    import msvcrt, \
           printf, 'printf', \
           _getch, '_getch'
Windows по умолчанию запрещают исполнение кода из секций данных. Чтобы буфер ожил, в коде используется вызов VirtualProtect. Без флага PAGE_EXECUTE_READWRITE (0x40) попытка передать управление на сгенерированные байты приведет к немедленному крашу. Это так, если будут вопросы про вот эту строку:
ASM:
Expand Collapse Copy
lea rcx, [shell]
mov rdx, 512
mov r8, 0x40
lea r9, [old_prot]
call [VirtualProtect]
Регистр RDI хранит в себе указатель на буфер. Каждая подфункция мутации обязана строго соблюдать инкремент этого регистра. Если инструкция занимает 4 байта, мы делаем add rdi, 4(зачем объяснялось выше)
ASM:
Expand Collapse Copy
;// примерстрогого соблюдения размера:
mov dword [rdi], 0xC0EF0F66 ;// pxor xmm0, xmm0 (4 байта)
add rdi, 4 ;// сдвигаем байты в буфере ровно на 4
Самое интересное это то, что программа не трогает регистры общего назначения напрямую. Все манипуляции по занулению или подготовке данных происходят в среде XMM регистров. Эмулятор видит работу с мультимедиа-командами и часто игнорирует их, не понимая, что в конце цепочки через movq результат упадет в RAX, подготавливая почву для системного вызова.

Для корректной работы в x64 архитектуре при выгрузке данных из SIMD в GPR используется префикс REX.W (0x48), это нужно для того, чтобы информамировать о том, что мы работает с полным 64-битныи регистров RAX, а не забиваем только младшие 32 бита в EAX. В консольке это выглядит как 0x66 0x48 0x0F 0x7E 0xC0.
1771672608936.png

ASM:
Expand Collapse Copy
.finalize:
mov word [rdi], 0x4866  ;// REX.W
mov dword [rdi+2], 0xC07E0F ;// movq rax, xmm0
add rdi, 5
Самое важное, хотел рассказать про непосредственно генератор, вместо предсказуемого rdtsc, который выдает просто число тактов, движок использует я решил использовать LCG вместо xorshift.
А в коде знакомого вообще использовался xorsift.

Также в коде есть junk-code функция:
1771672742481.png

Инструкции вроде movaps xmm1, xmm1 технически выполняются, но не меняют состояние флагов и нужных нам регистров, они нужны только для того, чтобы разбавить полезную нагрузку и изменить общую контрольную сумму блока. Обычный мусорный код который затрудняет анализ.
ASM:
Expand Collapse Copy
.junk_mov:
mov dword [rdi], 0xC9280F ;// movaps xmm1, xmm1
add rdi, 3   ;// мусорная команда меняющая хеш блока
Еще я бы хотел обхъяснить про то, что сильные движки не гадают, на каком процессоре они запущены, то есть, если вы используете современные SIMD инструкции типа AVX/AVX512 в своем движке, программа может слабо работать на старом железе.
Поэтому важно вшивать проверку в начало стаба через cpuid. Если процессор поддерживает AVX, движок ггенерирует жирные 256-битные инструкции, если нет - откат к SSE. Важна производительность как никак.
ASM:
Expand Collapse Copy
mov eax, 1
cpuid
and ecx, 0x10000000 ;// бит 28 - sucess
jz .callback_sse

3. Теория: где же в реальных малвари может использоваться полиморф с SIMD.
Честно, если говорить объективно, я еще не встречал малвари использующих полимофрные движки с SIMD, да и целом использующие полиморфные движки.
Дело в том, что малварь разработчики это мягко-говоря школьники которые пишут вирусы тяп-ляп на С и сервер на каком-то С# с кучей уязвмостями.
Подобные генераторы лучше писать на ассемблере, что довольно трудно для вирусописателей) Нейросеть тут уже не поможет, а знания должны быть.
Что тут уж говорить, за последние 10 лет не было ни одной малвари на ассемблере.

Если же встретится такая малварь, то там будет что-то вроде дескриптора использующая AVX-512 или AES-NI инструкции не для шифрования трафика, а для банального изменения своего тела что обсуждалось в статье. Для защитного ПО это выглядит как работа видеоредактора или архиватора. Ни один школьник на C# не сможет реализовать такую логику, да, возможно, через нейросеть он что-то там сделает, но..... я не думаю что из этого выйдет что-то крутое.
 
Последнее редактирование:
Статический сканер антивируса ищет знакомую последовательность байт, а её там тупо нет, хотя логика выполнения осталась прежней.
если я не ошибаюсь - это будет работать только если антивирус ищет сигнатурно, сейчас любой статический анализатор просто преобразует в абстрактное представление и получит eax = 0
 
если я не ошибаюсь - это будет работать только если антивирус ищет сигнатурно, сейчас любой статический анализатор просто преобразует в абстрактное представление и получит eax = 0
Да, не заметил ошибку.
Сигнатурный именно.
 
Назад
Сверху Снизу