воскресенье, 6 ноября 2016 г.

ZeroNights HackQuest Task 6: PACKER

Продолжаю участвовать в конкурсе ZeroNights HackQuest 2016 (см. предыдущую статью).
На этот раз попалось задание, почти никак не связанное с реверсом.

Задача:

Day 6 / Packer


Hey, there is a person in Moscow who keeps his important password on his Android phone. Sounds weird, we know.
It turns out that he is using some program for managing his data called Packer
We performed MITM attack on his phone during application-to-server synchronization and got database file named `packs.db`. But it looks like important data are kept encrypted there. So get your task: decrypt the data and give us the password!

Подсказки:
06/11/2016 13:21
To solve this task it's necessary to view ENTIRE of database and manifest. Also there is a hint in the task.
06/11/2016 14:38
You should to read more about security features of Android devices and especially about implementation of certain protocols.
06/11/2016 17:11
Keygen class gives different results on the phone and on the computer. The victim ran this code on the phone with settings from build.prop
06/11/2016 18:00
Keygen.java
06/11/2016 19:11
Provider: Crypto, Android version: 4.0.3
06/11/2016 19:40
These files can help you?

Первые ощущения от задания:

Когда задание только было выложено, я глянул на него немного, посмотрел, какие данные хранятся в базе, декомпильнул APK'шку и .class-файлы онлайн-декомпилятором. И понял, что я совсем не мобильный разработчик и даже пытаться решать это не буду... Лучше отдохну после решения 5-ого задания. Но на следующий день стало понятно, что задание так никто и не решил, а организаторы выложили уже целую кучу подсказок. И я всё же решил попробовать свои силы.

Решение: [Шаг 1 | Шаг 2 | Шаг 3 | Шаг 4 | Шаг 5]

Шаг 1. Исследуем базу packs.db:
cmd> sqlite3 packs.db

// Смотрим список таблиц
sqlite> .tables
android_metadata  packs

// Дампим содержимое таблицы 'android_metadata'
sqlite> .headers ON
sqlite> .mode line
sqlite> select * from android_metadata;
locale = en_US

// Дампим содержимое таблицы 'packs'
sqlite> select * from packs;
       pack_id = 2
     pack_name = /system/build.prop
    pack_value = # begin build properties
# autogenerated by buildinfo.sh
ro.build.id=IML74K
ro.build.display.id=crane_m97ft5x-eng 4.0.3 IML74K 20120327 test-keys
...
net.bt.name=Android
dalvik.vm.stack-trace-file=/data/anr/traces.txt

     pack_date = 2016.04.01
pack_encrypted = 0

       pack_id = 3
     pack_name = /mnt/sdcard/DCIM/bruteforce.jpg
    pack_value = <binary blob 3>
     pack_date = 2016.10.26
pack_encrypted = 0

       pack_id = 4
     pack_name = /mnt/sdcard/password.jpg
    pack_value = <binary blob 4>
     pack_date = 2016.10.27
pack_encrypted = 1

sqlite> ^C

// Дампим бинарные данные для строк pack_id=3 и pack_id=4
cmd> sqlite3 packs.db "select hex(pack_value) from packs where pack_id = 3" > pack_value.3.hex
cmd> sqlite3 packs.db "select hex(pack_value) from packs where pack_id = 4" > pack_value.4.hex

// И конвертируем их из шестнадцатеричного в бинарное представление
cmd> perl -pe "binmode(STDOUT);chomp;$_=pack('H*',$_);" <pack_value.3.hex >pack_value.3.bin
cmd> perl -pe "binmode(STDOUT);chomp;$_=pack('H*',$_);" <pack_value.4.hex >pack_value.4.bin

// Заглядываем внутрь файла pack_value.3.bin:
head -c 16 < pack_value.3.bin | hexdump
00000000: FF D8 FF E1 00 66 45 78 - 69 66 00 00 4D 4D 00 2A |     fExif  MM *|
// Это явно JPEG. Значит в поле 'pack_name' правильно записано, что это картинка (bruteforce.jpg)
ren pack_value.3.bin pack_value.3.jpg

// Заглядываем внутрь файла pack_value.4.bin:
head -c 64 < pack_value.4.bin | hexdump
00000000: DE 9E 2A 63 F9 8D FE 0B - 7F 7E 4B 13 19 D7 F5 4E |  *c     ~K    N|
00000010: C0 17 96 60 73 72 2F 07 - E8 6B F6 85 5A F5 DE 55 |   `sr/  k  Z  U|
00000020: B9 D6 52 F1 B0 3C A8 73 - CD 1A C3 6F 2C 7F 27 F6 |  R  < s   o, ' |
00000030: 1D C1 67 7B 1E C3 5E A3 - 5D 95 E3 47 05 9F C1 A0 |  g{  ^ ]  G    |
// Это явно не JPEG. А так как значение поля 'pack_encrypted' для этой записи равно '1', 
// то считаем, что это зашифрованный JPEG.

Содержимое файла 'bruteforce.jpg' как бы
очень тонко намекает на необходимость
использования метода грубой силы

Шаг 2. Чем это всё зашифровано?

Декомпилируем код (файлы .apk, .class) или берём исходники из подсказок и ищем по коду, кто работает с полем 'pack_encrypted'. Находится следующий код:
// ListModel.java
public class ListModel {
...
    public byte[] put(String file, byte[] data, boolean encrypted) {
        Date date = new Date();
        try(ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            ContentValues values = new ContentValues();
            values.put("pack_name", file);
            if (encrypted) {
                values.put("pack_encrypted", 1);
                try {
                    out.write((byte[]) Class
                            .forName(ListModel.class.getPackage().getName() + ".Keygen")
                            .getMethod("generateKey", long.class)
                            .invoke(null, date.getTime()));
                } catch (Exception e) {
                    throw new RuntimeException("Shouldn't happen! Encrypt with Premium version only!", e);
                }
            } else {
                values.put("pack_encrypted", 0);
            }
            values.put("pack_value", data);
            values.put("pack_date", new SimpleDateFormat("yyyy.MM.dd").format(date));
            db.insert("packs", null, values);
            listItems.add(file);
            return out.toByteArray();
        } catch (IOException io){
            return new byte[]{(byte)0xdef1, (byte)0x2a1a, (byte)0x5cfe, (byte)0xc054, (byte)0xbd4e, (byte)0x7e41, (byte)0x65c9, (byte)0x7699};
        }
    }
По этому коду понятно, что:
  • Ключ, которым шифруется файл, генерируется при добавлении записи в sqlite-базу
  • Ключ, которым шифруется файл, зависит от текущего времени, которое выдаёт функция date.getTime() с точностью до мс.
  • Дата добавления шифрованного файла в базу известна, так как она записывается вместе с зашифрованным файлом в поле 'pack_date'. В нашем случае это 2016.10.27
Далее смотрим код KeyGen.java:
public class Keygen {

    public static void main(String[] args) throws Exception {
        if (args.length < 2) {
            System.out.println("Usage: java -cp <jar> Keygen <input long number> <output key file>");
            System.exit(0);
        }
        write(get(args[1]), generateKey(Long.parseLong(args[0])));
    }

    public static byte[] generateKey(long input) throws Exception {
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        digest.update(ByteBuffer.allocate(8).putLong(input).array());
        Provider provider = Security.getProviders("SecureRandom.SHA1PRNG")[0];
        SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG", provider);
        secureRandom.setSeed(digest.digest());
        KeyGenerator keyGen = KeyGenerator.getInstance("AES");
        keyGen.init(128, secureRandom);
        SecretKey secretKey = keyGen.generateKey();
        return secretKey.getEncoded();
    }
}
Теперь становится понятно, что:
  • Ключ, которым шифруется файл, зависит ТОЛЬКО от того времени, когда файл добавляется в базу
  • Используемый алгоритм шифрования вероятнее всего: AES-128 ("вероятнее всего" - это потому, что точно это не узнать, так как кода шифрования в этой версии Packer'а нет. Шифровать умеет только в Premium-версии Packer'а, см. premium.html)
Шаг 3. Применяем грубую силу.

Итак, что мы имеем:
  • зашифрованный файл
  • знаем способ генерации ключа
  • предполагаем, какой алгоритм шифрования используется
  • с большой долей уверенности можем сказать, какие данные будут в первых 16-ти байтах расшифрованного файла (JPEG-заголовок [0xFF,0xD8,0xFF] и строка типа 'JFIF' или 'Exif')
Этого достаточно, чтобы написать свой брутфорсер:
public static void main(String[] args) throws Exception {
    Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("MM/dd/yyyy HH:mm");
    Date date = simpleDateFormat.parse("10/28/2016 00:00:00");
    long t = date.getTime();
    Date dateEnd = simpleDateFormat.parse("10/27/2016 00:00:00");
    long tEnd = dateEnd.getTime();
    // Первые 16 байт зашифрованного файла
    byte[] src_data = {
        (byte)0xDE,(byte)0x9E,(byte)0x2A,(byte)0x63,(byte)0xF9,(byte)0x8D,(byte)0xFE,(byte)0x0B,
        (byte)0x7F,(byte)0x7E,(byte)0x4B,(byte)0x13,(byte)0x19,(byte)0xD7,(byte)0xF5,(byte)0x4E};
    while(t >= tEnd)
    {
        t = t - 1;
        if((t % 100000) == 0) {
          System.out.format("Cur key %d\n", t);
        }
        byte[] key = generateKey(t);
        SecretKeySpec keyWrap = new SecretKeySpec(key, 0, 16, "AES");
        cipher.init(Cipher.DECRYPT_MODE, keyWrap);
        byte[] decryptedBytes = cipher.doFinal(src_data);
        // Проверка первых трёх байт расшифрованного файла
        if((decryptedBytes[0] & 0xFF) == 0xFF && 
           (decryptedBytes[1] & 0xFF) == 0xD8 && 
           (decryptedBytes[2] & 0xFF) == 0xFF)
        {
          System.out.format("Done %d\n", t);
          writeFile(decryptedBytes, String.format("data\\decrypted_%d", t));
        }
    }
}

Запускаем наш новенький брутфорсер на исполнение с помощью java, входящей в состав JDK (javac classes\TestClass.java; jar cfm test.jar MANIFEST.MF ./ ; java -jar test.jar).
Запустили и ждём пару часов, время от времени поглядывая, какие файлы data\decrypted_* создаются.
...
<прошло 2 часа>
...
WTF? Где же наш файл??? Почему среди 5 найденных файлов decrypted_* не нашлось ни одного JPEG'а???!!



Раскуриваем подсказки дальше...

Шаг 4. А причём тут Android 4.0.3?

В подсказках и в файле /system/build.prop из sqlite-базы встречалось упоминание Android 4.0.3. Но в нашем брутфорсере эта подсказка пока не используется, да и зачем она?
Ну ладно, откапываем на полке старый телефон с Android 4.0.4. И пробуем сгенерировать тестовый ключ на нём. Сравниваем этот тестовый ключ с аналогичным ключом, сгенерированным на PC и что мы видим: они отличаются! Как так?
Оказывается, реализация может различаться от платформы к платформе SHA1PRNG (или в Android 4.0 был некий баг, который влиял на то, какие байты генерирует этот рандомайзер).

Ну, теперь наш брутфорсер точно найдёт правильный ключ к JPEG'у.

Шаг 5. Брутфорсер под Android

Подготавливаем среду для разработки Android-приложений:
Дальше запускаем выбранную IDE, выбираем из шаблонов проект под Android и вставляем в него нашу логику брутфорсера.
(* на словах это одно предложение, а на деле для не мобильного разработчика на это уходит более часа)

OK, собрали APK-шку.
Заливаем её на древний телефон.
Запускаем...

Ждём. Ждём... Ждём.....

И тут выясняется, что скорость перебора на древнем телефоне такая, что за 10 часов реального времени пробрутится только 1 час из 24 часов, которые нужно перебрать.


Нет, так дело не пойдёт. Нужно брутфорсить на компе. И самым простым решением брутфорсить на компе для меня поставить виртуалку с Android 4.0.3

На виртуалке скорость перебора увеличилась, но была всё ещё раз в 10 медленнее перебора без использования Android'а (то есть для полного перебора одного дня требовалось около 20 часов). Тогда пришлось "распараллелить" брутфорсер:

10 виртуальных машин - не самый лучший способ
"распараллелить код", но всё же более простой, чем писать новый код :)
Итак, разбиваем время 24 часа на 10 виртуальных машин с брутфорсерами и каждая виртуалка перебирает по ~2.5 часа из 24 часов.

Теперь ждём.
Ждём...
<на этом месте конкурс заканчивается и решения более не принимаются>
Ждём ещё час......

И наконец мы получаем файл decrypted_1477601387613:
00000000: FF D8 FF E0 00 10 4A 46 - 49 46 00 01 01 01 00 48 |      JFIF     H|
Это то, что нам нужно!!! Ура!
Далее дело за малым - расшифровываем файл целиком и флаг наш.

decrypted_1477601387613.jpg
Итоги и выводы:
  • Чтобы успеть выиграть конкурс необходимо не лениться, а подумать об оптимальном алгоритме перебора заранее (или думать об оптимизации алгоритма, пока в фоне работает неоптимальный алгоритм).
  • Java-код под Android работает реально медленно. Почему так?! 
Полезные ссылки:
  • Исходники моего брутфорсера доступны на github