В статье разбирается недавно выявленная уязвимость CVE-2024-30052, которая позволяет злоумышленникам использовать дамп-файлы для запуска вредоносного кода через Visual Studio. Исследование охватывает процесс обнаружения этой проблемы, уязвимые механизмы встроенных PDB и детали успешной эксплуатации, подчеркивая важность внимания к безопасности даже в привычных инструментах разработчика.
Статья посвящена уязвимости CVE-2024-30052, позволяющей выполнить произвольный код при отладке файлов дампа в Visual Studio
Исследователь ynwarcs первый обнаружил и сообщил о проблеме компании Microsoft в августе 2023 года, и компания выпустила обновление с исправлением недостатка в июне 2024 года. Специалист ynwarcs поделился некоторыми деталями о данной уязвимости, а также предоставил PoC-код.
Многим специалистам по работе часто приходится отлаживать файлы дампа в Visual Studio. Это крайне полезно для исследования трудно воспроизводимых сбоев или состояний программы, которые мы хотим предотвратить. Часто эти дампы поступают из недоверенных источников — большинство крупных компаний, которые разворачивают нативные приложения, например, на Windows, используют автоматизированные системы для обнаружения сбоев, в которых файл дампа собирается как часть телеметрии и загружается на портал, к которому разработчики имеют доступ для анализа сбоев. Например, Google использует свою кастомную версию crashpad для фиксации и отчетности о сбоях в Google Chrome.
Это потенциально подвергает разработчиков атакам через файлы дампов. Если есть уязвимость в Visual Studio, которую можно вызвать, открыв специально созданный файл дампа, злоумышленник может вставить этот дамп в систему отчета о сбоях и просто дождаться, пока разработчик его откроет. Также не редкостью является ситуация, когда конечный пользователь вручную отправляет дамп-файл, например, через тикет в службу поддержки, что повышает шансы на открытие файла разработчиком. Основная возможность для атаки заключается в файлах PDB, которые могут быть предоставлены вместе с файлом дампа (при необходимости под произвольными расширениями), и которые VS охотно откроет в ходе отладки. В целом PDB считаются довольно небезопасными:
Однако визуализаторы, указанные через файлы PDB, отключены при отладке дампов, а серверные команды источников по умолчанию также отключены и требуют ручного включения пользователем. Поэтому в предыдущем исследовании я сосредоточился на поиске уязвимостей, не требующих использования ни одного из этих компонентов, и нашел многие, которые Microsoft впоследствии исправила. Большинство из них находилось в msdia140.dll — библиотеке, используемой для парсинга и запроса данных файлов PDB. Все эти проблемы были ошибками повреждения памяти. Несмотря на то что некоторые из них были достаточно эксплуатируемыми, мне казалось, что их реалистичное использование в реальных атаках маловероятно.
В прошлом году ynwarcs решил изучить другие библиотеки, которые Visual Studio использует во время своих сессий отладки, в надежде обнаружить логическую ошибку, позволяющую выполнить код без обращения к повреждению памяти. В итоге ynwarcs нашел способ выполнить произвольный код при отладке управляемого файла дампа.
Несколько лет назад Microsoft представила формат Portable PDB. Этот формат был задуман как замена классическому формату MSF для управляемых модулей, главным образом для поддержки кросс-платформенности и оптимизаций по сравнению со стандартным форматом. В то же время была добавлена возможность внедрения Portable PDB файлов в исполняемый файл во время компиляции с использованием команды -debug:embedded. Процедура внедрения не особо документирована, по крайней мере, насколько мне известно, но несложно выяснить, как это достигается, например, обратным анализом некоторых библиотек C# Runtime и следуя подсказкам в публичной документации Microsoft.
Мы можем увидеть это в действии, например, с помощью PETools для просмотра .NET core DLL, скомпилированного со встроенным PDB:
Запись каталога отладки в самом низу имеет тип 17, и мы можем видеть, что данные, на которые она указывает, отформатированы как "MPDB", за которым следует 0x2910 (несжатая длина PDB) и затем сжатые данные PDB.
Со временем поступили запросы на возможность внедрять исходные файлы в PDB. Это теперь возможно разными способами, например, установив EmbedAllSources в значение true в файле vcxproj или указав флаг -embed в командной строке компилятора. Исходные файлы внедряются в "Embedded Sources Stream" в файле Portable PDB и могут быть легко извлечены отладчиком при необходимости.
Две описанные выше функции очень полезны при итеративной разработке управляемого проекта и отладке устаревшей версии. Несмотря на то что такое происходит нечасто, это обычно вызывает сильное разочарование (и зачастую невозможно) при отладке старого дампа или старой версии исполняемого файла без резервного копирования PDB или исходного файла, связанного с ними. Благодаря встроенным PDB и встроенным исходникам информация хранится непосредственно в исполняемом файле (а значит и в дампе, если он захвачен с полной памятью), что обеспечивает полный опыт отладки.
Во время отладки файлов дампов данные, содержащиеся в них, полностью доверяются VS. Это означает, что встроенные PDB и исходники внутри этих PDB будут беспрепятственно приняты Visual Studio. Даже если данные на диске предпочтительнее, в случае их отсутствия будут использоваться данные, расположенные в файле дампа. Во время размышлений о возможных способах злоупотребления доверием, которое VS проявляет к встроенным исходным файлам, я вспомнил о странном поведении, с которым столкнулся ранее. Известно, что VS поддерживает открытие файлов изображений, но только некоторых форматов, таких как JPG или PNG. Однажды я попробовал открыть файл формата WebP и получил следующее сообщение:
Нажатие на "OK" или "X" привело к тому, что файл WebP открылся в Paint:
Предполагается, что VS может вызывать внешние программы, если он не знает, как обрабатывать определенный тип файла. Я запустил отладчик, чтобы отследить код, реализующий это поведение, и получил следующий стек вызовов:
... методы SHELL32 ... shell32.dll!ShellExecuteW msenv.dll!CExternalEditorFactory::CreateEditorInstance msenv.dll!CVsUIShellOpenDocument::LoadCreateEditorInstance msenv.dll!CVsUIShellOpenDocument::CreateInitEditorInstance msenv.dll!CVsUIShellOpenDocument::OpenStandardEditor msenv.dll!CVsUIShellOpenDocument::OpenStandardEditorAsync ... методы CLR ... msenv.dll!CVsUIShellOpenDocument::OpenDocumentViaProject2 msenv.dll!CVsUIShellOpenDocument::OpenDocumentViaProject
В ходе короткого расследования я выяснил, что:
Наиболее интересный элемент в стеке вызовов — CExternalEditorFactory::CreateEditorInstance. Вот его декомпилированная реализация:
HRESULT CExternalEditorFactory::CreateEditorInstance(..., const wchar_t* filePath, ...) { const wchar_t* extensionPtr = wcsrchr(filePath, L'\\'); if (extensionPtr && (CompareFilenames(extensionPtr, L".exe") == 0 || CompareFilenames(extensionPtr, L".com") == 0)) return 0x80041FEB; bool useOpenAssoc = false; wchar_t assocProgramPath[MAX_PATH + 4]; uint32_t assocProgramPathLen = MAX_PATH; HRESULT assocRes = AssocQueryStringW(ASSOCF_NOTRUNCATE | ASSOCF_VERIFY, ASSOCSTR_EXECUTABLE, extensionPtr, L"edit", assocProgramPath, &assocProgramPathLen); if (FAILED(assocRes)) { useOpenAssoc = true; assocProgramPathLen = MAX_PATH; assocRes = AssocQueryStringW(ASSOCF_NOTRUNCATE | ASSOCF_VERIFY, ASSOCSTR_EXECUTABLE, extensionPtr, L"open", assocProgramPath, &assocProgramPathLen); } if (FAILED(assocRes)) return 0x80041FEB; ... // Проверка, не совпадает ли имя программы с devenv.exe или специальными именами: // VBExpress, VCSExpress, VJSExpress, VCExpress, VWDExpress, VPDExpress, VSWinExpress, WDExpress, VSLauncher, vsgd, vsga // если совпадает, операция прерывается ... if (CompareFilenames(filePath, assocProgramPath) != 0) { wchar_t assocProgramShortPath[MAX_PATH+1]; GetShortPathNameW(a3, assocProgramShortPath, MAX_PATH); if (CompareFilenames(assocProgramPath, assocProgramShortPath) != 0) { SHELLEXECUTEINFOW execInfo = {}; ... // настройка параметров execInfo execInfo.lpVerb = useOpenAssoc ? "open" : "edit"; execInfo.lpFile = filePath; execInfo.nShow = 1; ShellExecuteW(&execInfo); ... } } ... }
Эта функция получает несколько параметров, включая полный путь к файлу, который необходимо открыть. Вызовы AssocQueryStringW предназначены для поиска программы по умолчанию, которая взаимодействует с расширением имени файла. VS сначала ищет программу, связанную с действием "edit", и, если такая не найдена, использует программу, связанную с действием "open". Если какая-либо из этих программ найдена, выполняется несколько проверок, и, если все они проходят успешно, вызывается ShellExecuteW для открытия файла в связанной программе.
Данное поведение указывает на контур возможной атаки через использование встроенных исходных файлов:
то мы сможем осуществить выполнение произвольного кода в результате простой отладки файла дампа.
Чтобы проверить реалистичность атаки, я попробовал создать простой poc, в котором заменил бы легитимный исходный файл в встроенном PDB на пример PDF-файла, в надежде, что VS:
Я выбрал PDF, так как знал, что он, скорее всего, будет содержать непечатные символы и определенно будет иметь связанную программу на системе (в данном случае Firefox). Создание poc потребовало следующих шагов:
В этот момент я столкнулся с тем же самым окном сообщения, что показано выше, и нажатие на "OK" или "X" привело к открытию примерного PDF-файла в Firefox. Это подтвердило, что проблемный путь кода можно вызвать при отладке файлов дампа и открытии встроенных источников.
После подтверждения гипотезы оставалось найти расширения, которые можно использовать для достижения ACE (Arbitrary Code Execution). Visual Studio фильтрует некоторые расширения, такие как .exe и .com, но я был убежден, что существуют и другие, которые смогут проскользнуть. В итоге я написал программу, которая итерирует все возможные 2-буквенные, 3-буквенные и 4-буквенные расширения и выводит программы, связанные с ними, с использованием AssocQueryStringW.
Это заняло всего около 20 минут, и вскоре у меня был полный список ассоциаций на ПК. Хотя многие файлы можно было использовать для выполнения произвольного кода при их открытии через программу по умолчанию, большинство из них имели ассоциацию "edit " с текстовым редактором, что нам не подходит. После тщательного анализа я выделил три расширения, которые выглядели особенно подходящими:
По умолчанию файлы CHM скомпилированы и содержат непечатные символы. С другой стороны, файлы HTA и PY текстовые и требуют внедрения непечатных символов при сохранении их функциональности. Это не представляет большого препятствия:
С учетом всего этого пришло время создать эксплойт. Однако вместо того, чтобы следовать тем же шагам, которые я описал ранее, я написал программу на C#, которая автоматизирует весь процесс, и передал ей три разных файла (CHM/HTA/PY), чтобы создать три разных дампа. Как только вы начнете отладку любого из них в VS, calc.exe запускается, демонстрируя ACE.
Программа, создающая файл дампа на основе входного исходного файла, доступна на GitHub. Вы можете найти инструкции по ее использованию в файле README репозитория. Здесь представлена демонстрация PoC с использованием входного файла CHM, который запускает calc.exe:
Мне не хватило терпения полностью обратнопроектировать исправление. Но можно увидеть одно новое изменение в CVsUIShellOpenDocument::OpenStandardEditor, которое выглядит примерно так:
HRESULT CVsUIShellOpenDocument::OpenStandardEditor(..., uint64_t flags, ...) { // ++++++++ if (flags & 0xF0000000) { return 0x80042010; } // ++++++++ ... CVsUIShellOpenDocument::CreateInitEditorInstance(...) }
Старший бит параметра flags, передаваемого функции, теперь устанавливается при открытии встроенных источников во время сеансов отладки, но не устанавливается, например, при перетаскивании файла в бездействующую VS. Если параметр установлен, функция отказывается продолжать выполнение CreateInitEditorInstance, что привело бы к поведению, описанному выше.
Если попытаться вручную открыть исходный файл в VS во время отладки дампа, теперь появляется следующее сообщение, что означает, что VS даже не позволяет пользователю вручную попасть в ловушку:
Уязвимость CVE-2024-30052 демонстрирует, насколько критически важным является обеспечение безопасности инструментов для разработчиков. Ошибка, способная вызвать выполнение произвольного кода, открывает возможности для злоумышленников атаковать компании через доверенные файлы дампов и встроенные PDB. Данный случай подчеркивает, насколько важно регулярно обновлять инструменты разработки, внимательно отслеживать отчеты о сбоях и помнить о потенциальных рисках при работе с внешними файлами. Оставайтесь в безопасности и всегда будьте на шаг впереди уязвимостей.
Храним важное в надежном месте