В этой статье я покажу раз­бор инте­рес­ной задач­ки в духе CTF. Мы получим уда­лен­ное выпол­нение кода на сер­вере SOAP. Все при­мити­вы экс­плу­ата­ции так или ина­че свя­заны с кучей, поэто­му ты узна­ешь мно­го нового о фун­кци­ях, которые с ней работа­ют. Нам пред­сто­ит поревер­сить бинарь для Linux, исполь­зуя фрей­мворк динами­чес­кой инс­тру­мен­тации.

Ав­торы тас­ка под­готови­ли нам три фай­ла:

  • ELF-бинарь gsoapNote;
  • ELF-бинарь libc-2.27.so;
  • XML-файл ns.wsdl.

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:

<resultCode>2</resultCode>

На­вер­но, двой­ка — это оцен­ка нашего зап­роса, который не пон­равил­ся 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 -t drcov 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(). Успех.

Продолжение доступно только участникам

Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

Вариант 2. Открой один материал

Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.


Подписаться
Уведомить о
2 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии