Содержание статьи
Авторы таска подготовили нам три файла:
- ELF-бинарь
gsoapNote
; - ELF-бинарь
libc-2.
;27. so - XML-файл
ns.
.wsdl
www
SOAP — это протокол на основе XML, который используется для удаленного вызова процедур (Remote Procedure Call). Файл ns.
(Web Services Description Language) описывает доступ к вызываемым процедурам. Наша задача — получить удаленное исполнение кода в SOAP-сервисе gsoapNote
.
Авторы таска намекают, что gsoapNote
запускается на тачке с Linux, где загружена библиотека libc-2.
. Поэтому сразу заставляем отладчик GDB загружать конкретно этот бинарь при старте сервиса. В .
добавим
user@ubuntu: cat .gdbinit
...
set exec-wrapper env 'LD_PRELOAD=./libc-2.27.so'
Извлечем из ns.
информацию c помощью утилиты SOAPUI.
SOAPUI автоматически формирует XML-шаблон запроса RPC. Мы сразу видим, на каком сетевом интерфейсе и порте стартует сервер — localhost:
, а также имя RPC-метода — handleCommand(
. SOAPUI ничего не знает об аргументах, поэтому на их месте стоит знак вопроса.
Запускаем gsoapNote
и через SOAPUI отправляем неполноценный шаблон. Получаем осмысленный ответ от сервера, закодированный в Base64:
<resultCode>2</resultCode>
Наверно, двойка — это оценка нашего запроса, который не понравился gsoapNote
!
Взглянем на бинарные митигации.
Из плохих новостей — стековые канарейки защищают gsoapNote
от переполнения буфера на стеке (2 — Canary found), NX делает некоторые страницы памяти неисполняемыми (3 — NX enabled).
Хороших новостей гораздо больше: строка RELRO
говорит о том, что мы можем переписывать адреса функций из shared-библиотек (1 — Partial
) и эти адреса не будут рандомизироваться (4 — No
), а это хорошее подспорье для перехвата управления. Ну и самая хорошая новость — в бинарнике есть символы (5 — 817
), значит, у нас будут хотя бы сигнатуры функций, а это очень облегчит реверс.
Реверс-инжиниринг
handleCommand
Не забываем, что gsoapNote
собран с символами, поэтому сразу грузим его в «Иду» и прыгаем к функции handleCommand(
.
info
Очень удобное расположение окон в «Иде» я подсмотрел у ребят с OALabs: окна с дизассемблерным и декомпилированным листингами разделяют воркспейс пополам и синхронизируются между собой.
Декомпилированный листинг не то чтобы ужасный, но понять, что происходит, сложно. Цикл for
, куча вложенных if
, функция executeCommand(
принимает девять аргументов...
Давай будем рассматривать листинг как картину импрессионистов — отойдем на пару шагов назад и поищем общие паттерны.
Во‑первых, сразу в нескольких местах видим, что если в if
условие не выполняется, то локальной переменной v11
присваивается значение 2
и пропускается куча кода. А двойка — это как раз тот result code, который пришел в ответ на шаблон SOAPUI. Все сходится.
Очень много кода выполняется внутри цикла for
. Обратим наше внимание на него.
Перед циклом вызывается функция xmlDocGetRootElement(
, возвращенная структура используется в цикле for
. Разыменовывается оффсет +24 для инициализации счетчика и оффсет +48 для итерации.
Вместо исследования внутренностей функции xmlDocGetRootElement(
посмотрим примеры исходного кода с ее использованием. В этом нам поможет grep.app. Этот сайт покажет примеры исходного кода качественных репозиториев с интересующей нас функцией.
По ссылке прыгаем в репозиторий проекта (я выбрал lastpass) и вникаем в исходный код:
// lastpass xml.c source code...#include <libxml/parser.h>#include <libxml/tree.h>...xmlNode *root;root = xmlDocGetRootElement(doc);...
Ага... значит, xmlDocGetRootElement(
возвращает указатель на структуру типа xmlNode
, а сама структура определена в libxml
. Гуглим libxml
и находим устройство структуры xmlNode. Особенно нас интересуют оффсеты, которые мы встретили в декомпиленном листинге.
Перенесем это знание в «Иду». Создаем структуру xmlNode
по тому же принципу.
info
Необязательно восстанавливать структуру полностью один в один. Делаем это до нужного нам оффсета +48 (
.
Теперь возвращаемся в декомпилированный листинг и присваиваем локальной переменной v16
тип указателя на xmlNode
.
И все стало гораздо лучше! То, что происходит в цикле for
, теперь как на ладони! Наш gsoapNote
парсит XML: достает рутовую ноду, инициализирует счетчик его потомком xmlNode->
и обходит соседние ноды этого потомка при помощи xmlNode->
. И strcmp(
теперь обрел смысл: сравнивается имя ноды curXmlNode->
с захардкоженной строкой "array"
.
Обрати внимание на функцию parseArray(
, она принимает текущую XML-ноду и зануленный двадцатибайтовый массив. Зануляется он, скорее всего, потому, что в него будет записан результат парсинга, не зря же функция parseArray(
имеет такое название.
Еще обратим внимание на то, как код возврата parseArray(
влияет на поток выполнения: если ноль, то вызывается функция с любопытным названием executeCommand(
, в противном случае обрабатываем следующую ноду.
Определенно нам надо попасть в executeCommand(
, но для этого нужно распарсить массив с нулевым кодом возврата.
parseArray
Функция принимает два аргумента: a1
и a2
. Мы уже выяснили, что первый — указатель на xmlNode
, а второй — зануленный байтовый массив. Давай посмотрим, что происходит с этим массивом. Для этого в декомпилированном листинге смотрим кросс‑референсы на a2
.
Сразу подмечаем оффсеты: +0, +8, +16, +24. Каждый следующий оффсет больше предыдущего на размер QWORD (8 байт). Это не 0x20-байтовый массив, а массив из четырех QWORD. К 85-й строке вся структура инициализирована.
По оффсетам +0 и +16 будут указатели на строки, по +8 и +24 будут int.
Предположим, что в эту структуру из четырех QWORD заносится результат парсинга. Назовем ее PARSE_RESULT
.
Присвоим аргументу a1
тип xmlNode
, а аргументу a2
тип PARSE_RESULT
и снова взглянем на parseArray(
. Внутри опять видим много однообразного кода парсинга XML. Поскольку мы удачно определили структуры входных аргументов, читаем декомпиленный листинг практически как исходный код.
Анализируя parseArray(
дальше, мы получаем практически целостную картину того, что нужно вставить вместо многозначительного знака вопроса. Не буду утомлять тебя дальнейшим разбором, а просто покажу, как выглядит ожидаемый XML.
info
Значение ноды <
должно быть равно количеству нод внутри <
минус один.
Правильное определение используемых структур позволило достать много информации из статического анализа gsoapNote
. Давай продолжать исследование в динамике. Отправим XML на рисунке выше, поставим брейк‑пойнт на выходе из parseArray(
(по адресу 0x403BE2) и посмотрим, что лежит в PARSE_RESULT
после парсинга XML из запроса выше:
#
pwndbg>
0x7ffffffd5a90:
0x7ffffffd5aa0:
pwndbg>
0x6562a0:
pwndbg>
0x7ffffffd5a98:
pwndbg>
0x6575b0:
pwndbg>
0x7ffffffd5aa8:
#
#
pwndbg>
rax
С удовольствием наблюдаем, как контролируемые нами данные оседают в памяти. Все поля структуры PARSE_RESULT
инициализированы, значит, выполнился почти весь код функции parseArray(
, но код возврата -1, а это не дает нам двигаться дальше...
Давай разбираться. Можно, конечно, пошагово выполнить код в отладчике, но это долго. Мы сделаем это быстро, одним выстрелом — подсветим выполненный код с помощью DynamoRIO.
DynamoRIO — это фреймворк для разработки инструментов динамического анализа. Нам понадобится встроенный в него инструмент drcov, который покажет нам все выполненные инструкции.
info
Если вдруг захочешь решать эту проблему в отладчике, то советую использовать кастомную команду для GDB step before. Вводишь в консоль sb
и брякаешься перед инструкцией call
. Это ускорит процесс отладки.
Запускаем gsoapNote
под инструментацией drcov
следующим образом:
<path to DynamoRIO>/bin64/drrun -t drcov — gsoapNote
Снова отсылаем XML через SOAPUI и завершаем процесс.
Наш drrun
сгенерировал файл drcov.
, в котором содержатся адреса выполненных инструкций. Для просмотра этого файла будем использовать плагин lighthouse для IDA.
info
На момент написания статьи последняя версия DynamoRIO — 9.0, c drcov
версии 3, но lighthouse не может распарсить файл третьей версии, поэтому я использую версию 8.0 с drcov
версии 2.
Зеленым цветом выделяются выполненные инструкции. На скрине выше приведен момент выхода с кодом возврата -1. И произошло это потому, что значение внутри <
не равно длине строки <
. В SOAPUI корректируем XML, пролетаем parseArray(
и попадаем в executeCommand(
. Успех.
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»