ОРИГИНАЛЬНАЯ СТАТЬЯПЕРЕВЕДЕНО СПЕЦИАЛЬНО ДЛЯ xss.pro
$600 на SSD для Jolah Molivski ---> 0x5B1f2Ac9cF5616D9d7F1819d1519912e85eb5C09 для поднятия ноды ETHEREUM и тестов
Введение
В этом посте описывается проделанная нами работа по фаззингу клиента и сервера Windows RDP, связанные с этим проблемы и некоторые результаты.
Протокол удаленного рабочего стола (RDP) от Microsoft продолжает привлекать внимание сообщества безопасности. От нескольких критических уязвимостей, обнаруженных в 2019 году, которые потенциально могли скомпрометировать миллионы серверов, подключенных к Интернету, до использования RDP в качестве одного из основных первоначальных векторов доступа злоумышленников. Эти риски еще больше усугубляются работой из дома, вызванной пандемией. Наш первоначальный интерес к этому проекту был связан с виртуальными машинами. Поскольку по умолчанию для подключения к машине Azure Windows или виртуальной машине Hyper-V используется RDP, мы решили, что RDP — идеальная цель. Как и большинство успешных проектов, наш тоже начался с интенсивного поиска в Google, и мы быстро наткнулись на отличный доклад BlackHat Europe 2019 о фаззинге RDP Пака, Джанга, Ким и Ли. Спикеры нашли пару уязвимостей всего за несколько часов с помощью не очень быстрого фаззера, поэтому мы решили взять на себя задачу дополнить их работу, расширить возможности фаззинга на другие каналы, улучшить его производительность и найдите нашу собственную RDP Remote Code Execution (RCE).
К сожалению (или к счастью, в зависимости от вашей точки зрения), не все проекты фаззинга приводят к критическим уязвимостям (см. Survivorship Bias ). Нам не удалось найти RCE RDP (пока), но нам удалось найти несколько ошибок и лучше понять протокол, его компоненты, а также процесс и инструменты фаззинга. Созданная нами фаззинговая инфраструктура достаточно универсальна, чтобы ее можно было использовать для фаззинга других целей. В этом посте мы собираемся поделиться нашим процессом выполнения всего, что упомянуто выше. Сначала мы предоставим обзор RDP и созданной нами настройки фаззинга, затем мы расскажем о проблемах, с которыми мы столкнулись, и о том, как мы с ними справились, и, наконец, мы рассмотрим пару ошибок, которые мы обнаружили в этом процессе.
Обзор удаленного рабочего стола
Протокол удаленного рабочего стола — это популярный протокол для удаленного доступа к компьютерам Windows. Недавно Malwaretech описала его как «протокол для протоколов». RDP позволяет работать нескольким каналам в каждом соединении, и каждый из них имеет свое назначение. Это означает, что каждый канал имеет свой собственный код, который обрабатывает его данные, свои собственные определения структуры и потоки данных. По сути, это означает, что в RDP действительно существует несколько протоколов.
Каналы RDP могут быть как статическими, так и динамическими, но различие между ними не имеет решающего значения для целей этой статьи. Если вы хотите узнать больше о внутренней работе RDP-подключения, мы рекомендуем прочитать наш предыдущий пост об этом. Если вы хотите узнать больше о процессе фаззинга, вы попали по адресу.
Проблемы фаззинга RDP
В «стандартном» сценарии фаззинга у вас есть программа, которая считывает ввод, управляемый фаззером, который может быть файлом или потоком данных любого типа. Затем программа обрабатывает данные, в то время как фаззер отслеживает покрытие кода сгенерированными данными. Основываясь на этом покрытии, фаззер изменяет ввод, снова отправляет измененный ввод в программу, и процесс повторяется. Фаззинг RDP отличается тем, что у нас всегда должно быть активное соединение RDP. Данные, которые фаззер может вводить в программу, должны быть отправлены в виде блока данных протокола (PDU) поверх определенного канала (который также должен быть активен во время фаззинга) в открытом соединении. Как упоминалось ранее, каждый канал представляет собой собственный протокол, поэтому фаззинг должен выполняться для каждого канала отдельно. Это вносит следующие проблемы в процесс фаззинга (которые также могут относиться к другим фаззерам, связанным с протоколами/сетями):
- Архитектура клиент-сервер . В традиционном фаззинге фаззер может просто запустить целевое приложение и предоставить его входные данные. В сценарии клиент-сервер целевое приложение работает на одной стороне соединения, а входные данные отправляются с другой стороны. В случае RDP обе стороны обычно находятся на разных машинах.
- Отслеживание состояния — RDP — это протокол с отслеживанием состояния, а это означает, что вы должны учитывать состояние соединения при фаззинге тестовых случаев. Это может плохо повлиять на стабильность фаззинга.
- Фаззинг с несколькими входами — при фаззинге цели, которая принимает файл в качестве входных данных (фаззинг файлового формата), все входные данные фаззера для цели содержатся в одном файле. Наоборот, когда вы фаззите протокол, вам может понадобиться отправить несколько последовательных сообщений, чтобы добраться до интересных путей кода.
- Нахождение целевого кода. Когда вы используете фаззинг на основе покрытия, вам обычно нужно указать фаззеру, в какой момент он должен начать отслеживать покрытие кода (т. е. какая целевая функция обрабатывает ваши входные данные?). RDP имеет множество компонентов, отвечающих за его работу. В некоторых случаях найти правильный может быть трудной задачей.
Эти четыре проблемы были основными, которые мы ожидали в этом проекте. В следующем разделе мы обсудим, как мы преодолели эти трудности, а также дополнительные, возникшие на более позднем этапе работы.
Технические детали: проблемы и способы их преодоления
В этом разделе мы рассмотрим детали реализации и проблемы, с которыми мы столкнулись в процессе работы. Мы также объясним, как мы справились с ними, чтобы получить работающую настройку фаззинга. Мы создали репозиторий проекта на GitHub , который содержит весь код, написанный нами в ходе этого проекта. Если вы заинтересованы в этой области, вы можете найти это полезным для начала работы.
Клиент-серверная архитектура
В этом проекте мы хотели фаззить RDP-сервер Windows, а также его RDP-клиент. Мотивация для фаззинга сервера RDP очевидна: злоумышленник может использовать его для удаленной компрометации сервера Windows и получения доступа к нему. Мотивация фаззинга RDP-клиента различна . Подумайте о сценарии, в котором злоумышленник уже скомпрометировал RDP-сервер, затем он готовит свой эксплойт и ждет, пока к нему не подключится RDP-клиент-жертва. Как только жертва подключится, злоумышленник может также скомпрометировать машину жертвы через клиент RDP. Это может произойти, когда администратор подключается к серверу, которым он управляет, и это может даже использоваться для выхода из виртуальной машины из-за того, что Hyper-V использует RDP для доступа к своим виртуальным машинам (используя функцию «расширенного сеанса»).
Фаззер для RDP должен иметь следующие основные компоненты:
- Инструментальный движок , отслеживающий покрытие кода и обнаруживающий сбои
- Механизм мутаций, который генерирует новые входные данные
- Отправитель ввода , который отправляет тестовые случаи фаззера по соответствующему целевому каналу.
- Целевой двоичный файл , который отслеживается механизмом инструментирования
Как вы понимаете, настройки фаззинга для клиента и сервера должны быть совершенно разными, но есть некоторые сходства.
На целевой стороне имеем следующее:
- Fuzzer — специально созданный файл afl-fuzz.exe
- инструментирования — пользовательская версия winafl.dll с использованием пользовательской сборки DynamoRIO с in_app. инструментирования
В следующем разделе описаны функции, добавленные в WinAFL и DynamoRIO в наших пользовательских сборках.
- Входные данные (или тестовые примеры) — мутированные PDU, которые записываются в промежуточный файл в каталоге, совместно используемом целью и отправителем входных данных.
А на стороне отправителя ввода у нас есть один компонент:
- Агент — читает промежуточные файлы и отправляет входные данные по целевому каналу.
Идея настройки довольно проста: мы пытаемся отправить наши фаззинговые входы по «живому» соединению RDP, ничего не имитируя (практически).
Для этого мы отделили генерацию входных данных от их передачи к цели, позволив входным данным перемещаться с одной стороны RDP-соединения на другую, прежде чем они будут обработаны целью. WinAFL. Чтобы фаззинг на основе покрытия работал, должно быть однозначное соответствие между входными данными и кодовыми путями, которые они инициировали. Для этого мы разработали «фоновый фаззинг», который отличает PDU фаззера от обычных PDU и отслеживает пути кода только для первых. Это было важно, потому что мы хотим, чтобы фаззер отслеживал покрытие только наших собственных тестовых случаев, а не случайных PDU, отправляемых по соединению. Чтобы проиллюстрировать это, давайте посмотрим, как что-то подобное может выглядеть при фаззинге виртуального канала RDPSND, который перенаправляет звук с RDP-сервера на клиент. Согласно документам , первый байт каждого PDU представляет тип отправленного сообщения.
Поддерживаемые значения для msgType поля 0x01 до 0x0D . В этом случае мы можем использовать старший бит первого байта в качестве маркера фаззинга следующим образом:
- Агент изменяет старший бит первого байта перед отправкой PDU.
- WinAFL проверяет старший бит первого байта перед обработкой сообщения. Если бит установлен, WinAFL отключает его и отслеживает покрытие кода этого сообщения. Если бит выключен, WinAFL просто игнорирует сообщение и не отслеживает никакого покрытия.
Увидев сходство между настройкой клиента и настройкой сервера, давайте посмотрим, чем они отличаются, начиная с клиента.
Настройка клиентского фаззинга
Клиент Windows RDP — mstsc.exe , но большая часть логики, которая обрабатывает данные виртуального канала, находится в mstscax.dll , которую загружает клиент.
Обратите внимание, что клиент удаленного доступа виртуальных машин Hyper-V, vmconnect.exe , также использует mstscax.dll для своих основных функций
Для простоты и эффективности мы выполнили и клиент, и сервер (цель и агент) на одной машине (т. е. использовали клиент для подключения к localhost/127.0.0). Чтобы разрешить параллельный фаззинг, мы также использовали mimikatz для исправления сервера, чтобы он позволял одновременные подключения RDP.
Вот компоненты настройки при фаззинге клиента:
- Цель — mstsc.exe и целевой модуль mstscax.dll.
- Инструментарий — DynamoRIO, который создает клиентский процесс, и winafl.dll, клиент DynamoRIO, который сообщает о покрытии кода.
- Механизм мутаций — AFL-Fuzz работает на том же компьютере и записывает новые тестовые примеры в файл.
- Отправитель ввода — наш RDPFuzzAgent, который открывает дескриптор сервера и отправляет PDU по выбранному виртуальному каналу. Агент берет каждый тестовый пример из файла, созданного AFL-Fuzz, и отправляет их на наш целевой канал.
Используя эти компоненты, мы смогли достичь скромной скорости выполнения ~ 50-100 выполнений в секунду. Это ни в коем случае не быстро, но это было быстрее, чем скорость, показанная в вышеупомянутом исследовании Парка и др., поэтому мы были в порядке с что.
Настройка фаззинга сервера
Чтобы найти целевой двоичный файл, содержащий основную логику сервера RDP, мы можем просто заглянуть в служб удаленных рабочих столов .
Код:
PS C:\> gci HKLM:\SYSTEM\CurrentControlSet\Services\$((Get-Service -Name "Remote Desktop Services").Name)
Hive: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\TermService
Name Property
---- --------
Parameters ServiceDll : C:\Windows\System32\termsrv.dll
. . .
Основная логика сервера RDP действительно заключается в termsrv.dll , который загружается в svchost.exe с помощью следующей командной строки: C:\Windows\System32\svchost.exe -k NetworkService -s TermService .
Первоначальный план фаззинга сервера был аналогичен плану клиента, то есть фаззить несколько экземпляров цели параллельно на одном компьютере, что требует запуска нескольких экземпляров TermService . Это оказалось довольно сложной задачей, так как Windows не поддерживает это по умолчанию. Когда мы попытались сделать это вручную, мы даже увидели несколько жестко закодированных строк в termsrv.dll , которые указывают на TermService и его ключи реестра, поэтому мы решили сосредоточить наши усилия на другом месте и просто использовать несколько виртуальных машин для параллельного фаззинга сервера.
В настройке фаззинга клиента мы использовали вызов API на стороне сервера WTSVirtualChannelWrite() для отправки входных данных фаззинга в цель. К сожалению, мы не смогли найти аналогичный API, который позволял бы нам отправлять входные данные на сервер через RDP-соединение. Следовательно, мы решили использовать специально созданный FreeRDP (популярный клиент RDP с открытым исходным кодом) с машины Ubuntu для отправки входных данных на нечеткий сервер. Обратите внимание, что это не идеальная установка для фаззинга, и эти ограничения привели к тому, что скорость фаззинга сервера составляла примерно 1/10 от скорости фаззинга клиента.
Вот компоненты настройки при фаззинге сервера:
- Цель — зависит от канала, который подвергается фаззингу. Это может быть termsrv.dll , audiodg.exe , rdpinput.exe . (см. «Поиск соответствующего кода» в задачах в следующем разделе, чтобы понять, как мы определили цель для каждого канала).
- Инструментарий — в этой настройке фаззер не контролирует инициализацию целевого процесса. Таким образом, чтобы отслеживать покрытие кода, мы должны оснастить работающую цель. Наш любимый фаззер, WinAFL, может использовать несколько инструментальных платформ, и мы выбрали DynamoRIO за его расширяемость и надежность.
- DynamoRIO (а значит, и WinAFL) не поддерживает присоединение к живому процессу из коробки. Мы изменили этот запрос на вытягивание , чтобы реализовать его, и изменили WinAFL, чтобы использовать эту функцию.
Стоит отметить, что эта функция присоединения открывает двери для фаззинговых процессов, которые раньше были «нефаззинговыми» с помощью WinAFL — процессов, создание которых пользователь не может контролировать. В частности, он позволяет выполнять фаззинг служб Windows.
- Механизм мутаций — AFL-Fuzz, работающий на компьютере с Windows и записывающий новые тестовые примеры в файл в общей папке.
- Отправитель входных данных — наша сборка FreeRDP, которая подключается к серверу на компьютере с Windows, отслеживает общую папку на наличие новых тестовых случаев и отправляет каждый тестовый случай по целевому каналу RDP.
Состояние
При запуске первого канала (фаззинг клиента) мы столкнулись с проблемой: как только на стороне клиента обнаруживалось два недопустимых сообщения, он немедленно разрывал соединение.
Чтобы избежать этой проблемы, мы ввели некоторые грамматики протокола в логику агента, т. е. ограничение пространства разрешенных входных данных. Среди прочего мы реализовали:
- Ограничения размера сообщения
- самый маленький, самый большой
- делимость (например, если сообщение содержит массив переменной длины из 4-байтовых элементов, он должен делиться на 4)
- Ограничения значений
- разрешать только определенные значения
- минимальное и максимальное значения
- значение должно быть размером PDU
- значение должно отличаться в каждом PDU
Мы извлекли часть грамматики RDP из документации RDP и его расширениям, часть — из обратного проектирования соответствующих двоичных файлов, а остальное — из отслеживания неудачных целевых исполнений.
Например, эту логику можно использовать, чтобы разрешать только сообщения, которые начинаются с одного из поддерживаемых msgTypes , за которыми следует размер PDU и уникальный идентификатор, а общий размер которых составляет от 22 до 122, а в остатке остается 2 по модулю 4. .
Важно отметить, что, применяя эти принудительные меры, вы практически ограничиваете способность движка мутаций изменять тестовые примеры по своему усмотрению, тем самым потенциально пропуская интересные мутации. По этой причине мы старались применять как можно меньше, но при этом гарантировать, что соединение не будет закрываться слишком часто.
Другим важным моментом здесь является то, что применение грамматических правил — не единственный вариант решения подобных проблем. В случае с одним конкретным каналом (GFX) мы обратились к исправлению фактической целевой функции, которую мы фаззили, чтобы она не закрывала соединение в случае недопустимого набора сообщений. Это позволило нам продолжить фаззинг недопустимых сообщений и постоянно держать соединение открытым. Здесь также вы рискуете обнаружить сбои, которые не будут воспроизводиться в исходном коде (без патча). Это отличный пример тонкого баланса, который требуется для фаззинга между обеспечением достаточной скорости выполнения при сохранении исходной функциональности целевой программы и свободой механизма мутации.
Фаззинг с несколькими входами
Само собой разумеется, что большая часть логики и, следовательно, большинство ошибок зависят от последовательности сообщений, а не от одного. Не случайно большинство обнаруженных нами ошибок включали как минимум два сообщения. Чтобы выявить эти ошибки, мы ввели фаззинг с несколькими входами. Мы использовали словарь фаззера , который идентифицирует начало нового сообщения и его тип. Затем агент разделит ввод на несколько PDU в соответствии с этими словарными словами и отправит их один за другим.
Таким образом, вход с несколькими входами может выглядеть примерно так:
Код:
___cmd07 <1st PDU data>
___cmd02 <2nd PDU data>
___cmd03 <3rd PDU data>
которые агент переводит в три сообщения с msgType 7, 2 и 3 и их соответствующим содержимым.
Чтобы поддерживать однозначное соответствие между входными данными, созданными фаззером, и вызванным им покрытием кода, мы ввели второй маркер, который идентифицировал последнее сообщение в последовательности. Только когда WinAFL определяет, что вызов с этим «последним в последовательности» маркером завершается, он завершает цикл и создает следующий ввод.
Хотя фаззинг с несколькими входами был решающим (и плодотворным) для наших усилий, мы также сочли необходимым ограничить количество PDU для каждого тестового случая. Это связано с тем, что фаззер обращается к входным данным, которые приводят к другой последовательности кода. Повторение одного и того же сообщения 100 раз приводит к другой кодовой последовательности, чем однократная отправка.
Проблемы с воспроизведением
Примерно через неделю фаззинга появился первый сбой. Однако сбой не повторился, когда мы снова попытались запустить тот же ввод. Это случалось довольно часто, и, вероятно, это было связано с природой протокола с отслеживанием состояния. Другими словами, один тестовый пример приводил клиента в определенное состояние, которое затем «использовалось» последующим тестовым набором для сбоя цели.
Чтобы понять невоспроизводимые сбои, мы модифицировали WinAFL для создания дампа памяти целевого процесса при каждом обнаружении сбоя.
Автоматизация анализа сбоев
Создание дампов по крашам решило одну проблему, но создало другую: если крах обнаружен, очень вероятно, что он будет повторяться неоднократно. Как правило, WinAFL пытается обнаруживать идентичные сбои и уведомлять только об «уникальных сбоях», но наш фаззинг нескольких сообщений сильно усложнил это обнаружение. Подумайте о случае, когда одно сообщение вызывает сбой цели. Фаззер может создать любой набор сообщений с этим сообщением в конце. Каждый из этих наборов сообщений приведет к сбою цели, а также приведет к другому битовому массиву покрытия, поскольку сообщения и их обработка различны (за исключением последнего, который действительно имеет значение). Это заставит WinAFL каждый раз сообщать об уникальном сбое.
Нам пришлось автоматизировать анализ сбоев по двум причинам. Во-первых, утомительно вручную анализировать каждый сбой (только для того, чтобы обнаружить, что это старая новость), а во-вторых, диск быстро заполнялся дампами памяти.
Чтобы преодолеть эту проблему, мы написали сценарий WinDBG, который анализирует сбой и извлекает из него стек сбоя. Затем мы запустили сценарий PowerShell, который периодически анализирует сбои и сохраняет только те, которые содержат новые стеки (и отправляет нам хорошие новости по электронной почте).
Длительное время запуска
В клиентской настройке фаззинга с момента создания цели ( mstsc.exe ) фаззером до момента установления соединения и возможности отправки первого сообщения прошло более 10 секунд. Следовательно, было крайне важно выполнить как можно больше итераций без перезапуска цели. Мы достигли этого, используя -fuzz_iterations AFL-Fuzz и предоставив как можно больше итераций (до того, как что-то начнет ломаться).
Многоканальный фаззинг
Подобно фаззингу с несколькими входами, некоторая логика требует последовательности сообщений на разных каналах. Например, отправка данных камеры с клиента на сервер поддерживается с использованием нескольких каналов, как описано в документации .
Таким образом, если мы хотим фаззить сервер, отправляя входные данные с камеры, мы должны сделать это как минимум на двух разных каналах.
Наше решение тоже было похожим — словарь фаззера также определял канал, по которому должно было быть отправлено сообщение.
Поиск соответствующего кода
Поскольку RDP имеет много разных компонентов в Windows, может быть сложно даже найти целевую функцию, которую нам нужно фаззить.
Код:
PS C:\> gci -Include *.exe, *.dll, *.sys -Recurse C:\Windows\ -ErrorAction SilentlyContinue | ?{[System.Diagnostics.FileVersionInfo]::GetVersionInfo($_).FileDescription -match "RDP|Remote Desktop"} | Measure-Object | select count
Count
-----
191
Чтобы сделать это быстро, мы создали небольшую базу данных всех символов, которые могут быть связаны с нашим проектом.
Идея заключалась в том, чтобы загрузить все PDB-файлы, относящиеся к нашей версии Windows, извлечь из них все имена функций и выгрузить их в файл (со ссылкой на exe/sys/dll), чтобы мы могли быстро искать имена функций и найдите функцию, связанную с нашим текущим целевым каналом. Это очень помогло. Поскольку почти все функции приема динамических каналов соответствуют следующему шаблону C<class-name>::OnDataReceived , мы могли бы быстро просмотреть список этих функций и выяснить, что, вероятно, связано с каналом, на который мы нацелены.
Канал AUDIO_PLAYBACK (сервер → клиент)
Канал AUDIO_PLAYBACK_DVC используется для воспроизведения звуков с сервера на клиенте. Его нормальный поток состоит из двух последовательностей: инициализация и передача данных. При обычном использовании протокола последовательность инициализации выполняется один раз в начале, за которой следует множество последовательностей передачи данных.
Последовательность инициализации — используется для установки версии и форматов , которые будут использоваться в следующих последовательностях данных.
Последовательность передачи данных — звуковые данные с сервера для воспроизведения на клиенте.
WaveInfo Wave и PDU содержат индекс массива форматов, которым обмениваются в последовательности инициализации, определяющей формат передаваемых аудиоданных.
Когда происходит изменение формата, т. е Wave или WaveInfo поступает с индексом, отличным от последнего использовавшегося, — клиент проверяет правильность нового индекса.
Код:
// в mstscax!CRdpAudioController::OnNewFormat
if ( ( unsigned int ) new_format_index > = this - > formatArray_size
Однако, пока индекс формата остается прежним, эта проверка пропускается ( OnNewFormat() ) не вызывается и код проверки находится в ней). Вот псевдокод соответствующих частей.
Код:
/ in mstscax!CRdpAudioController::OnWaveData
last_format_index = this->last_format_index;
format_index_from_pdu = *((_WORD *)pdu + 3); //pdu is controlled by the server
if ( last_format_index != format_index_from_pdu )
{
CRdpAudioController::OnNewFormat(this, (__int64 *)format_index_from_pdu); // this is where the bound check is being made
Уязвимый поток
- Сервер отправляет клиенту PDU с 0x1A , в результате чего клиент выделяет массив формата с этим размером.
- Сервер отправляет клиенту PDU Wave2 , который использует формат 0x5 из массива со звуковыми данными
- Клиент проверяет, идентичен ли этот формат последнему отправленному формату.
- Если это так, он использует последний декодер (разыменованный из элемента формата в массиве форматов), а если нет, он загружает новый указатель функции декодера из массива форматов.
- Сервер снова отправляет клиенту PDU серверных — на этот раз только с 0x2 , в результате чего клиент освобождает предыдущий массив форматов и выделяет новый с новым размером.
- Наконец, сервер отправляет еще один PDU Wave2 , используя последний использованный формат 0x5 .
- Поскольку формат не изменился, клиент не выполняет никаких проверок достоверности.
- Затем клиент выполняет внешнее чтение, пытаясь прочитать шестой формат из массива 2-формата, и аварийно завершает работу.
Обратите внимание, что эта ошибка в значительной степени зависит от фаззинга с несколькими входами, и мы не смогли бы найти ее без этой функции.
Канал AUDIO_INPUT (Клиент → Сервер)
канал AUDIO_INPUT используется для отправки звука от клиента на сервер. На стороне сервера входные аудиоданные обрабатываются audiodg.exe .
Как и в канале AUDIO_PLAYBACK_DVC , клиент и сервер сначала обмениваются массивом звуковых форматов, которые они поддерживают.
Начинается PDU звуковых форматов с заголовка из девяти байтов, который включает в себя команду, количество форматов и размер пакета, за которым следует массив форматов, каждый из которых имеет переменную длину (плюс необязательное поле дополнительных данных).
Код, обрабатывающий PDU звуковых форматов , находится в rdpendp.dll . Сначала он проверяет, что размер пакета составляет не менее девяти байтов, а затем считывает заголовок и проверяет, что размер из заголовка не превышает размер пакета.
Код:
// in rdpendp!CAudioInputHandler::OnFormatsReceived
if ( size < 9 )
{
// ...
}
// ...
size_from_msg = *(_DWORD *)(data + 5);
if ( size_from_msg > size )
{
// ...
}
Затем та же функция вычитает девять из размера , считанного из заголовка, и считывает количество форматов, указанное в заголовке, если оставшаяся длина достаточно велика.
Размер из заголовка не защищен от целочисленного потери значимости, что может привести к тому, что это вычитание будет зациклено и приведет к тому, что программа будет считывать «форматы» после конца пакета.
Код:
// in rdpendp!CAudioInputHandler::OnFormatsReceived
underflowed_size = size_from_pdu - 9;
format_definition_offset = (unsigned __int16 *)(pdu + 9);
if ( num_formats )
{
while ( underflowed_size >= 0x12 )
{
format_definition_size = format_definition_offset[8];
total_format_size = format_definition_size + 18;
if ( underflowed_size < (unsigned __int64)(format_definition_size + 18) )
break;
(*class_fomats_array)[format_index] = (struct SNDFORMATITEM *)operator new[](total_format_size);
local_format = (*class_fomats_array)[format_index];
if ( !local_format )
{
status = E_OUTOFMEMORY;
goto CLEAN_AND_RETURN;
}
memcpy_0(local_format, format_definition_offset, total_format_size);
format_definition_offset = (unsigned __int16 *)((char *)format_definition_offset + total_format_size);
underflowed_size -= total_format_size;
if ( ++format_index >= num_formats )
goto LABEL_50;
}
goto INVALID_ARG_EXIT;
}
Обратите внимание, что у нас нет контроля над инициализацией процесса audiodg . Таким образом, мы не смогли бы найти эту ошибку без функции подключения DynamoRIO.
Резюме
В этом блоге мы представили наш процесс решения сложной задачи фаззинга: RDP-клиент и сервер Windows. Мы хотели поделиться нашим процессом по нескольким причинам.
Во-первых, мы считаем важным поделиться процессом, даже если вы не смогли достичь своей первоначальной цели (например, RCE). Это может помочь вам задуматься о своем собственном процессе — что, казалось, работало хорошо, а что можно было бы улучшить. Это также может помочь сообществу безопасности извлечь уроки из прошлого опыта.
Во-вторых, несмотря на то, что настройка среды фаззинга может быть сложным процессом, мы считаем, что это цель, которую стоит преследовать — даже с более сложными задачами, такими как та, которую мы представили здесь. RDP — очень сложный протокол с множеством компонентов и разными кодовыми базами. В сочетании с тем фактом, что он настолько популярен (более 4 миллионов серверов с выходом в Интернет на основе Shodan.io ), он становится очень прибыльной целью для злоумышленников. Это означает, что мы, как сообщество безопасности, должны приложить большие усилия, чтобы сделать его более безопасным.