Файлы задания
SOAP — это протокол на основе XML, который используется для удаленного вызова процедур (Remote Procedure Call). Файл ns.wsdl (Web Services Description Language) описывает доступ к вызываемым процедурам. Наша задача — получить удаленное исполнение кода в SOAP-сервисе gsoapNote .
Авторы таска намекают, что gsoapNote запускается на тачке с Linux, где загружена библиотека libc-2.27.so . Поэтому сразу заставляем отладчик GDB загружать конкретно этот бинарь при старте сервиса. В .gdbinit добавим
user@ubuntu:cat.gdbinit
...
set exec-wrapper env 'LD_PRELOAD=./libc-2.27.so'
Извлечем из ns.wsdl информацию c помощью утилиты SOAPUI.
SOAPUI автоматически формирует XML-шаблон запроса RPC. Мы сразу видим, на каком сетевом интерфейсе и порте стартует сервер — localhost:33263 , а также имя RPC-метода — handleCommand() . SOAPUI ничего не знает об аргументах, поэтому на их месте стоит знак вопроса.
Запускаем gsoapNote и через SOAPUI отправляем неполноценный шаблон. Получаем осмысленный ответ от сервера, закодированный в Base64:
Наверно, двойка — это оценка нашего запроса, который не понравился gsoapNote !
Взглянем на бинарные митигации.
Из плохих новостей — стековые канарейки защищают gsoapNote от переполнения буфера на стеке (2 — Canary found), NX делает некоторые страницы памяти неисполняемыми (3 — NX enabled).
Хороших новостей гораздо больше: строка RELRO говорит о том, что мы можем переписывать адреса функций из shared-библиотек (1 — Partial RELRO ) и эти адреса не будут рандомизироваться (4 — No PIE ), а это хорошее подспорье для перехвата управления. Ну и самая хорошая новость — в бинарнике есть символы (5 — 817 Symbols ), значит, у нас будут хотя бы сигнатуры функций, а это очень облегчит реверс. Реверс-инжиниринг 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 (+0x30) .
Теперь возвращаемся в декомпилированный листинг и присваиваем локальной переменной v16 тип указателя на xmlNode .
И все стало гораздо лучше! То, что происходит в цикле for , теперь как на ладони! Наш gsoapNote парсит XML: достает рутовую ноду, инициализирует счетчик его потомком xmlNode->children и обходит соседние ноды этого потомка при помощи xmlNode->next . И strcmp() теперь обрел смысл: сравнивается имя ноды curXmlNode->name с захардкоженной строкой "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
Значение ноды <number> должно быть равно количеству нод внутри <array> минус один.
Правильное определение используемых структур позволило достать много информации из статического анализа gsoapNote . Давай продолжать исследование в динамике. Отправим XML на рисунке выше, поставим брейк‑пойнт на выходе из parseArray() (по адресу 0x403BE2) и посмотрим, что лежит в PARSE_RESULT после парсинга XML из запроса выше:
# Посмотрим, что лежит в PARSE_RESULT после выхода из parseArray
pwndbg> x/4gx $PARSE_RESULT
0x7ffffffd5a90: 0x00000000006562a0 0x0000000000001000
0x7ffffffd5aa0: 0x00000000006575b0 0x0000000000010000
pwndbg> x/s ((long*)$PARSE_RESULT)[0]
0x6562a0: "AAAAAA"
pwndbg> x/d ((long*)$PARSE_RESULT + 1)
0x7ffffffd5a98: 4096
pwndbg> x/s ((long*)$PARSE_RESULT)[2]
0x6575b0: "BBBBBB"
pwndbg> x/d ((long*)$PARSE_RESULT + 3)
0x7ffffffd5aa8: 65536
# Содержимое полностью соответствует XML
# А вот код возврата -1 подвел...
pwndbg> i r rax
rax 0xffffffff
С удовольствием наблюдаем, как контролируемые нами данные оседают в памяти. Все поля структуры PARSE_RESULT инициализированы, значит, выполнился почти весь код функции parseArray() , но код возврата -1, а это не дает нам двигаться дальше...
Давай разбираться. Можно, конечно, пошагово выполнить код в отладчике, но это долго. Мы сделаем это быстро, одним выстрелом — подсветим выполненный код с помощью DynamoRIO.
DynamoRIO — это фреймворк для разработки инструментов динамического анализа. Нам понадобится встроенный в него инструмент drcov, который покажет нам все выполненные инструкции.
info
Если вдруг захочешь решать эту проблему в отладчике, то советую использовать кастомную команду для GDB step before. Вводишь в консоль sb и брякаешься перед инструкцией call . Это ускорит процесс отладки.
Запускаем gsoapNote под инструментацией drcov следующим образом:
<path to DynamoRIO>/bin64/drrun -tdrcov — gsoapNote
Снова отсылаем XML через SOAPUI и завершаем процесс.
Наш drrun сгенерировал файл drcov.gsoapNote.X.Y.proc.log , в котором содержатся адреса выполненных инструкций. Для просмотра этого файла будем использовать плагин lighthouse для IDA.
info
На момент написания статьи последняя версия DynamoRIO — 9.0, c drcov версии 3, но lighthouse не может распарсить файл третьей версии, поэтому я использую версию 8.0 с drcov версии 2.
Зеленым цветом выделяются выполненные инструкции. На скрине выше приведен момент выхода с кодом возврата -1. И произошло это потому, что значение внутри <bulkString><number> не равно длине строки <bulkString><content> . В SOAPUI корректируем XML, пролетаем parseArray() и попадаем в executeCommand() . Успех.
Перейти обратно к новости
|