
Всем доброго времени суток, сегодня, 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, дальше есть несколько вариантов:
- Можно пропатчить сам apk файл, заменив соответствующие строки в smali файле, таким образом, чтобы в decrypt отправлялся верный IMEI;
- Либо переписать это на другой язык и сделать всё вручную.
Первый вариант более простой, а второй более удобный, ибо потом не придётся переписывать ключ с экрана телефона, а можно будет просто скопировать из консоли. На 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
На этом всё!
