Бесконтактные карты в киевском метро начали вводить в 2007 году (информация на сайте метро, укр), но широкое распространение и внедрение они получили только к концу 2008 года. На сегодняшний день существуют два основных типа проездных билетов: проездные со сроком действия, и проездные на количеств поездок. В проездных используются бесконтактные карты MIFARE Classic 1K.
Фото — Metromuseum.net
Об уязвимостях чипов MIFARE Classic стало известно в 2007 году. Подробную историю открытия уязвимостей можно почитать в статье. Статья хоть и 2008 года, но до сих пор актуальна, и в ней перечислены основные этапы нахождения уязвимостей. Совместив эти знания можно посмотреть, что же записывается в карты киевского метро на примере проездного на количество поездок.
Дисклаймер: Все действия и информация, описанные ниже, приведены исключительно для расширения личного кругозора, и не преследуют цели личной выгоды.
Настраиваем рабочее место
Для считывания бесконтактных карт, мы будем использовать:
- ридер — SCL3711;
- открытую библиотеку для работы с бесконтактными картами — libnfc;
- утилиту для получения ключей — mfoc из комплекта nfc-tools.
Ридер
Так, как мы будем использовать libnfc, ридер необходимо выбирать из тех, с которыми библиотека лучше всего совместима. Выбираем на странице совместимости подходящий и покупаем. Я остановился на ридере SCL3711 с чипом PN533 v2.7 (на фото, покупал на eBay за 35$).
Библиотека libnfc
Я буду описывать процедуру сборки libnfc для Ubuntu 12.04, для других платформ подробная инструкция по установке есть на сайте .
Устанавливаем зависимости необходимые для сборки пакетов и работы с SVN (для тех, у кого не установлены):
sudo apt-get install subversion dpkg-dev debhelper dh-autoreconf libtool
Устанавливаем пакеты, которые необходимы для сборки libnfc:
sudo apt-get install libuсsb-dev libpcsclite-dev
Скачиваем и распаковываем последнюю версию библиотеки (1.6.0-rc1), из репозитория загружаем файлы, необходимые для сборки deb пакета:
wget http://libnfc.googlecode.com/files/libnfc-1.6.0-rc1.tar.gz
tar -xvzf libnfc-1.6.0-rc1.tar.gz
cd libnfc-1.6.0-rc1/
svn checkout http://libnfc.googlecode.com/svn/tags/libnfc-1.6.0-rc1/debian
Я рекомендую убрать вывод отладочных сообщений. Для этого в файле debian/rules надо убрать ключ --enable-debug в строке dh_auto_configure.
Собираем пакеты:
dpkg-buildpackage -b -us -uc
Устанавливаем зависимости и пакеты:
sudo apt-get install libusb-0.1-4 libpcsclite1 libccid pcscd
sudo dpkg -i ../libnfc*.deb
Для проверки ридер должен быть подключен и рядом с ним (в радиусе его действия) лежать карточка. Проверяем работоспособность командой nfc-list, которая выводит список ридеров и карт, в поле ридера:
$ nfc-list
nfc-list uses libnfc 1.6.0-rc1 (rexported)
NFC device: SCM Micro / SCL3711-NFC&RW — PN533 v2.7 (0x07) opened
ATQA (SENS_RES): 00 04
UID (NFCID1): 5b b8 5f 28
SAK (SEL_RES): 08
Если вы увидели что-то подобное — значит все работатет. У меня сначала выдавало ошибку "libnfc.driver.pn53x_usb Unable to set USB configuration (Device or resource busy)". Дело в том, что В Ubuntu 12.04 по-умолчанию установлены какие-то драйвера для чипа PN533, и libnfc не может получить доступ к устройству. Лечится отключением встроенного драйвера "sudo modprobe -r pn533".
Утилита mfoc
Будем использовать утилиту mfoc из последней версии nfc-tools из репозитория. Проблем возникнуть не должно:
svn checkout http://nfc-tools.googlecode.com/svn/trunk/mfoc/ mfoc
cd mfoc
dpkg-buildpackage -b -us -uc
sudo dpkg -i mfoc_0.10.2pre3.1-0_amd64.deb
Заглядываем внутрь
Теоретическая часть
Карты MIFARE Classic 1K имеют 1 Кбайт памяти, которая разбита на 16 секторов. Каждый сектор состоит из 4 блоков по 16 байт. Каждый сектор защищен двумя 48-битными ключами A и B (которые хранятся в 4 блоке).
Для операции с конкретным сектором ридер должен авторизоваться с помощью одного из ключей (A или B) для этого сектора. Каждому ключу могут быть присвоены независимые права на чтение и записи. Уязвимость которую использует mfoc состоит в том, что если известен ключ хотя-бы к одному сектору, то после авторизации для этого сектора, mfoc пытается аутентифицироваться для другого сектора, и эта попытка раскрывает 32 бита ключа для нового сектора. Более подробно и корректно можно прочитать в статье Wirelessly Pickpocketing a Mifare Classic Card.
Практическая часть
Получаем ключи к карте с помощью утилиты mfoc:
mfoc -O keys.mfd
Где-то через минуту, вы получите ответ "Auth with all sectors succeeded, dumping keys to a file!". После этого в файл keys.mfd лежат ключи к карте. Ключи для всех карт одинаковы, и к слову довольно давно выложены в интернет (очевидно, одним из сотрудников) .
Для анализа сливаем дамп карты:
nfc-mfclassic r a new00-04-11.mfd keys.mfd
Повторяем процедуру после каждой поездки на метро или пополнения счета, и получаем достаточный набор дампов для анализа.
Анализ
Не буду приводить длинные размышления, остановлюсь на выводах. После каждой операции меняются два участка памяти заголовок и история операция.
Заголовок карточки — два идентичных блока по адресам 0x2D0 и 0x2E0. Каждый блок содержит информацию о порядковом номере операции с карточкой, оставшемся количестве поездок и дате и времени последней операции. Дата и время записаны странно: побитово, и секунды зачем-то разделены на 2 (см. коде ниже).
Блоки, содержащие историю последних шести операций, находятся по адресам 0xC0, 0xD0, 0xE0, 0x100, 0x110, 0x120. Каждая запись содержит:
- дату и время операции,
- номер терминала, который производил запись,
- номер записи в журнале терминала,
- количество поездок.
Номер терминала должен зависеть от станции метро, но я не до конца уверен. Дата и время записаны в другом, но таком же чудном формате, как и в заголовке.
Пример
Программа на питоне, которая читает данные из дампа полученного с помощью nfc-mfclassic:
#!/usr/bin/env python
import sys
from struct import unpack
from datetime import datetime
def get_crc(block):
"""XOR all bytes in block"""
return reduce(lambda x, y: x ^ ord(y), block, 0)
def get_bits(i, s, l):
""" Get l bits starting at s"""
mask = (1 << (l)) - 1
return int((i >> s) & mask)
def print_info(data):
# card number
number = unpack('<4H', data[0x46:0x4E])
print "Card #:tt{3:04X} {2:04X} {1:04X} {0:04X}".format(*number)
# card header blocks
header_block = data[0x2D0:0x2E0]
crc_2d = get_crc(header_block)
crc_2e = get_crc(data[0x2E0:0x2F0])
print "nHeader: t" + ("ok" if crc_2d == crc_2e else "err")
print "Block 0x2D CRC:t{:#x}".format(crc_2d)
print "Block 0x2E CRC:t{:#x}".format(crc_2e)
# last activity
activity_number, activity2, activity1 = unpack('>HLH', data[0x2E2:0x2EA])
activity = activity1 + (activity2 << 16)
activity_count = get_bits(activity, 38, 10)
y = get_bits(activity, 6, 5) + 2000
M = get_bits(activity, 11, 4)
d = get_bits(activity, 15, 5)
h = get_bits(activity, 20, 5)
m = get_bits(activity, 25, 6)
s = get_bits(activity, 31, 5) * 2
activity_date = datetime(y, M, d, h, m, s)
activity_position, = unpack('>H', data[0x147:0x149])
activity_index = (activity_position / 0x40) - 32
print "nLast activity #t{}".format(activity_number)
print "Positiont{:#x} ({})".format(activity_position, activity_index)
print "Date:tt" + activity_date.isoformat()
print "Counter:t{}".format(activity_count)
# last activities positions
positions = [0xC0, 0xD0, 0xE0, 0x100, 0x110, 0x120]
print "n{:^20}{:^5}{:^20}{:^12}".format("Date", "unk", "Terminal", "Operation")
print "{:>31}{:>5}{:>4}{:>10}{:>7}".format("ID", "Type", "Cnt", "Type", "Cnt")
# get correct order
i = 5 if activity_index > 5 else activity_index
positions_ordered = positions[(i+1):] + positions[:(i+1)]
for pos in positions_ordered:
block = data[pos:pos+0x10]
# if undefined or empty block
if (get_crc(block) <> 0) or (ord(block[0]) == 0):
continue
date_i, unk = unpack('>LH', block[1:7])
term_id, term_type, term_cnt = unpack('>BBH', block[7:11])
op_type, op_cnt_i = unpack('>HH', block[11:15])
op_cnt = op_cnt_i / 0x40
h = get_bits(date_i, 2, 5)
m = get_bits(date_i, 7, 6)
s = get_bits(date_i, 13, 5) * 2
y = 2000 + get_bits(date_i, 18, 5)
M = get_bits(date_i, 23, 4)
d = get_bits(date_i, 27, 5)
date = datetime(y, M, d, h, m, s)
print "{}{:>#5x}{:>#7x}{:>#5x}{:>#7x}{:>#9x}{:>4}
".format(date.isoformat(), unk, term_id, term_type, term_cnt, op_type, op_cnt)
def main(filename):
with open(filename, "rb") as f:
data = f.read(1024)
print_info(data)
if __name__ == "__main__":
main(sys.argv[1])
Пример работы программы:
Как можно использовать
Запись о последних 6 поездках можно использовать для просмотра перемещений. Даже, если не видно станцию, то видно точную дату и время поездки.
С другой стороны, можно собирать статистику по метрополитену. Так, как каждый турникет записывает свой номер транзакции на карточку, можно смотреть, сколько прошло людей через турникет. Например, на дампе сверху видно, что через турникет на Политехническом институте (Номер терминала 0x14) за сутки прошло приблизительно 1000 человек.
Автор: PatapSmile