Содержание статьи
- Что такое RuCTFE?
- Некоторые термины
- Cartographer
- Описание
- Уязвимость и эксплуатация
- TheBin
- Описание
- Уязвимости
- Защита
- Crash
- Описание
- Уязвимость 1
- Уязвимость 2
- AtlaBlog
- Описание
- Уязвимость
- Чекер
- Эксплоит
- Патч
- Sapmarine
- Описание
- Уязвимости
- Weather
- Описание
- Уязвимости
- Получаем шесть последних флагов
- Получаем все флаги
- Выполняем произвольные команды в шелле
- RuCTFE на «Хакере»
- Как получить доступ к заданиям
Что такое RuCTFE?
RuCTFE — международные онлайновые соревнования по информационной безопасности формата Capture The Flag (CTF) по классическим правилам attack-defence, проводятся командой HackerDom из Уральского федерального университета ежегодно с 2009 года. Всего на соревнования RuCTFE 2016 зарегистрировалась 451 команда из разных точек мира. 167 команд пришли к финишу с ненулевым результатом.
Настройка стабильной сетевой инфраструктуры для соревнований с таким количеством участников — тема отдельной статьи. Сегодня мы хотим познакомить тебя с уязвимыми сервисами, которые крутились в образах виртуальных машин команд.
Некоторые термины
- Флаг — строка, подходящая под некоторое правило, в нашем случае под регулярное выражение
/^[0-9A-Z]{31}=$/
. - Сервис — уязвимое приложение, запущенное на игровом сервере команды, хранит флаги.
- Проверяющая система — система, которая отвечает за начисление очков в соответствии с игровыми правилами.
- Чекер — модуль проверяющей системы, относящийся к одному из сервисов. Задачи чекера — проверять общую функциональность сервиса, устанавливать в сервис флаги, проверять наличие флагов в сервисе.
Создание сервиса для attack-defence CTF — очень творческий процесс. Прежде всего, сервис должен удовлетворять общей схеме проверяющей системы: нужна возможность положить флаг и получить его обратно спустя некоторое время. Хороший сервис обладает следующими свойствами:
- Логичность. Уязвимости в сервисах не должны быть надуманными или приделанными сбоку. Они должны быть вплетены в общую понятную функциональность сервиса.
- Уязвимости в сервисе не должны быть слишком простые, их описание не должно в чистом виде встречаться в открытом доступе.
- Эксплуатация уязвимостей не должна приводить к состоянию гонки между командами, подталкивая к созданию большой нагрузки на сервис.
- Быстродействие. Сервис должен справляться с нагрузкой от проверяющей системы и запущенных эксплоитов сотни-другой команд.
- Желательно, чтобы уязвимости было сложно отследить среди всего сетевого трафика.
- Сервисы и уязвимости в них должны быть разнообразными: разные языки, разные фреймворки, чтобы каждый участник смог найти что-то интересное для себя.
Все эти требования помогают достичь главной цели — сделать игру захватывающей. Насколько у нас это получилось — судить читателю. В этом году на RuCTFE было представлено шесть сервисов, с разборами которых мы хотим тебя познакомить.
Cartographer
Описание
Cartographer — сервис для хранения карт дна океана Атлантиды и обмена ими. Большая часть сервиса была написана на JVM-языке Kotlin с использованием фреймворка Spring MVC.
Основная функция сервиса — защищенное и отказоустойчивое хранение данных в сети из ненадежных узлов. Все блоки данных хранились распределенно на сервисах нескольких команд, зашифрованные симметричным алгоритмом шифрования. Пользователю при сохранении блока данных возвращался сессионный ключ. Можно выделить четыре главные составляющие сервиса: хранилище произвольных данных, компонент шифрования данных, репликатор и пингер.
Пингер каждую минуту отправляет запрос к каждому узлу в игровой сети, чтобы оценить, как далеко географически он находится. Для этого применяется протокол, очень отдаленно напоминающий NTP. В каждом запросе содержится локальное время отправки, а ответ включает в себя время получения и время отправки на удаленном узле. При помощи простой арифметики вычисляется задержка. «Ближайшие» узлы запоминаются как предпочтительные для репликации данных.
Первая уязвимость была в протоколе пинга. Команда могла пропатчить свой пингер так, чтобы отвечать с «локальным» временем из будущего. Это давало отрицательную задержку и позволяло стать одной из ближайших команд для любой другой команды и получать чужие данные.
У компонента шифрования было два обработчика. Первый обработчик принимал POST-запросы на /images/encrypt
, с произвольными бинарными данными в теле запроса. После получения данных обработчик генерировал 128-битный сессионный ключ, сериализовал его в JSON вместе с другими метаданными и шифровал мастер-ключом (он генерировался при первом запуске сервиса), используя AES в режиме сцепления блоков CBC. Переданные бинарные данные шифровались точно таким же образом, но уже сессионным ключом. Затем шифртексты объединялись, образуя чанк, и сохранялись локально, для чего использовался случайно сгенерированный уникальный идентификатор. Затем идентификатор чанка и сессионный ключ возвращались пользователю в ответе.
Второй обработчик принимал POST-запросы на /images/decrypt
. При получении такого запроса обработчик доставал чанк из локального хранилища, расшифровывал метаданные, доставал сессионный ключ и сравнивал его с тем, который был передан в запросе. Если ключи совпадали, обработчик расшифровывал данные, используя этот сессионный ключ, и возвращал их пользователю в ответе.
Хранилище данных было очень простым, оно обрабатывало запросы GET и PUT по пути /chunks/<id>
, где id — уникальный идентификатор чанка. PUT сохранял данные локально, GET отдавал ответ «200 OK» с данными, которые были сохранены ранее, либо «404 Not Found», если чанк не найден. Также хранилище позволяло получить список из тысячи последних сохраненных чанков с помощью запроса на /chunks/_recent
.
Репликатор выбирал пять случайных и пять ближайших узлов других команд, после чего отправлял им зашифрованные чанки. Получается, что сервис ищет ближайший узел и получает от него все данные.
Уязвимость и эксплуатация
Главная уязвимость содержалась в реализации алгоритма симметричного шифрования AES с режимом сцепления блоков CBC. Баг позволял провести атаку Padding oracle и расшифровать метаданные, основываясь на ошибке сервера о неправильном «паддинге».
Еще интереснее то, что, используя атаку Padding oracle и зная первый блок открытого текста, можно восстановить IV (Init Vector), если передать сообщение с нулями вместо первого блока. Первый блок метаданных был известен с точностью до одного символа: {"sessionKey":"X
, где X — неизвестный первый символ закодированного в Base64 сессионного ключа. После того как первые 15 байт IV восстановлены, можно перебором получить значение последнего байта.
В реализации сервиса IV всегда был равен ключу шифрования, поэтому, восстановив IV, который применялся для шифрования метаданных, мы получали мастер-ключ. После этого, используя либо список последних чанков, либо описанную уязвимость в пинг-протоколе, мы могли получать и расшифровывать чанки с сервисов других команд.
Код для получения мастер-ключа можно найти здесь, а код для получения флага по ID — тут.
TheBin
Описание
TheBin — сервис для публикации текстовых объявлений Атлантиды. Написан на Lua и использует nginx и Redis.
Текстовые объявления на этом сервисе могли быть публичными или приватными. При регистрации у пользователя, кроме стандартных логина и пароля, запрашиваются данные о его умениях. Публикуя объявление, можно сделать его доступным только для части пользователей, сделав ограничение по наличию некоторых умений. Объявления (в том числе приватные) доступны по секретной ссылке без авторизации при наличии соответствующих умений, если они заданы в объявлении. Публичные объявления рассылаются всем WebSocket-клиентам, приватные — только себе.
Уязвимости
Первая уязвимость достаточно простая и заключается в особенности реализации функции ipairs
в Lua — она производит итерацию по элементам в коллекции до первого nil
. Если же в списке умений пользователя при регистрации передать null в качестве одного из умений, метод json.decode конвертирует его в nil, и это прервет цикл проверки. В таком случае пользователь сможет прочитать любые объявления с известным идентификатором (вот готовый эксплоит).
Вторая уязвимость кроется в предсказуемости секретных идентификаторов объявлений. При создании объявления его идентификатор генерируется из имени пользователя и псевдослучайного числа rnd:
local rnd = rand(state)
local secret = rnd .. '@' .. username
local postname = md5.sumhexa(secret)
Для генерации rnd использовался слабый генератор псевдослучайных чисел, по сгенерированному значению которого можно узнать текущее состояние. Более того, из 64 бит внутреннего состояния генератора использовались только 32 бита. Таким образом, эксплуатация уязвимости могла выглядеть так:
- Слушая WebSocket, дождаться появления публичного объявления.
- Зная автора объявления, брутфорсом MD5 вычислить случайное значение (например, используя утилиту hashcat), время перебора должно быть не очень большим.
- Вычислить внутреннее состояние генератора псевдослучайных чисел.
- Получить несколько предыдущих и последующих идентификаторов объявлений.
- Запросить объявления по вычисленным идентификаторам и получить флаг!
Защита
- Правильно обрабатывать null-значения в JSON.
- Использовать криптографически стойкий генератор случайных чисел.
Crash
Описание
Crash — сервис для приема, хранения и отображения отчетов о сбоях и падениях произвольных сервисов, основанный на Google Breakpad. На стороне проверяющей системы был бинарный исполняемый файл, слинкованный с Breakpad. Когда проверяющей системе нужно было отправить флаг одной из команд, она запускала этот бинарник, флаг передавался аргументом командной строки. Бинарник падал с SIGSEGV и генерировал мини-дамп. Первая половина флага зашивалась в стек вызовов, а другая сохранялась на стеке.
Стек вызовов:
0|0|submarine_internal|_4()|submarine_internal.cpp|128|0xb6
0|1|submarine_internal|_4()|submarine_internal.cpp|128|0x21
0|2|submarine_internal|_3()|submarine_internal.cpp|127|0x21
0|3|submarine_internal|_D()|submarine_internal.cpp|137|0x21
0|4|submarine_internal|_6()|submarine_internal.cpp|130|0x21
0|5|submarine_internal|_5()|submarine_internal.cpp|129|0x21
0|6|submarine_internal|_2()|submarine_internal.cpp|126|0x21
0|7|submarine_internal|_8()|submarine_internal.cpp|132|0x21
0|8|submarine_internal|_3()|submarine_internal.cpp|127|0x21
0|9|submarine_internal|_B()|submarine_internal.cpp|135|0x21
0|10|submarine_internal|_C()|submarine_internal.cpp|136|0x21
0|11|submarine_internal|_B()|submarine_internal.cpp|135|0x21
0|12|submarine_internal|_8()|submarine_internal.cpp|132|0x21
0|13|submarine_internal|_B()|submarine_internal.cpp|135|0x21
0|14|submarine_internal|_3()|submarine_internal.cpp|127|0x21
0|15|submarine_internal|_B()|submarine_internal.cpp|135|0x21
0|16|submarine_internal|StartFlagProcessing()|submarine_internal.cpp|191|0x21
0|17|submarine_internal|main|submarine_internal.cpp|211|0x5
0|18|libc-2.23.so||||0x20830
0|19|submarine_internal|Execute()|submarine_internal.cpp|199|0x3
Stackframe последней функции в стеке вызовов (сигнатура репорта) — _4()
:
0 submarine_internal!_4() [submarine_internal.cpp : 128 + 0xb6]
rax = 0x0000000000000000 rdx = 0x0000000000000020
rcx = 0x00007f12780480c0 rbx = 0x0000000000000000
rsi = 0x00007ffc3fc5709d rdi = 0x00007ffc3fc5707d
rbp = 0x00007ffc3fc551a0 rsp = 0x00007ffc3fc55070
r8 = 0x0000000000000010 r9 = 0x0000000000000000
r10 = 0x000000000000034e r11 = 0x00007f12781141a0
r12 = 0x0000000000401aa0 r13 = 0x00007ffc3fc56820
r14 = 0x0000000000000000 r15 = 0x0000000000000000
rip = 0x000000000040259f
Found by: given as instruction pointer in context
Stack contents:
00007ffc3fc55070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00007ffc3fc55080 00 00 00 00 1e 00 00 00 00 00 00 00 00 00 00 00 ................
00007ffc3fc55090 48 45 52 45 20 49 53 20 54 48 45 20 52 45 53 54 HERE IS THE REST
00007ffc3fc550a0 20 4f 46 20 59 4f 55 52 20 46 4c 41 47 3d 38 31 OF YOUR FLAG=81
00007ffc3fc550b0 32 30 31 31 31 35 36 36 42 30 37 33 30 3d 00 00 20111566B0730=..
00007ffc3fc550c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00007ffc3fc550d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00007ffc3fc550e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00007ffc3fc550f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00007ffc3fc55100 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00007ffc3fc55110 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00007ffc3fc55120 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00007ffc3fc55130 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00007ffc3fc55140 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00007ffc3fc55150 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00007ffc3fc55160 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00007ffc3fc55170 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00007ffc3fc55180 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00007ffc3fc55190 00 00 00 00 00 00 00 00 00 e9 5c 43 2b 89 59 26 ..........C+.Y&
00007ffc3fc551a0 e0 52 c5 3f fc 7f 00 00 5e 26 40 00 00 00 00 00 .R.?....^&@.....
Флаг — B3B8BCB38256D3448120111566B0730=
.
Мини-дамп пакуется в ZIP и сохраняется в сервисы команд по секретному идентификатору ID. ID и архив с мини-дампом — это отчет. Сервис принимает их, хранит и отображает на странице. При получении отчета сервис создает папку reports/<ID>
, в которую распаковывает архив и там же генерирует файл .files_list
, в который записывает имена файлов в этом архиве. Также в базу данных запишется ID, имя исполняемого файла, время отчета, сигнатура и IP хоста, с которого пришел отчет.
Уязвимость 1
Первая уязвимость связана с обработкой архива и файла .files_list
. В архиве имя файла может содержать точку (.) и слеш (/). Можно добиться того, чтобы сервис сгенерировал .files_list
для отчета следующим образом:
434e-6a63-60e8.dmp
../../reports.db
Собрать соответствующий ZIP на питоне можно так:
zf = zipfile.ZipFile("434e-6a63-60e8.zip", mode="w")
zf.write("434e-6a63-60e8.dmp", arcname="434e-6a63-60e8.dmp")
zf.write("dummy.file", "../../reports.db")
Остается лишь отправить этот отчет сервису. ID 434e-6a63-60e8
взят для примера, это может быть любой валидный ID, важно только, чтобы у сервиса еще не было отчета с таким идентификатором. Файл мини-дампа обязан быть валидным, а dummy.file
может быть произвольным.
При распаковке архива сервис не перезаписывает существующие файлы, то есть файл reports.db
останется нетронутым. «Отравленный» таким образом репорт достаточно отправить один раз. После этого остается скачивать у сервиса этот отчет каждый раунд, делая запрос http://teamX:1080/434e-6a63-60e8/get
. Сервис на этот запрос прочитает файл reports/434e-6a63-60e8/.files_list
и соберет ZIP из следующих файлов:
reports/434e-6a63-60e8/434e-6a63-60e8.dmp
reports/434e-6a63-60e8/../../reports.db
Получив таким образом базу reports.db, можно вытащить из нее идентификаторы всех отчетов, запросить их и достать флаги из мини-дампов.
Уязвимость 2
Вторая уязвимость — это SQL-инъекция. В коде сервиса есть строка
cursor.execute("INSERT INTO reports VALUES ('%s', '%s', '%s', '%s', '%s')"
% (guid, service_name, strftime("%H:%M:%S", gmtime()), parser.signature, ip))
Спецсимволы не экранируются, поэтому в parser.signature можно вставить подзапрос. Чтобы это сделать, надо написать небольшую программу на C++:
#include "client/linux/handler/exception_handler.h"
#include <stdio.h>
static bool dumpCallback(const google_breakpad::MinidumpDescriptor& descriptor, void* context, bool succeeded) {
printf("%s", descriptor.path());
fflush(stdout);
return succeeded;
}
int main(int argc, char* argv[]) {
google_breakpad::MinidumpDescriptor descriptor("./");
google_breakpad::ExceptionHandler eh(descriptor, NULL, dumpCallback, NULL, true, -1);
volatile int* ptr = 0;
*ptr = 0;
return 0;
}
После этого собираем бинарник:
g++ -I$BREAKPAD/src -std=c++11 just_crash.cpp $BREAKPAD/src/client/linux/libbreakpad_client.a -pthread -g -O0 -o just_crash
и генерируем символы:
ruslan@ubuntu:~$ dump_syms -i just_crash
MODULE Linux x86_64 8A0D3C1EADB4865FDC91DA786A0E07640 just_crash
INFO CODE_ID 1E3C0D8AB4AD5F86DC91DA786A0E07648011AC9E
ruslan@ubuntu:~$ dumps_sys just_crash > symbols/just_crash/8A0D3C1EADB4865FDC91DA786A0E07640/just_crash.sym
Файл символов just_crash.sym имеет текстовый формат, так что его легко модифицировать. Так как наша программа падает в main()
, сигнатура у отчета будет main
. Откроем файл just_crash.sym
и заменим все слова main
на main',(SELECT guid from reports))--
. Зальем с помощью первой уязвимости файл символов по пути (сервис любезно создаст все нужные папки)
../../symbols/just_crash/8A0D3C1EADB4865FDC91DA786A0E07640/just_crash.sym
Остается вести себя, как проверяющая система: запускать каждый раунд just_crash
, паковать сгенерированный мини-дамп и отправлять сервису. Если зайти на страницу отчета, то в поле Remote IP
мы увидим результат инъекции. Можно использовать имя submarine_internal, чтобы мимикрировать под проверяющую систему. Более того, когда первую уязвимость закроют, эта останется работоспособной, пока ее не пофиксят.
AtlaBlog
Описание
AtlaBlog — атлантийский блог для научного сообщества (кодовое название Cross Social Science). Написан на языке Python с использованием веб-фреймворка Sanic. Регистрация в блоге открытая, публиковать посты и писать комментарии может любой зарегистрированный пользователь, модерация отсутствует. К комментариям и постам можно прикреплять произвольные документы.
Уязвимость
При прикреплении файла к посту или комментарию для него генерировалось случайное имя — UUID, при этом расширение оставалось нетронутым. При выводе полного имени файла <UUID>.*
расширение не экранировалось, то есть блог был подвержен хранимой XSS-атаке на пользователя. Самой сложной задачей для участников было понять, что XSS позволял получать флаги. Обычно на attack-defence CTF чекеры не исполняют JavaScript из-за трудностей развертывания соответствующей инфраструктуры. Представь: участвуют сотни команд, и для каждой команды нужно раз в минуту запускать полноценный браузер, причем хорошо изолированный — иначе можно ждать атак на проверяющую систему.
Обнаружить, что в качестве чекера выступает полноценный браузер, можно было по регулярным запросам к статике (JS, CSS). Дополнительную сложность составляло наличие в ответе сервера HTTP-заголовка Content-Security-Policy: default-src 'self' 'unsafe-inline'
. Из-за этого не получалось публиковать флаги на внешние ресурсы, и нужно было сформировать запрос от имени пользователя и сохранять его приватные данные в существующий пост.
Чекер
Чекер написан на языке Python, с использованием модуля Selenium (драйвер — PhantomJS). Сначала чекер создавал нового пользователя и помещал флаг в приватное поле с почтовым адресом. Затем публиковал пост, проверял работоспособность поллинга новых постов и прикреплял произвольный файл к уже созданному посту.
Спустя некоторое время чекер входил в сервис от имени зарегистрированного на предыдущем этапе пользователя и, используя PhantomJS, возвращался на страницу со своим постом, чтобы проверить наличие приватных полей.
Эксплоит
Для эксплуатации уязвимости нужно было дождаться появления поста, после чего добавить подготовленный файл со скриптом в расширении (в скрипте из-за этого не должно быть ни одной точки). Скрипт мог сохранять логин пользователя в комментарий к некоторому уже известному посту. Теперь оставалось только забрать из известного поста флаг. Дополнительно можно было шифровать флаг открытым ключом перед публикацией комментария, чтобы его не перехватила другая команда.
Патч
Кодировать расширение прикрепляемых файлов html.escape(extension)
.
Sapmarine
Описание
Сервис поиска попутчиков в мире Атлантиды. Написан на языке Swift 3.
Пользователи регистрируются в сервисе, затем некоторые из них публикуют заявки с описанием маршрута и пожеланиями. Другие пользователи смотрят эти заявки, а после поездки могут оценить пассажиров. Флаги хранятся в профилях пользователей, доступ к которым есть только у самих пользователей (проверка идет по логину).
Уязвимости
Первая уязвимость — это JSON-инъекция. Сервис регулярно сохраняет свое полное состояние в файлы в виде набора строк JSON. Поля этих строк содержат информацию о пользователях, причем данные не фильтруются и не экранируются. Соответственно, возможно сделать инъекцию и, например, подменить поле с паролем зарегистрированного пользователя. При перезапуске сервис считывает последнее сохраненное состояние с диска и заполняет структуры данных в памяти. Хоть атакующий и не в силах заставить владельца перезапустить сервис, тот может это сделать сам, например чтобы применить фикс найденной уязвимости.
Вторая уязвимость — path traversal в веб-фреймворке. Она позволяет получить файлы состояния, лежащие каталогом выше, в которых хранятся в том числе профили пользователей с флагами.
Третья уязвимость — логическая, она заложена в структуре хранения профилей пользователей. Использование красно-черного дерева не обеспечивает уникальность пользовательских логинов: после значительного изменения рейтинга пользователя поиск в дереве не находит существующий объект. В результате появляется возможность зарегистрировать одноименного пользователя и просматривать профиль жертвы.
Weather
Описание
Вот как выглядит главная страница сервиса Weather, которая показывает прогноз погоды в Атлантиде:
Прогноз делается на основе данных, которые присылает проверяющая система на специальный TCP-порт 16761. Каждое сообщение состоит из двенадцатисимвольного ключа и значения длиной 32 символа. Каждая команда могла подключиться к сервису любой другой команды и положить туда свои данные. Через этот же порт данные можно было получить обратно, зная ключ. На том же порте можно было получить текстовую версию главной страницы.
В текстовой версии видно поле «Signature: » (в веб-версии оно тоже есть, но на скриншоте его не видно). Это магическое значение, которое генерируется на основе текста запроса и проверяется чекером, чтобы сервис нельзя было заменить на произвольное хранилище key-value.
Уязвимости
Сервис состоит из единственного маленького 32-битного ELF’а weather
, скомпилированного с PIE (position-independent executable). Все уязвимости в нем эксплуатируются несложно и хорошо известными методами, чтобы даже неопытные реверсеры могли почувствовать свою силу и стащить несколько флагов.
Первая точка входа для атакующего — переполнение буфера в запросе к текстовой версии прогноза.
Буфер для запроса находится в секции неинициализированных данных прямо перед другим буфером, где лежит шаблон текстовой версии страницы. Этот шаблон потом заполняется данными с помощью sprintf
. Нужно лишь передать достаточно длинный запрос: первые 1024 байта заполняют буфер, а остаток перетирает шаблон страницы. Это классическая уязвимость форматной строки.
Первым делом понадобится узнать адреса: бинарник является PIE, поэтому загружается по случайному базовому адресу. Адрес стека тоже рандомизирован.
Послав запрос с достаточным количеством %p
, попадающих в шаблон, можно получить как адрес образа, так и адрес стека. Рассмотрим, как получить адрес, по которому загрузился бинарник.
В обычной ситуации шаблон заполняется значениями температуры и строками с описанием погоды. Таким образом, второй аргумент sprintf
и заодно второй int32 на стеке — это сегодняшняя погода. Перезаписав шаблон строкой вида %p %p
, мы узнаем указатель на одну из строк 'clear', 'rain' и так далее. Посмотрев, как эти строки расположены в секции .rodata
, можно заметить, что их адреса отличаются только последним байтом.
Рандомизация базового адреса это не меняет, так что можно узнать адрес строки 'clear', просто заменив последний байт адреса, который мы получили, на 0x60
. Теперь у нас есть точка отсчета, от которой можно вычислить адрес чего угодно в образе бинарника. Это понадобится для эксплуатации всех уязвимостей.
Получаем шесть последних флагов
Пореверсив бинарник, можно заметить, что прогноз погоды строится на основе последних шести флагов, которые для этой цели отложены в отдельное место (назовем его lastValues
) в секции данных.
Сами строки не дублируются, там лежат лишь указатели на них.
Теперь надо научиться читать данные по произвольному адресу. Заметим, что первые 4 байта запроса оказываются на стеке. Чтобы добраться до этого места, надо сдвинуться на 57 4 байта по стеку от вызова sprintf
. Осталось только построить запрос, который будет читать значение по произвольному указателю с помощью %s
: `
Итак, чтобы прочитать шесть последних флагов, нужно:
- Посчитать адрес
lastValues
в адресном пространстве жертвы, используя полученную ранее точку отсчета. - Прочесть значения указателей по адресам
lastValues
,lastValues + 4 .. lastValues + 20
. - Прочесть строки по этим указателям, это и будут флаги.
Получаем все флаги
Пореверсив еще немного, можно понять, что флаги хранятся в префиксном дереве, построенном по ключу. С помощью техники из предыдущего пункта можно обойти все дерево.
Каждый узел в дереве — массив из 26 указателей (в ключе можно использовать только строчные буквы от a до z). В обычных узлах указатели ссылаются на следующие узлы, а в последних узлах — на сами строки с данными.
Зная адрес корня дерева, который можно легко посчитать от точки отсчета, будем просто читать указатели на все дочерние узлы и так далее, а на глубине 12 по указателям окажутся флаги. Работающая реализация есть в эксплоите в репозитории.
Выполняем произвольные команды в шелле
Ты мог заметить, что в бинарнике импортирована функция system
. Есть несколько способов ей воспользоваться. Рассмотрим один из них, использующий уязвимость в функции подсчета подписи.
Получив запрос к любой из версий главной страницы, сервис вытаскивает из запроса все от символа /
до ближайшего пробела и передает функции, которая считает подпись. Код этой функции выглядит примерно так:
uint64 wt_sign(const char *data, int32 length)
{
int32 samples[8];
memset(samples, 0, sizeof(samples));
int32 dataLength = length;
int32 i = 0;
while (i < dataLength && i < sizeof(samples))
{
if (data[i] < 16 || !samples[i])
samples[i] = data[i];
i++;
}
while (i < dataLength)
{
if (data[i] < 16 || !samples[i % sizeof(samples)])
{
if (data[i] < 0)
samples[i % sizeof(samples)] -= data[i];
else
samples[i % sizeof(samples)] *= data[i];
}
i++;
}
...
}
Здесь есть ошибка, которая позволяет перезаписать 24 лишних значения int32 на стеке. Но перезаписать не произвольными данными, а путем прибавления и умножения на маленькие числа (байты строки запроса). Глядя на код, ты можешь заметить, что байты, меньшие 16 (знаково!), обрабатываются особым образом: значения на стеке умножаются на положительные байты и складываются с абсолютными значениями отрицательных байтов.
Идея в том, чтобы подменить адрес возврата из wt_sign
на адрес функции system
и подменить значение по адресу <return address location> + 8
, которое будет аргументом функции system
, на адрес буфера с запросом. Туда можно написать любые команды, которые захочется выполнить.
Чтобы проделать это, придется разбить адреса на последовательности сложений и умножений маленьких чисел, а потом составить из них длинную строку запроса, которая, переданная функции wt_sign
, подготовит нужные значения на стеке. Работающая реализация есть в эксплоите.
RuCTFE на «Хакере»
Часть тасков перед соревнованиями RuCTFE размещалась на сайте «Хакера». По легенде, чтобы дешифровать флаги, исследователю нужно было найти пять параметров для шифровальной машины «Энигма». Возможно, ты помнишь, что в какой-то период на сайте вверху появилась плашка, при клике на которую тебя перебрасывало на статью «Погружение в крипту. Часть 1: как работают самые известные шифры в истории?». Именно в этой статье мы их и спрятали. Поскольку это была «затравка» к основному CTF-таску, мы не ставили целью проэксплуатировать реальные уязвимости: задачи были на смекалку и внимательность исследователя.
Как получить доступ к заданиям
Чтобы активировать таски, в URL статьи нужно было дописать GET-параметр /?ctf=
. Значение параметра и регистр неважны. По сути, любой параметр, содержавший в себе подстроку ctf
, активировал таски: например, ['/?RuCTF=', '/?Ctf=', '/?RUctfE=']
. Итоговый URL для активации выглядел как https://xakep.ru/2015/12/24/crypto-part1/?RuCTF=
.
Чтобы до этого догадаться, нужно было заглянуть в исходный код плашки со ссылкой, которая висела все это время на сайте: она содержала data-атрибут data-hint='/?RuCTFE'
. Дополнительно в Твиттере запостили подсказку: Hint0: Add ?RuCTFE=2016 (could be obtained from data-hint attribute of welcome banner).
Флаг № 1: в таблице шифра Цезаря
- Оригинальный флаг:
{'Type':'M3'}
- Base64:
eydUeXBlJzonTTMnfQ==
- Цезарь (сдвиг +3):
hbgXhAEoMcrqWWPqiT
В статье в разделе про шифр Цезаря есть такой текст:
...Это количество позиций называется ключом. При ключе, равном трем, этот метод называется шифром Цезаря. Император использовал его для секретной переписки. Для того чтобы зашифровать сообщение, нужно построить таблицу подстановок.
При добавлении GET-параметра к URL статьи появлялось дополнительное предложение:
Император использовал его для секретной переписки. >>Выглядел он примерно так: hbgXhAEoMcrqWWPqiT.<< Для того чтобы зашифровать сообщение, нужно построить таблицу подстановок.
Кусочек hbgXhAEoMcrqWWPqiT
явно выглядел подозрительно. Ну а раз он в абзаце про шифр Цезаря, попробуем сдвинуть обратно на три позиции этот шифртекст. Получаем eydUeXBlJzonTTMnfQ==
. Несложно догадаться, что это Base64. Декодируем любым онлайн-декодером (я использовал первый попавшийся) и получаем {'Type':'M3'}
. Это и есть первый параметр «Энигмы»!
Флаг № 2: в response-хедерах
- Оригинальный флаг:
{'Umkehrwalze':'C'}
- Base64:
eydVbWtlaHJ3YWx6ZSc6J0MnfQ==
- inverse:
QfnM0J6cSZ6xWY3JHaltWbVdye
В «режиме CTF» сервер отдавал один необычный заголовок: X-Ructf. Значением его была строка QfnM0J6cSZ6xWY3JHaltWbVdye. Достаточно было просто перевернуть строку в обратном порядке, чтобы получить Base64 (последние ==
некритичны). Ну а дальше опять вставляем в Base64-декодер и получаем {'Umkehrwalze':'C'}
.
Мы получили второй параметр!
Флаг № 3: в изображении
- Оригинальный флаг:
{'Walzenlage':'III-I-II'}
- Base64:
eydXYWx6ZW5sYWdlJzonSUlJLUktSUknfQ==
- inverse:
QfnkUStkULJlUSnozJldWYs5WZ6xWYXdye
В третьем задании на страницу подгружалась картинка ructf.gif
, на которой написано QfnkUStkULJlUSnozJldWYs5WZ6xWYXdye
. Картинка была невидима на странице, так как для нее были прописаны CSS-стили:
{
position: absolute;
topleft: -over9000;
}
Узнать о ней (и не только о ней) можно было очень легко:
curl URL | grep 'ctf'
Но тут все не так просто. При открытии картинки в браузере исследователя ждала белая страница. Дело в том, что код был написан белым шрифтом на прозрачном фоне. Чтобы увидеть скрытый текст, можно было подменить цвет фона страницы в Chrome Developer Tools, скажем, на черный:
body{background-color: #000;}
Ну а дальше опять реверс, Base64, и готово: {'Walzenlage':'III-I-II'}
. Мы получили третий параметр!
Флаг № 4: GIF-картинка со спрятанным внутри ZIP-архивом
- Оригинальный флаг:
{'Ringstellung':'E F T'}
- Base64:
eydSaW5nc3RlbGx1bmcnOidFIEYgVCd9
- inverse:
9dCVgYEIFdiOncmb1xGblR3cn5WaSdye
В статье была фотография той самой «Энигмы». Разумеется, мы не могли обойти ее стороной и воспользовались известным трюком, с помощью которого можно спрятать данные в изображении.
При обычном открытии статьи Wget’ом или curl’ом отдавалась настоящая картинка. Но на сайте «Хакера» мы используем lazy load, «ленивую» подгрузку картинок, чтобы сократить время загрузки. Так вот, если открыть страницу в браузере, вместо https://xakep.ru/wp-content/uploads/2015/12/1450870121_509b_enigma.png
скриптами подгружалась другая GIF-картинка, на вид точь-в-точь как первая.
INFO
Та самая картинка с секретом до сих пор доступна на сайте. Ты можешь скачать и исследовать ее по этой ссылке.
Трюк состоял в следующем: для GIF-изображения неважно, что находится в конце файла, главное, чтобы основной набор данных изображения в файле был целый. А для ZIP-архивов все равно, что стоит в начале, архиватор просто пропустит непонятные «мусорные» данные и начнет разархивацию с первых «знакомых» байтов. Именно поэтому можно было просто склеить GIF-картинку и ZIP-файл в одно целое, и получившийся файл был одновременно и валидной гифкой, и рабочим ZIP-архивом.
Просто скачиваем картинку и натравливаем на нее unzip
.
В архиве был файл data.txt
, в нем и содержался искомый четвертый параметр 9dCVgYEIFdiOncmb1xGblR3cn5WaSdye
(после дешифровки {'Ringstellung':'E F T'}
).
Флаг № 5: в JSFuck
- Оригинальный флаг:
{'Grungstellung':'C U R'}
- Base64:
eydHcnVuZ3N0ZWxsdW5nJzonQyBVIFInfQ==
- inverse:
QfnIFIVByQnozJn5WdsxWZ0N3ZuVncHdye
В исходном коде страницы статьи на xakep.ru в конце текста можно было увидеть странный <div>
c JSFuck-кодом (разумеется, c display:none
).
Если пробовать просто выполнить его в консоли браузера, вылезала ошибка. Сам JSFuck-код был ко всему прочему запакован вот этим популярным упаковщиком. Код, запакованный этим или похожими упаковщиками, в начале имеет приметную сигнатуру:
eval(function(p,a,c,k,e,d){e=function(c){return c};...
Как видишь, она «сломана». Примерная function
заменена на явное fixme
. Достаточно было просто заменить fixme
на function
обратно и получившийся фарш вставить в консоль «Хрома». На выходе после знакомых преобразований получаем пятый, последний параметр нашей «Энигмы»: {'Grungstellung':'C U R'}
.
Редакция «Хакера» выражает благодарность команде HackerDom и организаторам RuCTFE за возможность принять участие в проведении RuCTFE, отличные таски и детальный райтап. Это было здорово :).