Акселерометр — он же G-сенсор — является одним из самых распространенных датчиков на сегодня. Встретить его можно практически в каждом современном гаджете. Акселерометр выполняет довольно простую задачу — измеряет ускорение устройства. Давайте посмотрим, как он это делает — разберем механизмы сенсоров API Android на примере задания №7 из online-этапа NeoQUEST-2019.
По легенде нам выдано 2 файла: 7.apk и 7.txt. Из текста задания (а все задания по-прежнему доступны тут) можно сделать следующие выводы: 7.apk – программа-шифратор, которая каким-то образом использует параметры акселерометра устройства; 7.txt – криптограмма, сгенерированная шифратором. Записано в ней следующее:
[1749054104147639][2.07154922][10.001905][4.5387093][1749056073889025][5.7193284][8.221763][0.01391537][1749058029180773][4.684068][12.05614][0.0377285][1749060105291613][4.6900544][6.9307165][4.7094293][1749062123327502][4.4682417][7.512769][6.037215][1749067640096818][1.0396843][8.798672][4.9335976][1749070016073380][2.3173676][10.180047][4.948362][1749072343679582][4.3660607][12.218135][0.5312999][1749073674459611][2.48394698][10.834006][6.306282][1749075827770391][0.2795044][13.279829][0.19391555]
Видим, что текст представляет собой повторяющиеся группы из 4 значений, одно из которых целочисленное, а 3 оставшихся – числа с плавающей точкой. Для удобства расставим их по отдельным строкам:
[1749054104147639][-2.07154922][10.001905][4.5387093]
[1749056073889025][5.7193284][8.221763][0.01391537]
[1749058029180773][-4.684068][12.05614][0.0377285]
[1749060105291613][4.6900544][6.9307165][-4.7094293]
[1749062123327502][4.4682417][7.512769][6.037215]
[1749067640096818][1.0396843][8.798672][-4.9335976]
[1749070016073380][-2.3173676][10.180047][4.948362]
[1749072343679582][-4.3660607][12.218135][0.5312999]
[1749073674459611][-2.48394698][10.834006][-6.306282]
[1749075827770391][0.2795044][13.279829][-0.19391555]
С форматом криптограммы разобрались, но что это за значения — неизвестно. Какие-то параметры акселерометра устройства, без всякой конкретики. А давайте-ка зайдем на сайт разработчиков Android и посмотрим, что вообще акселерометр может показать.
Видим следующее описание:
Выяснили, за что отвечают параметры с плавающей точкой — это ускорение устройства вдоль осей X, Y и Z. Но как понять, за какую ось отвечает каждый из них? Самое время запускать приложение. Оно выглядит следующим образом:
Здесь есть 2 возможности определения поведения приложения: декомпиляция .apk и анализ получаемых значений. Далее рассмотрим второй способ и приведем вставки декомпилированного кода, отвечающего за рассматриваемые действия приложения.
В криптограмме присутствуют положительные и отрицательные значения (мы знаем, что это ускорения по разным осям), поэтому можно сделать предположение: если вектора наклона устройства по оси будут противоположными, то значения ускорений будут приблизительно равны по модулю, а различие будет лишь в знаке.
Ниже представлен листинг кода, отвечающий за хранение информации об ускорениях:
public class MotionSnapshot
{
public final float angle_alpha;
public final float angle_beta;
public final float angle_gamma;
public final long event_time;
...
}
Исходя из данных рассуждений, перед нами возникают следующие задачи:
- Определить, какому значению в криптограмме соответствует наклон устройства
- Определить шаблон отклонений
- Каждой цифре поставить в соответствие шаблон отклонения для расшифрования
Первые 2 задачи будем выполнять параллельно. На телефоне мы можем тестировать 2 вида наклона:
- От себя/На себя
- Влево/Вправо
Код, отвечающий за обработку событий от акселерометра
public class SensorListener implements SensorEventListener
{
private MotionTrace Trace;
public SensorListener(MotionTrace trace, MainActivity activity)
{
Trace = trace;
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy)
{
}
@Override
public void onSensorChanged(SensorEvent event)
{
Trace.addSnaphot(new MotionSnapshot(event.values, event.timestamp));
}
}
Код, отвечающий за генерацию трассы наклонов устройства во время нажатия кнопки
public class MotionTrace
{
private ArrayList<MotionSnapshot> Deltas;
private long Length;
private MotionSnapshot LastSnapshot;
public MotionTrace(long len)
{
LastSnapshot = new MotionSnapshot(0,0,0,0);
Deltas = new ArrayList<>();
Length = len;
}
public void addSnaphot(MotionSnapshot snapshot)
{
if (Deltas.size() >= Length)
{
Deltas.remove(0);
}
MotionSnapshot delta = new MotionSnapshot(0,0,0,0);
if (Deltas.size() > 0)
{
delta = snapshot;
}
Deltas.add(delta);
}
public ArrayList<MotionSnapshot> getDeltas()
{
return new ArrayList<>(Deltas);
}
}
Код, отвечающий за генерацию криптограммы
public void SaveCiphertext()
{
Log.d(Config.MAIN_TAG, "SAVING - {{" + Ciphertext + "}}");
try
{
File root = new File(Environment.getExternalStorageDirectory(), Config.DIRNAME);
if (!root.exists())
{
Log.d(Config.MAIN_TAG, "Creating directory - [" + root + "]");
if (!root.mkdirs())
{
Log.d(Config.MAIN_TAG, "Error creating directory");
}
}
File out_file = new File(root, Config.FILENAME);
Log.d(Config.MAIN_TAG, "Out - [" + out_file + "]");
PrintWriter writer = new PrintWriter(out_file, "UTF-8");
writer.println(Ciphertext);
writer.close();
Toast.makeText(this, "Saved to - [" + out_file + "]", Toast.LENGTH_LONG).show();
}
catch (IOException ex)
{
Toast.makeText(this, "Error saving data", Toast.LENGTH_SHORT).show();
}
Ciphertext = "";
}
Начнем по порядку. Для тестирования наклонов первого типа выберем цифры 2 и 8. Нажмем на каждую по 3 раза с возрастающим усилием. Получаем следующий результат:
[2687418463227102][-0.23700714][10.764615][-0.9759079]
[2687419411042043][-3.5834892][13.591138][-1.7036858]
[2687420383026907][-5.575793][13.533228][-1.3104248]
[2687421461360546][0.6850295][6.0002656][0.5568123]
[2687422317256542][4.1720495][1.8675026][1.545407]
[2687423250514599][7.9689393][-3.600097][0.33846742]
Отлично, различия во 2 параметре видны невооруженным взглядом. Начнем заполнять шаблон.
Шаблон представим в виде диапазонов значений соответствующих полей в строке криптограммы. Вопросительным знаком пометим то, что мы еще не знаем.
[? ]
[ (< 0) — отклонение от себя; (> 0) — отклонение на себя ]
[?]
[?]
Аналогично проведем тесты с кнопками 4 и 6. Результаты:
[2688019191605386][1.7270225][9.541045][0.0397171]
[2688020247971353][1.0615791][9.794326][4.9135437]
[2688021887957875][1.0974716][7.5535636][7.8307548]
[2688023749896352][1.3328063][9.43923][-0.27600938]
[2688024849688832][1.1357567][9.9313135][-2.4410355]
[2688026002520864][0.30400848][6.4610033][-8.0956335]
Обновим шаблон с учетом исследованной закономерности:
[? ]
[ (< 0) — отклонение от себя; (> 0) — отклонение на себя ]
[?]
[ (< 0) — отклонение вправо; (> 0) — отклонение влево]
Очевидно, что третье значение — это отклонение вверх/вниз, так как это единственный оставшийся вектор. И нам особо нет смысла его проверять, потому что для однозначного определения нажатой кнопки нам хватит уже известного шаблона.
Теперь создадим шаблон для каждой цифры на клавиатуре, исходя из положения кнопок и разработанного шаблона (* — параметр нас не интересует):
1 – [*][ < 0 ][*][> 0]
2 – [*][< 0][*][ близко к 0]
3 – [*][< 0][*][< 0]
4 – [*][][*][]
5 – [*][близко к 0][*][близко к 0]
6 – [*][близко к 0][*][< 0]
7 – [*][> 0][*][> 0]
8 – [*][> 0][*][близко к 0]
9 – [*][> 0][*][< 0]
0 – [*][> 0][*][близко к 0]
Как видно из рисунка, кнопки 8 и 0 имеют одинаковые параметры, поэтому их расшифрование может быть неоднозначным — при встрече такой комбинации в криптограмме следует попробовать оба варианта. Теперь применим полученные шаблоны к криптограмме, и получим 2 варианта ответа: 1029761235 и 1829761235, верным из которых является 1829761235. Задание пройдено!
Уже совсем скоро — 26 июня — состоится «Очная ставка» NeoQUEST 2019! Успейте зарегистрироваться на сайте мероприятия. В ближайшем будущем выйдет хабрастатья с анонсом программы, не пропустите!