Reflective EFI-Loader: від ідеї до втілення на C та Python

💡 Усі статті, обговорення, новини про Python — в одному місці. Приєднуйтесь до Python спільноти!

Вітаю, колего!

Мене звати Владислав, мені 18 років, навчаюся в ХАІ на спеціальності G12 (112) «Авіаційна та ракетно-космічна техніка» (2 курс).

Приблизно у 8–10 років я почав цікавитися тим, як влаштовані складні системи. Спочатку встановлював і налаштовував різні операційні системи у віртуальних машинах, а згодом мій інтерес змістився в бік хакінгу: Kali Linux, Metasploit, вардрайвінг та перші проєкти на Python.

Але одного використання чужих інструментів мені стало недостатньо — я почав створювати власні інструменти, розбиратися, як вони працюють зсередини, та розширювати їхні можливості власними ідеями.

Мовою Python я займаюся понад 5 років, C — близько 3 років. Згодом заглибився в алгоритми, структури даних, архітектуру ОС та UEFI. Наразі основний фокус — низькорівнева розробка, робота з бінарними форматами та дослідження внутрішніх механізмів операційних систем. Також активно використовую ШІ-інструменти для прискорення розробки та опанування нових технологій.

Моя філософія розробки проста: мінімум абстракцій, структурований, простий і красивий код та висока продуктивність — без зайвих накладних витрат та сторонніх залежностей.

Я не завжди дотримуюся усталених підходів до написання коду й можу свідомо відходити від них, якщо це дозволяє зробити рішення простішим, швидшим або таким, що краще відповідає конкретній задачі.

Про що стаття

Цей проєкт народився випадково: до нього я вже писав завантажувачі для PE (peldr) та ELF (elflhr), і якось подумав — а чому б не зробити те саме для EFI? Так з’явився «efildr».

На той момент я майже не був знайомий із внутрішньою будовою UEFI та можливостями GNU-EFI, тому значна частина роботи полягала саме в дослідженні документації, механізмів UEFI та практичних експериментах.

Мета статті — коротко розібрати механіку Reflective EFI-Loader.

Якщо у вас виникнуть питання щодо архітектури чи загальних підходів — із задоволенням відповім у коментарях.

Стек: C (GNU-EFI) для завантажувача та Python для білдера
Платформа: x86_64

Тестування: QEMU (OVMF) та реальне обладнання під Ubuntu, де через efildr запускався тестовий EFI-додаток shimx64.efi

GitHub: Репозиторій efildr
Демонстрація: Efildr: Reflective EFI-Loader for UEFI.

У статті розберемо:

  1. Основні терміни
  2. Архітектура завантажувача
  3. Ручне відображення PE32+
  4. Захист від віртуальних машин та відладки
  5. Висновок
Застереження: стаття написана виключно для освітніх та дослідницьких цілей. Усі продемонстровані концепції призначені для вивчення внутрішніх механізмів завантаження та роботи з бінарними образами.

Примітка: у статті я не зупинятимусь на специфічних деталях реалізації шифрування та стиснення — це виходить за межі матеріалу.

Також варто зазначити, що я не позиціоную себе як експерт у цій галузі. Матеріал ґрунтується на власному досвіді, дослідженнях, експериментах та практичній роботі над проєктом.

У статті я не наводитиму повну реалізацію всього коду, щоб не перевантажувати матеріал. Необхідні фрагменти будуть наведені безпосередньо в процесі розбору, а повна реалізація доступна в репозиторії GitHub.

1. Основні терміни

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

Базові терміни:

  • IBV (Independent BIOS Vendor): Компанія, що розробляє прошивки для OEM-виробників. Приклади: AMI, Insyde Software.
  • OEM (Original Equipment Manufacturer): Виробник кінцевого пристрою, який адаптує його під конкретну модель.
  • Прошивка (Firmware): Низькорівневе програмне забезпечення, що зберігається у флеш-пам’яті пристрою та забезпечує його початковий запуск.
  • UEFI (Unified Extensible Firmware Interface): Специфікація, що визначає середовище та інтерфейси для роботи прошивки й EFI-програм до запуску ОС.
  • ESP (EFI System Partition): Спеціальний розділ диска для зберігання файлів, необхідних для завантаження через UEFI. Зазвичай використовується FAT32.
  • EFI-додаток (EFI Application): Виконуваний файл, який UEFI може безпосередньо завантажити та запустити.
  • PE32+ (Portable Executable): Формат 64-бітних виконуваних файлів, що використовується EFI-додатками на x86_64.
  • GNU-EFI: Набір бібліотек і заголовків для розробки EFI-додатків мовою C, який використовується замість EDK II через простішу та легшу структуру.
  • EFI_BOOT_SERVICES: Таблиця функцій UEFI для роботи з пам’яттю, протоколами, подіями та іншими ресурсами під час завантаження.
  • shimx64.efi: Підписаний EFI-завантажувач Ubuntu, який запускається UEFI та передає керування наступному етапу завантаження, зазвичай grubx64.efi.

Терміни завантажувача:

  • RLE (Run-Length Encoding): Простий алгоритм стиснення, який замінює послідовності однакових байтів на пару значень — кількість повторень та сам байт. У efildr використовується для стиснення payload перед шифруванням.
  • ARX (Add-Rotate-XOR): Клас алгоритмів шифрування, що використовує операції додавання, циклічних зсувів та XOR. У efildr використовується для шифрування payload перед його вбудовуванням у stub.
  • Stub: Попередньо скомпільований EFI-додаток efildr, який UEFI запускає першим. Він знаходить вбудований payload, відновлює його та передає йому керування.
  • Payload: EFI-додаток, який потрібно запустити. Під час пакування він стискається, шифрується та вбудовується у stub.
  • Оверлей (Overlay): Блок даних, доданий у кінець stub, що містить службову інформацію та payload.
  • Reflective loading: Завантаження образу в пам’ять без використання стандартного механізму UEFI. У efildr PE32+ payload самостійно відображається в пам’ять, після чого виконується його entry point.
  • Fileless execution: Виконання payload без окремого файлу на диску: він відновлюється з даних, вбудованих у stub, та завантажується безпосередньо в пам’ять.

2. Архітектура завантажувача

Проєкт складається з файлів:

  1. loader.c — вихідний код завантажувача. Містить EfiMain і всю логіку: перевірки захисту від аналізу, читання оверлея, розшифрування, розпакування, рефлективне відображення PE32+, виконання релокацій, оновлення EFI_LOADED_IMAGE_PROTOCOL та передачу керування payload.
  2. loader.h — заголовний файл завантажувача. Містить прапорці, що задаються під час збирання (USING_ANTI_VM, USING_ANTI_DEBUG, USING_ERASE_PE_HEADERS), макроси для викликів EFI-протоколів (EFI_CALL, EFI_CHAIN_CALL), визначення структур PE32+ (IMAGE_DOS_HEADER, IMAGE_NT_HEADERS64, IMAGE_SECTION_HEADER, IMAGE_BASE_RELOCATION), а також константи для перевірок віртуального середовища та відладки.
  3. setup.sh — скрипт збирання завантажувача. Компілює loader.c через GCC з прапорцями GNU-EFI, лінкує з crt0-efi-x86_64.o та elf_x86_64_efi.lds, конвертує результат у PE32+ за допомогою objcopy та видаляє проміжні файли. На виході отримуємо loader.efi, готовий до вбудовування у скрипт пакування.
  4. genarr.py — утиліта для конвертації бінарного файлу в C-сумісний масив байтів. Використовується для перетворення скомпільованого loader.efi у масив-ініціалізатор, який вбудовується в efildr.py як EFI_PE_STUB.
  5. efildr.py — скрипт пакування. Читає вхідний PE32+ EFI-додаток, перевіряє його коректність, стискає за допомогою RLE, шифрує ARX-шифром, формує оверлей та збирає фінальний EFI-файл, вбудовуючи в нього скомпільований завантажувач. Містить EFI_PE_STUB — бінарний образ завантажувача, а також описи структур PE32+ та константи формату.

Огляд

Усі компоненти утворюють дві чіткі фази: збирання (offline, на машині розробника) та виконання (runtime, на цільовій машині). Перша фаза відповідає за пакування payload і підготовку фінального EFI-файлу, друга — за його відновлення та запуск у пам’яті.

Схема нижче показує повний шлях від вихідного EFI-файлу (PE32+) до виконання payload: конвеєр пакування, структуру оверлея, роль UEFI та логіку роботи stub.

У разі виникнення помилки завантажувач виводить повідомлення про неї та перезавантажує систему.

Допоміжні макроси та функції

/********************
* ALIASES & HELPERS *
*********************/

#define _EFI_CALL_SELECTOR(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, N, ...) N
#define _EFI_CALL_ARGS(...) _EFI_CALL_SELECTOR(0, ##__VA_ARGS__, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)

/* uefi_call_wrapper with automatic argument counting */
#define EFI_CALL(func, ...) \
uefi_call_wrapper((func), _EFI_CALL_ARGS(__VA_ARGS__), ##__VA_ARGS__)

/* Compiler attribute wrapper helper */
#define _DEC_ATTR(...) \
__attribute__((__VA_ARGS__))

/* Assembly block shorthand */
#define DEC_ASM \
__asm__ volatile

/* Const-qualified pointer helper
(const data and const pointer) */
#define CONST_PTR(TYPE) \
const TYPE *const

/* Restrict-qualified pointer helper
(guarantees no memory aliasing) */
#define RESTR_PTR(TYPE) \
TYPE *restrict

/* Branch prediction hints for compiler optimization */

#define _UNLIKELY(x) __builtin_expect(!!(x), 0) // Cold path
#define _LIKELY(x) __builtin_expect(!!(x), 1) // Hot path

/* Check for EFI status success */
#define EFI_OK(status) \
_LIKELY((INTN)(status) >= EFI_SUCCESS)

/* Check for EFI status failure */
#define EFI_FAIL(status) \
_UNLIKELY((INTN)(status) < EFI_SUCCESS)

/* Short-circuit EFI call chainer */

#define _EFI_CHECK_CALL_N(...) (EFI_OK(EFI_CALL(__VA_ARGS__)))
#define _EFI_CHECK_CALL_1(a) (_EFI_CHECK_CALL_N a)
#define _EFI_CHECK_CALL_2(a, b) (_EFI_CHECK_CALL_1(a) && _EFI_CHECK_CALL_1(b))
#define _EFI_CHECK_CALL_3(a, b, c) (_EFI_CHECK_CALL_2(a, b) && _EFI_CHECK_CALL_1(c))
#define _EFI_CHECK_CALL_4(a, b, c, d) (_EFI_CHECK_CALL_3(a, b, c) && _EFI_CHECK_CALL_1(d))
#define _EFI_CHECK_CALL_5(a, b, c, d, e) (_EFI_CHECK_CALL_4(a, b, c, d) && _EFI_CHECK_CALL_1(e))

#define _EFI_CHECK_CALL_SELECTOR(_1, _2, _3, _4, _5, N, ...) _EFI_CHECK_CALL_##N

/* Automatic EFI call-chain validator */
#define EFI_CHAIN_CALL(...) \
_EFI_CHECK_CALL_SELECTOR(__VA_ARGS__, 5, 4, 3, 2, 1)(__VA_ARGS__)

/* Optimized branch control macros */

#define IF_UNLIKE(expr) if (_UNLIKELY(expr))
#define IF_LIKE(expr) if (_LIKELY(expr))

#define IF_EFIFAIL(status) if (EFI_FAIL(status))
#define IF_EFIFAIL_CALL(...) IF_EFIFAIL (EFI_CALL(__VA_ARGS__))
#define IF_EFIFAIL_CHAINCALL(...) IF_UNLIKE (!EFI_CHAIN_CALL(__VA_ARGS__))

/* Optimized loop control macros */

#define WHILE_LIKE(expr) \
while (_LIKELY(expr))

#define FOR_LIKE(init, cond, post) \
for (init; _LIKELY(cond); post)

/* Function declaration helper */
#define DEC_FUNC(TYPE) \
static inline _DEC_ATTR(always_inline) TYPE

/*********************
* PE-IMAGE UTILITIES *
**********************/

/* ASCII-only lowercase conversion */
#define ASCII_TOLOWER(chr) \
((chr) | 0×20)

/* Convert seconds to microseconds */
#define SEC_TO_USEC(n) \
((UINTN)(n) * 1000000)

/* Safe minimum and maximum value helpers */

#define MIN(a, b) ({ \
typeof((a)) _a = (a); \
typeof((b)) _b = (b); \
\
(_a < _b)? _a : _b; \
})

#define MAX(a, b) ({ \
typeof((a)) _a = (a); \
typeof((b)) _b = (b); \
\
(_a > _b)? _a : _b; \
})

/* Convert RVA to typed pointer */
#define RVA(TYPE, base, addr) \
((TYPE)((UINT8*)(base) + (addr)))

/* Extract e_lfanew from DOS header */
#define DOS_LFANEW(base) \
(((IMAGE_DOS_HEADER*)(base))->e_lfanew)

/* Get the current EFI image file path */
#define EFI_IMAGE_PATH(LoadedImage) \
(((FILEPATH_DEVICE_PATH*)(LoadedImage)->FilePath)->PathName)

/* Calculate the required EFI_FILE_INFO buffer size,
Uses the full path as buffer slack */
#define EFI_FILEINFO_SIZE(PathName) \
(sizeof(EFI_FILE_INFO) + \
(StrLen(PathName) * sizeof(CHAR16)))

/* Number of relocation entries in a block */
#define RELOC_ENTRY_COUNT(reloc) \
(((reloc)->SizeOfBlock — sizeof(IMAGE_BASE_RELOCATION) \
) / sizeof(UINT16))

/* True if relocation entry requests
64-bit absolute fixup */
#define RELOC_IS_DIR64(entry) \
(((entry) >> 12) == IMAGE_REL_BASED_DIR64)

/* Get intra‑page offset from relocation entry */
#define RELOC_BLOCK_OFFSET(entry) \
((entry) & 0×0FFF)

/* Retrieves a pointer to the first section header */
#define IMAGE_FIRST_SECTION(hdNt) RVA( \
IMAGE_SECTION_HEADER*, \
&(hdNt)->OptionalHeader, \
(hdNt)->FileHeader.SizeOfOptionalHeader \
)

/* Copy memory block */
#define MEMCPY(dst, src, sz) \
CopyMem((VOID*)(dst), (const VOID*)(src), (UINTN)(sz))

/* Fill memory with zeros */
#define ZEROS(dst, sz) \
ZeroMem((VOID*)(dst), (UINTN)(sz))

Цей набір макросів використовується як базовий шар для всього завантажувача. Він прибирає повторюваний код, спрощує роботу з EFI-викликами, PE32+ структурами та операціями з пам’яттю, а також уніфікує перевірки помилок і керування умовами.

Частина макросів навмисно реалізована складніше, ніж їхні прості аналоги. Це дозволяє, наприклад, автоматично визначати кількість аргументів EFI-виклику, об’єднувати декілька перевірок із коротким замиканням, задавати підказки компілятору щодо ймовірності гілок та скорочувати повторення в коді.

У результаті основна логіка завантажувача залишається компактнішою, а низькорівневі операції мають єдиний стиль використання по всьому проєкту.

Оверлей

Оверлей — це «рюкзак» завантажувача. Він розміщується в кінці фінального .efi-файлу й містить усе, що потрібно stub для відновлення payload: ключ, розміри та самі зашифровані дані. UEFI завантажує сам EFI-образ, але область оверлея після PE32+ образу не розглядається як частина виконуваного образу та не обробляється його завантажувальним механізмом.

Структура оверлея в порядку запису:

ПолеРозмірПризначення
loader_stub~36 КіБСкомпільований EFI-завантажувач
key_size1 байтДовжина ключа: 16, 32, 64 або 128 байт
keyN байтКлюч ARX-шифру
raw_len4 байтиОригінальний розмір payload до стиснення
payloadM байтСтиснений і зашифрований payload
total_len4 байтиЗагальна довжина оверлея

Поле total_len записується останнім і читається першим. Stub знаходить кінець файлу, зчитує останні 4 байти та отримує total_len, після чого визначає початок і розмір оверлея. Це дозволяє не знати заздалегідь, де саме у файлі починається оверлей: він завжди розташований у кінці.

Ключ (key_size + key) записується у відкритому вигляді. Це не помилка: мета шифрування полягає не в приховуванні ключа, а в ускладненні статичного аналізу самого payload. Без stub відновлення payload за передбаченим механізмом неможливе.

Розмір оверлея обчислюється за формулою:

overlay = sizeof(BYTE) + N +sizeof(UINT32)+ M +sizeof(UINT32)

де N — довжина ключа, M — розмір стисненого та зашифрованого payload. Саме total_len дорівнює цьому значенню та дозволяє stub коректно визначити межі оверлея.

Повна реалізація (ReadOverLay):

/* Extracts the appended payload overlay
from the current process image file */
DEC_FUNC(const UINT8*) ReadOverLay(
CONST_PTR(EFI_LOADED_IMAGE_PROTOCOL) LoadedImage
) {
/* Initialize with size marker length
for the initial read */
UINT32 tlLn = DAT_LEN_SZ;

UINT8 *dtBuf = NULL;
CONST_PTR(CHAR16) PathName = EFI_IMAGE_PATH(LoadedImage);

EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *FsProt = NULL;
EFI_FILE_PROTOCOL *Volume = NULL;
EFI_FILE_PROTOCOL *File = NULL;
EFI_FILE_INFO *FileInfo = NULL;

UINTN InfoSize = (UINTN)EFI_FILEINFO_SIZE(PathName);
UINT32 FileSize;
UINTN RdLen;

/* Get and Open the EFI Simple File System protocol */
IF_EFIFAIL_CHAINCALL (
IN(BS->HandleProtocol, LoadedImage->DeviceHandle,
&FileSystemProtocol, (VOID**)&FsProt),

IN(FsProt->OpenVolume, FsProt, &Volume)
) goto _ret;

IF_EFIFAIL_CHAINCALL (
/* 1. Open the current EFI image */
IN(Volume->Open, Volume, &File,
PathName, EFI_FILE_MODE_READ, 0),

/* 2. Allocate the EFI_FILE_INFO buffer */
IN(BS->AllocatePool, EfiLoaderData,
InfoSize, (VOID**)&FileInfo),

/* 3. Read the EFI_FILE_INFO structure */
IN(File->GetInfo, File,
&GenericFileInfo, &InfoSize, FileInfo)
) goto _ret;

FileSize = (UINT32)FileInfo->FileSize;
RdLen = (UINTN)tlLn;

/* Read overlay data length directly
into the length variable */
IF_EFIFAIL_CHAINCALL (
IN(File->SetPosition, File, (UINT64)(FileSize — tlLn)),
IN(File->Read, File, &RdLen, &tlLn)
) goto _ret;

/* Account for the size marker itself
and verify bounds */
tlLn += DAT_LEN_SZ;
IF_UNLIKE (tlLn > FileSize) goto _ret;

/* Allocate a pool buffer
to hold the complete overlay structure */
IF_EFIFAIL_CALL (BS->AllocatePool,
EfiLoaderData, (UINTN)tlLn, (VOID**)&dtBuf
) goto _ret;

/* Store overlay size at buffer start
for unpacker parsing */
*(UINT32*)dtBuf = tlLn;

RdLen = (UINTN)(tlLn — DAT_LEN_SZ);

/* Read the remainder of the payload from disk,
skipping the total length header slot */
IF_EFIFAIL_CHAINCALL (
IN(File->SetPosition, File, (UINT64)(FileSize — tlLn)),
IN(File->Read, File, &RdLen, dtBuf + DAT_LEN_SZ)
) FREE_POOL_BUF(&dtBuf); // dtBuf is assigned NULL

_ret:
IF_LIKE (FileInfo) FREE_POOL_BUF(&FileInfo);
IF_LIKE (File) EFI_CALL(File->Close, File );
IF_LIKE (Volume) EFI_CALL(Volume->Close, Volume);

return (const UINT8*)dtBuf;
}

stub розбирає структуру:

/* Overlay layout
 1: total_len : DAT_LEN_SZ
 2: key_size : DAT_KEY_SZ
 3: key : PE_KEY
 4: raw_len : DAT_LEN_SZ
 5: payload : PE_EXE
 */
 const UINT8 *PE_DAT = ReadOverLay(LoadedImage);
 IF_UNLIKE (!PE_DAT) LDR_EXIT(LDR_ERR_OVERLAY);

 const UINT32 PE_DAT_SZ = *(UINT32*)PE_DAT;
 const UINT8 PE_KEY_SZ = *(PE_DAT + DAT_LEN_SZ);

 const UINT32 RW_EXE_SZ = *(UINT32*)(
 (PE_DAT + DAT_LEN_SZ) +
 (DAT_KEY_SZ + PE_KEY_SZ )
 );

 const UINT32 DT_EXE_SZ = PE_DAT_SZ — (
 (DAT_LEN_SZ + DAT_KEY_SZ) +
 (PE_KEY_SZ + DAT_LEN_SZ)
 );

 const UINT32 PE_EXE_SZ = MAX(DT_EXE_SZ, RW_EXE_SZ);

 const UINT8
 *PE_KEY = (UINT8*)(PE_DAT + (DAT_LEN_SZ + DAT_KEY_SZ)),
 *PE_EXE = (UINT8*)(PE_KEY + (PE_KEY_SZ + DAT_LEN_SZ));

Цей фрагмент розбирає структуру оверлея та отримує з буфера всі необхідні поля.

Спочатку ReadOverLay повертає буфер PE_DAT, у якому на початку знаходиться total_len. Далі за зміщеннями зчитуються key_size, ключ PE_KEY та оригінальний розмір payload RW_EXE_SZ.

DT_EXE_SZ визначає фактичний розмір даних payload у буфері, без службових полів. PE_EXE_SZ бере більше зі значень DT_EXE_SZ та RW_EXE_SZ, щоб отримати достатній розмір буфера для подальшого відновлення payload.

У результаті PE_KEY вказує на ключ, а PE_EXE — безпосередньо на зашифрований payload.

3. Ручне відображення PE32+

Навіщо ручне відображення

UEFI має функцію LoadImage, яка вміє завантажувати PE32+ образ та готувати його до виконання. Але в нашому випадку payload уже знаходиться в буфері пам’яті, а окремого EFI-файлу на диску немає.

Після читання оверлея stub спочатку розшифровує та розпаковує payload, отримуючи готовий PE32+ образ у пам’яті. Далі він самостійно виконує його відображення та підготовку до запуску.

Такий підхід дозволяє запускати payload швидше та без створення окремого файлу на диску.

Тому stub виконує основні етапи завантаження сам:

  1. Виділяє пам’ять під образ
  2. Копіює заголовки та секції
  3. Застосовує релокації
  4. Оновлює EFI_LOADED_IMAGE_PROTOCOL
  5. Передає керування на entry point

Це і є reflective loading — ручне відображення PE32+ образу в пам’ять із подальшою передачею керування на його entry point.

Примітка: у випадку EFI-додатка не всі стандартні механізми PE потребують окремої обробки під час ручного відображення. Зокрема, тут не виконується обробка Import Table, Export Table та інших каталогів, пов’язаних зі звичайним завантаженням PE-модулів. Необхідні для роботи EFI-додатка функції та сервіси надаються середовищем UEFI через EFI_BOOT_SERVICES, EFI_RUNTIME_SERVICES і протоколи. Тому в efildr основна увага приділяється відображенню секцій та обробці базових релокацій.

Виділяє пам’ять під образ

Для початку завантажувач визначає необхідний обсяг пам’яті за полем SizeOfImage з PE32+ заголовка та переводить його у кількість сторінок. Після цього через AllocatePages виділяється пам’ять під майбутній образ.

EFI_PHYSICAL_ADDRESS imgAddr = 0;
UINTN imgPages = (UINTN)EFI_SIZE_TO_PAGES(hdOpt->SizeOfImage);

IF_EFIFAIL_CALL (BS->AllocatePages,
AllocateAnyPages, EfiLoaderCode,
imgPages, &imgAddr
) LDR_EXIT(LDR_ERR_ALLOC_IMAGE);

img = (VOID*)imgAddr;

У результаті img вказує на область пам’яті, у яку далі будуть скопійовані заголовки та секції PE32+ образу.

Копіює заголовки та секції

Повна реалізація (CopyImageData):

/* Copy PE image data into allocated memory */
DEC_FUNC(VOID) CopyImageData(
 RESTR_PTR(VOID) const img,
 RESTR_PTR(VOID) const buf,
 CONST_PTR(IMAGE_NT_HEADERS64) hdNt
) {
 /* Copy PE headers */
 MEMCPY(img, buf, hdNt->OptionalHeader.SizeOfHeaders);

 const IMAGE_SECTION_HEADER *sn =
 IMAGE_FIRST_SECTION(hdNt);

 CONST_PTR(IMAGE_SECTION_HEADER) se =
 sn + hdNt->FileHeader.NumberOfSections;

 /* Copy each section */
 FOR_LIKE (, sn < se, sn++) {
 const UINT32 vrSz = sn->Misc.VirtualSize,
 rwSz = sn->SizeOfRawData;

 /* Skip virtual-only sections
 with no data */
 IF_UNLIKE (!vrSz) continue;

 VOID *const dst = RVA(VOID*, img, sn->VirtualAddress );
 CONST_PTR(VOID) src = RVA(VOID*, buf, sn->PointerToRawData);
 const UINT32 sz = MIN(vrSz, rwSz);

 /* Copy section data to image */
 IF_LIKE (sz) MEMCPY(dst, src, sz);

 /* Zero out the rest of the section */
 IF_UNLIKE (vrSz > rwSz) ZEROS((UINT8*)dst + rwSz, vrSz — rwSz);
 }
}

CopyImageData копіює PE32+ образ із вхідного буфера buf у виділену область пам’яті img. Спочатку копіюються заголовки образу, після чого посекційно переносяться дані за відповідними VirtualAddress. Якщо в пам’яті секція має більший розмір, ніж її дані у файлі, залишок області обнуляється.

Застосовує релокації

Повна реалізація (ApplyImageRelocations):

/* Apply base relocations to the mapped image */
DEC_FUNC(VOID) ApplyImageRelocations(
 VOID *const img,
 CONST_PTR(IMAGE_OPTIONAL_HEADER64) hdOpt
) {
 /* Relocation delta actual
 load address offset from ImageBase */
 const UINT64 dlt = (UINT64)img — hdOpt->ImageBase;

 CONST_PTR(IMAGE_DATA_DIRECTORY) ldr =
 &hdOpt->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];

 /* Skip if no base relocation table is present */
 IF_UNLIKE (!ldr->VirtualAddress) return;

 const IMAGE_BASE_RELOCATION *rl =
 RVA(IMAGE_BASE_RELOCATION*, img, ldr->VirtualAddress);

 CONST_PTR(IMAGE_BASE_RELOCATION) rlEnd =
 RVA(IMAGE_BASE_RELOCATION*, rl, ldr->Size);

 /* Process each relocation block */
 WHILE_LIKE (rl < rlEnd) {
 UINT8 *const blkVA = RVA(UINT8*, img, rl->VirtualAddress);
 const UINT16 *it = (UINT16*)(rl + 1);
 CONST_PTR(UINT16) itEnd = it + RELOC_ENTRY_COUNT(rl);

 /* Apply all fixups in this block */
 FOR_LIKE (, it < itEnd, it++) {
 const UINT16 entry = *it;

 IF_LIKE (RELOC_IS_DIR64(entry)) {
 const UINT32 ofs = RELOC_BLOCK_OFFSET(entry);
 *(UINT64*)(blkVA + ofs) += dlt;
 }
 }

 /* Advance to next relocation block */
 rl = RVA(IMAGE_BASE_RELOCATION*, rl, rl->SizeOfBlock);
 }
}

Завантажений образ не обов’язково розміщується за адресою, вказаною в ImageBase. Тому завантажувач обчислює різницю між фактичною адресою розміщення образу та його базовою адресою, після чого проходить таблицю базових релокацій і додає це зміщення до відповідних 64-бітних адрес у образі.

У результаті всі адреси, які залежать від бази завантаження, коректно відповідають фактичному розташуванню PE32+ образу в пам’яті.

Передає керування на entry point

Перед передачею керування завантажувач оновлює EFI_LOADED_IMAGE_PROTOCOL відповідно до щойно відображеного образу.

Після цього встановлюються аргументи, з якими EFI-додаток зазвичай запускається через UEFI: ImageHandle та SystemTable.

/* Transfer control to the loaded executable */
DEC_ASM (
 "movq %0, %%rax\n\t" // Move entry point address
 "movq %1, %%rcx\n\t" // ImageHandle (1st argument)
 "movq %2, %%rdx\n\t" // SystemTable (2nd argument)

 "andq $-16, %%rsp\n\t" // Align stack to 16-byte boundary
 "subq $40, %%rsp\n\t" // Reserve 32-byte shadow space + 8 for alignment

 "jmpq *%%rax\n\t" // Jump to the loaded image entry point

 : : "r"(AddrOfEntryPoint), "r"(ImageHandle), "r"(SystemTable)
 : "memory", "cc", "rax", "rcx", "rdx"
 );

Після цього виконання безпосередньо переходить на entry point відображеного PE32+ образу, який продовжує роботу як звичайний EFI-додаток.

4. Захист від віртуальних машин та відладки

Захист вбудований у завантажувач і активується ще на етапі збирання через прапорці USING_ANTI_VM та USING_ANTI_DEBUG. Якщо прапорець вимкнено — відповідний код не потрапляє у фінальний бінарник. Якщо увімкнено — перевірки виконуються до того, як stub прочитає оверлей, і повторюються між етапами через макрос _ANTITRAP.

Виявлення віртуального середовища:

AntiVM повертає TRUE, якщо виявлено ознаки віртуального середовища. Перевірка складається з трьох незалежних частин — достатньо спрацювання будь-якої з них.

  1. Firmware vendor: Завантажувач читає рядок ST->FirmwareVendor і шукає в ньому відомі назви IBV: megatrends, insyde, phoenix, byosoft. Якщо назву не вдалося знайти, перевірка вважає середовище підозрілим і завершується як виявлення VM. Порівняння виконується без урахування регістру.
  2. CPUID: Через інструкцію CPUID з функцією 1 читається регістр ECX. Біт 31 — Hypervisor Present Bit. Якщо він встановлений, процесор повідомляє про наявність гіпервізора.
  3. PCI vendor ID: Завантажувач перебирає PCI-пристрої через EFI_PCI_IO_PROTOCOL і читає Vendor ID з конфігураційного простору.Якщо серед пристроїв знайдено відомий ідентифікатор виртуальної платформи, вважается, що виконання відбувається у VM:
  • 0x80EE — VirtualBox
  • 0x15AD — VMware
  • 0x1AF4, 0x1B36, 0x1234 — QEMU
  • 0x5853 — Xen
  • 0x1414 — Hyper-V
  • 0x1AB8 — Parallels

Якщо PCI-пристрої взагалі не вдалося перерахувати, перевірка також завершується як виявлення VM.

Виявлення відладки

AntiDebug перевіряє два апаратні індикатори за допомогою inline asm.

  1. Trap Flag: Зчитується регістр RFLAGS і перевіряється біт 8 (CPU_RFLAGS_TF). Якщо він встановлений, процесор перебуває у покроковому режимі виконання.
  2. Debug Register 7: Зчитується DR7 та перевіряються біти, що відповідають за активацію апаратних точок зупинки. Якщо активна хоча б одна з них, вважається, що встановлено hardware breakpoint.

Обидві перевірки виконуються без використання зовнішніх API — значення регістрів читаються безпосередньо через inline asm.

Реакція

Якщо AntiVM або AntiDebug повертає TRUE, завантажувач припиняє подальше виконання: оверлей не читається, а payload не відновлюється. Після цього виводиться повідомлення про помилку та виконується перезавантаження системи.

_ANTITRAP викликається не лише на старті, а й між основними етапами роботи завантажувача: після ініціалізації, після читання оверлея, після виділення пам’яті та після відображення образу. Тому перевірки виконуються повторно протягом усього процесу завантаження до передачі керування payload.

5. Висновок

У результаті був реалізований Reflective EFI-Loader, який працює без окремого файлу payload на диску. У процесі збирання EFI-додаток пакується, стискається, шифрується та вбудовується в оверлей разом із завантажувачем. Під час виконання stub самостійно знаходить оверлей, відновлює payload, відображає PE32+ образ у пам’ять, виконує релокації та передає керування його entry point.

Окремо були реалізовані перевірки віртуального середовища та відладки, а також оновлення EFI_LOADED_IMAGE_PROTOCOL, необхідне для коректної роботи завантаженого EFI-додатка.

Основна мета проєкту полягала не лише у створенні готового завантажувача, а й у практичному дослідженні внутрішньої механіки UEFI, PE32+ та ручного відображення образів у пам’ять.

Повний вихідний код та реалізація доступні у репозиторії GitHub.
Це моя перша технічна стаття, тому буду вдячний за конструктивний зворотний зв’язок.

Дякую за увагу!

👍ПодобаєтьсяСподобалось2
До обраногоВ обраному1
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Вітаю Владиславе.
Дякую за технічну статтю. Не знаток по темі низкорівневої розробки, але деякі питання по Python коду зʼявилися:
Дісклеймер — деякі з них це більше про «вкусовщину», тому радий почути контр аргументи

1) Неймінг Ваших функцій- в репо всього 2 файла з пітоном а в них і camel 🐫 і snake 🐍

def FillInCStructure(data, struct): vs def genarr(

1* назви взагалі теж можна допрацювати
Як каже відомий Zen
Explicit is better than implicit
Вгадувати що fp це файл пасс весело тільки перші 3 рази коли читаєш

2) Type hints & doc strings — знову десь хінти є , десь нема. З док стрінг потім можна генерувати всякі маркдаун документи
def genarr(
fp: str,
2* якщо фп має вказувати на існуючий файл- то може краще в функцію передавати не стрінгу рандомно а обʼєкт Path? Який теоретично впаде якщо файлу не існує

3) Перейменування існуючих вбудованих прямо з коробки функцій:
jn = cm.join
mp = map

Оцей трюк я не зрозумів- вибачте щоб шо?
Памʼятаємо про KISS & YAGNI

4) Обробка помилок:
if len(argv) != 2:
# Enforce proper usage
print(
f’Usage: python {basename(argv[0])} ’
’ <-> ’
’Make bin file into C byte array )
Чому прінтуємо а не raise Exception?

5) Print замість логування:
Оці меседжі
Centralized informational message strings
INF_START = ’Start processing -> {name}\n’

Краще обрати якийсь вбудований Logger і туди передавати замість простого print

Дякую за детальний фідбек! Більшість зауважень справді стосуються стилю й тут я з вами погоджуюся — Python-код можна було зробити послідовнішим. Частина скорочень і перейменувань була зроблена свідомо, щоб не перевантажувати код для невеликого службового скрипта, але, думаю, деякі з них справді були зайвими.

Чому на GitHub локалізація документації проєкта мовою країни агресорки і терористки є, а державною мовою немає?

Приходят к Сталину соратники по Политбюро и спрашивают, что делать с накопившимися проблемами в стране.Сталин подумал, раскурил трубку и говорит:— У меня есть два предложения. Первое: расстрелять половину ЦК. Второе: перекрасить Мавзолей в зеленый цвет.Все удивляются, начинают перешептываться и наконец спрашивают:— Товарищ Сталин, а почему именно в зеленый?Сталин победоносно улыбается:— Я так и знал, что по первому вопросу у вас возражений не будет.

Здатність до взаємоповаги — необхідна передумова здатності до максимізації синергії спільнодії.

Тому, що особисто ви не зробили нічого для її локалізації.

Висновок пана не відповідає дійсності, оскільки було піднято питання про відсутність авторської україномовної редакції документації.

Звертаюсь до пана на ви і прохаю пана чинити так само.

тяжко зробити пул реквест, а не хамити?

Якщо під час війни за порятунок української державності, пан автор статті дозволяють собі ігнорувати державна мову, створюючи натомість окрему редакцію документації штучно створеним київськими попа́ми церковно-слов’янським есперанто, вічно бідної та вічно відсталої терористичної деспотії.

То це говорить про те, що Україна, для пана автора статті, нічого не означає, порожнє місце. Тому навички і досягнення пана можуть посилити наших ворогів, принаймні з не меншою імовірністю, ніж посилити нас.

Невмотивована упередженість, що заохочує до зневаги українства.

Українська і англійська документації через автоперекладач перекладуться на 100%. На це треба одну хвилину. Якщо будуть неясні місця — то спитають декілька LLM і порівняють. Якщо пан думає, що відсутність російської версії тут чимось допоможе, він морально застиг в 2014 року.
Те ж саме стосується і розповідей про «штучно створеним київськими попа́ми церковно-слов’янським есперанто» — це є просто антинаукова брехня, і якщо станом на 2014 ще був якийсь сенс говорити таке, щоб розбудити критичне мислення, то вже через пару років це вже стало не просто маячнею, а ознакою, що її промовець має мислення (в цьому питанні, щонайменше) на рівні амеби.

розповідей про «штучно створеним київськими попа́ми церковно-слов’янським есперанто» — це є просто антинаукова брехня

Чи можна попрохати посилання на джерела, що підтверджують висновок пана.

Будь-який підручник історії російської мови.
Краще новіший (наприклад, Хабургаев, Горшкова), але і класичний Иванов або полемічний Колесов (у нього суттєво інший зір на фонетичні процеси) підійдуть не гірше.
Там і зведення висновків, і джерела. Знайти всіх їх в інтернеті не проблема. Хоча до деяких сайтів треба VPN.
Російська в сучасній формі (з поправкою на «акання») склалась як торговельне койне між Суздальським і Новгородським князівствами к середині 16-го століття. Далі вже через Московське царство поширювалась на суміжні території. «Київські попи» прийшли вже коли мова була сформована.
Сучасна ситуація виготована ідеологічним фактором, а не мовним.

то може забацаєш нарешті український укроуукрб (можна із ШІ, можна без)

укроуукрб

що це???

мовнючаснький

хабрахабр

на який пропонує перейти товариш notus

Ну, у мене є щось біля 10% такої собі «блакитної мрії» створити форум. Але як уявляю собі обсяг усякого щоденного плину, одразу бажання проходить.

в «борцунів», навіть і тих 10% нема, і вони якось вибірково реагують, от в темі про нейроники мовнюки мовчть, язик не дратує

Важко знайти більш упереджене, маніпулятивне та брехливе джерело з реальної історії московицької мови, ніж затверджений московицьким міністерством освіти підручник з історії московицької мови.

Московицька мова — є штучно створеним церковно-слов’янським есперанто, вигаданим київськими попа́ми на християнських книжках, написаних староболгарською мовою, через рішення київських князів про християнізацію підконтрольної тоді московії.

В цьому можна легко переконатися через аномальну схожість московицької та болгарської мов, наприклад, порівнявши сторінки московицькою та болгарською мовою у будь-якій настанові з користування побутовою технікою.

Московицька мова в Україні може бути «життєво необхідною» лише для україноненависників, налаштованих на ведення гібридної війни, спрямованої на знищення української державності зсередини, через використання московицької мови для вичавлювання української мови з ужитку.

Що, у стратегічній перспективі невідворотно матиме за наслідки:

— Розмивання української ідентичності через насадження комплексу меншовартості та лояльності до кремля.
— Штучне створення внутрішньої залежності від кремля.
— Перетворення мовної відмінності на джерело перманентного внутрішнього політичного конфлікту.
— Створення постійного приводу для зовнішньої агресії для захисту московитомовних.
— Гальмування деколонізації та інтеграції у західний світ.

І Московія, а не Росія тому, що Петром I, у плагіатський спосіб, було перейменовано Московію на Росію, що з грецької мови перекладається як Русь.

Невіглас Петро I думав, що про його плагіатську крадіжку чужої назви ніхто не здогадається.

Через це безглуздо ретранслювати плагіатські вигадки найшанованішого московитами терориста та серійного вбивці. Тобто Московія має називатися Московією, а московити — московитами.

Важко знайти більш упереджене, маніпулятивне та брехливе джерело з реальної історії московицької мови, ніж затверджений московицьким міністерством освіти підручник з історії московицької мови.

Вони всі спираются на реальні джерела. Спробуйте спростувати джерела, я на це подивлюсь:)
Мабуть, скажете, що і тисяча берестяних грамот з новгородських розкопок це підробки?;)

В цьому можна легко переконатися через аномальну схожість московицької та болгарської мов, наприклад, порівнявши сторінки московицькою та болгарською мовою у будь-якій настанові з користування побутовою технікою.

Типовий аргумент невігласа, який
1) не розуміє історію різних шарів мови,
2) не знає, що у болгар після їх визволення від османів (187x) була довга історія «виправлення» мови шляхом запозичення усталених російських(!) слів;
3) не пробував прочитати не щось технічне, а прості тексти болгарською (хоч з тої же Вікіпедії) і побачити, що рівень розуміння впав в рази, якщо не до нуля. Або самому, навпаки, сконструювати щось болгарською і порівняти з тим, що дає хоча б google translate.

Ви саме такий агресивний невіглас: нічого не знаєте, не хочете перевірити власне «знання», але активно переказуєте тупу брехню.

Московицька мова в Україні може бути «життєво необхідною»

А тепер ввернули тезу, якої не було і для якої не було у мене жодних підстав, після чого активно її спростовуєте. Imago Чапека в повний зріст.

Що, у стратегічній перспективі невідворотно матиме за наслідки:

Нічого з цього списку не доведено. Воно виглядає досить імовірно, але принципово хибне: розширення імперії є їх ціллю незалежно від мови.

Невіглас Петро I думав, що про його плагіатську крадіжку чужої назви ніхто не здогадається.

«Царство Русское» було задовго до Петра — щонайменше з 1547. (А «государь всея Руси» вони звали себе з 1462.) Точно так же як і «Воєводство Руське» навколо Львову у складі Речі Посполитої.
Невіглас — це ви, а не Петро I. Він, звісно, самодур, клієнт для психіатра, сатрап, масовий вбивця і все таке, але не невіглас.

Цікаво, що всі подібні вам папуги повторюють один і той же тезіс про Петра I. Один раз цю брехню вам розлили і ніхто з вас не пробує хоча б базово перевірити факти.

Тобто Московія має називатися Московією, а московити — московитами.

Може, і має сенс (я періодично так роблю), але вся інша маячня, що ви тут напапужили без жодної власної думки, не стане навіть частково правдою.

під час війни за порятунок української державності

а крим чій?

Я роблю документацію лише англійською та російською мовами — так мені зручніше підтримувати README в єдиному форматі та не роздувати його за обсягом.

технічну тему звели до агітпропа, хоча і раніше було почитати щось по мікроконтролерах хіба що на недружніх сайтах ворожою мовою, укроукрб мовнюки не витягують

технічну тему звели до агітпропа

за руську мову нас буквально вбивають, в прямому етері.

тут бдительне око не муляє ?
dou.ua/...​rums/topic/62000/#3122322
де гнівні вигуки виявленої зради та вимоги «чемодан вокзал хабр»