CVE-2024-30052: как дамп-файлы могут открыть дверь хакерам в Visual Studio

CVE-2024-30052: как дамп-файлы могут открыть дверь хакерам в Visual Studio

В статье разбирается недавно выявленная уязвимость CVE-2024-30052, которая позволяет злоумышленникам использовать дамп-файлы для запуска вредоносного кода через Visual Studio. Исследование охватывает процесс обнаружения этой проблемы, уязвимые механизмы встроенных PDB и детали успешной эксплуатации, подчеркивая важность внимания к безопасности даже в привычных инструментах разработчика.

image

Статья посвящена уязвимости CVE-2024-30052, позволяющей выполнить произвольный код при отладке файлов дампа в Visual Studio

Исследователь ynwarcs первый обнаружил и сообщил о проблеме компании Microsoft в августе 2023 года, и компания выпустила обновление с исправлением недостатка в июне 2024 года. Специалист ynwarcs поделился некоторыми деталями о данной уязвимости, а также предоставил PoC-код.

Введение

Многим специалистам по работе часто приходится отлаживать файлы дампа в Visual Studio. Это крайне полезно для исследования трудно воспроизводимых сбоев или состояний программы, которые мы хотим предотвратить. Часто эти дампы поступают из недоверенных источников — большинство крупных компаний, которые разворачивают нативные приложения, например, на Windows, используют автоматизированные системы для обнаружения сбоев, в которых файл дампа собирается как часть телеметрии и загружается на портал, к которому разработчики имеют доступ для анализа сбоев. Например, Google использует свою кастомную версию crashpad для фиксации и отчетности о сбоях в Google Chrome.

Это потенциально подвергает разработчиков атакам через файлы дампов. Если есть уязвимость в Visual Studio, которую можно вызвать, открыв специально созданный файл дампа, злоумышленник может вставить этот дамп в систему отчета о сбоях и просто дождаться, пока разработчик его откроет. Также не редкостью является ситуация, когда конечный пользователь вручную отправляет дамп-файл, например, через тикет в службу поддержки, что повышает шансы на открытие файла разработчиком. Основная возможность для атаки заключается в файлах PDB, которые могут быть предоставлены вместе с файлом дампа (при необходимости под произвольными расширениями), и которые VS охотно откроет в ходе отладки. В целом PDB считаются довольно небезопасными:

  • Они могут содержать визуализаторы, которые способны выполнить произвольный код при попытке визуализировать некоторые данные в VS.
  • Они могут содержать конфигурацию серверов исходников, которые выполняют произвольные команды при попытке получить исходный файл.

Однако визуализаторы, указанные через файлы PDB, отключены при отладке дампов, а серверные команды источников по умолчанию также отключены и требуют ручного включения пользователем. Поэтому в предыдущем исследовании я сосредоточился на поиске уязвимостей, не требующих использования ни одного из этих компонентов, и нашел многие, которые Microsoft впоследствии исправила. Большинство из них находилось в msdia140.dll — библиотеке, используемой для парсинга и запроса данных файлов PDB. Все эти проблемы были ошибками повреждения памяти. Несмотря на то что некоторые из них были достаточно эксплуатируемыми, мне казалось, что их реалистичное использование в реальных атаках маловероятно.

В прошлом году ynwarcs решил изучить другие библиотеки, которые Visual Studio использует во время своих сессий отладки, в надежде обнаружить логическую ошибку, позволяющую выполнить код без обращения к повреждению памяти. В итоге ynwarcs нашел способ выполнить произвольный код при отладке управляемого файла дампа.

Встроенные PDB, встроенные исходники

Несколько лет назад Microsoft представила формат Portable PDB. Этот формат был задуман как замена классическому формату MSF для управляемых модулей, главным образом для поддержки кросс-платформенности и оптимизаций по сравнению со стандартным форматом. В то же время была добавлена возможность внедрения Portable PDB файлов в исполняемый файл во время компиляции с использованием команды -debug:embedded. Процедура внедрения не особо документирована, по крайней мере, насколько мне известно, но несложно выяснить, как это достигается, например, обратным анализом некоторых библиотек C# Runtime и следуя подсказкам в публичной документации Microsoft.

  1. Сначала создается обычный файл Portable PDB, затем он сжимается с использованием потока Deflate.
  2. Создается "blob" данных, содержащий два 32-битных значения (магическое число ("MPDB") + несжатый размер PDB), за которыми следует сжатые данные PDB.
  3. Этот blob вставляется в исполняемый файл в специальный раздел и ссылается на запись каталога отладки PE через:
    • DataPointer, указывающий на blob.
    • DataSize, равное размеру blob.
    • IsPortableCodeView, установленное в true.
    • Type, установленный на 17 (т.е. Embedded Portable PDB).

Мы можем увидеть это в действии, например, с помощью 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

В ходе короткого расследования я выяснил, что:

  • При открытии файла VS попытается найти внутренний редактор/просмотрщик, связанный с расширением файла.
  • Если связанный редактор не найден, файл будет открыт в стандартном (текстовом) редакторе.
  • Если файл содержит непечатные символы, выполнение перейдет к коду, показанному выше.

Наиболее интересный элемент в стеке вызовов — 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 для открытия файла в связанной программе.

Данное поведение указывает на контур возможной атаки через использование встроенных исходных файлов:

  • Если мы сможем заставить VS открыть произвольный встроенный исходный файл при отладке дампа,
  • Если мы сможем заставить этот исходный файл иметь произвольное расширение,
  • Если найдется расширение, связанное с программой, которая выполнит произвольный код на основе данных файла при его открытии,

то мы сможем осуществить выполнение произвольного кода в результате простой отладки файла дампа.

Создание PoC

Чтобы проверить реалистичность атаки, я попробовал создать простой poc, в котором заменил бы легитимный исходный файл в встроенном PDB на пример PDF-файла, в надежде, что VS:

  • Распознает его как легитимный встроенный исходный файл,
  • Откроет его через внешний редактор во время сеанса отладки, включая сессии отладки дампов.

Я выбрал PDF, так как знал, что он, скорее всего, будет содержать непечатные символы и определенно будет иметь связанную программу на системе (в данном случае Firefox). Создание poc потребовало следующих шагов:

  1. Создать простой проект .NET и переименовать главный файл из Program.cs в Program.pdf.
  2. Скомпилировать проект с флагом -debug:portable. Это создает exe-файл и dll-файл с портативными PDB на диске. Исходный файл внедряется в PDB, связанный с DLL.
  3. Модифицировать файл Portable PDB так, чтобы заменить данные оригинального исходного файла на данные PDF-файла, который мы хотим внедрить.
    • Для этого я нашел, где именно в файле сериализуется встроенный исходный файл. Его формат описан в спецификации формата.
    • Затем я заменил данные на данные PDF-файла. Эти данные сжимаются с использованием deflate. Чтобы упростить себе задачу, я сделал оригинальный исходный файл достаточно большим, чтобы можно было вместить новые данные без перемещения данных и риска повредить формат.
    • Я также обновил хеш исходного файла в таблице документа, чтобы Visual Studio не отвергала его как недействительный.
  4. Внедрить вновь созданный Portable PDB в исполняемый файл. Я использовал кастомную программу для этого, так как не знал о существовании инструментов, способных выполнить подобное. Программа просто расширила каталог отладки исполняемого файла и вставила в него новый раздел с данными PDB, на который ссылалась новая запись.
  5. Запустить исполняемый файл, позволить ему завершиться сбоем и создать дамп полной памяти. Чтобы автоматически захватывать дампы, я следовал этому руководству, установив DumpType на 2 (дамп с полной памятью).
  6. Удалить/переименовать exe, dll, pdb и исходные файлы с диска. Это заставит VS использовать встроенную информацию из дампа, а не доступные на диске данные.
  7. Открыть файл дампа в VS и выбрать "Debug With Managed".

В этот момент я столкнулся с тем же самым окном сообщения, что показано выше, и нажатие на "OK" или "X" привело к открытию примерного PDF-файла в Firefox. Это подтвердило, что проблемный путь кода можно вызвать при отладке файлов дампа и открытии встроенных источников.

Поиск ACE

После подтверждения гипотезы оставалось найти расширения, которые можно использовать для достижения ACE (Arbitrary Code Execution). Visual Studio фильтрует некоторые расширения, такие как .exe и .com, но я был убежден, что существуют и другие, которые смогут проскользнуть. В итоге я написал программу, которая итерирует все возможные 2-буквенные, 3-буквенные и 4-буквенные расширения и выводит программы, связанные с ними, с использованием AssocQueryStringW.

Это заняло всего около 20 минут, и вскоре у меня был полный список ассоциаций на ПК. Хотя многие файлы можно было использовать для выполнения произвольного кода при их открытии через программу по умолчанию, большинство из них имели ассоциацию "edit " с текстовым редактором, что нам не подходит. После тщательного анализа я выделил три расширения, которые выглядели особенно подходящими:

  • CHM, или Microsoft Compiled HTML. Файлы этого типа открываются с помощью hh.exe. Этот формат в основном используется для файлов справки на Windows и иногда всплывает, если случайно нажать F1 в некоторых программах. Файлы CHM могут содержать произвольный VB-код, который будет выполнен при их открытии.
  • HTA, или HTML-приложение, связанное с mshta.exe. Это также расширенные HTML-файлы, которые могут содержать VB-код, исполняющийся при открытии файла.
  • PY, или Python-скрипты. В чистой установке Windows такая ассоциация отсутствует, но вероятно, что у разработчика установлен Python, и в этом случае расширение будет связано с Python. Конечно, открытие Python-скрипта может привести к выполнению произвольного кода.

По умолчанию файлы CHM скомпилированы и содержат непечатные символы. С другой стороны, файлы HTA и PY текстовые и требуют внедрения непечатных символов при сохранении их функциональности. Это не представляет большого препятствия:

  • Добавление непечатных символов после закрывающего тега HTML в источнике HTA не мешает hh.exe выполнить код, указанный внутри тега.
  • Добавление комментария, за которым следуют некоторые нулевые символы в Python-скрипте, не препятствует выполнению остального кода.

Эксплуатация

С учетом всего этого пришло время создать эксплойт. Однако вместо того, чтобы следовать тем же шагам, которые я описал ранее, я написал программу на 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. Данный случай подчеркивает, насколько важно регулярно обновлять инструменты разработки, внимательно отслеживать отчеты о сбоях и помнить о потенциальных рисках при работе с внешними файлами. Оставайтесь в безопасности и всегда будьте на шаг впереди уязвимостей.

Бэкап знаний создан успешно!

Храним важное в надежном месте

Синхронизируйтесь — подпишитесь