Как работает разрешение имён DLL в Windows — порядок поиска и SxS
· Обновлено: · Го Комура · Windows, DLL, Загрузчик, Безопасность, Разработка Windows
История изменений (7 обновлений, последнее 30 Aug 2026)
Журнал изменений этой статьи. Там, где версия до правки была заархивирована, она остаётся доступной для чтения по постоянной ссылке с DOI.
- Русский текст переписан как полноценный технический перевод, а не калька с японского. Утверждения статьи не менялись.
- Добавлены 23 схемы Mermaid, чтобы по рисункам было видно предварительные правила разрешения имён DLL, как сужать пространство поиска API и как проверять, откуда модуль реально загрузился (по правилу «не меньше одной схемы на 500–750 знаков основного текста»). К уже существующей схеме оценки предварительных правил добавлена подпись, номера рисунков проставлены сквозной нумерацией. Сам текст статьи не менялся.
- В начало статьи добавлен раздел «Карта знаний этой статьи». Понятия, которые разбирает текст, и связи между ними собраны в краткое описание, схему и ссылку на страницу сведений. Утверждения статьи не менялись.
- Текст обновлён по итогам внешнего ревью (1283 замечания). Содержание отдельных правок смотрите в записях ниже.
- В примере загрузки плагина на C# исправлено: DLL из отдельной папки пытались подхватить флагами поиска ApplicationDirectory и System32. Первый указывает на папку exe, второй — на папку ОС, папку плагинов ни тот ни другой не смотрит. Загрузку переписали на полный путь и пояснили, что в DllImportSearchPath нет значения «произвольная папка» и что зависимые DLL этим не фиксируются.
- Добавлены минимальные примеры на C/C++ и C# (от SetDefaultDllDirectories до RemoveDllDirectory; в C# показан не только атрибут, но и реальный вызов). Новая глава — как проверить, какая DLL откуда загрузилась. В начало добавлена таблица терминов, порядок поиска сведён в таблицу из 12 строк.
- В японской статье заголовки «Related articles» и «References» оставались на английском — заменены японскими, формулировки ссылок на связанные статьи приведены к текущим заголовкам.
- Первая публикация
Цитирование статьи(DOI: 10.5281/zenodo.21619782)
Статья заархивирована на Zenodo. Ниже приведены DOI, который всегда ведёт к последней версии, и DOI, закреплённый за версией, которую вы читаете.
Го Комура (2026). Как работает разрешение имён DLL в Windows — порядок поиска и SxS. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21619782 https://comcomponent.com/ru/blog/2026/03/24/002-windows-dll-name-resolution/
- DOI (последняя версия)
- 10.5281/zenodo.21619782
- DOI (эта версия)
- 10.5281/zenodo.21619783
Когда речь заходит о нативных DLL в Windows, путаница почти всегда одна и та же.
- Куда на самом деле идёт
LoadLibrary("foo.dll")? - DLL лежит рядом с exe — почему тогда подхватывается другая?
- Что раньше:
System32или папка приложения? - На каком шаге срабатывают manifest, API set и Known DLLs?
- Что меняется от
SetDllDirectoryилиAddDllDirectory? - Из-за чего чаще случаются посадка DLL и DLL hijacking?
Заучить порядок поиска одной строкой для работы недостаточно. На деле загрузчик Windows сначала оценивает несколько особых правил и только потом обходит файловую систему.
flowchart TB
accTitle: Почему заучить порядок поиска недостаточно
accDescr: Разрешение имён DLL нельзя свести к одной строке порядка поиска. Загрузчик Windows сначала оценивает особые правила и только потом ищет по файловой системе.
a0["Заучить порядок поиска одной строкой"] -.-> a1["В работе это не помогает"]
a2["Как устроен загрузчик Windows"] --> a3["Сначала оценивает особые правила"]
a3 --> a4["Потом ищет по файловой системе"]
Рис. 1: Разрешение имени начинается не с обхода папок, а с предварительных особых правил.
В статье разбираем разрешение имён DLL в Windows с практической стороны, включая различия unpackaged app и packaged app, Known DLLs, loaded-module list, API set, side-by-side manifest и влияние семейства API LoadLibraryEx.
Материал опирается на открытую документацию Microsoft Learn по состоянию на март 2026 года.123456789
Термины, которые дальше идут без пояснения
Слова ниже дальше встречаются как есть. Подробности — в соответствующих главах.
| Термин | Коротко |
|---|---|
| packaged app / unpackaged app | Приложение, которое ставят и разносят как пакет (MSIX и подобное), или обычное, где exe лежит в папке. Само определение порядка поиска у них разное (главы 3 и 4) |
| safe DLL search mode | По умолчанию включён: отодвигает current folder назад в порядке поиска. Выключается, если реестровое значение SafeDllSearchMode поставить в 01 |
| DLL redirection | Если рядом с exe положить метку имя-приложения.exe.local, загрузчик сначала смотрит папку exe. Ещё называют .local и DotLocal (глава 7)7 |
| SxS (side-by-side) | Несколько версий одной DLL живут рядом, а какую версию привязать, записывают в manifest. SxS — сокращение от side-by-side (глава 7)9 |
| loaded-module list | Система проверяет, не загружена ли уже в память процесса DLL с тем же именем модуля. Откуда её когда-то взяли, неважно: если уже загружена, используют её (п. 5.1)1 |
| Known DLLs | Список DLL, которые эта версия Windows считает известными. Для них берут системную копию (п. 5.2)1 |
| API set | Контрактное имя вида api-ms-win-.... Виртуальный псевдоним, который прячет физическую DLL с реализацией (глава 6)3 |
| package dependency graph | Сам пакет приложения плюс пакеты, записанные как PackageDependency в секции Dependencies манифеста. Ищут в том порядке, в каком они записаны в манифесте1 |
1. Сначала выводы
Практические выводы — сразу списком.
- Разрешение имён DLL в Windows — это не «сначала поиск по файловой системе». DLL redirection, API set, SxS manifest, loaded-module list и Known DLLs входят в предварительный этап порядка поиска.1
- В обычной схеме unpackaged app при включённом safe DLL search mode папка приложения стоит высоко, но перед ней всё равно оценивают перечисленные особые правила.1
- Даже если DLL загрузили по полному пути, её зависимые DLL сами собой на тот же полный путь не фиксируются. Их ищут только по имени модуля, поэтому они могут разрешиться из другого места.1
Known DLLs— механизм, которым ОС привязывает известные ей DLL к системной копии. Обычное приложение это не перебьёт, просто положив одноимённый файл рядом с собой.1- API set — не «имя физической DLL как есть», а виртуальный псевдоним, который прячет DLL с реализацией. Увидев имя вида
api-ms-win-..., легко ошибиться, если думать о нём как об обычном поиске DLL.3 SetDllDirectoryне просто меняет порядок поиска: у неё есть поведение, которое по сути отключает safe DLL search mode. Неосторожный вызов может выйти боком с точки зрения безопасности.1- На практике безопаснее явно сужать область поиска связкой полного пути,
SetDefaultDllDirectories,AddDllDirectoryи флаговLOAD_LIBRARY_SEARCH_*уLoadLibraryEx.4562
Иначе говоря, с рабочей точки зрения разрешение имён DLL в Windows определяется не только тем, какая папка на каком месте, но и тем, какие предварительные правила разрешают имя и как API изменили пространство поиска.
flowchart TB
accTitle: Три вещи, из которых складывается разрешение имени DLL
accDescr: Результат разрешения имени DLL в Windows складывается не только из порядка папок, но и из предварительных правил, которые решают, во что превращается имя, и из пространства поиска, которое меняют API.
b1["Предварительные правила для имени"] --> b4["DLL, которую реально загружают"]
b2["Порядок поиска по папкам"] --> b4
b3["Пространство поиска, изменённое API"] --> b4
b1 -.-> b1d["redirection, Known DLLs"]
b3 -.-> b3d["флаги LoadLibraryEx"]
Рис. 2: Итог складывается из предварительных правил, порядка папок и API.
Карта знаний этой статьи
Статья разбирает, что разрешение имён DLL в Windows — это не просто порядок обхода папок: перед поиском в файловой системе оцениваются правила предыдущего этапа — DLL redirection, API set, манифест SxS, loaded-module list и Known DLLs. У packaged app и unpackaged app сами определения порядка поиска различаются, а положение current folder зависит от того, включён или выключен safe DLL search mode. SetDllDirectory фактически отключает safe DLL search mode, поэтому сужение пространства поиска сочетанием SetDefaultDllDirectories, флагов поиска LoadLibraryEx и AddDllDirectory снижает DLL hijacking. Даже задание полного пути автоматически не фиксирует зависимые DLL; фактический источник загрузки проверяют в Process Monitor или ListDLLs.
flowchart LR
accTitle: Карта знаний разрешения имён DLL в Windows
accDescr: Схема показывает, что DLL redirection, API set, манифест SxS, loaded-module list, Known DLLs и package dependency graph оцениваются раньше поиска в файловой системе; что у packaged app и unpackaged app сами определения порядка поиска различаются; что SetDllDirectory ослабляет safe DLL search mode, а флаги поиска SetDefaultDllDirectories и LoadLibraryEx сужают пространство поиска и снижают DLL hijacking; и как фактический источник загрузки проверяют средствами вроде Process Monitor
dll_search_order["порядок поиска DLL"]
dll_hijacking["DLL hijacking (атака DLL preloading)"]
dll_redirection["DLL redirection (.local)"]
api_set["API set"]
sxs_manifest_redirection["перенаправление манифеста SxS"]
loaded_module_list["список загруженных модулей"]
known_dlls["Known DLLs"]
package_dependency_graph["package dependency graph"]
current_folder_search["поиск в current folder"]
safe_dll_search_mode["safe DLL search mode"]
packaged_app["packaged app"]
unpackaged_app["unpackaged app"]
setdlldirectory["SetDllDirectory"]
setdefaultdlldirectories["SetDefaultDllDirectories"]
adddlldirectory["AddDllDirectory"]
loadlibraryex["LoadLibraryEx"]
dll_search_path_hardening["сужение пространства поиска DLL"]
bare_name_loadlibrary["LoadLibrary по имени без пути"]
fullpath_load["загрузка DLL по полному пути"]
dependent_dll_resolution["разрешение зависимых DLL"]
dllimportsearchpath["DllImportSearchPath (.NET)"]
procmon["Process Monitor (procmon.exe)"]
listdlls_tool["ListDLLs"]
tasklist_command["tasklist /m"]
dll_search_order -->|"использует"| dll_redirection
dll_search_order -->|"использует"| api_set
dll_search_order -->|"использует"| sxs_manifest_redirection
dll_search_order -->|"использует"| loaded_module_list
dll_search_order -->|"использует"| known_dlls
dll_search_order -.->|"использует"| package_dependency_graph
current_folder_search -->|"настраивается"| safe_dll_search_mode
packaged_app -->|"несовместимо с"| unpackaged_app
packaged_app -->|"использует"| package_dependency_graph
setdlldirectory -->|"несовместимо с"| safe_dll_search_mode
setdefaultdlldirectories -->|"снижает"| dll_hijacking
adddlldirectory -.->|"требует"| setdefaultdlldirectories
loadlibraryex -->|"использует"| adddlldirectory
setdlldirectory -->|"не рекомендуется"| dll_search_path_hardening
loadlibraryex -->|"рекомендуется для"| dll_search_path_hardening
dll_search_path_hardening -->|"снижает"| dll_hijacking
bare_name_loadlibrary -->|"может вызвать"| dll_hijacking
fullpath_load -.->|"снижает"| dll_hijacking
fullpath_load -->|"не рекомендуется"| dependent_dll_resolution
dllimportsearchpath -->|"использует"| loadlibraryex
dll_search_order -->|"проверяется"| procmon
loaded_module_list -->|"проверяется"| listdlls_tool
loaded_module_list -->|"проверяется"| tasklist_command
На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 23, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle
2. До «поиска по папкам» у разрешения имён DLL есть предварительные правила
По описанию DLL search order на Microsoft Learn при загрузке DLL как часть порядка поиска сначала обрабатывают такие элементы.1
- DLL redirection
- API sets
- SxS manifest redirection
- loaded-module list
- Known DLLs
И только потом начинается поиск по файловой системе: папка приложения, System32, папка Windows, PATH и так далее.1
Если это пропустить, кажется странным, что что-то решается раньше папки приложения. Для описания загрузчика Windows как раз это и есть основная логика.
flowchart TD
accTitle: Предварительные правила до поиска по файловой системе
accDescr: Имя DLL сначала проходит DLL redirection, API set, SxS manifest redirection, loaded-module list и Known DLLs. К поиску по файловой системе переходят, только если ни одно из этих правил не сработало.
A["Нужно разрешить имя DLL"] --> B["DLL redirection"]
B --> C["API set"]
C --> D["SxS manifest redirection"]
D --> E["loaded-module list"]
E --> F["Known DLLs"]
F --> G["Порядок поиска по файловой системе"]
G --> H["Определяется, какая DLL реально загружается"]
Рис. 3: Предварительные правила идут по очереди; поиск по файловой системе начинается, только если ни одно не сработало.
3. Стандартный порядок поиска для unpackaged app
Для обычного desktop-приложения, которое загружает DLL без полного пути, Microsoft Learn описывает стандартный порядок поиска unpackaged app. В состоянии по умолчанию, когда включён safe DLL search mode, порядок такой.1
| # | Куда смотрят | Тип | Пояснение |
|---|---|---|---|
| 1 | DLL redirection | Предварительное правило | Есть ли имя-приложения.exe.local (глава 7) |
| 2 | API sets | Предварительное правило | От контрактного имени к DLL с реализацией (глава 6) |
| 3 | SxS manifest redirection | Предварительное правило | Binding по manifest (глава 7) |
| 4 | loaded-module list | Предварительное правило | Загружен ли уже одноимённый модуль (п. 5.1) |
| 5 | Known DLLs | Предварительное правило | Для известной DLL — системная копия (п. 5.2) |
| 6 | package dependency graph | Предварительное правило | Начиная с Windows 11 версии 21H2. Ищут в порядке, записанном в манифесте |
| 7 | Папка, из которой загрузили приложение | Файловая система | Отсюда начинается собственно обход папок |
| 8 | Системная папка | Файловая система | Обычно %SystemRoot%\System32. Это то, что возвращает GetSystemDirectory |
| 9 | 16-bit system folder | Файловая система | Папка System времён 16 бит. Функции, которая вернула бы этот путь, нет, но искать здесь всё равно ищут. Современному приложению это почти никогда не нужно — достаточно знать, что пункт в порядке остался |
| 10 | Папка Windows | Файловая система | То, что возвращает GetWindowsDirectory |
| 11 | current folder | Файловая система | Если safe DLL search mode выключен, этот пункт поднимается на место 8 |
| 12 | Директории из PATH |
Файловая система | Отдельный путь приложения из реестрового ключа App Paths сюда не входит |
Таблица не для заучивания. Она нужна, чтобы попасть, на каком шаге решился ваш случай. Если ответ уже на шагах 1–6, раскладывание файлов по папкам 7 и дальше ничего не изменит.
flowchart TB
accTitle: Как пользоваться таблицей порядка поиска
accDescr: Таблицу порядка поиска не заучивают. По ней определяют, на каком шаге решился ваш случай. Если ответ уже на предварительных правилах, раскладка по папкам ничего не меняет.
c1["Симптом: подхватывается не та DLL"] --> c2{"На каком шаге решилось?"}
c2 -->|"Предварительные правила (1–6)"| c3["Менять раскладку по папкам бесполезно"]
c2 -->|"Файловая система (7–12)"| c4["Здесь помогают раскладка и пути"]
Рис. 4: До правки попадите, на каком шаге решилась проблема.
На практике особенно важны три момента.
- По умолчанию current folder стоит довольно далеко в конце. Safe DLL search mode как раз мешает выдвинуть его вперёд.1
- Но «в конце» не значит «безопасно». Пока в области поиска остаётся директория, которой может управлять злоумышленник, место для DLL preloading сохраняется.2
- Начиная с Windows 11 21H2 package dependency graph входит и в описание поиска для unpackaged app. Это легко пропустить, если помнить только старое описание.1
flowchart TB
accTitle: Место current folder и безопасность
accDescr: При включённом по умолчанию safe DLL search mode current folder стоит далеко в конце порядка поиска. Это не значит, что так безопасно: если в области поиска остаётся место, которым может управлять злоумышленник, место для DLL preloading сохраняется.
d1["safe DLL search mode включён (по умолчанию)"] --> d2["current folder стоит далеко в конце"]
d2 -.-> d3["«В конце» не значит «безопасно»"]
d3 --> d4["Если злоумышленник управляет местом в поиске, место для атаки остаётся"]
Рис. 5: Current folder отодвинули назад, но одной этой меры для защиты мало.
4. Packaged app и unpackaged app — это не одно и то же
Для packaged app Microsoft Learn задаёт отдельный порядок поиска. Там package dependency graph срабатывает раньше, и сама логика поиска немного другая.1
Если это различие пропустить, после перехода на MSIX или внедрения Windows App SDK начинается такая путаница:
- DLL, которая находится при unpackaged-запуске на разработке, в production-пакете не находится
- зависимости из package manifest смешиваются со старой опорой на
PATH, и условия воспроизведения меняются - «порядок поиска DLL в Windows вот такой» объясняют одной таблицей и теряют отличия packaged app
В статье и на ревью архитектуры безопаснее сначала разделить: «это packaged app или unpackaged app?»1
flowchart TB
accTitle: Сначала разделить packaged и unpackaged
accDescr: У packaged app свой порядок поиска, и package dependency graph срабатывает раньше. Отсюда путаница, когда DLL находится при unpackaged-запуске на разработке и не находится в production-пакете. На ревью архитектуры сначала спрашивают, о каком типе приложения речь.
e1{"О каком приложении речь?"}
e1 -->|"unpackaged app"| e2["Стандартный порядок поиска из главы 3"]
e1 -->|"packaged app"| e3["Другой порядок (dependency graph раньше)"]
e3 -.-> e4["После MSIX поведение на разработке и в production расходится"]
Рис. 6: Порядок поиска — не одна таблица: сначала packaged это или unpackaged.
5. Что делают Known DLLs и loaded-module list
Две части разрешения DLL, которые чаще всего идут против интуиции, — loaded-module list и Known DLLs.
5.1 loaded-module list
Microsoft Learn пишет, что система может проверить, не загружена ли уже в память DLL с тем же именем модуля.1
То есть до поиска по файловой системе есть проверка:
- это имя DLL уже загружено?
- и, следовательно, нужно ли вообще идти искать заново?
Если при разборе упустить, что «в этом процессе одноимённая DLL из другой папки уже была загружена раньше», условия воспроизведения прочитаете неверно.
flowchart TB
accTitle: Как срабатывает loaded-module list
accDescr: До поиска по файловой системе система проверяет, не загружена ли уже в память процесса DLL с тем же именем модуля. Если загружена, её и используют — откуда когда-то взяли, неважно. Искать заново уже не нужно.
f1["Запрос на разрешение имени DLL"] --> f2{"Одноимённый модуль уже загружен?"}
f2 -->|"Загружен"| f3["Берут этот модуль (папка не важна)"]
f2 -->|"Ещё нет"| f4["Идут дальше по поиску"]
f3 -.-> f5["Если одноимённую DLL из другой папки загрузили раньше, легко заблудиться"]
Рис. 7: Если одноимённый модуль уже загружен, загрузчик за новым экземпляром не идёт.
5.2 Known DLLs
Known DLLs — список DLL, которые Windows считает известными для данной версии; его можно посмотреть в HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs. Для DLL из списка система берёт свою копию известной DLL.1
Важно: Known DLLs — не та история, где обычное приложение «выиграет», положив одноимённую DLL в свою папку.
Если понимать это как простую гонку «кто первый» с System32, поведение прочитаете неверно.
flowchart TB
accTitle: Как работают Known DLLs
accDescr: Known DLLs — список DLL, которые эта версия Windows считает известными. Для них берут системную копию. Обычное приложение не перебьёт это, положив одноимённый файл в свою папку: это не гонка с System32.
g1["Имя DLL, которое нужно разрешить"] --> g2{"Есть в Known DLLs?"}
g2 -->|"Есть"| g3["Берут системную копию"]
g2 -->|"Нет"| g4["Идут дальше по поиску"]
g3 -.-> g5["Положить файл в папку приложения и перебить это нельзя"]
Рис. 8: Известную DLL привязывают к системной копии, гонки «кто первый» здесь нет.
6. API set — это не имя физической DLL, а контрактное имя
Увидев имя вроде api-ms-win-core-..., легко подумать: «откуда искать этот файл DLL?» Microsoft Learn объясняет, что API set — это виртуальный псевдоним физической DLL, механизм, который отделяет реализацию от контракта.3
То есть представление вида:
- имя API set = имя физического файла DLL как есть
- разрешение API set = такой же файловый поиск, как для обычной DLL
неточно.
Если держать в уме модель API set, проще объяснить, что:
- всё остаётся согласованным, даже если имя DLL с реализацией отличается в зависимости от версии Windows или типа устройства
- вызывающей стороне не нужно заранее знать, «какая хост-DLL реализует эту функцию»3
flowchart TB
accTitle: API set — контрактное имя
accDescr: Имя вида api-ms-win- — не имя физического файла DLL, а виртуальный псевдоним, который прячет DLL с реализацией. Контракт и реализация разделены, поэтому имя реализации может отличаться по версии Windows и типу устройства, а вызывающей стороне не нужно заранее знать хост-DLL.
h1["Контрактное имя api-ms-win-…"] --> h2["Разрешается как виртуальный псевдоним"]
h2 --> h3["Физическая DLL с реализацией скрыта"]
h3 --> h4["Реализация может отличаться по версии и устройству, согласованность сохраняется"]
h1 -.-> h5["Думать об этом как об обычном файловом поиске неточно"]
Рис. 9: API set — не «имя файла, который ищут», а контрактное имя, которое прячет реализацию.
7. Manifest и side-by-side (SxS) — другой ответ на проблему DLL versioning
DLL redirection и SxS manifest описывают не как мелкий трюк с порядком поиска, а как механизм, который не даёт версиям DLL столкнуться.789
По Microsoft Learn:
- manifest — это XML, который описывает side-by-side assembly или isolated application
- side-by-side assembly — единица именования, binding, versioning и deployment
- по зависимостям, записанным в manifest, загрузчик решает, к какой версии выполнить bind
flowchart TB
accTitle: Как связаны manifest и side-by-side
accDescr: Manifest — XML, который описывает side-by-side assembly или isolated application. Side-by-side assembly — единица именования, binding, versioning и deployment. По зависимостям в manifest загрузчик решает, к какой версии выполнить bind. Так избегают столкновения версий DLL.
i1["manifest (XML)"] --> i2["Описывает зависимый side-by-side assembly и версию"]
i2 --> i3["Загрузчик решает, к какой версии выполнить bind"]
i3 --> i4["Несколько версий одной DLL могут жить рядом"]
Рис. 10: SxS — не трюк с порядком поиска, а другой ответ на столкновение версий.
Поэтому на практике эти три вещи нужно рассматривать отдельно:
- просто положить private DLL в папку приложения
- использовать DLL redirection, например
.local - использовать side-by-side binding через manifest
Все они похожи тем, что «влияют на разрешение DLL», но замысел у них разный. Microsoft Learn подсказывает развилку: если нужно обойтись без правок существующего приложения — DLL redirection; если приложение новое — side-by-side-компоненты.7
flowchart TB
accTitle: Как выбирать между тремя средствами
accDescr: Положить private DLL в папку приложения, использовать DLL redirection (.local) и side-by-side binding через manifest — все влияют на разрешение DLL, но замысел разный. Без правок существующего приложения берут DLL redirection, для нового — side-by-side.
j0{"О каком средстве речь?"}
j0 --> j1["Положить private DLL в папку приложения"]
j0 --> j2["DLL redirection (.local)"]
j0 --> j3["SxS manifest binding"]
j2 -.-> j4["Когда нужно починить без правок существующего приложения"]
j3 -.-> j5["Управление зависимостями нового приложения"]
Рис. 11: Три похожих средства выбирают по замыслу, а не потому что «все влияют на DLL».
7.1 Что именно делает .local
Это легко проскочить одной строкой, поэтому поведение для unpackaged app разложено отдельно.7
- Что кладут: имя файла перенаправления —
имя-exe.local. ДляEditor.exeрядом с exe кладутEditor.exe.local. DLL, которую нужно подхватить, кладут в ту же папку - Содержимое: содержимое файла игнорируется. Сам факт, что файл есть, — сигнал загрузчику при загрузке DLL сначала смотреть папку exe
- Где срабатывает: и на загрузку по полному пути, и на загрузку только по имени модуля. Какой путь передали в
LoadLibraryилиLoadLibraryEx, неважно: если в папке exe есть одноимённая DLL, берут её. Спецификация как раз закрывает случаи вроде COM, где точка регистрации одна - Если не нашли: если в папке exe файла нет, возвращаются к обычному порядку поиска
- Вариант с папкой: работает и схема, когда создают папку
Editor.exe.localи кладут DLL внутрь неё - Включить на всю машину: в
HKLM\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Optionsзаводят DWORDDevOverrideEnableсо значением 1 и перезагружаются. После этого перенаправление через.localсрабатывает, даже если у приложения есть application manifest - Для packaged app: место другое — смотрят
<каталог установки пакета>\microsoft.system.package.metadata\application.local\
Есть побочный эффект, без которого разбор легко уходит не туда.
Если используют DLL redirection и приложение не может обратиться ко всем дискам и директориям из порядка поиска, LoadLibrary обрывает поиск в момент отказа в доступе. Без DLL redirection недоступную директорию пропускают и поиск продолжается.7
Если только в окружении с .local говорят «DLL, которая должна быть дальше, не находится» — подозревайте эту разницу.
flowchart TB
accTitle: Поведение .local
accDescr: Если рядом с exe есть метка имя-exe.local, сам факт её наличия — сигнал сначала смотреть папку exe, содержимое файла не важно. Срабатывает и на загрузку по полному пути. Если одноимённой DLL нет, возвращаются к обычному порядку. Побочный эффект: при отказе в доступе поиск обрывается.
k1["Кладут имя-exe.local"] --> k2["Сначала смотрят сторону exe"]
k2 --> k3{"Есть одноимённая DLL?"}
k3 -->|"Есть"| k4["Берут её, даже если путь полный"]
k3 -->|"Нет"| k5["Возвращаются к обычному порядку поиска"]
k1 -.-> k6["Побочный эффект: отказ обрывает поиск"]
Рис. 12: .local срабатывает самим фактом наличия и действует даже на загрузку по полному пути.
8. Что меняется с LoadLibraryEx, SetDllDirectory и AddDllDirectory
8.1 SetDllDirectory
SetDllDirectory меняет порядок поиска, но Microsoft Learn прямо пишет, что она по сути отключает safe DLL search mode.1
То есть даже если вызывали её с намерением «просто добавить одну папку приложения», в результате меняется пространство поиска, включая обращение с current folder.
Больше того, если вызвать SetDllDirectory в родительском процессе, влияние может дойти и до стандартного порядка поиска в дочернем.1
Поэтому на практике безопаснее не держать SetDllDirectory как повседневный инструмент, а опираться на:
SetDefaultDllDirectoriesAddDllDirectoryLOAD_LIBRARY_SEARCH_*уLoadLibraryEx
flowchart TB
accTitle: Ловушка SetDllDirectory
accDescr: SetDllDirectory вызывают, чтобы добавить одну папку приложения, но по сути отключают safe DLL search mode и меняют всё пространство поиска. Вызов в родителе может затронуть порядок поиска у потомка. Безопаснее опереться на SetDefaultDllDirectories, AddDllDirectory и флаги поиска LoadLibraryEx.
l1["SetDllDirectory, чтобы добавить одну папку"] --> l2["safe DLL search mode по сути отключается"]
l2 --> l3["Меняется всё пространство поиска, включая current folder"]
l2 -.-> l4["Может затронуть порядок поиска у дочернего процесса"]
l3 --> l5["Вместо этого опираться на семейство SetDefaultDllDirectories"]
Рис. 13: Хотели «добавить одну папку» — ослабили всё пространство поиска.
8.2 AddDllDirectory
Пути, добавленные через AddDllDirectory, используют вместе с LOAD_LIBRARY_SEARCH_USER_DIRS.
Microsoft Learn пишет, что порядок поиска при нескольких добавленных директориях не определён.15
Поэтому лучше не проектировать так:
- добавили несколько директорий
- и от порядка их обхода строго что-то ждут
flowchart TB
accTitle: Как пользоваться AddDllDirectory и на что смотреть
accDescr: Пути из AddDllDirectory работают в паре с LOAD_LIBRARY_SEARCH_USER_DIRS. Порядок поиска при нескольких добавленных директориях не определён, поэтому нельзя строить логику, которая ждёт строгого порядка обхода.
m1["AddDllDirectory добавляет путь"] --> m2["Срабатывает в паре с LOAD_LIBRARY_SEARCH_USER_DIRS"]
m2 -.-> m3["Порядок при нескольких добавлениях не определён"]
m3 --> m4["Не строить логику, которая зависит от порядка обхода"]
Рис. 14: На порядок между добавленными пользовательскими директориями в спецификации опираться нельзя.
8.3 SetDefaultDllDirectories
SetDefaultDllDirectories описывают как API, которым из стандартного DLL search path убирают директории, склонные становиться уязвимостью, и ограничивают область поиска.4
Три свойства стоит держать в голове.
- Действует на уровне процесса
- После вызова держится всю жизнь процесса
- Заданный стандартный путь поиска нельзя просто вернуть к исходной стандартной форме
С точки зрения безопасности это удобный API для схемы «сразу после запуска перейти к более безопасному пространству поиска».4
flowchart TB
accTitle: Свойства SetDefaultDllDirectories
accDescr: SetDefaultDllDirectories убирает из стандартного пути поиска DLL директории, склонные становиться уязвимостью, и ограничивает область поиска. Действует на процесс, держится всю его жизнь, к исходной стандартной форме путь не вернуть. Удобно вызвать сразу после запуска и сместить процесс в безопасную сторону.
n1["Сразу после запуска SetDefaultDllDirectories"] --> n2["Убирает из поиска места, склонные становиться уязвимостью"]
n2 --> n3["На процесс, всю его жизнь"]
n3 -.-> n4["К исходной стандартной форме не вернуть"]
Рис. 15: API, который один раз вызывают сразу после запуска, чтобы сместить весь процесс в безопасную сторону.
8.4 LoadLibraryEx
LoadLibraryEx позволяет менять поведение поиска флагами LOAD_WITH_ALTERED_SEARCH_PATH и LOAD_LIBRARY_SEARCH_*.61
На практике этот API хорошо ложится на такие требования, как:
- включить в область поиска и папку исходной DLL, вместе с зависимыми DLL
- ограничить поиск папкой приложения,
System32и явно добавленными пользовательскими директориями
Перед тем как писать вызов, стоит зафиксировать четыре ограничения.6
| Ограничение | Суть |
|---|---|
| Второй аргумент | hFile зарезервирован на будущее, всегда передают NULL |
| Нельзя сочетать | LOAD_WITH_ALTERED_SEARCH_PATH не сочетается ни с одним флагом LOAD_LIBRARY_SEARCH_* |
| Нужен полный путь | Если используете LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR, первый аргумент обязан быть полным путём |
| Порядок при нескольких флагах | Ищут в порядке LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR → LOAD_LIBRARY_SEARCH_APPLICATION_DIR → LOAD_LIBRARY_SEARCH_USER_DIRS → LOAD_LIBRARY_SEARCH_SYSTEM32. Порядок внутри USER_DIRS при этом не определён |
Если задан хотя бы один флаг LOAD_LIBRARY_SEARCH_*, стандартный путь поиска не используется вообще.
То есть при одном только LOAD_LIBRARY_SEARCH_SYSTEM32 папку приложения не смотрят. «Сузить» значит именно это.
flowchart TB
accTitle: Если задан флаг поиска, стандартный путь не используется
accDescr: Даже один флаг семейства LOAD_LIBRARY_SEARCH_ отключает стандартный путь поиска целиком. Если передан только LOAD_LIBRARY_SEARCH_SYSTEM32, папку приложения не смотрят. В этом и смысл «сузить».
o1{"Задан флаг семейства LOAD_LIBRARY_SEARCH_?"}
o1 -->|"Нет"| o2["Используется стандартный путь поиска"]
o1 -->|"Хотя бы один"| o3["Стандартный путь поиска не используется вообще"]
o3 --> o4["Ищут только в диапазоне заданных флагов"]
o4 -.-> o5["Только SYSTEM32 — папку приложения не смотрят"]
Рис. 16: Флаг не «добавляет», а «подменяет диапазон целиком».
8.5 Минимальный пример (C/C++)
Если сложить API из этой главы в один вызов, получается такая форма.
SetDefaultDllDirectories и LOAD_LIBRARY_SEARCH_* — API начиная с Windows 8, поэтому в заголовках нужно явно задать целевую версию.46
/* cl /W4 loader.c (Visual Studio 2022 + Windows SDK 10)
* SetDefaultDllDirectories / AddDllDirectory / LOAD_LIBRARY_SEARCH_* —
* начиная с Windows 8. Если в целях ещё Windows 7, нужен KB2533623
* и получение адресов из Kernel32.dll через GetProcAddress во время выполнения. */
#define _WIN32_WINNT 0x0602 /* Windows 8 */
#include <windows.h>
#include <stdio.h>
int wmain(void)
{
/* 1. Сместить процессное пространство поиска в безопасную сторону.
* Цель — убрать из поиска current folder и PATH.
* LOAD_LIBRARY_SEARCH_DEFAULT_DIRS — это связка
* APPLICATION_DIR + SYSTEM32 + USER_DIRS.
* Без USER_DIRS шаг 2 (AddDllDirectory) в процессное
* умолчание не попадёт. */
if (!SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS)) {
wprintf(L"SetDefaultDllDirectories failed: %lu\n", GetLastError());
return 1;
}
/* 2. Явно добавить только свою папку плагинов.
* В AddDllDirectory передают абсолютный путь. */
const wchar_t *pluginDir = L"C:\\MyApp\\plugins";
DLL_DIRECTORY_COOKIE cookie = AddDllDirectory(pluginDir);
if (cookie == NULL) {
wprintf(L"AddDllDirectory failed: %lu\n", GetLastError());
return 1;
}
/* 3. Загрузить.
* Второй аргумент hFile зарезервирован, всегда NULL.
* Если здесь передать флаги, берётся не процессное умолчание
* из шага 1, а только эта комбинация флагов.
* APPLICATION_DIR нет, поэтому папку приложения не смотрят. */
HMODULE h = LoadLibraryExW(
L"foo.dll",
NULL,
LOAD_LIBRARY_SEARCH_USER_DIRS | LOAD_LIBRARY_SEARCH_SYSTEM32);
if (h == NULL) {
wprintf(L"LoadLibraryExW failed: %lu\n", GetLastError());
RemoveDllDirectory(cookie);
return 1;
}
/* 4. Обязательно проверить, откуда загрузили.
* При разборе важнее не «получилось ли», а «откуда получилось». */
{
wchar_t loadedPath[MAX_PATH];
DWORD cap = (DWORD)(sizeof(loadedPath) / sizeof(loadedPath[0]));
DWORD len = GetModuleFileNameW(h, loadedPath, cap);
if (len > 0 && len < cap) {
wprintf(L"loaded from: %s\n", loadedPath);
} else {
wprintf(L"GetModuleFileNameW failed: %lu\n", GetLastError());
}
}
FreeLibrary(h);
RemoveDllDirectory(cookie);
return 0;
}
В этом коде зафиксированы четыре вещи.
- Сначала вызывают
SetDefaultDllDirectories. Один вызов действует всю жизнь процесса; к стандартному пути поиска вернуться нельзя4 AddDllDirectoryимеет смысл только в паре сLOAD_LIBRARY_SEARCH_USER_DIRS. Если вSetDefaultDllDirectoriesне включилиUSER_DIRS, добавленная директория используется только в тех вызовахLoadLibraryEx, где заданLOAD_LIBRARY_SEARCH_USER_DIRS5- Делают зачистку. Cookie, который вернул
AddDllDirectory, снимают черезRemoveDllDirectory5 - Печатают, откуда загрузили. Одна строка
GetModuleFileNameWсильно меняет время разбора, когда что-то ломается
Зависимые DLL у foo.dll тоже ищут в диапазоне этих LOAD_LIBRARY_SEARCH_*.
Если папку самой foo.dll нужно включить в поиск зависимостей, первый аргумент делают полным путём и добавляют LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR.
flowchart TB
accTitle: Ход минимального примера
accDescr: SetDefaultDllDirectories смещает процессное пространство поиска в безопасную сторону, AddDllDirectory явно добавляет папку плагинов, LoadLibraryEx загружает с флагами поиска, GetModuleFileNameW проверяет, откуда загрузили, RemoveDllDirectory делает зачистку.
p1["1. SetDefaultDllDirectories смещает умолчание в безопасную сторону"] --> p2["2. AddDllDirectory добавляет разрешённую папку"]
p2 --> p3["3. LoadLibraryEx загружает с флагами"]
p3 --> p4["4. GetModuleFileNameW проверяет источник загрузки"]
p4 --> p5["5. RemoveDllDirectory делает зачистку"]
Рис. 17: Каркас примера — пять шагов: сузить, добавить, загрузить, проверить, зачистить.
8.6 Если вызывать из C#
В .NET работает та же идея. Путь поиска для P/Invoke задают атрибутом DefaultDllImportSearchPaths, явную загрузку — через NativeLibrary.Load.
// .NET 8 / C# 12
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
// Для всех P/Invoke в сборке ограничить умолчательный поиск System32.
// Ограничение: на P/Invoke с абсолютным путём атрибут не действует.
// На платформах кроме Windows эффекта тоже нет.
[assembly: DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
internal static class Program
{
[DllImport("kernel32.dll", SetLastError = true)]
private static extern uint GetTickCount();
private static void Main()
{
// Атрибут сам по себе ничего не делает. Срабатывает только при вызове.
Console.WriteLine($"GetTickCount = {GetTickCount()}");
// Плагин лежит в отдельной папке. Нельзя писать
// NativeLibrary.Load("foo.dll", asm, ApplicationDirectory | System32)
// ApplicationDirectory — папка exe, System32 — папка ОС,
// папку плагинов ни тот ни другой не смотрит.
// Либо загрузка не найдёт файл, либо подхватит чужой foo.dll,
// который случайно лежит рядом с exe.
// Если место известно, надёжнее задать полный путь.
string pluginDir = Path.Combine(AppContext.BaseDirectory, "plugins");
string pluginPath = Path.GetFullPath(Path.Combine(pluginDir, "foo.dll"));
if (!File.Exists(pluginPath))
{
throw new FileNotFoundException("Плагин не найден.", pluginPath);
}
// Перегрузка с путём читает этот файл напрямую (поиск не идёт).
IntPtr handle = NativeLibrary.Load(pluginPath);
try
{
// Проверить, откуда загрузили.
foreach (ProcessModule module in Process.GetCurrentProcess().Modules)
{
if (string.Equals(module.ModuleName, "foo.dll", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine($"loaded from: {module.FileName}");
}
}
}
finally
{
NativeLibrary.Free(handle);
}
}
}
Значения DllImportSearchPath соответствуют флагам LOAD_LIBRARY_SEARCH_*.
Поэтому если на стороне C# написали «только System32», ограничения из п. 8.4 действуют как есть. Каталог приложения не смотрят.
Здесь важно, что у DllImportSearchPath нет значения «произвольная папка». ApplicationDirectory — папка exe, System32 — папка ОС, значения под папку плагинов, которую выбрали сами, нет. Поэтому DLL из отдельной папки флагами поиска не достать — задают полный путь, как выше.
И даже при загрузке по полному пути, как в главе 9, зависимые DLL у foo.dll этим не фиксируются. Если плагин тащит свои зависимости, нужна та же C-практика: добавить папку через AddDllDirectory и включить LOAD_LIBRARY_SEARCH_USER_DIRS, либо сложить зависимости в одну папку и использовать LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR.
flowchart TB
accTitle: Как в C# читать DLL из отдельной папки
accDescr: У DllImportSearchPath нет значения «произвольная папка»: ApplicationDirectory — папка exe, System32 — папка ОС. Плагин из отдельной папки флагами поиска не достать, его читают напрямую по полному пути. Зависимые DLL по-прежнему требуют той же практики, что и на C.
q1["Плагин кладут в отдельную папку"] --> q2{"Достанут флагами поиска?"}
q2 -.->|"Значения «произвольная папка» нет"| q3["Флагами не достать"]
q2 -->|"Надёжный способ"| q4["Собрать полный путь и прочитать через NativeLibrary.Load"]
q4 -.-> q5["Зависимые DLL по-прежнему требуют практики со стороны C"]
Рис. 18: И в .NET папку, которую выбрали сами, надёжнее читать по полному пути.
9. Даже полный путь не фиксирует зависимые DLL
Это место в теме, которое на практике очень важно и которое легко пропустить. Microsoft Learn пишет, что даже когда первую DLL загружают по полному пути, её зависимые DLL ищут только по имени модуля.14
То есть из того, что:
- явно загрузили
C:\\MyApp\\plugins\\foo.dll
не следует, что:
bar.dll, от которой зависитfoo.dll, обязательно возьмут из той же папки.
Из этого заблуждения получается довольно неприятный сбой:
- в среде разработки всё работает
- на стороне распространения разрешается другая
bar.dll - конфликт зависимых DLL становится зависимым от окружения при воспроизведении
flowchart TB
accTitle: Полный путь не фиксирует зависимые DLL
accDescr: Даже если первую DLL загрузили по полному пути, её зависимости ищут только по имени модуля и могут разрешиться из другого места. Отсюда сбой, зависимый от окружения: в разработке работает, у заказчика подхватывается другая зависимая DLL.
r1["foo.dll загружают по полному пути"] --> r2["Саму foo.dll читают как указали"]
r1 --> r3["Зависимую bar.dll ищут только по имени модуля"]
r3 --> r4["В другом окружении может разрешиться из другого места"]
r4 -.-> r5["В разработке работает, у заказчика ломается"]
Рис. 19: Полным путём фиксируется только первая DLL, зависимости — отдельно.
10. Как не допустить DLL preloading / hijacking
В документации Microsoft Learn по DLL security написано, что к DLL preloading attack и binary planting attack приводит сочетание динамической загрузки без полного пути и директорий в области поиска, которыми может управлять злоумышленник.2
На практике проще держаться такой базовой схемы.
- Сокращать загрузку по «голому» имени вроде
LoadLibrary("foo.dll") - Где нужно — указывать полный путь
- Сужать процессный путь поиска через
SetDefaultDllDirectories - Через
AddDllDirectoryдобавлять только явно разрешённые директории - Явно задавать область поиска флагами вроде
LOAD_LIBRARY_SEARCH_SYSTEM32,LOAD_LIBRARY_SEARCH_APPLICATION_DIR,LOAD_LIBRARY_SEARCH_USER_DIRS - Не опираться на current folder и на
PATHбез нужды
Особенно опасно, когда процесс с правами администратора ходит по неоднозначному пути поиска. Microsoft Learn тоже пишет: если загрузится вредоносная DLL, она выполняется с привилегиями этого процесса.2
flowchart TB
accTitle: Сочетание, при котором срабатывает атака DLL preloading
accDescr: Динамическая загрузка без полного пути плюс директория в области поиска, которой может управлять злоумышленник, складываются в DLL preloading attack. Загруженная вредоносная DLL выполняется с привилегиями процесса. Мера — меньше «голых» имён и уже пространство поиска.
s1["Динамическая загрузка без полного пути"] --> s3["Складываются условия DLL preloading"]
s2["Директория в поиске, которой может управлять злоумышленник"] --> s3
s3 --> s4["Вредоносная DLL выполняется с привилегиями процесса"]
s3 -.-> s5["Мера: меньше «голых» имён, уже пространство поиска"]
Рис. 20: Атака срабатывает как произведение «неоднозначного имени» и «места, которым можно управлять».
11. Как проверить, какая DLL откуда загрузилась
До сих пор это была спецификация. На реальном разборе быстрее не гадать, а проверить. Четыре средства по задаче.
| Что нужно узнать | Чем смотреть | Куда смотреть |
|---|---|---|
| Где искали и где нашли | Process Monitor10 | Журнал обращения к файлам |
| Что сейчас загружено | Process Explorer, tasklist /m11 |
Список модулей процесса |
| Какой процесс держит данную DLL | ListDLLs12 | Список по всем процессам |
| Какой путь реально прочитали во время отладки | Окно модулей Visual Studio | «Отладка» > «Окна» > «Модули»7 |
11.1 След поиска в Process Monitor
Здесь больше всего информации. Порядок такой.10
- Запустить Process Monitor от имени администратора и начать запись
- Открыть «Filter» > «Filter…», добавить
Process Nameisи имя целевого exe - На том же экране добавить
Pathends with.dll - Запустить целевое приложение и остановить запись, когда проявится проблема
Смотреть нужно колонку Result.
Загрузчик пытается открывать файлы сверху вниз по порядку поиска, поэтому в местах, где не нашли, стоит NAME NOT FOUND, а там, где открыли, — SUCCESS.
Иначе говоря, строка сразу после последней пачки NAME NOT FOUND — путь, который реально взяли.
Сверив с таблицей главы 3, видно, на каком шаге решился этот случай. Если ответ уже на предварительных правилах (строки 1–6 таблицы), записей о походе за файлом может не быть вообще. Это тоже важный след.
flowchart TB
accTitle: Как читать журнал Process Monitor
accDescr: Загрузчик пытается открывать файлы сверху вниз по порядку поиска, поэтому в местах, где не нашли, стоит NAME NOT FOUND, а там, где открыли, — SUCCESS. Строка сразу после последней пачки NAME NOT FOUND — путь, который взяли. Если записей нет, ответ уже на предварительных правилах.
t1["Читать колонку Result сверху"] --> t2["Пачка NAME NOT FOUND = места, где искали и не нашли"]
t2 --> t3["Следующий SUCCESS — путь, который реально взяли"]
t1 -.-> t4["Если записей нет, ответ уже на предварительных правилах"]
Рис. 21: След остаётся в колонке Result; отсутствие записей само по себе тоже след.
11.2 Список уже загруженных модулей
Если процесс уже запущен и нужно только увидеть, что и откуда прочитано, это можно сделать и без дополнительных утилит.
tasklist /m foo.dll
Команда перечисляет имена и PID процессов, которые загрузили указанный модуль.11 Сузив процесс, в нижней панели Process Explorer переключаются на отображение DLL и смотрят полный путь каждой загруженной DLL.
Sysinternals ListDLLs делает то же из командной строки и сразу по всем процессам.12
Сработало ли loaded-module list из п. 5.1, видно только так. Если одноимённую DLL уже загрузили из другой папки, этот процесс за новым экземпляром не пойдёт.
flowchart TB
accTitle: Как смотреть уже загруженные модули
accDescr: tasklist /m сужает процессы, которые держат указанный модуль, затем Process Explorer в режиме DLL или ListDLLs показывает полный путь. Сработало ли loaded-module list, видно только здесь.
u1["tasklist /m foo.dll сужает процессы, которые держат модуль"] --> u2["Полный путь смотрят в Process Explorer или ListDLLs"]
u2 --> u3["Откуда загрузили, фиксируется как факт"]
u3 -.-> u4["Влияние loaded-module list видно только здесь"]
Рис. 22: Что загружено сейчас, подтверждают списком модулей, а не догадкой.
12. Практический чек-лист для решений
Когда ревьюите загрузку DLL в Windows, минимум такая проверка снижает число инцидентов.
- Приложение — packaged app или unpackaged app
- Какие DLL из статической линковки, какие грузятся динамически
- Указан полный путь или только имя модуля
- Не вызывается ли
SetDllDirectory - Можно ли в этой конфигурации использовать
SetDefaultDllDirectoriesиLOAD_LIBRARY_SEARCH_* - Не вызывается ли
AddDllDirectoryнесколько раз с неявным ожиданием порядка - Чем управляют зависимости — manifest, SxS, private DLL или redirection
- Нет ли слабых с точки зрения безопасности предположений про current folder или
PATH - Не разрешаются ли зависимые DLL из другого места в другом окружении
- На среде, где виден симптом, проверили ли, откуда реально загрузили (глава 11)
Если эти 10 пунктов смотреть по отдельности, такие проблемы, как «DLL не найдена», «загрузилась не та DLL», «не стартует только в production» и «остановка на ревью уязвимостей», можно отсечь довольно рано.
flowchart TB
accTitle: Ход проверки на ревью
accDescr: На ревью загрузки DLL смотрят форму приложения (packaged / unpackaged), способ загрузки (полный путь или только имя), использование API вроде SetDllDirectory и флагов поиска, управление зависимостями через manifest и зависимые DLL, и на среде с симптомом — откуда реально загрузили. Так инцидентов меньше.
v1["Форма приложения (packaged / unpackaged)"] --> v2["Способ загрузки (полный путь или только имя)"]
v2 --> v3["Использование API (SetDllDirectory, флаги)"]
v3 --> v4["Управление зависимостями (manifest / SxS / PATH)"]
v4 --> v5["На месте проверить источник загрузки (глава 11)"]
Рис. 23: Чек-лист можно пройти в порядке: форма → загрузка → API → зависимости → проверка на месте.
13. Итог
Разрешение имён DLL в Windows — это не просто «порядок обхода папок». На деле оно складывается из DLL redirection, API set, SxS manifest, loaded-module list, Known DLLs и пространства поиска, которое меняют вызовы API.134
На практике важнее всего эти шесть пунктов.
- Не заучивать порядок поиска как одну таблицу. Таблица нужна, чтобы попасть, на каком шаге решился ваш случай
- Разделять packaged и unpackaged
- Понимать, что даже при полном пути зависимые DLL могут обрабатываться иначе
- Не вызывать
SetDllDirectoryбез нужды - Для безопасного варианта использовать
SetDefaultDllDirectoriesи флаги поискаLoadLibraryEx - Не останавливаться на догадке: в Process Monitor и подобных средствах смотреть путь, который реально прочитали
Разрешение имени DLL — место, где сразу всплывают сбои запуска, различия окружений и вопросы безопасности. Именно поэтому стоит понимать не только «в каком порядке ищет Windows», но и «что Windows вообще считает предпосылками разрешения имён».
flowchart TB
accTitle: Вывод статьи
accDescr: Разрешение имени DLL — место, где сразу всплывают сбои запуска, различия окружений и вопросы безопасности. Нужно понимать не только порядок поиска, но и то, что Windows считает предпосылками разрешения имён.
w1["В каком порядке ищут (порядок папок)"] --> w3["Объяснение сходится, только когда понятны оба"]
w2["Что считают предпосылками (предварительные правила и пространство поиска)"] --> w3
w3 --> w4["Сбои запуска, различия окружений и безопасность можно закрыть вместе"]
Рис. 24: Практическое знание разрешения имён DLL — не заучить порядок, а понять предпосылки.
Связанные статьи
- Минимальный чек-лист безопасности при разработке Windows-приложений
- Распространение Windows-приложения одним файлом — единый бинарный файл и пределы зависимости от ОС
- Когда на Windows действительно требуются права администратора — UAC, защищённые области и как это определить на этапе проектирования
- Что такое Reg-Free COM — механизм использования COM без регистрации
Источники
- Microsoft Learn: Dynamic-link library search order
- Microsoft Learn: Dynamic-Link Library Security
- Microsoft Learn: Windows API sets
- Microsoft Learn: SetDefaultDllDirectories function
- Microsoft Learn: AddDllDirectory function
- Microsoft Learn: LoadLibraryEx function
- Microsoft Learn: Dynamic-link library redirection
- Microsoft Learn: Manifests
- Microsoft Learn: About Side-by-Side Assemblies
- Microsoft Learn: Process Monitor
- Microsoft Learn: ListDLLs
- Microsoft Learn: tasklist
-
Microsoft Learn, Dynamic-link library search order, дата обращения: 24 марта 2026 г. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17 ↩18 ↩19 ↩20 ↩21 ↩22 ↩23 ↩24 ↩25
-
Microsoft Learn, Dynamic-Link Library Security, дата обращения: 24 марта 2026 г. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Windows API sets, дата обращения: 24 марта 2026 г. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, SetDefaultDllDirectories function, дата обращения: 24 марта 2026 г. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
-
Microsoft Learn, AddDllDirectory function, дата обращения: 24 марта 2026 г. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, LoadLibraryEx function, дата обращения: 24 марта 2026 г. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, Dynamic-link library redirection, дата обращения: 24 марта 2026 г. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, Manifests, дата обращения: 24 марта 2026 г. ↩ ↩2 ↩3
-
Microsoft Learn, About Side-by-Side Assemblies, дата обращения: 24 марта 2026 г. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Process Monitor ↩ ↩2
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Когда в Windows нужны права администратора — UAC, защищённые области и как это отличить на этапе проектирования
Разбираем на практике, когда в Windows нужны права администратора: UAC, защищённые области, службы, драйверы и проектирование per-user / ...
Внутреннее устройство виртуализации Windows (часть 2) — память, которую не видит даже ядро: как устроены VBS, HVCI и Credential Guard
На совместимом оборудовании при чистой установке VBS включена по умолчанию: гипервизор и SLAT создают изоляцию сильнее ядра. Разбираем ус...
Именованные каналы на практике — от проектирования до безопасности IPC в Windows
Практический разбор именованных каналов — стандартного IPC в Windows. По первоисточникам: выбор байтового режима и режима сообщений, серв...
DllMain и блокировка загрузчика — почему говорят «ничего не делайте при инициализации DLL»
Почему из DllMain нельзя вызывать LoadLibrary и синхронизироваться с другими потоками. По первоисточникам: как блокировка загрузчика сери...
Учётные записи служб Windows: LocalSystem, виртуальные учётные записи и gMSA
Службы Windows всё ещё запускают от LocalSystem? Статья сравнивает права и то, кем служба выглядит в сети, у LocalService, NetworkService...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Разработка приложений для Windows
Размещение DLL и способ загрузки напрямую влияют на то, запустится ли Windows-приложение, как его распространяют и насколько воспроизводимы сбои. Тема стоит в контексте разработки Windows-приложений.
Технические консультации и ревью дизайна
Разрешение имён DLL лежит на пересечении разбора дефектов, портирования, закрытия уязвимостей и проектирования распространения. Удобно разбирать это как ревью архитектуры или техническую консультацию.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- В каком порядке Windows ищет DLL, если в LoadLibrary указано только имя?
- До обхода файловой системы срабатывают предварительные правила. Сначала DLL redirection, API set, SxS manifest redirection, loaded-module list и Known DLLs, и только потом поиск по файловой системе: папка приложения, System32, папка Windows, current folder, PATH. В обычном состоянии, когда включён safe DLL search mode, current folder стоит довольно далеко в конце. У packaged app и unpackaged app сама модель порядка поиска разная.
- Если загрузить DLL по полному пути, зависимые DLL тоже возьмутся из той же папки?
- Не обязательно. Даже если первую DLL загрузили по полному пути, её зависимости ищутся только по имени модуля и могут разрешиться из другого места. Из этого заблуждения получается неприятная зависимость от окружения: в разработке всё работает, а у заказчика подхватывается другая зависимая DLL. Если нужно контролировать и зависимости, задайте пространство поиска явно — например флагами LOAD_LIBRARY_SEARCH_* у LoadLibraryEx.
- Почему не стоит вызывать SetDllDirectory?
- Она не просто меняет порядок поиска, а по сути отключает safe DLL search mode. Даже если хотели добавить одну папку приложения, меняется всё пространство поиска, включая обращение с current folder, — с точки зрения безопасности это может выйти боком. Если вызвать её в родительском процессе, эффект может дойти и до порядка поиска в дочернем. Безопаснее опираться на SetDefaultDllDirectories, AddDllDirectory и флаги поиска LoadLibraryEx.
- Как не допустить DLL hijacking (атаку DLL preloading)?
- Атака складывается из динамической загрузки без полного пути и директорий в области поиска, которыми может управлять злоумышленник. Нужно сужать оба фактора: меньше вызовов LoadLibrary по «голому» имени, где нужно — полный путь, процессный путь поиска сузить через SetDefaultDllDirectories, через AddDllDirectory добавлять только явно разрешённые директории, не опираться на current folder и на PATH без нужды. Особенно опасно, когда процесс с правами администратора ходит по неоднозначному пути поиска: вредоносная DLL выполняется уже с привилегиями этого процесса.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.