Анализ механизма ручного TLS вPE-загрузчике Вступление Разберём интересную реализацию ручного управления TLS (Thread Local Storage) в самодельном PE-загрузчике, который маппит образ прямо поверх собственной памяти без участия Windows Loader. Проблема, которую решает этот код Когда Windows грузит PE-файл штатно через LoadLibrary, загрузчик (ntdll!LdrpHandleTls) автоматически: выделяет TLS-слот через внутренний bitmap в PEB копирует шаблон данных (StartAddressOfRawData..EndAddressOfRawData) в TLS-блок каждого потока вызывает TLS-коллбэки при DLL_PROCESS_ATTACH / DLL_THREAD_ATTACH Здесь же образ маппится вручную, минуя LDR — значит всё это надо воспроизвести самостоятельно. Структура состояния: PE_MANUAL_TLS_STATE Код (C++): typedef struct _PE_MANUAL_TLS_STATE { kPointers Pointers; // кэшированные указатели на WinAPI PVOID ImageBase; // база загруженного образа DWORD TlsIndex; // выделенный слот в PEB.TlsBitmap DWORD_PTR TlsIndexAddress; // куда писать индекс (IMAGE_TLS_DIRECTORY.AddressOfIndex) DWORD_PTR RawDataStart; // шаблон инициализации TLS SIZE_T RawDataSize; SIZE_T ZeroFillSize; // нулевое заполнение после шаблона SIZE_T TemplateSize; // RawDataSize + ZeroFillSize BOOL Initialized; } PE_MANUAL_TLS_STATE; Важный момент — g_ManualTlsState намеренно вынесен за пределы секции .manPe, чтобы не быть затёртым при маппинге образа поверх себя. Шаг 1 — Выделение TLS-слота: allocate_static_tls_index() Код (C++): PPEB peb = GET_PEB(); for (int slot = (PE_STATIC_TLS_SLOT_LIMIT - 1); slot >= 0; --slot) { DWORD mask = (1u << (slot % 32)); if ((peb->TlsBitmapBits[slot / 32] & mask) == 0) { peb->TlsBitmapBits[slot / 32] |= mask; return (DWORD)slot; } } Вместо вызова TlsAlloc() (который честно пошёл бы в ntdll!RtlpAllocateOpaqueOsResourceId) код напрямую сканирует PEB.TlsBitmapBits и выставляет бит занятости вручную. Поиск идёт с конца (слот 63 → 0), чтобы не конфликтовать со слотами, которые Windows уже выделила низким номерам. Лимит PE_STATIC_TLS_SLOT_LIMIT = 64 — потому что статический TLS занимает только первые 64 слота (дальше идут динамические через FLS). Шаг 2 — Инициализация состояния: initialize_manual_tls_state() Код (C++): *(PDWORD)tlsIndexAddress = tlsIndex; Это ключевая строка: в IMAGE_TLS_DIRECTORY.AddressOfIndex записывается выделенный индекс — именно так скомпилированный код образа потом читает __declspec(thread) переменные через FS/GS:[TlsIndex * sizeof(PTR)]. Дополнительно проверяется геометрия: AddressOfIndex должен лежать внутри маппированного образа StartAddressOfRawData / EndAddressOfRawData — тоже rawDataEnd < rawDataStart → отказ (санитарная проверка) overflow при rawDataSize + zeroFillSize → отказ Шаг 3 — TLS-вектор потока: ensure_tls_vector_slot() Это самая сложная часть. На каждом потоке TEB.ThreadLocalStoragePointer указывает на массив PVOID[], где по индексу TlsIndex лежит указатель на TLS-блок этого потока. TEB.ThreadLocalStoragePointer → [ ptr0 | ptr1 | ... | ptrN ] ↑ [TlsIndex] → TLS блок потока Проблема: Если вектор ещё не существует, или слишком короткий — надо выделить новый и скопировать старые записи. На x64 вектор выделяется через HeapAlloc с заголовком PE_NATIVE_TLS_VECTOR_HEADER (EntryCount + Reserved + ReservedPointer) — это имитирует формат, который Windows создаёт сама в ntdll!LdrpInitializeTls. Без этого заголовка LdrpFreeTls при завершении потока прочитает мусор и упадёт. На x86 используется VirtualAlloc (нет такого заголовка, другая схема). При копировании старого вектора критичная защита: Код (C++): // Копируем только биты, которые реально заняты в PEB.TlsBitmapBits if (peb2 && (peb2->TlsBitmapBits[bit / 32] & (1u << (bit % 32)))) newVector[bit] = currentVector[bit]; // иначе — NULL, чтобы LdrpFreeTls не трогал мусор Шаг 4 — Установка TLS для потока: install_tls_for_current_thread() Код (C++): tlsBlock = kPointers->VirtualAlloc(NULL, allocationSize, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE); memzero(tlsBlock, allocationSize); if (tlsState->RawDataSize != 0) memcopy(tlsBlock, (PVOID)tlsState->RawDataStart, tlsState->RawDataSize); // ZeroFill уже нулевой после VirtualAlloc tlsVector[tlsState->TlsIndex] = tlsBlock; На x64 специально VirtualAlloc (не HeapAlloc) — чтобы Windows не отслеживала эту память и не пыталась её освободить при завершении потока своими механизмами. Шаг 5 — Перехват CreateThread: патч IAT Поскольку новые потоки создаются кодом уже загруженного образа, для них тоже нужен TLS. Решение — перехват через IAT: Код (C++): if (strcmp(procName, "CreateThread") == 0) { g_OriginalCreateThread = (PVOID)(*funcAddress); *funcAddress = (DWORD_PTR)wrapped_CreateThread; } wrapped_CreateThread / wrapped_beginthreadex оборачивают реальный старт: Выделяют PE_MANUAL_TLS_THREAD_CTX с оригинальным StartRoutine + Parameter Запускают вместо него manual_tls_thread_start Тот вызывает emulate_tls(..., DLL_THREAD_ATTACH) → устанавливает TLS для нового потока Запускает оригинальный StartRoutine После завершения — emulate_tls(..., DLL_THREAD_DETACH) → освобождает TLS-блок Шаг 6 — TLS-коллбэки: emulate_tls() После установки TLS-блока вызываются коллбэки: Код (C++): PIMAGE_TLS_CALLBACK* pCallbacks = (PIMAGE_TLS_CALLBACK*)pTls->AddressOfCallBacks; for (int i = 0; i < TLS_CALLBACK_MAX_ATTEMPTS; i++) { pCallback = pCallbacks; if (!pCallback) break; pCallback(ImageData, reason, NULL); } Лимит 100 итераций — защита от битого образа с незавершённым массивом коллбэков. Перед чтением массива проверяется VirtualQuery — если страница недоступна, делается VirtualProtect(PAGE_READONLY). Общая схема потока выполнения manPe() └─ emulate_emulate_importtls(DLL_PROCESS_ATTACH) ├─ initialize_manual_tls_state() ← выделяет слот, пишет TlsIndex в образ ├─ install_tls_for_current_thread() ← выделяет блок, копирует шаблон └─ вызывает TLS-коллбэки enualte_import() └─ patch_thread_import() ← подменяет CreateThread/_beginthreadex в IAT новый поток (через CreateThread) └─ wrapped_CreateThread() └─ manual_tls_thread_start() ├─ emulate_tls(DLL_THREAD_ATTACH) ← TLS для нового потока ├─ оригинальный StartRoutine() └─ emulate_tls(DLL_THREAD_DETACH) ← освобождение Ключевые тонкости МоментПочему важноg_ManualTlsState вне .manPeСекция перезаписывается при маппинге — состояние погибло быПоиск слота с конца (63→0)Не конфликтует со слотами CRT/WindowsЗаголовок вектора на x64LdrpFreeTls читает EntryCount перед освобождениемVirtualAlloc для TLS-блокаHeapAlloc — Windows сама освободит при завершении потока, дваждыКопирование только занятых битовМёртвые записи → crash в LdrpFreeTlsПатч IAT, не сплайсЧище, не ломает CFG, работает в .manPe контекстеРеализация полностью обходит ntdll!LdrpHandleTls и воспроизводит его логику вручную — достаточно корректно, чтобы работал статический __declspec(thread) и TLS-коллбэки в произвольных PE-образах без участия системного загрузчика.
Research, а компилировать пробовали? Компилируется? А то может это ИИ нагенерировал, а за ним нужен глаз, да глаз.
Судя по описанию это кусок загрузчика. Когда зачем-то пихают код в длл, а потом думают что с этим теперь делать. Есть видимо в этом какое-то мрачное садистское удовольствие: без понимания элементарных базовых вещей решать задачу ну хоть как-нибудь, пусть через жопу и по возможности даже не своей головой. Собственной проделанной работы ноль, вменяемых применений загрузке пе модуля в своем процессе суррогатным загрузчиком тоже не существует, пустое бесцельное дрочево.
Длинный текст. Такое ощущение что автор хоярит его для тренировки рук. Для интеллектуальных снобов: Пример будет куда ценнее теории. Я не о хелло-ворде задал вопрос. Твой линк по сути - балласт, информационный шум.
Research Это ответы обученного бота по весьма обширной теме: создать анклав. В пару строк и без понятия не сделать, rtfm Вопросы справа кратко, типо: Начни с создания базы - секвенсора(визора).)
Выходит что эмулится тлс вручную --- Сообщение объединено, 13 мар 2026 --- ну береш и пишешь с логами --- Сообщение объединено, 13 мар 2026 --- теоретично можешь архив допилиить если есть sgx железка
galenkane, Ahimov, спасибо. Может быть слишком грубое упрощение: хочется иметь возможность передать на вход одну строку, на выходе получить другую, и чтобы это в целом не поддавалось анализу. Даже если злоумышленник дампит память, чтобы он видел зашифрованные данные.
пак чО мин ты шо на приколе)) sgx под жылезо посмотри скок процов там ограничка похлеще сталинских времен
Research Тогда нужны не анклавы, а виртуализация(интерпретаторы), vmp.. - тело вирты можно прочитать, а что оно делает не понять
делаю просто чтобы серв возвращал данные в потоке как ллмки свои ответы щас отдают ток в зашифрованном виде + виртой накрыть там никто такое не станет реверсить память то прочесть смогут а вот реверсануть врятли
ишка норм советы дает- если поле боя ето памяти то можно подпортить данныые из рандом места в памяти чтоб прог падала при таймчеке
Попробовал посмотреть лоадер, весь диз в чат не влазит, но суть не в этом. В принципе можно разобрать за вечер ВЕСЬ загрузчик!
Есть более простые пути, так и гипервизор заресерчит. В ранней инициализации загрузку системного драйвера запускает I/O manager через IopLoad..., после чего управление переходит в MmLoadSystemImageEx, который координирует создание секции образа и передаёт работу в низкоуровневые Mi*-функции. Дальше Memory Manager в MiCreateImageFileMap/MiReadImageHeaders читает заголовки файла, а в MiVerifyImageHeader проверяет, что это корректный PE-образ: сигнатуры MZ и PE, тип optional header, число секций и ключевые поля образа. После успешной проверки ядро строит control area, мапит образ в системное адресное пространство через MiMapSystemImage и заносит метаданные модуля в loader bookkeeping структуры вроде _KLDR_DATA_TABLE_ENTRY.
galenkane, Ну и что ты понял из этого выхлопа ? Полная чепуха, без связи. Бредогенератор. Какие боты в виндебаг, вы умом двинулись ?