Статья FASM - формат и назначение манифестов

В программинге существует много деталей, которые отделяют любительскую поделку от профессионального софта. Одной из таких деталей является манифест приложения - XML-файл, который управляет тем, как ОС взаимодействует с программой. В Visual Studio и других IDE этот файл добавляется парой кликов мыши. Но как быть, если мы пишем на чистом ассемблере? В отличие от распространённого мнения, fasm предоставляет элегантные механизмы для встраивания манифестов. Более того, работа с ними на ассме даёт нам полный контроль над каждым байтом ресурсов. В этой статье мы разберём, что такое манифесты, зачем они нужны, и как правильно встраивать их в наши проекты.

1. Введение​
2. Ресурс RT_MANIFEST​
3. Информация о версии VERSIONINFO​
4. Реализация на практике​
5. Под занавес​



1. Введение

Манифест - это XML, который встраивается в исполняемый EXE/DLL в качестве ресурса. ОС читает этот файл при запуске программы, чтобы определить её поведение. Представьте манифест как паспорт вашей программы. В нём указано:

1. Кто вы (идентификатор сборки)
2. Что вам нужно (зависимости от библиотек элементов управления comctl32.dll)
3. Какие у вас права (требуется ли запуск от имени админа)
4. С какой версией Win вы совместимы (важно для корректной работы таких функций как GetVersionEx).​

При отсутствии манифеста ОС считает, что перед ней стандартная программа для XP, и применяет к ней устаревшие правила поведения. Именно из-за отсутствия правильного манифеста многие ASM-программы выглядят древними, и имеют проблемы с отображением интерфейса на современных Win10/11.

1.1. Зачем нужен манифест в FASM-проектах?

Манифест критически важен по нескольким причинам:

• Активация новых визуальных стилей Common Controls v6.0. Без манифеста кнопки, полосы прокрутки и другие элементы управления будут иметь вид из Win2k. Манифест же подключает современную либу comctl32.dll, даруя программе современный внешний вид (тени, прозрачность, округлые края, и т.п).​
• Правильное определение версии ОС. Начиная с Win8 функция GetVersionEx() перестала возвращать реальную версию системы, если приложение не имеет совместимого манифеста. Без него программа всегда будет думать, что работает на Win8, даже если запущена на последней Win11.​
• Запрос прав админа через UAC (User Account Control). Если коду нужно писать в системные каталоги или реестр, она должна запросить повышение прав. Манифест позволяет указать уровень привилегий: asInvoker = права текущего юзера, requireAdministrator = требовать права админа, или highestAvailable = макс.доступные.​
• Профессиональный вид. Встраивание манифеста с описанием программы, версии и другой информацией через ресурс VERSIONINFO, придаёт приложению ухоженный вид.​


2. Тип ресурса "RT_MANIFEST"

Для встраивания используется специальный ресурс RT_MANIFEST, константа которого равна 24 (см.инклуд фасма \equates\kernel32.inc). Для стандартного EXE используется ID=1. Существует два способа встроить XML манифеста:

• Внедрение из внешнего файла (рекомендуется для больших XML)
• Встраивание XML прямо в код (удобно для коротких манифестов)

2.1. Подключение внешнего XML-файла

Это самый чистый и поддерживаемый метод. Вы сохраняете манифест в отдельный файл (например, app.manifest), а затем в fasm указываете путь к этому файлу с помощью директивы file. Вот структура файла app.manifest (минимальный пример):

XML:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
 
  <!-- 1. ИДЕНТИФИКАТОР ВАШЕЙ ПРОГРАММЫ (обязательный) -->
  <assemblyIdentity
      version="1.0.0.0"
      processorArchitecture="amd64"
      name="MyApp"          <---------------- имя ВАШЕГО файла (без .exe)
      type="win32" />
 
  <!-- 2. ОПИСАНИЕ ЗАВИСИМОСТЕЙ (опционально, но часто нужно) -->
  <dependency>
    <dependentAssembly>
      <!-- Common Controls 6.0 для современного визуального оформления -->
      <assemblyIdentity
          type="win32"
          name="Microsoft.Windows.Common-Controls"
          version="6.0.0.0"
          processorArchitecture="*"
          publicKeyToken="6595b64144ccf1df"
          language="*" />
    </dependentAssembly>
  </dependency>
 
  <!-- 3. ЗАПРОС ПРАВ (ваша основная задача) -->
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
    <security>
      <requestedPrivileges>
        <requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
      </requestedPrivileges>
    </security>
  </trustInfo>
 
</assembly>

Код для подключения этого файла:

C-подобный:
section '.rsrc' resource data readable

;// Определяем каталог для манифестов
    directory RT_MANIFEST, manifests
    
;// Определяем сам ресурс: ID=1, язык нейтральный, данные из блока 'resmanifest'
    resource manifests, 1, LANG_NEUTRAL, resmanifest
    
;// Подключаем внешний файл!
    resdata resmanifest
        file  'app.manifest'
    endres

2.2. Встраивание XML непосредственно в код

Данный метод удобен, когда манифест короткий (например только для UAC, или только для визуальных стилей). XML просто вставляется как строка байт с помощью db. Пример для запроса прав админа (requireAdministrator) будет выглядеть так:

C-подобный:
section '.rsrc' resource data readable

    directory  RT_MANIFEST, manifests
    resource   manifests, 1, LANG_NEUTRAL, resmanifest
    
;// XML-строка прямо в коде
    resdata resmanifest
        db '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
        db '<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">'
        db '  <assemblyIdentity version="1.0.0.0" processorArchitecture="X86" name="MyAdminApp" type="win32"/>'
        db '  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">'
        db '    <security>'
        db '      <requestedPrivileges>'
        db '        <requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>'
        db '      </requestedPrivileges>'
        db '    </security>'
        db '  </trustInfo>'
        db '</assembly>'
    endres

После сборки и запуска программы с таким манифестом, Win будет показывать диалог UAC, запрашивая разрешение на выполнение с правами админа. Определить, что манифест исправно подключён к программе можно по наличию "щита" на иконке исполняемого файла, как это показано ниже (слева xml, справа exe).

_1.webp


2.3. Альтернативный метод

Хотя встраивание манифеста в ресурсы является предпочтительным и надёжным способом, существует ещё вариант. Если вы поместите файл с именем MySoft.exe.manifest в ту же папку, что и MySoft.exe, системный загрузчик образов подхватит его на автомате. Этот метод удобен на этапе отладки, но для финальной сборки использовать его не рекомендуется по сл.причинам:

• При копировании программы можно забыть скопировать манифест.​
• Внешние файлы могут быть изменены/удалены антивирусами или юзером.​
• Это выглядит непрофессионально.​


3. Добавление ресурса VERSIONINFO

Хотя манифест - это важнейший ресурс, профессиональное приложение должно содержать и блок VERSIONINFO. Именно эти данные отображаются в свойствах файла, когда мы кликаем по нему правой кнопкой мыши. В fasm для его создания используется спец.структура, и директива versioninfo.

C-подобный:
section '.rsrc' resource data readable

;// Определяем каталоги: RT_VERSION и RT_MANIFEST
    directory  RT_VERSION, versions,\
               RT_MANIFEST, manifests
    
;// Описываем ресурс версии: ID=1, язык нейтральный, данные из блока 'version'
    resource versions, 1, LANG_NEUTRAL, version
    
;// Блок VERSIONINFO
    versioninfo version, VOS__WINDOWS32, VFT_APP, VFT2_UNKNOWN, LANG_ENGLISH + SUBLANG_DEFAULT, 0, \
        'FileDescription',  'Краткое описание приложения', \
        'LegalCopyright',   '(C)2024, Codeby.net', \
        'FileVersion',      '1.1.0.0', \
        'ProductVersion',   '1.0.0.0', \
        'OriginalFilename', 'MyApp.exe'
    
;// Описываем ресурс манифеста
    resource manifests, 1, LANG_NEUTRAL, resmanifest
    
    resdata resmanifest
        file 'app.manifest'
    endres

Обратите внимание, что оба ресурса и манифест и версия имеют одинаковый идентификатор ID=1. Это принципиально важно, т.к. при других значениях загрузчик посчитает блок невалидным и проигнорирует его. Но как могут быть одинаковые ID у двух ресурсов, ведь по логике вещей это не допустимо?

Короткий ответ: ID=1 в разных типах ресурсов RT_MANIFEST и RT_VERSION - это разные пространства имён. Они не конфликтуют, потому что идентифицируются не только по ID, а именно парой Type + ID. В секции .rsrc ресурсы организованы в трёхуровневую иерархию:

1. Type: RT_VERSION(16), RT_MANIFEST(24), RT_DIALOG(5) и т.д.
2. Name/ID: Уникальный идентификатор внутри этого типа.
3. Language: LANG_ENGLISH, LANG_NEUTRAL и т.д.

Обычно код создаёт в секции .rsrc три разные ветки:

• Ветка RT_DIALOG -> ID 37 -> Lang
• Ветка RT_MANIFEST -> ID 01 -> Lang
• Ветка RT_VERSION -> ID 01 -> Lang

Для загрузчика образов RT_MANIFEST#1 и RT_VERSION#1 - это совершенно разные объекты, как файлы C:\Windows\System32\kernel32.dll и D:\MyProject\kernel32.dll. Путь к ним разный, поэтому они живут мирно. Так почему-же версия не отображается, если ID не 1?

Секрет в том, как система ищет инфу о версии. Когда в свойствах файла мы выбираем "Подробно", под катом вызывается GetFileVersionInfo() с таким алго: -"Найти в секции ресурсов тип RT_VERSION(16) с идентификатором VS_VERSION_INFO(1)". Если мы ставим ID=1, Win находит ресурс и версия отображается. Если-же задать ID=2 (42, 1000), api ищет ID=1, не находит его и молча решает «Версии нет». Никакой ошибки не будет, просто поля версии в свойствах файла останутся пустыми.

А почему-же манифест работает с ID=1?
Здесь похожая, но не такая строгая история. Win подхватывает манифест в нескольких случаях:

1. Встроенный, ищется по хард ID=1.​
2. Внешний (appname.exe.manifest). Если встроенного ID=1 нет, загрузчик ищет манифест рядом с EXE.​
3. Динамическая загрузка через FindResource() + LoadResource(). Если мы врукопашную ищем манифест, то сработает любой ID, однако при старте процесса загрузчик этого не делает.​

Вот сводная таблица разных вариантов:

Ресурс Тип ID Как работает
----------- --- -- -------------
RT_MANIFEST 24 1 Загрузчик ищет строго ID=1 для встроенного манифеста.
RT_VERSION 16 1 GetFileVersionInfo() ищет строго ID=1.
RT_DIALOG 05 37 DialogBoxParam() использует ID=37, хотя будет работать и при ID=1.


4. Реализация на практике

Ниже представлен код программы, которая запрашивает желаемую привилегию (как у текущего юзера, админ, или макс.возможную), и на основании выбора генерит готовый файл-манифеста, который можно будет подключить к своей программе. Помимо того, что код создаёт внешний XML-файл, он и сам в своей тушке имеет оба ресурса - и RT_VERSION и RT_MANIFEST для отображения визуальных стилей Win10/11.

C-подобный:
format pe64 gui 6.0
include 'win64ax.inc'
include 'encoding\win1251.inc'
entry start

ID_Invok  = 101
ID_Admin  = 102
ID_High   = 103
ID_Logo   = 104

;//-----------------
section '.text' code readable executable
start:  push    rbp
        invoke  InitCommonControls
        invoke  GetModuleHandle,0
        mov    [hModule],eax
        invoke  DialogBoxParam,rax,37,0,DialogProc,0
        invoke  ExitProcess,0

proc  DialogProc uses rsi rdi rbx, hWnd,Msg,wParam,lParam
        mov    [hWnd],  rcx
        mov    [wParam],r8

        cmp     edx,WM_INITDIALOG
        je      @initialize
        cmp     edx,WM_COMMAND
        je      @command
        cmp     edx,WM_CLOSE
        je      @close
        xor     rax,rax
        jmp     @fin

@initialize:
;//---- Вставляем в окно логотип в формате *.bmp ----------
        invoke  LoadImage,[hModule],55,IMAGE_BITMAP,0,0,0
        mov     [hBitmap],eax
        invoke  GetDlgItem,[hWnd],ID_Logo
        invoke  SendMessage,eax,STM_SETIMAGE,IMAGE_BITMAP,[hBitmap]

;//---- Выберем и взведём чек-бокс по умолчанию -----------
        invoke  CheckRadioButton,[hWnd],ID_Invok,ID_High,ID_Invok
        jmp     @next

@command:
        cmp     [wParam],BN_CLICKED shl 16 + IDCANCEL
        je      @close
        cmp     [wParam],BN_CLICKED shl 16 + IDOK
        jne     @next

;//---- Если нажата пимпа "Создать" -----------------------
;//********************************************************
        invoke  _lcreat,<'NewManifest.xml',0>,0
        mov     [hFile],eax
        invoke  _lwrite,eax,xmlHdr,admin-xmlHdr

        invoke  IsDlgButtonChecked,[hWnd],ID_Invok   ;// тест радио-буттонов
        cmp     eax,BST_CHECKED
        jne     @admin
        invoke  _lwrite,[hFile],user,31              ;// длина строка = 31
        jmp     @f

@admin: invoke  IsDlgButtonChecked,[hWnd],ID_Admin
        cmp     eax,BST_CHECKED
        jne     @hight
        invoke  _lwrite,[hFile],admin,42
        jmp     @f

@hight: invoke  _lwrite,[hFile],hight,38

@@:     invoke  _lwrite,[hFile],endHdr,endBss-endHdr
        invoke  _lclose,[hFile]
        invoke  MessageBox,0,<'Файл NewManifest.xml успешно создан!',0>,\
                             <'Генератор манифестов',0>,0
        jmp     @next
;//**********************************

@close: invoke  EndDialog,[hWnd],0
@next:  mov     rax,1
@fin:   ret
endp

;//--------------------
section '.idata' import data readable writeable
library  kernel32,'kernel32.dll',\
         comctl32,'comctl32.dll',user32,'user32.dll'
include  'api\kernel32.inc'
include  'api\comctl32.inc'
include  'api\user32.inc'

;//--------------------
section '.rsrc' resource data readable

directory RT_DIALOG,  dialogs,\
          RT_VERSION, version,\
          RT_BITMAP,  bitmaps,\
          RT_MANIFEST,manifest

resource  dialogs,\
          37,LANG_ENGLISH+SUBLANG_DEFAULT,form
resource  version,\
          1,LANG_NEUTRAL,vInfo
resource  bitmaps,\
          55,LANG_NEUTRAL,logo
resource  manifest,\
          1,LANG_NEUTRAL,manifest_data

dialog form, 'Генератор манифеста PE32/64',0,0,177,104,WS_CAPTION + WS_SYSMENU + WS_POPUP + DS_CENTER,,,'Verdana',8
    dialogitem 'BUTTON',' Права ',-1,10,10,090,060, WS_VISIBLE + BS_GROUPBOX
    dialogitem 'BUTTON','Текущие',      ID_Invok, 20,023,60,13,WS_VISIBLE + BS_AUTORADIOBUTTON + WS_TABSTOP + WS_GROUP
    dialogitem 'BUTTON','Администратор',ID_Admin, 20,037,70,13,WS_VISIBLE + BS_AUTORADIOBUTTON
    dialogitem 'BUTTON','Максимальные', ID_High,  20,051,70,13,WS_VISIBLE + BS_AUTORADIOBUTTON

    dialogitem 'STATIC','Лого',ID_Logo,  115,013,0,0,  WS_VISIBLE + SS_BITMAP
    dialogitem 'BUTTON','Создать',IDOK,  010,080,90,14,WS_VISIBLE + WS_TABSTOP + BS_DEFPUSHBUTTON
    dialogitem 'BUTTON','Выход',IDCANCEL,115,080,52,14,WS_VISIBLE + WS_TABSTOP + BS_PUSHBUTTON
enddialog

versioninfo  vInfo,\
             VOS__WINDOWS32, VFT_APP, VFT2_UNKNOWN,\
             LANG_ENGLISH + SUBLANG_DEFAULT, 0,\   ;//<-------- Lang/CodePage
            'LegalCopyright'  , 'Copyright (c) 2020-2026. Marylin',\
            'ProductName'     , 'Manifest utility',\
            'ProductVersion'  , '6.1.7601',\
            'FileDescription' , 'Генератор манифестов fasm',\
            'FileVersion'     , '1.0.0',\
            'OriginalFilename', 'ManifestUtil.exe'

resdata manifest_data
  file 'Manifest.xml'  ;//<-------- Подключаем внешний манифест!
endres

;//-----------------
section '.data' data readable writeable
hFile    dd  0
hBitmap  dd  0
hModule  dd  0

;//----- Данные для записи в файл --------------
xmlHdr   db  '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>',13,10
         db  '<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">',13,10
         db  '<assemblyIdentity version="1.0.0.0" processorArchitecture="amd64" name="AppName" type="win32" />',13,10
         db  '  <dependency>',13,10
         db  '    <dependentAssembly>',13,10
         db  '      <assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*" />',13,10
         db  '    </dependentAssembly>',13,10
         db  '  </dependency>',13,10
         db  '  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">',13,10
         db  '    <security>',13,10
         db  '      <requestedPrivileges>',13,10
         db  '<requestedExecutionLevel level="'

admin    db  'requireAdministrator" uiAccess="false"/>',13,10  ;// длина строки = 42 байта
hight    db  'highestAvailable" uiAccess="false"/>',13,10
user     db  'asInvoker" uiAccess="false"/>',13,10

endHdr   db  '      </requestedPrivileges>',13,10
         db  '    </security>',13,10
         db  '  </trustInfo>',13,10
         db  '</assembly>',13,10
endBss:

bitmap   logo,'logo.bmp'

Как результат получим такое окно. Чтобы обозначить разницу, здесь я сделал 2 скрина - слева с манифестом, а справа без. Как видим стиль кнопок приобрёл уже другой вид: с голубой подсветкой, края округлённые, а если в окне были-бы и другие элементы управления (полосы прокрутки, списки комбо/лист-бокс, и прочее), то изменились-бы и они на более современные. В общем профит от манифестов на лицо.

example.webp

Просмотреть содержимое манифеста (и даже подправить пару строк) можно в крутой тулзе "CFF-Explorer" как показано на скрине ниже. Ну и конечно специально заточенная для этих нужд утилита "Resource Hacker" позволяет создавать манифесты из своих шаблонов - вписываем в строку "Name" имя своей прожки, и всё пучком. Одним словом, здесь есть из чего выбирать:

CffExp.webp


RHacker.webp

А по коду, после того-как получили дескриптор открытого файла (здесь я использовал функцию _lopen(), которая под катом вызывает CreateFile() с дефолтными настройками безопасности), можно в любой момент сбрасывать в него фрагменты данных, и они будут дописываться в конец по текущему указателю. Этот указатель хранится в файловом объекте ядра, и при каждой записи увеличивается на указанное при вызове _lwrite() кол-во байт. Он будет всегда смотреть в конец файла, пока мы не закроем дескриптор по CloseHandle(). Вот внутренняя структура любого файла в отладчике WinDbg, где по смещению(0x68) лежит позиция указателя в файле:

Код:
0: kd> dt _file_object
nt!_FILE_OBJECT
   +0x000 Type                 : Int2B
   +0x002 Size                 : Int2B
   +0x008 DeviceObject         : Ptr64 _DEVICE_OBJECT
   +0x010 Vpb                  : Ptr64 _VPB  <------- Volume Param Block (инфа о разделе диска)
   +0x018 FsContext            : Ptr64 Void
   +0x020 FsContext2           : Ptr64 Void
   +0x028 SectionObjectPointer : Ptr64 _SECTION_OBJECT_POINTERS
   +0x030 PrivateCacheMap      : Ptr64 Void
   +0x038 FinalStatus          : Int4B
   +0x040 RelatedFileObject    : Ptr64 _FILE_OBJECT
   +0x048 LockOperation        : UChar
   +0x049 DeletePending        : UChar
   +0x04a ReadAccess           : UChar ----+
   +0x04b WriteAccess          : UChar     |
   +0x04c DeleteAccess         : UChar     |
   +0x04d SharedRead           : UChar     +---> Атрибуты доступа
   +0x04e SharedWrite          : UChar     |
   +0x04f SharedDelete         : UChar     |
   +0x050 Flags                : Uint4B ---+
   +0x058 FileName             : _UNICODE_STRING
   +0x068 CurrentByteOffset    : _LARGE_INTEGER  <------ Текущее смещение в файле!
   +0x070 Waiters              : Uint4B
   +0x074 Busy                 : Uint4B
   +0x078 LastLock             : Ptr64 Void
   +0x080 Lock                 : _KEVENT
   +0x098 Event                : _KEVENT
   +0x0b0 CompletionContext    : Ptr64 _IO_COMPLETION_CONTEXT
   +0x0b8 IrpListLock          : Uint8B
   +0x0c0 IrpList              : _LIST_ENTRY
   +0x0d0 FileObjectExtension  : Ptr64 Void

0: kd>


5. Заключение

Манифесты в FASM это не магия и не проблема. Освоив технику вы перестаёте быть "тем парнем, который пишет ассм под DOS", и становитесь проф.разработчиком под Windows, способным создавать современные приложения с правильным поведением и внешним видом. Используйте директиву resdata для встраивания XML, не забывайте про RT_MANIFEST и ID=1, добавляйте VERSIONINFO для информации о файле, и ваши программы будут выглядеть так, как будто они написаны на самом современном компиляторе. В мире FASM нет границ: если вы можете описать это в коде - ассемблер это соберёт.

Ссылки по теме

MSDN - Описание всех элементов манифеста:
Манифесты приложения - Win32 apps

MSDN - Включение визуальных стилей:
Включение визуальных стилей - Win32 apps

Зачем приложению манифест:
Зачем Win32-приложению манифест?
 

Вложения

Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab

Похожие темы

🚀 Первый раз на Codeby?
Гайд для новичков: что делать в первые 15 минут, ключевые разделы, правила
Начать здесь →
🔴 Свежие CVE, 0-day и инциденты
То, о чём ChatGPT ещё не знает — обсуждаем в реальном времени
Threat Intel →
💼 Вакансии и заказы в ИБ
Pentest, SOC, DevSecOps, bug bounty — работа и проекты от проверенных компаний
Карьера в ИБ →

HackerLab