https://github.com/NadavLor/windbg-ext-mcp Ну шо посоны, финита ля эксплория начинается! Кто-то насмехался против визоров, но они живы как никогда. Ниже под спойлером эксплейн для интереса. Спойлер: тык Архитектурная схема для отладки VM в VMware с одного хоста: Код (Text): ┌─────────────────────────────────────────────────────────────┐ │ ФИЗИЧЕСКИЙ ХОСТ │ │ │ │ ┌─────────────────┐ ┌───────────────────┐ │ │ │ HOST OS │ │ VMware VM │ │ │ │ (Windows) │ │ (Guest OS) │ │ │ │ │ │ │ │ │ │ ┌─────────────┐ │ Named Pipe │ ┌───────────────┐ │ │ │ │ │ WinDbg │ │ ◄──────────────► │ │ Kernel Debug │ │ │ │ │ │ (Debugger) │ │ \\.\pipe\com1 │ │ Settings │ │ │ │ │ └─────────────┘ │ │ └───────────────┘ │ │ │ │ │ │ │ │ │ │ ┌─────────────┐ │ │ ┌───────────────┐ │ │ │ │ │ Symbols │ │ │ │ Serial Port │ │ │ │ │ │ Store │ │ │ │ (Virtual) │ │ │ │ │ └─────────────┘ │ │ └───────────────┘ │ │ │ └─────────────────┘ └───────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ Детализированная схема коммуникации: Код (Text): WinDbg Process VMware Hypervisor Guest VM Kernel ┌─────────────┐ ┌─────────────────┐ ┌─────────────┐ │ │ │ │ │ │ │ Debug │ ────── │ Named Pipe │ ────── │ KD Stub │ │ Engine │ read/ │ Handler │ Serial │ (kdcom.dll) │ │ │ write │ │ I/O │ │ │ │ │ │ │ │ │ Symbol │ │ VM Memory │ │ Target │ │ Engine │ ────── │ Access Layer │ ────── │ Process │ │ │ │ │ │ │ └─────────────┘ └─────────────────┘ └─────────────┘ Схема данных: Код (Text): ┌─── HOST MEMORY SPACE ────┐ ┌─── VM MEMORY SPACE ────┐ │ │ │ │ │ WinDbg.exe │ │ Windows Kernel │ │ ├─ Debug Commands │ │ ├─ KDCOM Driver │ │ ├─ Symbol Cache │ │ ├─ Target Process │ │ └─ Memory Analysis │ │ └─ System State │ │ │ │ │ │ \\.\pipe\com1 │ │ COM1: (Virtual Serial) │ │ ▲ │ │ ▲ │ └──────────┼───────────────┘ └──────────┼─────────────┘ │ │ └─────── VMware Pipe ─────────┘ Сетевая схема (если нужна): Код (Text): Host OS Network Stack ┌─────────────────────┐ │ Real NIC │ ──── Внешняя сеть │ VMnet1 (Host-only) │ ──── VM отладка │ VMnet8 (NAT) │ ──── VM интернет └─────────────────────┘ Минимальные требования для комфортной работы: CPU: 4+ ядра (Intel i5/AMD Ryzen 5) RAM: 8+ GB (лучше 16+ GB) SSD: 50+ GB свободного места Сеть: не критична при использовании named pipe Архитектура загрузки символов: Код (Text): ┌──── WinDbg Process Memory ────┐ │ │ │ ┌─── Symbol Engine ─────┐ │ │ │ │ │ │ │ ┌─ Symbol Cache ────┐ │ │ │ │ │ • Loaded PDBs │ │ │ │ │ │ • Type info │ │ │ │ │ │ • Function names │ │ │ │ │ │ • Line numbers │ │ │ │ │ └──────────────────┘ │ │ │ │ │ │ │ │ ┌─ Lazy Loading ───┐ │ │ │ │ │ • On-demand load │ │ │ │ │ │ • LRU eviction │ │ │ │ │ └──────────────────┘ │ │ │ └───────────────────────┘ │ └───────────────────────────────┘ Механизм работы с символами: Ленивая загрузка (Lazy Loading): Символы загружаются по мере необходимости При первом обращении к модулю Не все символы сразу в памяти Кэширование: Код (Text): Disk Symbol Store → Memory Cache → Active Symbols ┌─────────────────┐ ┌─────────────┐ ┌─────────────┐ │ Local Cache │ ──→ │ Recently │ ──→ │ Currently │ │ • .pdb files │ │ Used PDBs │ │ Needed │ │ • 1-5 GB │ │ • 100-500MB │ │ • 10-50 MB │ └─────────────────┘ └─────────────┘ └─────────────┘ Типичное потребление памяти символами: Windows Kernel символы: ntoskrnl.exe: ~15-30 MB hal.dll: ~2-5 MB Драйверы: ~1-10 MB каждый Пользовательские модули: Большие приложения: ~20-100 MB Системные DLL: ~5-20 MB каждая --- Сообщение объединено, 16 июн 2025 --- Это часы Спойлер: тык --- Сообщение объединено, 16 июн 2025 --- Еще пример Спойлер: тыкани Код (Text): Okay, let's use WinDbg commands to examine how modules find functions in other modules, focusing on the Import Address Table (IAT) and Export Address Table (EAT). We'll use `KERNEL32.dll` importing a function from `ntdll.dll` as an example. A common function `KERNEL32.dll` might import from `ntdll.dll` is `NtCreateFile`. **Step 1: List loaded modules to get base addresses.** First, let's list the loaded modules to find the base addresses of `ntdll.dll` and `KERNEL32.dll`. Ran tool Ясно, давайте используем WinDbg, чтобы посмотреть, как модули находят функции в других модулях, сосредоточив внимание на таблице импорта (IAT) и таблице экспорта (EAT). Мы уже получили базовые адреса `ntdll.dll` и `KERNEL32.dll`. Теперь давайте найдем адрес функции `NtCreateFile` в `ntdll.dll` (из таблицы экспорта) и затем посмотрим, куда указывает запись этой функции в таблице импорта `KERNEL32.dll`. **Шаг 2: Найти адрес `NtCreateFile` в `ntdll.dll` (Таблица экспорта).** Я использую команду `x ntdll!NtCreateFile`, чтобы найти ее адрес. Ran tool Мы выяснили, что `ntdll!NtCreateFile` находится по адресу `00007ffc`8344df80`. Теперь давайте посмотрим, как `KERNEL32.dll` "знает" об этом адресе. Модули используют **Таблицу адресов импорта (IAT)** для хранения адресов функций, импортируемых из других DLL. **Шаг 3: Найти запись `NtCreateFile` в IAT `KERNEL32.dll`.** Обычно импортированные функции представлены в IAT с префиксом `_imp_`. Давайте попробуем найти это, используя `x KERNEL32!_imp_NtCreateFile`. Ran tool Мы нашли адрес записи `KERNEL32!_imp_NtCreateFile` в IAT `KERNEL32.dll` как `00007ffc`815b60e0`. Теперь, чтобы подтвердить, что эта запись действительно указывает на `ntdll!NtCreateFile`, мы можем прочитать содержимое памяти по этому адресу. Во время загрузки модуля загрузчик Windows "фиксирует" (patch) эту запись, чтобы она содержала фактический адрес импортируемой функции. **Шаг 4: Прочитать содержимое IAT записи `KERNEL32!_imp_NtCreateFile`.** Я использую команду `dps` (display pointer and symbol) для просмотра содержимого памяти по адресу `00007ffc`815b60e0. Ran tool Мы успешно выяснили, как базовые модули взаимодействуют в WinDbg: 1. Мы нашли адрес функции `ntdll!NtCreateFile` в `ntdll.dll`, который оказался `00007ffc`8344df80`. Это адрес функции в **таблице экспорта (EAT)** `ntdll.dll`. 2. Затем мы нашли запись `KERNEL32!_imp_NtCreateFile` в **таблице импорта (IAT)** `KERNEL32.dll` по адресу `00007ffc`815b60e0`. 3. Наконец, мы прочитали содержимое памяти по этому адресу `00007ffc`815b60e0` и увидели, что оно указывает точно на адрес `00007ffc`8344df80`, то есть на `ntdll!NtCreateFile`. **Вывод:** Таким образом, `KERNEL32.dll` "знает" о местонахождении функции `NtCreateFile` в `ntdll.dll` благодаря **загрузчику Windows**. Во время загрузки системы или процесса, загрузчик "заполняет" (patching) запись в таблице импорта `KERNEL32.dll` (`_imp_NtCreateFile`) фактическим адресом функции `NtCreateFile`, который он получает из таблицы экспорта `ntdll.dll`. Это позволяет `KERNEL32.dll` напрямую вызывать эту функцию из `ntdll.dll`. Спойлер: истоки Код (Text): Хороший вопрос! "Первая точка" загрузчика Windows, с которой начинается выполнение пользовательского приложения, довольно интересна. В контексте отладки пользовательского режима с помощью WinDbg, самая ранняя и обычно видимая точка входа, относящаяся к загрузчику, это функция **`ntdll!LdrInitializeThunk`**. Эта функция является "связующим звеном" (thunk) между низкоуровневой инициализацией, которая уже произошла в режиме ядра, и началом выполнения пользовательского кода. Именно `LdrInitializeThunk` в конечном итоге вызывает `LdrpInitializeProcess`, которую мы рассматривали ранее, для выполнения значительной части инициализации нового процесса. Давайте посмотрим на ассемблерный код `ntdll!LdrInitializeThunk` в WinDbg. Из нашей предыдущей трассировки стека мы видели `ntdll!LdrInitializeThunk+0xe` по адресу `00007ffc`83425c1e`. Это означает, что сама функция `ntdll!LdrInitializeThunk` начинается чуть раньше, по адресу `00007ffc`83425c10`. **Шаг 1: Дизассемблируем `ntdll!LdrInitializeThunk`:** { "success": true, "result": "ntdll!LdrInitializeThunk:\n00007ffc`83425c10 4053 push rbx\n00007ffc`83425c12 4883ec20 sub rsp,20h\n00007ffc`83425c16 488bd9 mov rbx,rcx\n00007ffc`83425c19 e81a000000 call ntdll!LdrpInitialize (00007ffc`83425c38)\n00007ffc`83425c1e b201 mov dl,1\n00007ffc`83425c20 488bcb mov rcx,rbx\n00007ffc`83425c23 e818810200 call ntdll!NtContinue (00007ffc`8344dd40)\n00007ffc`83425c28 8bc8 mov ecx,eax\n00007ffc`83425c2a e811c90800 call ntdll!RtlRaiseStatus (00007ffc`834b2540)\n00007ffc`83425c2f cc int 3\n00007ffc`83425c30 cc int 3\n00007ffc`83425c31 cc int 3\n00007ffc`83425c32 cc int 3\n00007ffc`83425c33 cc int 3\n00007ffc`83425c34 cc int 3\n00007ffc`83425c35 cc int 3\n", "execution_method": "optimized", "performance_info": { "cached": false, "response_time": 0.114588, "retries_used": 0, "timeout_category": "execution", "optimization_level": "direct" }, "resilience_info": null } Ran tool Как видно из дизассемблированного кода, **`ntdll!LdrInitializeThunk`** действительно является самой ранней и обычно первой точкой, которую мы видим в WinDbg в контексте инициализации загрузчика Windows для пользовательского режима. Вот почему это "первая точка" и как она работает: * **Передача управления из ядра:** После того как ядро Windows завершает свои начальные этапы загрузки для нового процесса, оно передает управление в пользовательский режим, и первой функцией, которая выполняется в контексте пользовательского процесса, является именно `ntdll!LdrInitializeThunk`. Это своего рода "трамплин" из режима ядра в режим пользователя. * **Цепочка инициализации:** 1. **`ntdll!LdrInitializeThunk`**: Эта функция, как вы видите в дизассемблере, выполняет начальную настройку стека и регистров, а затем делает **вызов (`call`)** к `ntdll!LdrpInitialize`. 2. **`ntdll!LdrpInitialize`**: Эта функция (которая затем, в свою очередь, вызывает `ntdll!LdrpInitializeProcess`, как мы видели в стеке ранее) отвечает за более глубокую инициализацию, такую как: * Загрузка необходимых DLL (например, `KERNEL32.dll`, `user32.dll` и т.д.). * Разрешение импортов (заполнение IAT). * Выполнение функций инициализации для загруженных DLL (например, `DllMain`). * Подготовка среды для выполнения основного исполняемого файла приложения. 3. **`ntdll!LdrpInitializeProcess`**: Как мы уже обсуждали, это часть `LdrpInitialize`, которая выполняет фактическую работу по инициализации процесса. Таким образом, хотя `LdrInitializeThunk` сама по себе не выполняет всю работу по загрузке, она является **самой первой функцией в пользовательском режиме**, которая запускает всю цепочку операций загрузчика Windows. Это то место, где операционная система передает "эстафетную палочку" управлению пользовательскому пространству, чтобы подготовить приложение к запуску. --- Сообщение объединено, 16 июн 2025 --- Имба или нет Спойлер: смотри меня Код (Text): Отлично! Давайте пошагово проследим, как происходит передача управления внутри `ntdll!RtlUserThreadStart` до функции вашего потока. Как мы видели в дизассемблере, `ntdll!RtlUserThreadStart` готовится вызвать `Kernel32ThreadInitThunkFunction`. Эта функция из `kernel32.dll` является "переходником" (thunk), который в конечном итоге вызывает вашу реальную функцию потока, которую вы указали при его создании. Давайте выполним несколько шагов, чтобы пройти через этот процесс: **Шаг 1: Выполним первую инструкцию `sub rsp, 78h`** Эта инструкция является частью пролога функции и выделяет место на стеке. Ran tool Я выполнил первую инструкцию. Теперь давайте продолжим. **Шаг 2: Выполним `mov r9,rcx`** Эта инструкция перемещает значение из `rcx` в `r9`. `rcx` часто содержит первый аргумент функции, а `r9` может быть использован для передачи аргументов или временного хранения. Ran tool Я выполнил вторую инструкцию. Теперь давайте продолжим. **Шаг 3: Выполним `mov rax,qword ptr [ntdll!Kernel32ThreadInitThunkFunction]`** Эта инструкция загружает адрес функции `Kernel32ThreadInitThunkFunction` из `ntdll.dll` в регистр `RAX`. Ran tool Я выполнил третий шаг, который загрузил адрес `Kernel32ThreadInitThunkFunction` в регистр `RAX`. Теперь `RAX` содержит адрес функции из `kernel32.dll`, которая будет вызвана следующей. **Шаг 4: Выполним `call qword ptr [ntdll!_guard_dispatch_icall_fptr]`** Это ключевой шаг, который передаст управление `Kernel32ThreadInitThunkFunction`. Мы используем `p` (step over), чтобы выполнить весь вызов, не входя в детали `_guard_dispatch_icall_fptr`, который является частью механизма CFG. Ran tool Я выполнил инструкцию `call`. Теперь мы должны быть внутри `KERNEL32!BaseThreadInitThunk` (или `Kernel32ThreadInitThunkFunction`, как она называется в `ntdll`), которая является точкой входа для потоков в `kernel32.dll`. Давайте проверим текущее состояние, чтобы увидеть, куда мы попали: **Шаг 5: Проверим текущее состояние (регистры и стек) после вызова.** **1. Регистры:** Ran tool Мои извинения, в предыдущем шаге я использовал `p` (step over) вместо `t` (trace into) для вызова, который должен был перевести нас в `Kernel32ThreadInitThunkFunction`. Это моя ошибка. Давайте исправим это и пошагово войдем в функцию, чтобы увидеть, как происходит передача потока выполнения. Мы начнем с текущего `rip`, который, судя по последнему выводу, находится на `00007ffc`833fcc81` (инструкция `je`). **Шаг 1: Дойдем до инструкции `call`** Нам нужно выполнить несколько инструкций, чтобы добраться до `call qword ptr [ntdll!_guard_dispatch_icall_fptr]` по адресу `00007ffc`833fcc8b`. Выполним `p` три раза: 1. `00007ffc`833fcc81 7410 je ntdll!RtlUserThreadStart+0x23` (Предполагаем, что переход не состоится, так как `rax` не был нулевым) 2. `00007ffc`833fcc83 4c8bc2 mov r8,rdx` 3. `00007ffc`833fcc86 488bd1 mov rdx,rcx` 4. `00007ffc`833fcc89 33c9 xor ecx,ecx` Ran tool Я выполнил предыдущие инструкции, и теперь мы находимся прямо перед `call` к `Kernel32ThreadInitThunkFunction`. **Шаг 2: Выполним `call qword ptr [ntdll!_guard_dispatch_icall_fptr]` с трассировкой (`t`)** Это позволит нам *войти* в функцию, на которую указывает `RAX` (то есть `KERNEL32!BaseThreadInitThunk`), и проследить передачу управления. Ran tool Я выполнил команду `t` (trace into). Теперь мы должны находиться внутри `KERNEL32!BaseThreadInitThunk`, которая является фактической функцией-оберткой для потоков в `kernel32.dll`. Давайте проверим текущее состояние, чтобы увидеть, куда мы попали, и как происходит дальнейшая передача управления к вашей функции потока. **Шаг 3: Проверим текущее состояние (регистры и стек) после входа в `KERNEL32!BaseThreadInitThunk`.** **1. Регистры:** Ran tool Отлично! Мы успешно вошли в `KERNEL32!BaseThreadInitThunk`. Судя по выводу регистров: * `rip=00007ffc`81547360`, что является началом `KERNEL32!BaseThreadInitThunk`. * `rsp=000000000014ff58`, стек скорректирован. * `rcx=0000000000000000`, `rdx=00007ff7a58e11b3`, `r8=00000000002b9000`, `r9=00007ff7a58e11b3`. Эти регистры, скорее всего, содержат аргументы, переданные в `BaseThreadInitThunk`, включая адрес вашей функции потока и ее аргумент. Обычно, адрес функции потока передается как первый аргумент (в `rcx`), а аргумент для этой функции потока - как второй (в `rdx`). Однако, здесь `rcx` нулевой, что может указывать на то, что аргументы были переданы по-другому или это какая-то служебная функция. Мы увидим это при дизассемблировании. **Шаг 1: Проверим стек после входа в `KERNEL32!BaseThreadInitThunk`:** Ran tool Вы находитесь в `KERNEL32!BaseThreadInitThunk`. Это обертка, которая подготавливает среду и затем вызывает вашу функцию потока. Из стека видно, что она была вызвана из `ntdll!RtlUserThreadStart+0x21`. Чтобы понять, как именно происходит передача потока выполнения отсюда к вашей функции, нам нужно **дизассемблировать `KERNEL32!BaseThreadInitThunk`**. **Шаг 2: Дизассемблируем `KERNEL32!BaseThreadInitThunk`:** Ran tool Вы отлично подметили! Теперь, когда мы находимся в `KERNEL32!BaseThreadInitThunk`, давайте проследим, как именно эта функция передает управление вашей функции потока. Как мы видим из дизассемблирования `KERNEL32!BaseThreadInitThunk`, она выполняет следующие ключевые действия: ```assembly KERNEL32!BaseThreadInitThunk: ... 00007ffc`81547368 498bc8 mov rcx,r8 ; Перемещаем аргумент потока (который был в r8) в rcx 00007ffc`8154736b 488bc2 mov rax,rdx ; Перемещаем адрес функции потока (который был в rdx) в rax 00007ffc`8154736e ff15d4f20600 call qword ptr [KERNEL32!_guard_dispatch_icall_fptr (00007ffc`815b6648)] ; Вызываем функцию, на которую указывает rax ... ``` Из предыдущего анализа регистров, у нас было: * `rdx=00007ff7a58e11b3` (вероятно, адрес вашей функции потока) * `r8=00000000002b9000` (вероятно, аргумент для вашей функции потока) Итак, `BaseThreadInitThunk` берет адрес вашей функции потока из `rdx` и помещает его в `rax`, а аргумент потока из `r8` и помещает его в `rcx`. Затем он выполняет `call`, которая, благодаря CFG и конечному адресу в `rax`, перенесет выполнение прямо в вашу функцию потока. Давайте выполним эти шаги: **Шаг 1: Выполняем `sub rsp,28h`** Ran tool Я выполнил `sub rsp,28h`. Теперь давайте продолжим. **Шаг 2: Выполняем `test ecx,ecx` и потенциальный `jne`** Эти инструкции проверяют значение `ecx` и, если оно не равно нулю, переходят к другой части функции. В случае обычной инициализации потока, этот переход, скорее всего, не состоится. Ran tool Я выполнил предыдущие инструкции. Теперь мы находимся прямо перед инструкциями, которые перемещают адрес вашей функции потока и ее аргумент в правильные регистры (`RAX` и `RCX`) для вызова. **Шаг 3: Выполним `mov rcx,r8` и `mov rax,rdx`** Эти инструкции подготавливают регистры `RCX` (первый аргумент, который будет передан вашей функции потока) и `RAX` (адрес вашей функции потока). Ran tool Я выполнил предыдущие инструкции, которые подготовили регистры `RCX` (для аргумента вашей функции потока) и `RAX` (для адреса вашей функции потока). Теперь мы находимся прямо перед ключевым вызовом, который передаст управление вашей функции потока. **Шаг 4: Выполним `call qword ptr [KERNEL32!_guard_dispatch_icall_fptr]` с трассировкой (`t`)** Эта инструкция фактически передаст управление вашей функции потока, адрес которой теперь находится в `RAX`. Мы используем `t` (trace into), чтобы войти в вашу функцию. Ran tool Я выполнил команду `t` (trace into). Теперь выполнение должно было перейти в вашу функцию потока. Давайте проверим текущее состояние, чтобы подтвердить, что мы находимся в вашей функции потока и как произошла передача управления. **Шаг 5: Проверим текущее состояние (регистры и стек) после входа в вашу функцию потока.** **1. Регистры:** Ran tool Мои извинения за неточность в предыдущем объяснении! Моя ошибка, я должен был быть более внимательным к выводу `r` после команды `t`. Вывод `r` показывает, что `rip=00007ffc`8154736e`, что является адресом инструкции `call qword ptr [KERNEL32!_guard_dispatch_icall_fptr...]` внутри `KERNEL32!BaseThreadInitThunk`. Это означает, что **мы только что *выполнили* эту инструкцию `call`**, и теперь выполнение должно было перейти к ее цели, которая, как указано в выводе, является **`ntdll!LdrpDispatchUserCallTarget`**. Мы еще не в вашей функции потока, но мы очень близко! `ntdll!LdrpDispatchUserCallTarget` - это служебная функция в `ntdll.dll`, которая обрабатывает вызовы пользовательских функций после проверок CFG (Control Flow Guard), и именно она затем передаст управление вашей функции потока. Давайте подтвердим наше текущее местоположение и затем пошагово перейдем в вашу функцию. **Шаг 1: Проверим текущую инструкцию, чтобы убедиться, что мы находимся в `ntdll!LdrpDispatchUserCallTarget`.** Мы используем команду `u rip L1` (unassemble current instruction for 1 line). Ran tool Мои глубокие извинения за повторную ошибку! Вы совершенно правы, я снова неточно отследил выполнение. Я понял. Мы все еще находимся на инструкции `call` внутри `KERNEL32!BaseThreadInitThunk`, которая должна была привести нас к `ntdll!LdrpDispatchUserCallTarget`. Моя цель - пошагово показать, как происходит передача управления *до вашей функции потока*. Давайте исправим это прямо сейчас. Мы снова выполним `t` (trace into) из текущего положения, чтобы действительно войти в `ntdll!LdrpDispatchUserCallTarget`. **Шаг 1: Войдем в `ntdll!LdrpDispatchUserCallTarget`** Текущий `rip` находится на `00007ffc`8154736e` (`KERNEL32!BaseThreadInitThunk+0xe`), который является инструкцией `call` к `ntdll!LdrpDispatchUserCallTarget`. Используем `t`. Ran tool Я снова выполнил команду `t`. Теперь мы должны были войти в `ntdll!LdrpDispatchUserCallTarget`. Давайте проверим текущее состояние (регистры и стек), чтобы убедиться, что мы находимся в этой функции, и как происходит дальнейшая передача управления к вашей функции потока. **Шаг 1: Проверим текущее состояние (регистры и стек) после входа в `ntdll!LdrpDispatchUserCallTarget`.** **1. Регистры:** Ran tool Отлично! Мы наконец-то добрались до вашего приложения! Текущая точка выполнения: `MyCustomApp!ILT+430(mainCRTStartup)` по адресу `00007ff7`a58e11b3`. Это означает, что передача потока выполнения от системных библиотек к вашему пользовательскому коду успешно завершена. Вот как это произошло пошагово: 1. **`ntdll!RtlUserThreadStart`**: Как мы обсуждали, это самая ранняя точка входа для *любого* нового потока в пользовательском режиме. Она получает контроль от ядра. 2. **`KERNEL32!BaseThreadInitThunk`**: `RtlUserThreadStart` передает управление этой функции в `kernel32.dll`. `BaseThreadInitThunk` является оберткой, которая готовит среду для вашей функции потока, извлекая ее адрес и аргументы. 3. **`ntdll!LdrpDispatchUserCallTarget`**: `BaseThreadInitThunk` не вызывает вашу функцию напрямую, а использует служебную функцию `ntdll!LdrpDispatchUserCallTarget` (через механизм CFG). Эта функция выполняет дополнительные проверки и, наконец, безопасно передает управление в целевой адрес, который в данном случае является вашим приложением. 4. **`MyCustomApp!ILT+430(mainCRTStartup)`**: Это первая реальная точка входа в ваш исполняемый файл. `ILT` (Import-Local Thunk) указывает, что это "трамплин" к настоящей функции `mainCRTStartup`. **Что такое `mainCRTStartup`?** `mainCRTStartup` (или `WinMainCRTStartup` для графических приложений) — это точка входа, предоставляемая стандартной библиотекой времени выполнения C/C++ (CRT). Прежде чем вызвать вашу основную функцию `main()` или `WinMain()`, `mainCRTStartup` выполняет ряд важных задач инициализации, таких как: * Инициализация глобальных переменных. * Установка обработчиков исключений. * Инициализация стандартных потоков ввода/вывода (stdin, stdout, stderr). * Вызов TLS-колбэков (которые мы упоминали в статье). **Текущая инструкция:** `00007ff7`a58e11b3 e938170000 jmp MyCustomApp!mainCRTStartup (00007ff7`a58e28f0)` Эта инструкция `jmp` просто перенаправляет выполнение к фактическому началу функции `mainCRTStartup`.