Всем доброго времени суток, сегодня, 10 марта закончился онлайн этап NeoQuest 2017. Пока жюри подводят итоги и рассылают пригласительные на финал, предлагаю ознакомиться с райтапом одного из заданий: Greenoid за который судя по таблице рейтинга, можно было получить до 85 очков.

Как обычно, задания будут доступны ещё некоторое время, кто не успел, можете теперь спокойно дорешать, или ознакомиться.

Начнём


Скачиваем файл NeoQuest.apk и после декомпиляции получаем листинг:

MainActivity.java
package com.neobit.neoquest;

import android.app.Activity;
import android.content.res.AssetManager;
import android.os.Bundle;
import android.telephony.TelephonyManager;
import android.util.Base64;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.TextView;
import dalvik.system.DexClassLoader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.Arrays;

public class MainActivity extends Activity implements OnClickListener {
    private Method f1373a;

    static {
        System.loadLibrary("neolib");  // Подгружается внешняя so библиотека
    }
    // Объявление функций из подгруженной либы
    public native byte[] decrypt(String str, byte[] bArr);

    public native int nativeCRC32sum(byte[] bArr);

    public void onClick(View view) {
        int i = 0;
        CharSequence charSequence = "";
        try {
            InputStream open = getAssets().open("cred");
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            byte[] bArr = new byte[1024];
            // Считываем содержимое файла cred
            while (true) {
                int read = open.read(bArr, 0, 1024);
                if (read == -1) {
                    break;
                }
                byteArrayOutputStream.write(bArr, 0, read);
            }
            byteArrayOutputStream.flush();
            byte[] toByteArray = byteArrayOutputStream.toByteArray();
            byteArrayOutputStream.close();
            open.close();
            while (i < 1024 && bArr[i] != (byte) 10) {
                i++;
            }
            // Разбиваем содержимое файла на строки
            String login = new String(toByteArray, 0, i - 1, "UTF-8");
            int i2 = i + 1;
            i = i2;
            while (i < toByteArray.length && bArr[i] != (byte) 10) {
                i++;
            }
            String key = new String(toByteArray, i2, (i - i2) - 1, "UTF-8");
            String comment = Base64.encodeToString(Arrays.copyOfRange(toByteArray, i + 1, toByteArray.length), 2);
            byteArrayOutputStream.close();
            // Высчитываем CRC32 для всего содержимого файла cred
            String crc32 = Integer.toHexString(nativeCRC32sum(toByteArray)).toUpperCase();
            // Отправляем данные на сервер
            charSequence = (String) this.f1373a.invoke(null, new Object[]{login, key, comment, crc32});
        } catch (Exception e) {
        }
        ((TextView) findViewById(2131492970)).setText(charSequence);
    }

    protected void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        setContentView(2130968601);
        AssetManager assets = getAssets();
        try {
            // Получаем текущий IMEI девайса
            String deviceId = ((TelephonyManager) getSystemService("phone")).getDeviceId();
            // Считываем содержимое файла 1.dex
            InputStream open = assets.open("1.dex");
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            byte[] bArr = new byte[1024];
            while (true) {
                int read = open.read(bArr);
                if (read != -1) {
                    byteArrayOutputStream.write(bArr, 0, read);
                } else {
                    // Расшифровываем 1.dex
                    byte[] decrypt = decrypt(deviceId, byteArrayOutputStream.toByteArray());
                    File file = new File(getCacheDir(), "1.dex");
                    file.delete();
                    FileOutputStream fileOutputStream = new FileOutputStream(file, false);
                    fileOutputStream.write(decrypt);
                    fileOutputStream.close();
                    // Грузим метод get из класса com.neobit.neoquest.Server
                    this.f1373a = new DexClassLoader(file.getAbsolutePath(), getDir("outdex", 0).getAbsolutePath(), null, getClassLoader()).loadClass("com.neobit.neoquest.Server").getMethod("get", new Class[]{String.class, String.class, String.class, String.class});
                    findViewById(2131492969).setOnClickListener(this);
                    return;
                }
            }
        } catch (Throwable th) {
            // В случае ошибки ругаемся на IMEI
            ((TextView) findViewById(2131492970)).setText("Phone IMEI is not correct");
        }
    }
}


Код был снабжён комментариями, поэтому объяснять его думаю не стоит. Переходим к следующему этапу.

Расшифровываем 1.dex


Для начала нужно распаковать APK файл:

$ apktool d NeoQuest.apk

Находим там несколько библиотек под разные архитектуры. Откроем одну из них в IDA. Код, который отвечает за расшифровывание выглядит так:



И Java обёртка для него:


Как видно, тут присутствует верный IMEI, дальше есть несколько вариантов:

  1. Можно пропатчить сам apk файл, заменив соответствующие строки в smali файле, таким образом, чтобы в decrypt отправлялся верный IMEI;
  2. Либо переписать это на другой язык и сделать всё вручную.

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

def getLbits(number):
    bits = '%08x' % number
    return int(bits[-2:], 16)

def setLbits(dst, src):
    bits = '%08x' % src
    bits = int(bits[-2:], 16)
    dst = '%08x' % dst
    return int('%s%02x' % (dst[:-2], bits), 16)

def decrypt(data, data_len, key, key_len):
    prekey = {}
    prekey2 = {}
    for i in range(0x100):
        prekey[i] = i
        prekey2[i] = ord(key[i % key_len])
    y = 0x0
    for i in range(0x100):
        rdi = prekey[i]
        key_len = setLbits(key_len, prekey[i] + prekey2[i] + y)
        y = key_len
        prekey[i] = getLbits(getLbits(prekey[key_len]) & 0xFF)
        prekey[key_len] = getLbits(rdi)
    result = []
    if data_len != 0x0:
        i = 0x0
        y = 0x0
        k = 0x0
        while i < data_len:
            k = (k + 0x1) & 0xFF
            rax = getLbits(prekey[k])
            y = (y + rax) & 0xFF
            prekey[k] = getLbits(prekey[y])
            prekey[y] = rax
            rax += prekey[k]
            result.append(data[i] ^ getLbits(prekey[getLbits(rax)]))
            i += 0x1
    return result

dex = open('1.dex', 'rb').read()
imei = '352612062282062'
result = decrypt(dex, len(dex), imei, len(imei))
outdex = open('out.dex', 'wb')
outdex.write(bytes(result))
outdex.close()

P.S. Код не идеален и его можно оптимизировать, но данный вариант на мой взгляд более нагляден.

После запуска, получаем расшифрованный файл out.dex, который в декомпилируется в следующий код:

Server.java
package com.neobit.neoquest;

import android.os.AsyncTask;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.concurrent.ExecutionException;

public class Server {
    private static final String address = "http://213.170.100.214/neoquest.php";

    /* renamed from: com.neobit.neoquest.Server.1 */
    static final class C00001 extends AsyncTask<Void, Void, String> {
        final /* synthetic */ String val$comment;
        final /* synthetic */ String val$crc32;
        final /* synthetic */ String val$keyWorld;
        final /* synthetic */ String val$login;

        C00001(String str, String str2, String str3, String str4) {
            this.val$login = str;
            this.val$keyWorld = str2;
            this.val$comment = str3;
            this.val$crc32 = str4;
        }

        protected String doInBackground(Void... voidArr) {
            try {
                HttpURLConnection httpURLConnection = (HttpURLConnection) new URL(Server.address).openConnection();
                httpURLConnection.setRequestMethod("POST");
                httpURLConnection.addRequestProperty("Content-Type", "application/json");
                DataOutputStream dataOutputStream = new DataOutputStream(httpURLConnection.getOutputStream());
                dataOutputStream.writeBytes(String.format("{\"login\":\"%s\",\"key_word\":\"%s\",\"comment\":\"%s\",\"crc32\":\"%s\"}", new Object[]{this.val$login, this.val$keyWorld, this.val$comment, this.val$crc32}));
                dataOutputStream.flush();
                dataOutputStream.close();
                InputStream inputStream = httpURLConnection.getInputStream();
                String access$000 = Server.isToString(inputStream);
                inputStream.close();
                httpURLConnection.disconnect();
                return access$000;
            } catch (Exception e) {
                e.printStackTrace();
                return "";
            }
        }
    }

    public static String get(String str, String str2, String str3, String str4) throws ExecutionException, InterruptedException {
        return (String) new C00001(str, str2, str3, str4).execute(new Void[0]).get();
    }

    private static String isToString(InputStream inputStream) throws IOException {
        BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        for (int read = bufferedInputStream.read(); read != -1; read = bufferedInputStream.read()) {
            byteArrayOutputStream.write((byte) read);
        }
        return byteArrayOutputStream.toString();
    }
}


Окей! Можно приступать к последней части задания.

Отправка данных на сервер


Ниже представлено содержимое файла cred:

cred
Admin
26892263f3d18dfabb665e2d2a680899b2577f0f4daa77287fadb3e4ae581ec1
NeoQuestNeoQuestNeoQuestNeoQuestNeoQuestNeoQuestNeoQuestNeoQuestNeoQuest


Сначала идёт логин, затем ключ и комментарий.

Если отправить это как есть, то получим сообщение о том, что логин уже занят.

Если изменить логин, то сервер ругается на не верную CRC32 подпись.

Если отправить оригинальную подпись и изменённые данные, то сервер сообщает о том, что подпись не соответствует.

Вот так в IDA выглядит алгоритм подсчёта контрольной суммы:



Исходя из вышесказанного, следует, что нужно отправить такие данные, которые будут соответствовать оригинальной подписи, но при этом с верным логином. Так как блок в CRC32 занимает всего 4 байта, а подпись высчитывается на основе всего содержимого файла cred то нужно просто сбрутить эти 4 байта:

#!/usr/bin/python3
from struct import pack

crc_tab = [
    0, 0x2BDDD04F, 0x57BBA09E, 0x7C6670D1, 0x0AF77413C, 0x84AA9173,
    0x0F8CCE1A2, 0x0D31131ED, 0x0F6DD1A53, 0x0DD00CA1C, 0x0A166BACD,
    0x8ABB6A82, 0x59AA5B6F, 0x72778B20, 0x0E11FBF1, 0x25CC2BBE, 0x4589AC8D,
    0x6E547CC2, 0x12320C13, 0x39EFDC5C, 0x0EAFEEDB1, 0x0C1233DFE, 0x0BD454D2F,
    0x96989D60, 0x0B354B6DE, 0x98896691, 0x0E4EF1640, 0x0CF32C60F, 0x1C23F7E2,
    0x37FE27AD, 0x4B98577C, 0x60458733, 0x8B13591A, 0x0A0CE8955, 0x0DCA8F984,
    0x0F77529CB, 0x24641826, 0x0FB9C869, 0x73DFB8B8, 0x580268F7, 0x7DCE4349,
    0x56139306, 0x2A75E3D7, 0x1A83398, 0x0D2B90275, 0x0F964D23A, 0x8502A2EB,
    0x0AEDF72A4, 0x0CE9AF597, 0x0E54725D8, 0x99215509, 0x0B2FC8546, 0x61EDB4AB,
    0x4A3064E4, 0x36561435, 0x1D8BC47A, 0x3847EFC4, 0x139A3F8B, 0x6FFC4F5A,
    0x44219F15, 0x9730AEF8, 0x0BCED7EB7, 0x0C08B0E66, 0x0EB56DE29, 0x0BE152A1F,
    0x95C8FA50, 0x0E9AE8A81, 0x0C2735ACE, 0x11626B23, 0x3ABFBB6C, 0x46D9CBBD,
    0x6D041BF2, 0x48C8304C, 0x6315E003, 0x1F7390D2, 0x34AE409D, 0x0E7BF7170,
    0x0CC62A13F, 0x0B004D1EE, 0x9BD901A1, 0x0FB9C8692, 0x0D04156DD, 0x0AC27260C,
    0x87FAF643, 0x54EBC7AE, 0x7F3617E1, 0x3506730, 0x288DB77F, 0x0D419CC1,
    0x269C4C8E, 0x5AFA3C5F, 0x7127EC10, 0x0A236DDFD, 0x89EB0DB2, 0x0F58D7D63,
    0x0DE50AD2C, 0x35067305, 0x1EDBA34A, 0x62BDD39B, 0x496003D4, 0x9A713239,
    0x0B1ACE276, 0x0CDCA92A7, 0x0E61742E8, 0x0C3DB6956, 0x0E806B919, 0x9460C9C8,
    0x0BFBD1987, 0x6CAC286A, 0x4771F825, 0x3B1788F4, 0x10CA58BB, 0x708FDF88,
    0x5B520FC7, 0x27347F16, 0x0CE9AF59, 0x0DFF89EB4, 0x0F4254EFB, 0x88433E2A,
    0x0A39EEE65, 0x8652C5DB, 0x0AD8F1594, 0x0D1E96545, 0x0FA34B50A, 0x292584E7,
    0x2F854A8, 0x7E9E2479, 0x5543F436, 0x0D419CC15, 0x0FFC41C5A, 0x83A26C8B,
    0x0A87FBCC4, 0x7B6E8D29, 0x50B35D66, 0x2CD52DB7, 0x708FDF8, 0x22C4D646,
    0x9190609, 0x757F76D8, 0x5EA2A697, 0x8DB3977A, 0x0A66E4735, 0x0DA0837E4,
    0x0F1D5E7AB, 0x91906098, 0x0BA4DB0D7, 0x0C62BC006, 0x0EDF61049, 0x3EE721A4,
    0x153AF1EB, 0x695C813A, 0x42815175, 0x674D7ACB, 0x4C90AA84, 0x30F6DA55,
    0x1B2B0A1A, 0x0C83A3BF7, 0x0E3E7EBB8, 0x9F819B69, 0x0B45C4B26, 0x5F0A950F,
    0x74D74540, 0x8B13591, 0x236CE5DE, 0x0F07DD433, 0x0DBA0047C, 0x0A7C674AD,
    0x8C1BA4E2, 0x0A9D78F5C, 0x820A5F13, 0x0FE6C2FC2, 0x0D5B1FF8D, 0x6A0CE60,
    0x2D7D1E2F, 0x511B6EFE, 0x7AC6BEB1, 0x1A833982, 0x315EE9CD, 0x4D38991C,
    0x66E54953, 0x0B5F478BE, 0x9E29A8F1, 0x0E24FD820, 0x0C992086F, 0x0EC5E23D1,
    0x0C783F39E, 0x0BBE5834F, 0x90385300, 0x432962ED, 0x68F4B2A2, 0x1492C273,
    0x3F4F123C, 0x6A0CE60A, 0x41D13645, 0x3DB74694, 0x166A96DB, 0x0C57BA736,
    0x0EEA67779, 0x92C007A8, 0x0B91DD7E7, 0x9CD1FC59, 0x0B70C2C16, 0x0CB6A5CC7,
    0x0E0B78C88, 0x33A6BD65, 0x187B6D2A, 0x641D1DFB, 0x4FC0CDB4, 0x2F854A87,
    0x4589AC8, 0x783EEA19, 0x53E33A56, 0x80F20BBB, 0x0AB2FDBF4, 0x0D749AB25,
    0x0FC947B6A, 0x0D95850D4, 0x0F285809B, 0x8EE3F04A, 0x0A53E2005, 0x762F11E8,
    0x5DF2C1A7, 0x2194B176, 0x0A496139, 0x0E11FBF10, 0x0CAC26F5F, 0x0B6A41F8E,
    0x9D79CFC1, 0x4E68FE2C, 0x65B52E63, 0x19D35EB2, 0x320E8EFD, 0x17C2A543,
    0x3C1F750C, 0x407905DD, 0x6BA4D592, 0x0B8B5E47F, 0x93683430, 0x0EF0E44E1,
    0x0C4D394AE, 0x0A496139D, 0x8F4BC3D2, 0x0F32DB303, 0x0D8F0634C, 0x0BE152A1,
    0x203C82EE, 0x5C5AF23F, 0x77872270, 0x524B09CE, 0x7996D981, 0x5F0A950,
    0x2E2D791F, 0x0FD3C48F2, 0x0D6E198BD, 0x0AA87E86C, 0x815A3823
]

def crc32(array, array_len):
    v3 = 2910424328  # Расчитанное предварительно значение до предпоследнего шага
    for v4 in array[-4:]:
        v3 = (v3 >> 8) ^ crc_tab[getLbits(v3 ^ v4)]
    return NOT(v3)

def checkCRC(item):
    if crc32(item, len(item)) == 0x3E9A75C2:
        print('CRC Found: %s' % item)

creds = b'AdminAdmin\r\n26892263f3d18dfabb665e2d2a680899b2577f0f4daa77287fadb3e4ae581ec1\r\nNeoQuestNeoQuestNeoQuestNeoQuestNeoQuestNeoQuestNeoQuestNeoQuestNeo'
x1 = 0xFFFFFFFF
while x1 > 0:
    checkCRC(creds + pack('>I', x1))
    x1 -= 1

Дабы не высчитывать подпись заного для всего сообщения, её можно просчитать заранее для выбранного участка, а затем просто досчитывать оставшиеся 4 байта. Запускаем, и через некоторое время получаем ответ:

$ ./libneo.py
CRC Found: b'AdminAdmin\r\n26892263f3d18dfabb665e2d2a680899b2577f0f4daa77287fadb3e4ae581ec1\r\nNeoQuestNeoQuestNeoQuestNeoQuestNeoQuestNeoQuestNeoQuestNeoQuestNeo\xfe\xa3\x0f#'

Теперь отправим это на сервер и заберём флаг:

#!/usr/bin/python3
import requests
import base64
import json

def connect():
    url = 'http://213.170.100.214/neoquest.php'
    header = {'Content-Type': 'application/json'}
    data = {"comment": "", "login": "AdminAdmin", "crc32": "3E9A75C2", "key_word": "26892263f3d18dfabb665e2d2a680899b2577f0f4daa77287fadb3e4ae581ec1"}
    data['comment'] = base64.b64encode(b'NeoQuestNeoQuestNeoQuestNeoQuestNeoQuestNeoQuestNeoQuestNeoQuestNeo\xfe\xa3\x0f#').decode()
    data = json.dumps(data)
    req = requests.post(url, data, header).text
    if 'wrong' not in req and 'not your checksum!' not in req:
        print(req)

connect()

После отправки данных получаем ответ:
login — OK
key_word — OK
CRC32 — OK

ce91ecbefd83b69a88055e151800f4ebec7cda1a93b94cb0b420251a169e5abf

На этом всё!
Поделиться с друзьями
-->

Комментарии (9)


  1. mityada
    12.03.2017 22:10

    Код decrypt у вас какой-то очень раздутый получился. Если практически построчно переписать на С, выходит гораздо меньше.

    Может кто-нибудь подсказать куда надо смотреть в Espion? А то оно сделано у всех подряд, и очков за него много можно было получить, обидно даже.


    1. NWOcs
      13.03.2017 21:24
      +1

      Мы скоро начнем публиковать разборы заданий! Пока что просто намекнем: первым шагом к решению являлись EXIF и синяя питерская социальная сеть)


      1. licoxe
        14.03.2017 12:31
        +1

        Вас не дождались)
        https://habrahabr.ru/post/323830/


  1. braindamagedman
    13.03.2017 08:12

    А чем пользуетесь для декомпиляции?


    1. GH0st3rs
      13.03.2017 10:32

      Java — JADX или jd-gui
      C/C++ — IDA + HexRays


  1. spot62
    13.03.2017 11:06

    > Можно пропатчить сам apk файл
    а как в этом случае обходится api23 runtime permissions (при запросе imei приложение в любом случае валится в исключение)? или его не нужно обходить? )

    про декомпиляцию библиотек ida даже в страшном сне представить себе не мог — думал все проще.


    1. GH0st3rs
      13.03.2017 12:44
      +1

      а как в этом случае обходится api23 runtime permissions (при запросе imei приложение в любом случае валится в исключение)? или его не нужно обходить? )

      В процессе прохождения, изначально я патчил именно сам apk файл
      Скрины




      1. spot62
        13.03.2017 22:07

        Да, с таким imei все работает. Супер!
        Спасибо…


  1. NWOcs
    13.03.2017 21:25
    +1

    Спасибо за writeup! :)