NeoQuest 2017: Реверс андроид приложения в задании «Почини вождя!»
Всем доброго времени суток, сегодня, 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):
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 {
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 — OKce91ecbefd83b69a88055e151800f4ebec7cda1a93b94cb0b420251a169e5abf
На этом всё!