Skip to content

Latest commit

 

History

History

flagboard

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 

rev | FlagBoard

Information

Пробираясь по коридорам небоскрёба Арбалетов Сибири, вы пришли к укреплённой стальной двери.
Рядом с дверью - сенсорный экран. Очевидно, на нём нужно набрать секретный код.
Но почему он подсказывает, какую кнопку нужно нажать дальше?!

Making you way through the corridors of Arbalest of Siberia's skyscraper, you find yourself before a reinforced steel door.
Next to said door is a touchscreen. Apparently, a secret code needs to be typed on it.
But why on Earth is the touchscreen telling you what to click next?!

Deploy

Public

Provide zip archive

TL;DR

The app's code was minified, so brute force reversing is suboptimal. To find exactly where we need to look, search for usages of R.raw.lookforme, which is a text file in res/raw containing info about the levels.

Writeup (ru)

Посмотрим на приложениe. Задача ясна: нужно нажимать на кнопки, чтобы напечатать целиком флаг. Беда в том, что после девятого символа флага игра начинает нас троллить: на поле попросту нет кнопок маджентового цвета.

Пойдём разбираться в декомпиляторе. Нас встречает минифицированный код. Классы, их поля, методы, даже пакеты часто называются всего одной буквой.

Минификация байткода была включена для данного таска осознанно. В рамках формата казино участникам, конечно, не предлагалось досконально отреверсить приложение. Достаточно было найти важную зацепку: файл res/raw/lookforme. Содержимое файла такое:

y3docWvMedRmzRzpdiawuosZr6vH+8GyE4N1gjA4VdQ=
+jz6r8FG/Ji9+qGOZc5KaEsZwSCRcma4djoeB2fkXmY=
Oz8uCCD+z32KuaPW7NNSUrk9VT0t4rF6KRCNyZhvwvI=
fN5+VUyu+jHFeC8/rm2HLwD8yxX0aGUUlUpksCxs7rg=
mB/QtJBStMlUUq9ONF3ylK8YzyB9Jq+KdLgAq8+WMdM=
fTSFFVbAXC6illudKRyXHD8HBpnAQqvTD2vT3cZPd5o=
...

Вспомним, что для чтения ресурсов используются методы Android API, и что каждому raw ресурсу соответствует поле класса R.raw. Тогда тот самый файлик почти наверняка где-то читается через вызов Resources.openRawResource(R.raw.lookforme).

package ru.ctfcup.flagboard;

/* JADX INFO: This class is generated by JADX */
public final class R {
    ...
    public static final class raw {
        public static final int lookforme = 0x7f090000;
    }
    ...
}

Жмём x. Приходим в некий c3.a(Context). Немного почитав, понимаем, что именно в этом классе реализована почти вся логика игры.

Осталось понять, что каждая строка - Base64 от зашифрованной AES-ECB строки, где ключ - SHA-256 от уже введенных символов флага. А символы флага записаны по диагонали. Этот скрипт печатает флаг.

Теперь о более интересном решении, на мысль о котором меня навели действия участников на площадке. Многие использовали Frida, чтобы лучше понимать, что происходит в приложении. А мы воспользуемся Frida, чтобы осуществить то самое, заветное, решение ревёрса, почти ничего не читая™.

Собственно, достаточно предположить, что в классе c3.a реализована логика игры, в методе void a() реализована смена уровней, и при этом UI никак, кроме вызова a(), на состояние c3.a не влияет. Наивно попробуем вызвать метод на живом экземпляре класса - и о чудо, мы видим следующую букву флага. Пишем небольшой скрипт, запускаем, смотрим кино. Не забываем записывать символы флага на салфетке.

setTimeout(() => {
  let nextLevel = null;
  Java.perform(() => {
    Java.choose("c3.a", {
      onMatch: (instance) => {
        console.log(instance);
        var game = instance;
        nextLevel = () => {
          Java.scheduleOnMainThread(() => {
            game.a();
          });
        }
      },
      onComplete: () => {}
    });
  });

  setInterval(nextLevel, 2000);
}, 3000);

Writeup (en)

Let's try this app out. Our goal is clear: we need to click the required buttons, which spell out the flag. The game is obviously trolling us, though. Past level nine, there simply are no buttons with a magenta outline.

Okay, time do dig into the bytecode, which was minified: most names (of fields, methods, classes etc.) are just a single letter.

This was done intentionally, to make players look for clues, and not to spend their limited time per the Casino format on reversing the entire thing. The particular clue here is a raw resource file at res/raw/lookforme, which contains following:

y3docWvMedRmzRzpdiawuosZr6vH+8GyE4N1gjA4VdQ=
+jz6r8FG/Ji9+qGOZc5KaEsZwSCRcma4djoeB2fkXmY=
Oz8uCCD+z32KuaPW7NNSUrk9VT0t4rF6KRCNyZhvwvI=
fN5+VUyu+jHFeC8/rm2HLwD8yxX0aGUUlUpksCxs7rg=
mB/QtJBStMlUUq9ONF3ylK8YzyB9Jq+KdLgAq8+WMdM=
fTSFFVbAXC6illudKRyXHD8HBpnAQqvTD2vT3cZPd5o=
...

Let's remember how resources are read in Android. A file in res/raw has a static final int field in class R.raw associated with it. So a call like Resources.openRawResource(R.raw.lookforme) should be somewhere in the app's code.

package ru.ctfcup.flagboard;

/* JADX INFO: This class is generated by JADX */
public final class R {
    ...
    public static final class raw {
        public static final int lookforme = 0x7f090000;
    }
    ...
}

Press x to win. We find ourselves in c3.a, a class in which apparently, all of the game's logic is implemented.

Now we can deduce the format of lookforme: each line is a Base64-encoded AES-EBC ciphertext, with key equal to SHA-256 of flag characters pressed so far. The flag characters are placed on a diagonal. This script prints the flag.

This would be it for our writeup, but I noticed how onsite participants used Frida to better understand what's happening in the code. That prompted me to search for a holy grail of sorts: A Solution To Reversing Challenge Where You Don't Read The Code™.

When we first encounter c3.a, let's assume that a) this class implements the game logic, b) void a() implements changing of levels, c) UI code can only alter the state of c3.a by calling a(). It should then mean that we can call a() on a live instance using Frida. Sure enough, we see another letter of the flag appear. Write a short script:

setTimeout(() => {
  let nextLevel = null;
  Java.perform(() => {
    Java.choose("c3.a", {
      onMatch: (instance) => {
        console.log(instance);
        var game = instance;
        nextLevel = () => {
          Java.scheduleOnMainThread(() => {
            game.a();
          });
        }
      },
      onComplete: () => {}
    });
  });

  setInterval(nextLevel, 2000);
}, 3000);

Launch it, and enjoy the sight of the game completing itself. Oh, and don't forget to jot down the flag.

Flag

ctfcup{Gu4axf_Sbe-Cynl1at}