Исследование TLS, загрузчика и CRT-инициализации в conhost.exe
Введение
Целью данного исследования было изучение того, как Thread Local Storage (TLS) используется в процессе conhost.exe, а также как TLS взаимодействует с:
Анализ проводился на живом процессе conhost.exe с использованием WinDbg.
- механизмами загрузчика Windows,
- стартовой последовательностью процесса,
- инициализацией MSVC CRT,
- и механизмом потокобезопасной инициализации локальных статических объектов.
Основной задачей было установить:
1. Начальное состояние отладчика
- какую роль TLS играет до входа в main,
- используется ли TLS для callback-инициализации,
- и как именно TLS применяется в рантайме MSVC.
После подключения WinDbg к процессу conhost.exe был получен следующий стек:
ntdll!DbgBreakPoint
ntdll!DbgUiRemoteBreakin
KERNEL32!BaseThreadInitThunk
ntdll!RtlUserThreadStart
Это означает, что:
Поэтому дальнейший анализ проводился через структуры процесса.
- текущий поток — служебный поток, созданный отладчиком,
- стек не относится к рабочему стеку приложения.
2. Обход структур загрузчика
Были исследованы следующие структуры:
Состояние загрузчика оказалось корректным.
- TEB
- PEB
- PEB->Ldr
- списки загрузчика
- _LDR_DATA_TABLE_ENTRY
Основной модуль процесса был определён как:
conhost.exe
3. Путь запуска приложения
Точка входа образа указывала на:
conhost!wWinMainCRTStartup
Полная цепочка запуска процесса выглядит следующим образом:
ntdll!LdrInitializeThunk
ntdll!LdrpInitializeProcess
conhost!wWinMainCRTStartup
conhost!__scrt_common_main_seh
conhost!wWinMain
После этого управление передаётся рабочей логике:
ConsoleCreateIoThreadLegacy
↓
ConsoleIoThread
Именно этот поток выполняет основную работу консольного хоста.
4. TLS-директория образа
Файл conhost.exe содержит реальную TLS-директорию PE.
Обнаруженные символы:
conhost!tls_used
conhost!tls_start
conhost!tls_end
conhost!tls_index
Размер TLS-шаблона:
8 байт
Содержимое шаблона:
+0x0 = 0x00000000
+0x4 = conhost!Init_thread_epoch
Во время выполнения:
tls_index = 0
Это означает, что conhost использует первый слот TLS.
5. Чего TLS здесь не делает
Анализ показал отсутствие:
Следовательно, TLS не используется как механизм выполнения кода до main.
- значимых TLS callbacks
- CRT TLS initialization callbacks
6. Реальная роль TLS
TLS используется CRT MSVC для реализации:
thread-safe local static initialization
Были обнаружены следующие функции CRT:
Init_thread_header
Init_thread_footer
Init_thread_abort
Init_thread_wait
Init_global_epoch
Init_thread_epoch
Это стандартный механизм MSVC для защиты локальных статических объектов.
7. Примеры guarded static
Telemetry::Instance
Типичная схема:
CommandLine::Instance
- чтение TLS через gs:[58h]
- получение thread_epoch
- сравнение с guard-переменной
- вызов Init_thread_header
- конструирование singleton
- регистрация atexit
- Init_thread_footer
Использует тот же механизм.
Guard-переменная хранится:
__PchSym_+0x4
TermTelemetry::Instance
Использует аналогичный шаблон.
NtPrivApi::_Instance
Lazy-инициализация:
LoadLibraryExW("ntdll.dll")
GetProcAddress(...)
Также используется atexit.
Lazy-инициализация системных вызовов
Кэшируются указатели на:
NtOpenProcess
NtQueryInformationProcess
NtClose
8. Отличие от глобальных конструкторов
Глобальные конструкторы выполняются через стандартные CRT-таблицы:
dynamic initializer for ...
Это другой механизм, не связанный напрямую с TLS guarded static.
9. Наблюдение TLS в живых потоках
Глобальный epoch:
conhost!Init_global_epoch = 0x80000007
Поток ConsoleIoThread
TEB.ThreadLocalStoragePointer = 0x44c6e0
TLS slot 0 = 0x4366a0
Содержимое:
+0x0 = 0
+0x4 = 0x80000007
Render thread
TLS slot 0 = 0x02c3d720
Содержимое:
+0x0 = 0
+0x4 = 0x80000000
Это показывает, что epoch в разных потоках может отличаться.
10. Значения guard-переменных
Telemetry::Instance = 0x80000001
CommandLine::Instance = 0x80000004
TermTelemetry::Instance = 0x80000007
NtPrivApi::_Instance = 0
Интерпретация:
0 — не инициализировано
FFFFFFFF — инициализация выполняется
epoch value — объект уже создан
11. Работа функций CRT
Init_thread_header
Init_thread_footer
- входит в критическую секцию
- если guard = 0 → устанавливает 0xFFFFFFFF
- если guard = 0xFFFFFFFF → ожидает
- иначе обновляет TLS epoch
Init_thread_abort
- увеличивает Init_global_epoch
- записывает новый epoch:
- в guard
- в TLS текущего потока
Сбрасывает guard:
guard = 0
12. Итог по роли TLS
В данной сессии conhost.exe:
Таким образом, TLS здесь выступает механизмом синхронизации рантайма, а не способом выполнения кода до main.
- загрузчик создаёт TLS до main
- TLS содержит epoch-состояние CRT
- CRT использует TLS для потокобезопасной инициализации локальных статических объектов
13. Наблюдение lazy-инициализации NtPrivApi
В момент анализа состояние было:
module handle = 0
NtOpenProcess = 0
NtQueryInformationProcess = 0
NtClose = 0
Это означает, что lazy-инициализация ещё не выполнялась.
14. Когда происходит инициализация NtPrivApi
Её вызывает функция:
NtPrivApi::s_GetProcessParentId
Она выполняет:
NtOpenProcess
NtQueryInformationProcess
NtClose
15. Реальный вызывающий код
Статический анализ показал единственный вызов:
ApiDispatchers::ServerGenerateConsoleCtrlEvent
Алгоритм:
16. Жизненный цикл TLS в загрузчике
- поиск процесса в списке консоли
- если не найден
- запрос parent PID через NtPrivApi
- повторный поиск
LdrpInitializeTls
Функция:
LdrpAllocateTls
- проходит список модулей
- ищет TLS-директории
- создаёт записи TLS
Для каждого потока:
LdrpInitializeThread
- выделяет TLS-вектор
- копирует TLS-шаблон модуля
- записывает указатель в TEB+0x58
При старте потока:
LdrShutdownThread
- вызывается LdrpAllocateTls
- затем выполняются TLS callbacks
При завершении потока:
17. Начальное значение TLS
- вызываются TLS callbacks (THREAD_DETACH)
- освобождается TLS память
Шаблон TLS:
00 00 00 00
00 00 00 80
То есть:
Init_thread_epoch = 0x80000000
Поток получает это значение при создании.
18. Особенность потока отладчика
Поток:
ntdll!DbgUiRemoteBreakin
имел:
ThreadLocalStoragePointer = 0
Причина:
SameTebFlags = 0x8
Этот флаг заставляет LdrpInitializeThread использовать специальный путь, который пропускает LdrpAllocateTls.
Поэтому поток отладчика не является нормальным примером TLS-инициализации.
19. Реальный путь создания потоков в conhost
Были обнаружены функции:
CreateConsoleInputThread
CreateAndStartSignalThread
HostSignalInputThread::Start
PtySignalInputThread::Start
Из них CreateConsoleInputThread может быть вызвана повторно.
Цепочка вызова:
ConsoleHandleConnectionRequest
→ ConsoleAllocateConsole
→ CreateConsoleInputThread
20. Практический триггер
Для вызова этого пути использовался:
AttachConsole(client_pid)
Важно:
AttachConsole(conhost_pid) → ошибка
Нужно передавать PID клиентского процесса, использующего консоль.
Итог
В результате исследования было установлено:
Таким образом, в conhost.exe TLS не является механизмом ранней инициализации кода, а служит внутренней инфраструктурой рантайма MSVC для потокобезопасной ленивой инициализации объектов.
- conhost.exe использует статический TLS
- TLS-блок имеет размер 8 байт
- загрузчик создаёт TLS-копию для каждого потока
- MSVC CRT использует этот TLS для реализации guarded static initialization
- TLS здесь выполняет роль механизма синхронизации и кэширования состояния CRT
Если хочешь, я могу ещё:
- сделать версию уровня Habr / блог-статьи (читается намного легче)
- добавить диаграммы запуска процесса и TLS
- или превратить это в очень сильную реверс-инженерную статью уровня RE community.
Как работает Thread Local Storage
Дата публикации 7 мар 2026
| Редактировалось 31 мар 2026