NeoQuest 2017: Реверс андроид приложения в задании «Почини вождя!»

в 15:44, , рубрики: android, ctf, information security, python, reverse engineering, информационная безопасность, Разработка под android, реверс-инжиниринг

NeoQuest 2017: Реверс андроид приложения в задании «Почини вождя!» - 1
Всем доброго времени суток, сегодня, 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.
Код, который отвечает за расшифровывание выглядит так:
NeoQuest 2017: Реверс андроид приложения в задании «Почини вождя!» - 2

И Java обёртка для него:
NeoQuest 2017: Реверс андроид приложения в задании «Почини вождя!» - 3

Как видно, тут присутствует верный 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):
    rcx = key_len
    result = []
    for item in data:
        result.append(item)
    prekey = {}
    prekey2 = {}
    i = 0x0
    while True:
        temp_1 = i % key_len
        prekey[i] = i
        prekey2[i] = getLbits(ord(key[temp_1]) & 0xff)
        i += 0x1
        if i != 0x100:
            continue
        else: break
    i = 0x0
    y = 0x0
    while True:
        rdi = getLbits(prekey[i]) & 0xff
        rcx = setLbits(rcx, getLbits(rdi)) + getLbits(prekey2[i])
        rcx = getLbits(y + rcx) & 0xff
        y = rcx
        prekey[i] = getLbits(getLbits(prekey[rcx]) & 0xff)
        i = i + 0x1
        prekey[rcx] = getLbits(rdi)
        if i != 0x100:
            continue
        else: break
    if data_len != 0x0:
        i = 0x0
        y = 0x0
        k = 0x0
        while True:
            k = getLbits(k + 0x1) & 0xff
            rax = getLbits(prekey[k]) & 0xff
            y = getLbits(y + rax) & 0xff
            prekey[k] = getLbits(getLbits(prekey[y]) & 0xff)
            prekey[y] = getLbits(rax)
            rax = setLbits(rax, getLbits(rax) + getLbits(prekey[k]))
            rax = getLbits(prekey[getLbits(rax) & 0xff]) & 0xff
            result[i] = getLbits(data[i]) ^ getLbits(rax)
            i += 0x1
            if i < data_len:
                continue
            else: break
    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 выглядит алгоритм подсчёта контрольной суммы:
NeoQuest 2017: Реверс андроид приложения в задании «Почини вождя!» - 4

Исходя из вышесказанного, следует, что нужно отправить такие данные, которые будут соответствовать оригинальной подписи, но при этом с верным логином. Так как блок в 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'AdminAdminrn26892263f3d18dfabb665e2d2a680899b2577f0f4daa77287fadb3e4ae581ec1rnNeoQuestNeoQuestNeoQuestNeoQuestNeoQuestNeoQuestNeoQuestNeoQuestNeo'
x1 = 0xFFFFFFFF
while x1 > 0:
    checkCRC(creds + pack('>I', x1))
    x1 -= 1

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

$ ./libneo.py
CRC Found: b'AdminAdminrn26892263f3d18dfabb665e2d2a680899b2577f0f4daa77287fadb3e4ae581ec1rnNeoQuestNeoQuestNeoQuestNeoQuestNeoQuestNeoQuestNeoQuestNeoQuestNeoxfexa3x0f#'

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

#!/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'NeoQuestNeoQuestNeoQuestNeoQuestNeoQuestNeoQuestNeoQuestNeoQuestNeoxfexa3x0f#').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

На этом всё!

Автор: GH0st3rs

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js