Карточный sokoban. Выигрываем в смарт-карты вместе с Clojure
15–16 ноября в Москве прошла конференция OFFZONE Moscow, где участникам выдали пластиковые смарт-карты и предложили задания на получение флагов с их помощью. Для решения этих задач требовался картридер, который можно было арендовать или купить за внутреннюю валюту. На сайте мероприятия выложили задания и дали управляющие последовательности, с помощью которых можно выполнять функции апплетов, записанных на карту. Разберем задание по шагам.
Почему Clojure?
Рандомный посетитель
Началось все с того, что я узнал о возможности взять картридер и, используя некоторые APDU-команды протокола ISO/IEC 7816, выполнять задания с пластиковой смарт-карты, которая была выдана всем участникам вместе с бейджами. Это мне очень сильно напомнило PHDays V, когда я, как обычный посетитель конференции, просто подошел к стенду с электроподстанцией, поснифал пакеты через Wireshark, за ночь написал наивную имплементацию протокола IEC 61850 на Scapy, а на следующий день устроил с ноутбука DoS-атаку всему стенду SCADA (до плавки электропроводов я тогда, конечно же, не добрался).
В этот раз я решил попробовать пойти тем же путем: в первый день поэкспериментировать с протоколом, попробовать решить хотя бы тренировочную задачу на Python, используя библиотеку pyscard, как рекомендовали организаторы конференции, а ночью порешать все остальные задачи, которые получится. В шестом часу утра я понял, что свернул куда-то не туда, но останавливаться было уже поздно.
В итоге во второй день на моей карте благодаря взятым флагам появилось 500 единиц внутренней валюты, что позволило мне уйти с конференции с толстовкой, картридером и еще некоторым мерчем, а также заопенсорсить библиотеку на Clojure, которую я написал в рамках конференции.
Сокобан
Самым красивым заданием (не считая финального с ботом для игры в танки, до которого я добрался, но не успел решить, ибо окирпичил свою карту на запись) с эстетической точки зрения был Vault Warehouse Management System (он же «Кладовщик», он же «Мудрый крот», он же «Сокобан»). Это псевдографическая игра, в которой нужно толкать условные коробки с провиантом таким образом, чтобы они встали на предназначенные для них в бункере-лабиринте места, и при этом не заблокировать эти самые коробки, случайно прижав их к стенам.
Как это работает? В апплете карты предусмотрено восемь команд: одна для получения текущего состояния поля, четыре команды движения кладовщика на одну клетку в любую из сторон, а также дополнительные команды сброса поля, взятия и проверки флага. Посылая эти команды в карту через картридер, нужно дотолкать ящики в нужные позиции. Всего на пластиковой карте было записано два уровня. И если первый несложно решался вручную, то над вторым нужно было хорошо поломать голову либо воспользоваться готовым солвером (о нем я расскажу чуть дальше).
Гораздо веселее рассматривать это задание как игру. Для того чтобы выиграть в нее, я поставил своей целью подготовить окружение, написать код, запустить приложение и немного поиграть.
Готовим окружение
Чтобы получить возможность взаимодействовать с картридером и, соответственно, пластиковой смарт-картой, нужно установить необходимое программное обеспечение. В качестве дистрибутива я использую Fedora, поэтому для тебя команды могут отличаться, но смысл остается примерно тот же.
Сначала надо поставить и запустить PCSC Lite — демон и консольную утилиту для управления картой. Делается это как-то так:
Дополнительные зависимости при установленном Leiningen нам не понадобятся, но если ты собираешься воплотить что-то аналогичное на Python, то рекомендую не заморачиваться с установкой swig, redhat-rpm-config и прочего, а поставить pyscard из пакетного менеджера твоего дистрибутива:
Отлично. Теперь вставим картридер в разъем USB и запустим утилиту для поиска активных смарт-карт:
В случае успеха при вставке пластиковой смарт-карты с мероприятия ты увидишь что-то такое:
Закрываем утилиту нажатием C-c и приступаем к написанию кода игры.
Пишем код
Тут я подразумеваю, что ты подготовил окружение и прочел книгу Clojure for the Brave and True, чтобы познакомиться с чудесным языком программирования Clojure.
В командной строке создадим новое приложение в папке с проектами и перейдем в него:
Так как я использую Fedora, то для моего дистрибутива необходимо указать путь /usr/lib64/libpcsclite.so.1 к разделяемой библиотеке для управления смарт-картами в проектном файле, добавив его в системное свойство sun.security.smartcardio.library (в проектном файле задаются по ключу jvm-opts). Также я добавляю дополнительные зависимости org.clojure/tools.namespace для REPL driven development и jline для чтения символов с клавиатуры. После внесенных изменений мой файл project.clj выглядит так:
REPL driven development
Что такое REPL driven development? Это итеративный подход к созданию программного обеспечения, который подразумевает разработку без перезапуска разрабатываемой программы, когда твой интерпретатор напрямую подключен к сердцу программы, а при сохранении кода в редакторе код автоматически приезжает в контекст выполняемого приложения. Подробно о том, что это такое и почему каждому стоит хотя бы раз попробовать такой подход, расписал у себя в блоге Никита Прокопов.
В файле src/scard_sokoban/core.clj создадим минимальный boilerplate для того, чтобы далее можно было итерациями добавлять функции, получив в итоге рабочую программу.
Мы добавили импортирование нужных классов Java: Terminal для обработки нажатия клавиш и два класса из пакета javax.smartcardio. Для меня стало полной неожиданностью их наличие в дефолтной поставке JVM. Да-да, для работы со смарт-картами можно просто импортировать два Java-класса и сразу же использовать их — ничего больше и не требуется.
В своем редакторе Emacs я сохраняю по C-x C-s и запускаю приложение с помощью CIDER, используя сочетание C-c M-j, что вызовет команду cider-jack-in и запустит (предварительно скачав зависимости) приложение, открыв при этом буфер REPL. Переключаться между буфером редактирования и REPL можно по сочетанию C-x o — проделай эту операцию, чтобы вернуться в буфер редактирования текста и продолжить создание нашего приложения. Для других редакторов способы взаимодействия с REPL отличаются — рекомендую проконсультироваться с документацией к твоему редактору и плагинам Clojure для него.
Работа с картой
Для работы со смарт-картой нам нужна небольшая дополнительная кодовая обвязка вокруг классов Java, чтобы была возможность переиспользовать тот же самый код для других заданий и не писать его заново. Я добавляю к существующему коду вот такой кусок:
Сохранив файл по C-x C-s, я получаю в буфере REPL сообщение
Это значит, что сохраненный файл автоматически загрузился в REPL без ошибок и уже можно вызвать из него функцию (get-smartcard) и посмотреть результат.
Когда картридер отключен, при вызове (get-smartcard) произойдет ошибка PCSCException SCARD_E_NO_READERS_AVAILABLE; при включенном картридере, но не вставленной карте — PCSCException SCARD_E_NO_SMARTCARD. Если все прошло гладко, то вернется запись типа scard_sokoban.core.SmartCard, которую можно продолжать использовать для взаимодействия с картой.
Эта самая запись поддерживает функцию select-applet для выбора апплета с карты (ей в качестве аргумента передается в виде вектора байтов AID — applet ID), функцию transmit, куда передается вектор байтов для вызова какой-либо функции апплета карты, и функцию disconnect для отключения соединения.
Реализация
Давай же напишем тот самый кусок кода, который будет выполнять функцию движения, а затем получать состояние доски со смарт-карты. Выглядит он следующим образом:
Функция sokoban присоединяется к смарт-карте, выбирает апплет, идентификатор которого предоставили организаторы мероприятия, опционально вызывает функцию из аргумента, рисует доску, по 12 колонок в каждой строке (доска 12 на 8), и отсоединяется от карты. Вызовем ее без аргументов, чтобы посмотреть, как выглядит доска.
Теперь нужно добавить функции движения (чтобы не дублировать себя — в виде макроса) и сброса доски (данные, которые нужно передавать в функции, описаны там же, где и само задание, — на сайте мероприятия):
После этого можно из REPL передавать вторым параметром имя функции направления — (sokoban left) или сброса поля — (sokoban reset). На этом можно было бы и закончить, продолжив решать задание с помощью ввода таких команд, но так как мы решили делать игру, то давай еще допишем функцию -main:
Здесь если в качестве аргумента командной строки передана последовательность, состоящая из символов [LlRrUuDd], то она будет исполнена в виде сценария движения кладовщика. В ином случае (или если последовательность закончилась) управление производится с помощью клавиш i, j, k, l.
Первый уровень игры пройдем вручную, запустив нашу игру из командной строки без аргументов:
Солвер
Второй уровень пройти не так просто, поэтому, чтобы ускорить дело, найдем готовое решение. По запросу в гугле «sokoban solution c++» нашлась такая ссылка — это то, что нам нужно. В идеале можно пристыковать это решение к коду на Clojure через JNI-интерфейс (да, такое работает с использованием небольшого количества кода на Java, я проверял), но в рамках этой статьи я не буду этого делать (но ты можешь попробовать).
Подставив наше поле второго уровня в переменную level кода и собрав его (предварительно доставив boost из пакетного менеджера), запустим приложение, скопируем полученный сценарий и вставим его аргументом командной строки нашей игры.
После того как сценарий второго уровня завершится и ящики окажутся на своих местах, вместо доски нам отобразится заветный флаг. Чтобы поиграть еще, можно сбросить состояние апплета с помощью (sokoban reset) или в игре нажав клавишу пробела.
Заключительное слово
Я рекомендую тебе на досуге подробнее ознакомиться с концепцией REPL driven development и поковыряться с Clojure. Да, в том числе если ты никогда не планируешь использовать его в продакшене.
Я понимаю, что вероятность наличия у тебя той самой смарт-карты и картридера довольно мала, и поэтому предлагаю тебе в качестве домашнего задания написать без использования этого железа функции формирования карты, перемещения кладовщика и ящиков с провиантом (по возможности без использования глобального состояния).
И небольшой совет: если ты так же, как и я, никогда не позиционировал себя хакером, то все равно старайся принимать участие в подобного рода мероприятиях. Во-первых, это весело и занимательно, а во-вторых, никто не осудит, если что-то не получится. В крайнем случае обретешь неоценимый опыт, а в идеальном еще и мерч, строчку в портфолио и радость от того, что у тебя все получилось. Ближайшим крупным мероприятием, где можно понажимать на кнопки, мне видится Positive Hack Days 21–22 мая — встретимся там.
Веселых экспериментов!
(c) Сергей Собко, взял с хакер ру
15–16 ноября в Москве прошла конференция OFFZONE Moscow, где участникам выдали пластиковые смарт-карты и предложили задания на получение флагов с их помощью. Для решения этих задач требовался картридер, который можно было арендовать или купить за внутреннюю валюту. На сайте мероприятия выложили задания и дали управляющие последовательности, с помощью которых можно выполнять функции апплетов, записанных на карту. Разберем задание по шагам.
Почему Clojure?
Для решения таска предлагалось взять Python-библиотеку pyscard. Но решать такого рода задачи гораздо продуктивнее, используя полноценный REPL driven development, с чем нам может помочь Clojure.
Рандомный посетитель
Началось все с того, что я узнал о возможности взять картридер и, используя некоторые APDU-команды протокола ISO/IEC 7816, выполнять задания с пластиковой смарт-карты, которая была выдана всем участникам вместе с бейджами. Это мне очень сильно напомнило PHDays V, когда я, как обычный посетитель конференции, просто подошел к стенду с электроподстанцией, поснифал пакеты через Wireshark, за ночь написал наивную имплементацию протокола IEC 61850 на Scapy, а на следующий день устроил с ноутбука DoS-атаку всему стенду SCADA (до плавки электропроводов я тогда, конечно же, не добрался).
В этот раз я решил попробовать пойти тем же путем: в первый день поэкспериментировать с протоколом, попробовать решить хотя бы тренировочную задачу на Python, используя библиотеку pyscard, как рекомендовали организаторы конференции, а ночью порешать все остальные задачи, которые получится. В шестом часу утра я понял, что свернул куда-то не туда, но останавливаться было уже поздно.
В итоге во второй день на моей карте благодаря взятым флагам появилось 500 единиц внутренней валюты, что позволило мне уйти с конференции с толстовкой, картридером и еще некоторым мерчем, а также заопенсорсить библиотеку на Clojure, которую я написал в рамках конференции.
Сокобан
Самым красивым заданием (не считая финального с ботом для игры в танки, до которого я добрался, но не успел решить, ибо окирпичил свою карту на запись) с эстетической точки зрения был Vault Warehouse Management System (он же «Кладовщик», он же «Мудрый крот», он же «Сокобан»). Это псевдографическая игра, в которой нужно толкать условные коробки с провиантом таким образом, чтобы они встали на предназначенные для них в бункере-лабиринте места, и при этом не заблокировать эти самые коробки, случайно прижав их к стенам.
Как это работает? В апплете карты предусмотрено восемь команд: одна для получения текущего состояния поля, четыре команды движения кладовщика на одну клетку в любую из сторон, а также дополнительные команды сброса поля, взятия и проверки флага. Посылая эти команды в карту через картридер, нужно дотолкать ящики в нужные позиции. Всего на пластиковой карте было записано два уровня. И если первый несложно решался вручную, то над вторым нужно было хорошо поломать голову либо воспользоваться готовым солвером (о нем я расскажу чуть дальше).
Гораздо веселее рассматривать это задание как игру. Для того чтобы выиграть в нее, я поставил своей целью подготовить окружение, написать код, запустить приложение и немного поиграть.
Готовим окружение
Чтобы получить возможность взаимодействовать с картридером и, соответственно, пластиковой смарт-картой, нужно установить необходимое программное обеспечение. В качестве дистрибутива я использую Fedora, поэтому для тебя команды могут отличаться, но смысл остается примерно тот же.
Сначала надо поставить и запустить PCSC Lite — демон и консольную утилиту для управления картой. Делается это как-то так:
Код:
sudo dnf install -y pcsc-lite pcsc-tools
sudo service pcscd start
Код:
sudo dnf install -y python2-pyscard python3-pyscard
Код:
pcsc_scan
Закрываем утилиту нажатием C-c и приступаем к написанию кода игры.
Пишем код
Тут я подразумеваю, что ты подготовил окружение и прочел книгу Clojure for the Brave and True, чтобы познакомиться с чудесным языком программирования Clojure.
В командной строке создадим новое приложение в папке с проектами и перейдем в него:
Код:
lein new app scard-sokoban
cd scard-sokoban
Код:
(defproject scard-sokoban "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.8.0"]
[org.clojure/tools.namespace "0.2.11"]
[jline "0.9.94"]]
:jvm-opts ["-Dsun.security.smartcardio.library=/usr/lib64/libpcsclite.so.1"]
:main ^:skip-aot scard-sokoban.core
:target-path "target/%s"
:profiles {:uberjar {:aot :all}})
REPL driven development
Что такое REPL driven development? Это итеративный подход к созданию программного обеспечения, который подразумевает разработку без перезапуска разрабатываемой программы, когда твой интерпретатор напрямую подключен к сердцу программы, а при сохранении кода в редакторе код автоматически приезжает в контекст выполняемого приложения. Подробно о том, что это такое и почему каждому стоит хотя бы раз попробовать такой подход, расписал у себя в блоге Никита Прокопов.
В файле src/scard_sokoban/core.clj создадим минимальный boilerplate для того, чтобы далее можно было итерациями добавлять функции, получив в итоге рабочую программу.
Код:
(ns scard-sokoban.core
(:import (javax.smartcardio TerminalFactory
CommandAPDU)
(jline Terminal))
(:gen-class))
В своем редакторе Emacs я сохраняю по C-x C-s и запускаю приложение с помощью CIDER, используя сочетание C-c M-j, что вызовет команду cider-jack-in и запустит (предварительно скачав зависимости) приложение, открыв при этом буфер REPL. Переключаться между буфером редактирования и REPL можно по сочетанию C-x o — проделай эту операцию, чтобы вернуться в буфер редактирования текста и продолжить создание нашего приложения. Для других редакторов способы взаимодействия с REPL отличаются — рекомендую проконсультироваться с документацией к твоему редактору и плагинам Clojure для него.
Работа с картой
Для работы со смарт-картой нам нужна небольшая дополнительная кодовая обвязка вокруг классов Java, чтобы была возможность переиспользовать тот же самый код для других заданий и не писать его заново. Я добавляю к существующему коду вот такой кусок:
Код:
(defprotocol SmartCardProto
(disconnect [card])
(transmit [card data])
(select-applet [card aid]))
(def ^:dynamic *select-cmd* [0x00 0xA4 0x04 0x00])
(defrecord SmartCard [conn channel]
SmartCardProto
(disconnect [card] (.disconnect conn false))
(transmit [card data] (.transmit channel (-> data
byte-array
(CommandAPDU.))))
(select-applet [card aid] (let [select-management *select-cmd*
data (concat select-management
[(count aid)]
aid)]
(.transmit card data))))
(defn get-smartcard []
(let [terminal-factory (TerminalFactory/getDefault)
terminal (-> terminal-factory
.terminals
.list
first)
conn (.connect terminal "*")
channel (.getBasicChannel conn)]
(->SmartCard conn channel)))
Код:
:reloading (scard-sokoban.core scard-sokoban.core-test)
Когда картридер отключен, при вызове (get-smartcard) произойдет ошибка PCSCException SCARD_E_NO_READERS_AVAILABLE; при включенном картридере, но не вставленной карте — PCSCException SCARD_E_NO_SMARTCARD. Если все прошло гладко, то вернется запись типа scard_sokoban.core.SmartCard, которую можно продолжать использовать для взаимодействия с картой.
Эта самая запись поддерживает функцию select-applet для выбора апплета с карты (ей в качестве аргумента передается в виде вектора байтов AID — applet ID), функцию transmit, куда передается вектор байтов для вызова какой-либо функции апплета карты, и функцию disconnect для отключения соединения.
Реализация
Давай же напишем тот самый кусок кода, который будет выполнять функцию движения, а затем получать состояние доски со смарт-карты. Выглядит он следующим образом:
Код:
(defn bytes-to-str [data]
(apply str (map char data)))
(defn sokoban [& [function]]
(let [card (get-smartcard)
result (atom nil)
aid [0x4f 0x46 0x46 0x5a 0x4f 0x4e 0x45 0x32 0x10 0x01]
get-state [0x10 0x30 0x00 0x00 0x00]]
(.select-applet card aid)
(when function
(reset! result (function card)))
(doall (for [line (->> (.transmit card get-state)
.getData
bytes-to-str
(partition 12)
(map (partial apply str)))]
(println line)))
(.disconnect card)
@result))
Теперь нужно добавить функции движения (чтобы не дублировать себя — в виде макроса) и сброса доски (данные, которые нужно передавать в функции, описаны там же, где и само задание, — на сайте мероприятия):
Код:
(defmacro defdirection [function byte]
`(defn ~function [card#]
(.transmit card# [0x10 0x20 0x00 0x00 0x01 ~byte])))
(defdirection left 0x61)
(defdirection right 0x64)
(defdirection up 0x77)
(defdirection down 0x73)
(defn reset [card]
(.transmit card [0x10 0x40 0x00 0x00 0x00]))
Код:
(defn -main [& [steps]]
(let [term (Terminal/getTerminal)
remaining-steps (atom steps)]
(while true
(print "\033[H\033[2J")
(sokoban (case (if @remaining-steps
(let [[next-step & others] @remaining-steps]
(reset! remaining-steps others)
(case next-step
\u 105 \U 105 \l 106 \L 106
\d 107 \D 107 \r 108 \R 108))
(.readCharacter term System/in))
105 up 106 left
107 down 108 right
32 reset
identity))
(when @remaining-steps
(Thread/sleep 100)))))
Первый уровень игры пройдем вручную, запустив нашу игру из командной строки без аргументов:
Код:
lein run
Солвер
Второй уровень пройти не так просто, поэтому, чтобы ускорить дело, найдем готовое решение. По запросу в гугле «sokoban solution c++» нашлась такая ссылка — это то, что нам нужно. В идеале можно пристыковать это решение к коду на Clojure через JNI-интерфейс (да, такое работает с использованием небольшого количества кода на Java, я проверял), но в рамках этой статьи я не буду этого делать (но ты можешь попробовать).
Подставив наше поле второго уровня в переменную level кода и собрав его (предварительно доставив boost из пакетного менеджера), запустим приложение, скопируем полученный сценарий и вставим его аргументом командной строки нашей игры.
После того как сценарий второго уровня завершится и ящики окажутся на своих местах, вместо доски нам отобразится заветный флаг. Чтобы поиграть еще, можно сбросить состояние апплета с помощью (sokoban reset) или в игре нажав клавишу пробела.
Заключительное слово
Я рекомендую тебе на досуге подробнее ознакомиться с концепцией REPL driven development и поковыряться с Clojure. Да, в том числе если ты никогда не планируешь использовать его в продакшене.
Я понимаю, что вероятность наличия у тебя той самой смарт-карты и картридера довольно мала, и поэтому предлагаю тебе в качестве домашнего задания написать без использования этого железа функции формирования карты, перемещения кладовщика и ящиков с провиантом (по возможности без использования глобального состояния).
И небольшой совет: если ты так же, как и я, никогда не позиционировал себя хакером, то все равно старайся принимать участие в подобного рода мероприятиях. Во-первых, это весело и занимательно, а во-вторых, никто не осудит, если что-то не получится. В крайнем случае обретешь неоценимый опыт, а в идеальном еще и мерч, строчку в портфолио и радость от того, что у тебя все получилось. Ближайшим крупным мероприятием, где можно понажимать на кнопки, мне видится Positive Hack Days 21–22 мая — встретимся там.
Веселых экспериментов!
(c) Сергей Собко, взял с хакер ру