[Перевод] Реверс-инжиниринг британских билетов на поезд
Долгие годы, начиная ещё задолго до моего рождения, в Великобритании использовались билеты на поезд размером с кредитную карту. Изначально это были билеты APTIS1, которые позже заменили на чуть более удобочитаемую версию, печатаемую в том же формате.
1 Я до сих пор помню, как покупал их в детстве, чтобы добраться до лондонского вокзала Ватерлоо!
Сегодня эта отрасль стимулирует нас отказываться от бумажного билета в пользу электронного, со штрих-кодом (или смарт-карты ITSO2); компании-перевозчики не только не хотят тратить деньги на печать билетов, но и получают возможность точно отслеживать использование билетов в сети и минимизировать случаи мошенничества.
2 Если вам любопытно, то в этом обсуждении есть достаточно подробный обзор того хаоса, который представляет из себя ITSO.
Реклама преимуществ электронных билетов
Для пользователя тоже есть очевидные преимущества — я почти уверен, что большинство людей покупает мобильные билеты, потому что их легче заказать в приложениях наподобие Trainline.
Но какие же данные хранятся внутри штрих-кода мобильного билета и как ни устроены? Могут ли не работающие контролёрами люди получать из них данные? Оказалось, ответ на этот вопрос чуть интереснее, чем я ожидал!
Начало исследований
Мобильный билет — это просто Aztec Code, отображаемый или внутри приложения, или в PDF, который можно распечатать:
Загуглив результаты работы, которые проводили другие люди для расшифровки мобильных билетов, я обнаружил длинное обсуждение (на немецком) спецификации UIC 918.3, которая используется немецкой железнодорожной компанией Deutsche Bahn для электронных билетов. В них тоже используется формат Aztec Code, и внешне они выглядят очень похожими на британские. Нашлись люди, которые уже написали код для их чтения. Возможно, он поможет мне?
(Почему я ожидал, что это сработает? UIC — это международная организация по стандартизации (как минимум в Европе), поэтому логично было предположить, что в Великобритании может использоваться стандартный формат.)
Однако, к сожалению, в форматах не было ничего схожего: расшифровав наш британский код при помощи zxing, мы получим:
06DNQL4XHVK00TTRCGPUQWNTHPGHWBPOUTKRWXAJKGHFBAPBCTOGUZQVTZTKKDEBQXPGRWZJRJBXJZPOHNJGIPDJWEGYWJXLVPGEEZBCUUELIJMOINPRZMSDQCZJGLIZLUTQHXMTPKWCMJISUXQLORAOVYXSOLGXXGMVUDXTMHAYMBLUTKPUPFCRNNTDBBDLNWSBPDUXYKSIMJSBYBURSCPUMFBZPEUTECHTIOXAH
…что совершенно непохоже на то, как должен выглядеть код UIC, учитывая, что он должен начинаться с »#UT» (согласно упомянутому выше обсуждению на немецком).
На самом деле, это специальный формат, используемый только внутри Великобритании, о чём намекает »06» в начале данных; это билет «RSP-6» (RSP расшифровывается как Rail Settlement Plan), и Google почти ничего о нём не знает. Можно найти сделанный кем-то запрос согласно закону о свободе информации с требованием публикации спецификации. К сожалению, Rail Delivery Group (RDG) — это частная компания и не обязана отвечать на такие запросы, поэтому мне придётся разбираться во всём самостоятельно.
На помощь приходит друг
На этом этапе я, по сути, понятия не имел, как двигаться дальше. При сравнении нескольких билетов данные по большей части казались случайными, за исключением неизменных заголовков; это намекало, что данные каким-то образом зашифрованы — я не мог просто получить кучу билетов и попытаться искать в них схожие признаки.
Вывод binwalk -W cdq-cys.bin pad-aml.bin
, демонстрирующий различия и схожие черты между двумя билетами3
3 Я попробовал сравнить только два билета, потому что иначе изображение было бы слишком хаотичным.
Мой друг Харли («unlobito») рассказал мне о жалобах на билеты в групповом чате и дал мне подсказку: слово «masabi», которое оказалось названием компании, разрабатывающей системы продажи билетов.
На веб-сайте Masabi есть вот такая милая страница, на которой любезно объяснено, как она изобрела мобильную продажу билетов в Великобритании в 2007 году, и говорится, что на самом деле национальный стандарт RSP6 написала она! Также компания хвастается своим пакетом приложений «JustRide Inspect», который можно использовать для декодирования этих сообщений.
К сожалению, нельзя просто скачать это приложение из Play Store. Однако погуглив — можно найти его на одном из не совсем официальных веб-сайтов рехостинга APK.
Получив APK, мы можем сделать с ним многое. Можно просто установить его на устройство с Android и посмотреть, не выдаст ли приложение что-нибудь интересное таким образом; можно попытаться «декомпилировать» его, чтобы лучше понять, как работает приложение (и парсер билетов).
Запускаем приложение
Так как у меня не было запасных телефонов с Android, которых не жалко, а APK мог оказаться malware, первым делом я просто запустил его внутри Android Virtual Device (при помощи эмулятора в Android Studio). Я решил, что так будет немного безопаснее, чем просто устанавливать его на мой основной телефон с Android4.
4 VM обеспечивает приличный уровень защиты от выполнения ненадёжного кода, но он далеко не идеален; предполагаю, эмулятор Android не особо сильно защищён от людей, пытающихся из него вырваться, поэтому в общем случае, вероятно, это не самая надёжная стратегия для запуска подозрительных приложений.
Немного поэкспериментировав, я заставил его работать:
К сожалению, приложение оказалось не особо полезным — в таком виде оно не могло сканировать штрих-коды стандартных билетов. Кнопка «Login manually» позволяет выбрать железнодорожную компанию-оператора (Train Operating Company, TOC) для входа (некоторые из приведённых в списке компаний уже не существуют!), что довольно любопытно, но не особо полезно для нашей цели:
Я не работаю в компании-операторе, поэтому, очевидно, не могу сделать следующий шаг5. Возможно, нам чего-то удастся добиться, если мы изучим приложение иным способом?
5 Оказалось, что приложение скачивает множество ключей и данных, необходимых для декодирования билетов при помощи этого логина. Поэтому, даже если бы мне удалось заставить его обманом запустить сканер, он бы не работал.
Декомпилируем приложение
Можно передать Android Studio файл APK, и она проанализирует, что находится внутри. Отчасти.
Если попробовать сделать это (нажать на меню из трёх точек и при выборе проекта нажать на «Profile or Debug APK»), то можно получить нечто не особо полезное: набор странно выглядящих файлов «smali».
Так получилось, потому что APK содержит только скомпилированный байт-код, а не полезный для нас исходный код; как написано в жёлтом предупреждении, эти файлы хранятся внутри APK в формате .dex («Dalvik Executable», где Dalvik — это название Android VM), а smali — это просто человекочитаемое описание, изобретённое кем-то для этого формата (как язык ассемблера).
В идеале бы нам хотелось каким-то образом превратить байт код обратно в Java. Такой инструмент существует, это jadx — очень удобная программа, выполняющая эту задачу наряду с другими полезными функциями, например, выводом проекта, который может загрузить Android Studio (а теоретически и скомпилировать)!
$ jadx --deobf -e -d out ~/Downloads/justride-inspect.apk
INFO - loading ...
INFO - processing ...
INFO - done
$ ls out/
app build.gradle settings.gradle
Разбираемся в декомпилированном выводе
Когда я заглянул в папку вывода jadx, то нашёл там идеальную копию оригинального исходного кода, написанного Masabi, после чего моя работа стала крайне простой. Хотя я, конечно же, вру.
Перед публикацией APK для конечного пользователя разработчики приложений для Android обычно выполняют с ним несколько этапов операций уменьшения размеров, в том числе и обфускацию, превращающую длинные имена классов и членов наподобие «mContext» в минимально допустимую строку, например, «a». Это значит, что в сгенерированном jadx коде довольно сложно разобраться:
public class TicketInspectActivity extends BaseActivity implements InterfaceC2526a {
/* renamed from: c */
private ViewPager f4615c;
/* JADX INFO: Access modifiers changed from: private */
/* renamed from: h */
public C2496p m1419h() {
return (C2496p) this.f4615c.getAdapter();
}
@Override // com.masabi.app.android.ticketcheck.activities.BaseActivity
/* renamed from: a */
public final void mo1410a() {
super.mo1410a();
if (isFinishing()) {
return;
}
m1419h().m1467a(this.f4615c.getCurrentItem());
}
/* ... */
}
О да, я точно знаю, что они имели в виду, называя класс C2496p
. Разумеется! Нам достаточно просто оттрассировать исполнение void mo1410a()
, и мы во всём разберёмся6!
6 jadx немного упрощает работу, переменовывая некоторые идентификаторы (выполняет «деобфускацию»): добавляет числа и префиксы наподобие «C» («class»), помогающие нам различать кучу элементов с именами «a» или «b».
Хотя поначалу это выглядит пугающе, на самом деле это вполне неплохо, учитывая инструменты, которые даёт нам Android Studio. Не всё полностью обфусцировано: некоторые имена классов нужно оставлять необфусцированными, например, действия (допустим, это TicketInspectActivity
). Это даёт нам некоторое представление о том, с чего начать. Также время от времени код содержит сообщения об ошибках, подсказывающие нам, какими должны быть классы и методы:
/* renamed from: com.masabi.c.a */
/* loaded from: classes.dex */
public final class C2666a {
/* renamed from: a */
public static final int m552a(Calendar calendar) {
if (calendar == null) {
System.err.println("DateTimeUtils.packDate() ERROR - Attempt to pack a null date!");
}
return (C2668c.m542a(calendar) << 16) | (C2667b.m548a(calendar) & 65535);
}
/* ... */
}
В данном случае строка лога позволяет нам сразу же выполнить преобразование C2666a
→ DateTimeUtils
и m552a
→ packDate
.
Также у Android Studio есть превосходная поддержка выполнения одновременного переименования по всей кодовой базе, поэтому после того, как я уделил долгий полдень разбору всего, структура начала быстро вырисовываться и наш обфусцированный код стал походить на то, как должен выглядеть оригинальный исходный код7.
7 Без реального исходного кода невозможно узнать, как на самом деле назывались классы, но я могу дать им имена, кажущиеся мне логичными, а это главное. puck смог найти приложение, в котором используется часть не обфусцированных классов из Masabi SDK, что позволило мне сравнить их и проверить точность придуманных мной имён с реальными!
Интересные способы использования RSA
Моё исследование приложения подтвердило подозрения о том, что данные и в самом деле зашифрованы. Точнее, не совсем. Строго говоря, данные билетов действительно подписаны RSA и PKCS#1 (как мне кажется). Выпускающая билеты компания генерирует полезную нагрузку, содержащую данные билетов, а затем использует свой приватный ключ RSA для создания подписанного сообщения, которое помещается в штрих-код. В сканере билетов хранится набор публичных ключей компании для проверки подписи и считывания исходной полезной нагрузки.
В качестве более конкретного примера приведу код на Rust, выполняющий этапы верификации и чтения:
// BigUint - это беззнаковый integer произвольного размера.
// Билет закодирован base26, поэтому сначала нужно обратить эту операцию:
let ticket: BigUint = base26_decode(&ticket_str[15..]);
// здесь выполняется "S^e mod N";
// то есть часть проверки подписи RSA
let message = ticket.modpow(&key.public_exponent, &key.modulus);
// преобразуем большой integer в сырые байты (big-endian)
let message: Vec = message.to_bytes_be();
// пытаемся избавиться от паддинга PKCS#1; если не получается, то ключ неверен
if let Some(unpadded) = strip_padding(&message) {
eprintln!("[+] decrypt done: {:?}", unpadded);
}
Я не криптограф, поэтому это было для меня довольно новым! Я привык, что подписи являются хэшем исходного сообщения (то есть ты отправляешь открытый текст plaintext и затем sign(hash(plaintext))
вместе с ним), что обычно делается, чтобы можно было подписывать сообщения длиннее, чем размер ключей. В данном случае разработчики поместили всё сообщение внутрь подписи, чтобы сэкономить место в штрих-коде, то есть для чтения сообщения нужен публичный ключ.
Также при помощи этой схемы невозможно создавать собственные поддельные билеты; для этого понадобится приватный ключ RSA одной из компаний, выпускающих билеты или собственный публичный ключ, добавленный в сеть считывателей на турникетах и в приложения контролёров. И то и другое сделать довольно непросто.
Схема 1: padded = [0x00, 0x01, padding-string, 0x00, message]
(где padding-string
— это длина 0xFF
октетов)
ИЛИ
Схема 2: padded = [0x00, 0x02, padding-string, 0x00, message]
(где padding-string
— это длина случайных ненулевых октетов)
Если полезная нагрузка не походит ни на одну из этих схем, операция RSA завершается сбоем, то есть вы, похоже, используете неверный ключ и нужно попробовать другой.
Итак, для декодирования этих билетов обязательно знать публичные ключи. Откуда же их взять?
Получаем неуловимые публичные ключи
Публичные ключи не публикуют в очевидных местах, а реверс-инжиниринг приложения Masabi дал понять, что оно скачивает ключи с сервера конфигураций после упомянутого выше сканирования штрих-кода конфигурации.
Global.logger.log(getClass().getSimpleName(), "loadAllKeys() - Fetched " + barcodeKeysList.length + " barcode keys from metadata");
for (int i = 0; i < barcodeKeysList.length; i++) {
AbstractJSONObject key = (barcodeKeysList[i2];
if (ExtendedGlobal2.clock.getCurrentTime() < Global.f4949d.mo915a(key.getString("expiryDate")) * 1000 &&
(decoder = makeDecoder(key.getString("issuerId"), key.getString("ticketType"), key.getString("modulus"), key.getString("exponent"), key.getLong("mQ"))) != null) {
decoders.addElement(decoder);
}
}
Можно решить, что это тупик, ведь у нас нет данных для логина, однако разработчики оставили несколько ключей и внутри самого APK. Насколько я понял, приложение нигде их не читает; возможно, оно делало это в прошлом, а может, они использовались для тестирования и разработчики забыли убрать их из продакшен-версии приложения.
$ find . | grep 'keys' | grep rsp6
./app/src/main/assets/keys/rsp6_rsa_ao.dat
./app/src/main/assets/keys/rsp6_rsa_ua.dat
./app/src/main/assets/keys/rsp6_rsa_tt-qa.dat
./app/src/main/assets/keys/rsp6_rsa_tt.dat
./app/src/main/assets/keys/rsp6_rsa_t3.dat
./app/src/main/assets/keys/rsp6_rsa_t2.dat
Ключи разделяются по выпускающим билеты компаниям: коду из двух символов, образующему первую часть ID билета. Билет, который мы изучали выше, был выпущен Trainline, код которой имеет вид TT…
…а ключи для декодирования этого билета находятся в rsp6_rsa_tt.dat
. Отлично!8
8 Формат этих файлов .dat немного странный и нестандартный, и чтобы разобраться в нём, нужно выполнить реверс-инжиниринг кода шифрования RSA. Или, если вы ленивый, как я, можете просто скопипастить декомпилированный код в новый проект Java, и работать с ним, как с «чёрным ящиком».
Однако это лишь подмножество используемых сегодня ключей, как я вскоре выяснил, когда unlobito дал мне для расшифровки билет Avanti West Coast. Имеющаяся у меня копия приложения датирована 2016 годом и с тех пор мобильные билеты начало выпускать гораздо большее количество компаний-перевозчиков!
ttkMobile
Примерно тогда, когда я разбирался со всем этим, puck подсказал мне веб-сайт The Ticket Keeper — ещё одной фирмы, занимающейся инструментарием для проверки и выпуска билетов. У неё есть приложение iOS для контролёров под названием ttkMobile, которое можно просто скачать прямо из App Store и использовать для проверки билетов дома!9
9 По умолчанию оно ожидает, что нужно сообщать о выполненных сканированиях билетов, поэтому прежде чем оно заработает, придётся немного потрудиться и послушать его жалобы. Кстати, если вам интересно, кнопка выпуска билетов «Ticket Issuing» не работает, для этого нужен логин.
Если вы установите это приложение, то после удаления переустановить его будет невозможно. При первой загрузке оно регистрирует UUID вашего устройства на каком-то сервере и генерирует случайный пароль, который хранит в локальном хранилище. Удаление приложения удаляет и пароль, но UUID устройства не меняется, поэтому при переустановке оно не сможет аутентифицироваться и окажется бесполезным (потому что для работы ей нужно получать ключи и данные).
(«Но постойте», — возразите вы, — «разве постоянный ID устройства не запрещает создавать сама Apple?» И вы будете правы! Строго говоря, мне кажется, что UUID устройства на самом деле меняется между установками, но копия хранится в кейчейне устройства, который не удаляется после удаления приложения. Это глупо, и, скорее всего, противоречит политике App Store.)
У меня нет iPhone, но он есть у нескольких друзей. unlobito и ещё одна моя подруга Ева («thejsa»), поэкспериментировали немного и смогли достать мне свободный от DRM10.ipa
, содержащий приложение, которое я смог развернуть и декомпилировать при помощи Ghidra. Это позволило мне разобраться в некоторых частях билета, на которые не смотрит приложение Masabi.
10 Насколько я понимаю, обычно для этого требуется телефон с jailbreak. В противном случае можно просто копировать платные приложения между телефонами и делиться ими с людьми, которые за них не платили.
thejsa также потратила время на запуск приложения через прокси, чтобы разобраться, как оно общается с сервером11; оказалось, что просто существует конечная точка, в которой можно получить все публичные ключи:
11 Именно так она выявила причину, по которой приложение не работало после удаления. Она даже написала твик для jailbreak, позволяющий менять ID устройства, чтобы приложение снова работало. Это очень круто!
$ curl 'https://device.theticketkeeper.com/download_keys?device_name=abc' | jq .
{
"return_code": "ok",
"message": null,
"keys": {
"AA": [
{
"valid_from": "20000101000000",
"valid_until": "29991231000000",
"public_exponent_hex": "10001",
"modulus_hex": "9140AA61F7D9A2E943C0510BACA5FA9CA7D12D78E301A36D640F2D28D8C0AA4D6A7102555CECF138E467730B797509EC1AB5BBA77CA6384BC8F483F609B121E75AE42660EDFE15EF91ADD4DA68C355F830FAAC6FFB25FBCFE1E61C7AF37C4AE8C85E264C151BD9C9AA4DE41D2756A9E260C0CC89AE2ADDD19E452A675E88DA47",
"public_key_x509": null,
"test_only": "N",
"updated": "20200313175331"
},
[etc]
Как говорилось выше, это критически важная информация, необходимая для декодирования билетов, поэтому огромное спасибо разработчикам The Ticket Keeper за то, что они так легко ею поделились!
▍ Краткое отступление о свободе информации
Не знаю, будут ли люди из этой отрасли (например, Rail Delivery Group) расстроены тем, что я публикую эту информацию. Надеюсь, что нет: я искренне считаю, что публичные ключи должны раскрываться публике вместе с официальными спецификациями по декодированию. Билеты подписаны, поэтому на практике опасность отсутствует — например, люди не могут использовать всё это, чтобы массово подделывать билеты — однако существует множество инновационных способов применения этих данных. Представьте, например, приложение-логгер путешествия, который использует сканирование билетов для автоматического отслеживания вашего маршрута, или систему учёта расходов, использующую закодированную в билете информацию о цене для автоматической фиксации трат!
Хотя железные дороги управляются консорциумом частных компаний, но на самом деле они являются общественной службой, которой владеет и управляет правительство12 (на январь 2023 года), поэтому они должны подпадать под те же положения закона о свободе информации, что и другие общественные организации.
12 Да, на первый взгляд наши железные дороги выглядят приватизированными, однако COVID привёл к появлению так называемых Emergency Recovery Measures Agreements, принудивших компании-перевозчики к государственному владению и контролю (фактически теперь — они подрядчики, управляющие сервисом, а не стороны, рискующие доходами). Это значит, что на самом деле за такие вещи, как диспуты с профсоюзами о работе и условиях труда, отвечает правительство, несмотря на то, что хотелось бы внушить вам некоторым министрам!
У некоторых людей в отрасли уже возникла правильная идея; в переписке с одним из разработчиков Ticket Keeper мне сообщили, что публичность приложения ttkMobile наряду с частью его данных — это намеренный выбор, и это очень радостно слышать!
▍ Бонус: логи eTVD
Веб-сайт также сообщает, что у него есть база данных валидации электронных билетов (Ticket Validation Database, eTVD), содержащая копии всех сканов билетов, сделанных в турникетах и людьми, использующими приложение. Это та самая защита от мошенничества, о которой я говорил в самом начале; предположительно, подобные данные очень полезны для отделов защиты доходов, пытающихся выявлять систематические уклонения от оплаты, например, short-faring13.
13 Так называется практика покупки билета, покрывающего не всю поездку (например, с пропуском нескольких остановок в начале или конце) в надежде, что вас проверят только между теми станциями, где билет действителен.
Однако на сайте не говорится, что приложение отдаёт эту информацию без аутентификации, для этого достаточен лишь ID билета (!).
19 января 2023 года разработчикам сообщили об этом как о потенциальной угрозе безопасности/утечки данных. Они подтвердили, что это предусмотренное поведение, но согласились, что, вероятно, стоит его ограничить; мне сказали, что это скоро случится.
Эта информация может быть неприятно подробной, например, в ней указывается точное имя пользователя отсканировавшего билет контролёра, место сканирования, компания, предоставляющая услуги перевозок, успешность сканирования и многое другое. Также иногда выдаёт данные штрих-кода целиком и то, как, по мнению сервера, они декодируются!
# получение информации сканирования для билета CBCZSCDPVFF
# (данные сильно урезаны; на самом деле полей гораздо больше)
$ curl 'https://device.theticketkeeper.com/get_ticket_details?device_name=abc&utn=CBCZSCDPVFF' | jq '.["ticket_detail"]["scans"]'
[
{
"event_time_iso": "2022-06-10T18:30:47",
"created": "2022-06-10T18:30:48",
"device_type": "ttkMobile",
"device_id": 1001,
"device_name": "f95396f5-da22-47f9-8e85-dff4b2294a5d",
"device_alias": "2021-TK10212",
"username": "JLazlo01",
"action_name": "Accepted",
"rsp_action_code": 4001,
"event_trigger": "scan",
"scan_mode": "clip",
"scan_nlc": "2728",
"validation_result": "warning",
"message_displayed": "16-25 Railcard",
"gate_id": "OPN-3002i[021502]",
"latitude": 51.7824963,
"longitude": -0.2141781,
"device_scan_id": 74402,
"train_uid": "L77572",
"departure_date": "2022-06-10",
"barcode": "06CZSCDPVFF00…",
"train_info": "Fr1803 KGX-SKI 1D26/GR2600",
# etc
},
# etc
]
Так что да, штрих-код вашего билета или его ID, который часто написан под кодом обычным текстом, может дать кому-нибудь доступ к неожиданно большому объёму подробной информации о том, где вы находитесь и на каких поездах ездите! (Аналогично ссылке на заказ, которую отправляют при заказе билета на самолёт.)
Как попробовать это самостоятельно
Благодаря декомпилированному приложению Masabi и приложению ttkMobile — было не так уж сложно приблизительно понять, на что похож формат билетов. Я создал небольшой репозиторий с написанным на Rust инструментом для декодирования билета, а также небольшую спецификацию с моими теориями о том, что значат поля.
Также есть удобный веб-инструмент, который я набросал примерно за вечер: вы можете навести телефон на штрих-код (или загрузить его скриншот), чтобы получить достаточно полную сводку хранящейся внутри информации. Попробуйте! (Если вам нужен штрих-код, то просто возьмите один из поста!)
Благодарности
Я выражаю благодарность unlobito, puck и thejsa (и множеству других людей в чат-комнатах) за помощь со всем этим.
Telegram-канал с розыгрышами призов, новостями IT и постами о ретроиграх