• XSS.stack #1 – первый литературный журнал от юзеров форума

Карточный sokoban. Выигрываем в смарт-карты вместе с Clojure

tabac

CPU register
Пользователь
Регистрация
30.09.2018
Сообщения
1 610
Решения
1
Реакции
3 332
Карточный sokoban. Выигрываем в смарт-карты вместе с Clojure

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 (он же «Кладовщик», он же «Мудрый крот», он же «Сокобан»). Это псевдографическая игра, в которой нужно толкать условные коробки с провиантом таким образом, чтобы они встали на предназначенные для них в бункере-лабиринте места, и при этом не заблокировать эти самые коробки, случайно прижав их к стенам.

Как это работает? В апплете карты предусмотрено восемь команд: одна для получения текущего состояния поля, четыре команды движения кладовщика на одну клетку в любую из сторон, а также дополнительные команды сброса поля, взятия и проверки флага. Посылая эти команды в карту через картридер, нужно дотолкать ящики в нужные позиции. Всего на пластиковой карте было записано два уровня. И если первый несложно решался вручную, то над вторым нужно было хорошо поломать голову либо воспользоваться готовым солвером (о нем я расскажу чуть дальше).

sokoban-laptop.jpg


Гораздо веселее рассматривать это задание как игру. Для того чтобы выиграть в нее, я поставил своей целью подготовить окружение, написать код, запустить приложение и немного поиграть.


Готовим окружение
Чтобы получить возможность взаимодействовать с картридером и, соответственно, пластиковой смарт-картой, нужно установить необходимое программное обеспечение. В качестве дистрибутива я использую Fedora, поэтому для тебя команды могут отличаться, но смысл остается примерно тот же.

Сначала надо поставить и запустить PCSC Lite — демон и консольную утилиту для управления картой. Делается это как-то так:
Код:
sudo dnf install -y pcsc-lite pcsc-tools
sudo service pcscd start
Дополнительные зависимости при установленном Leiningen нам не понадобятся, но если ты собираешься воплотить что-то аналогичное на Python, то рекомендую не заморачиваться с установкой swig, redhat-rpm-config и прочего, а поставить pyscard из пакетного менеджера твоего дистрибутива:
Код:
sudo dnf install -y python2-pyscard python3-pyscard
Отлично. Теперь вставим картридер в разъем USB и запустим утилиту для поиска активных смарт-карт:
Код:
pcsc_scan
В случае успеха при вставке пластиковой смарт-карты с мероприятия ты увидишь что-то такое:

sokoban-scan.png


Закрываем утилиту нажатием C-c и приступаем к написанию кода игры.


Пишем код
Тут я подразумеваю, что ты подготовил окружение и прочел книгу Clojure for the Brave and True, чтобы познакомиться с чудесным языком программирования Clojure.

В командной строке создадим новое приложение в папке с проектами и перейдем в него:
Код:
lein new app scard-sokoban
cd scard-sokoban
Так как я использую Fedora, то для моего дистрибутива необходимо указать путь /usr/lib64/libpcsclite.so.1 к разделяемой библиотеке для управления смарт-картами в проектном файле, добавив его в системное свойство sun.security.smartcardio.library (в проектном файле задаются по ключу jvm-opts). Также я добавляю дополнительные зависимости org.clojure/tools.namespace для REPL driven development и jline для чтения символов с клавиатуры. После внесенных изменений мой файл project.clj выглядит так:
Код:
(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))
Мы добавили импортирование нужных классов 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, чтобы была возможность переиспользовать тот же самый код для других заданий и не писать его заново. Я добавляю к существующему коду вот такой кусок:
Код:
(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)))
Сохранив файл по C-x C-s, я получаю в буфере REPL сообщение
Код:
:reloading (scard-sokoban.core scard-sokoban.core-test)
Это значит, что сохраненный файл автоматически загрузился в REPL без ошибок и уже можно вызвать из него функцию (get-smartcard) и посмотреть результат.

Когда картридер отключен, при вызове (get-smartcard) произойдет ошибка PCSCException SCARD_E_NO_READERS_AVAILABLE; при включенном картридере, но не вставленной карте — PCSCException SCARD_E_NO_SMARTCARD. Если все прошло гладко, то вернется запись типа scard_sokoban.core.SmartCard, которую можно продолжать использовать для взаимодействия с картой.

sokoban-repl.png


Эта самая запись поддерживает функцию 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))
Функция sokoban присоединяется к смарт-карте, выбирает апплет, идентификатор которого предоставили организаторы мероприятия, опционально вызывает функцию из аргумента, рисует доску, по 12 колонок в каждой строке (доска 12 на 8), и отсоединяется от карты. Вызовем ее без аргументов, чтобы посмотреть, как выглядит доска.

sokoban-board.png


Теперь нужно добавить функции движения (чтобы не дублировать себя — в виде макроса) и сброса доски (данные, которые нужно передавать в функции, описаны там же, где и само задание, — на сайте мероприятия):
Код:
(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]))
После этого можно из REPL передавать вторым параметром имя функции направления — (sokoban left) или сброса поля — (sokoban reset). На этом можно было бы и закончить, продолжив решать задание с помощью ввода таких команд, но так как мы решили делать игру, то давай еще допишем функцию -main:
Код:
(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)))))
Здесь если в качестве аргумента командной строки передана последовательность, состоящая из символов [LlRrUuDd], то она будет исполнена в виде сценария движения кладовщика. В ином случае (или если последовательность закончилась) управление производится с помощью клавиш i, j, k, l.

Первый уровень игры пройдем вручную, запустив нашу игру из командной строки без аргументов:
Код:
lein run

Солвер
Второй уровень пройти не так просто, поэтому, чтобы ускорить дело, найдем готовое решение. По запросу в гугле «sokoban solution c++» нашлась такая ссылка — это то, что нам нужно. В идеале можно пристыковать это решение к коду на Clojure через JNI-интерфейс (да, такое работает с использованием небольшого количества кода на Java, я проверял), но в рамках этой статьи я не буду этого делать (но ты можешь попробовать).

Подставив наше поле второго уровня в переменную level кода и собрав его (предварительно доставив boost из пакетного менеджера), запустим приложение, скопируем полученный сценарий и вставим его аргументом командной строки нашей игры.

sokoban-scenario.png


После того как сценарий второго уровня завершится и ящики окажутся на своих местах, вместо доски нам отобразится заветный флаг. Чтобы поиграть еще, можно сбросить состояние апплета с помощью (sokoban reset) или в игре нажав клавишу пробела.


Заключительное слово
Я рекомендую тебе на досуге подробнее ознакомиться с концепцией REPL driven development и поковыряться с Clojure. Да, в том числе если ты никогда не планируешь использовать его в продакшене.

Я понимаю, что вероятность наличия у тебя той самой смарт-карты и картридера довольно мала, и поэтому предлагаю тебе в качестве домашнего задания написать без использования этого железа функции формирования карты, перемещения кладовщика и ящиков с провиантом (по возможности без использования глобального состояния).

И небольшой совет: если ты так же, как и я, никогда не позиционировал себя хакером, то все равно старайся принимать участие в подобного рода мероприятиях. Во-первых, это весело и занимательно, а во-вторых, никто не осудит, если что-то не получится. В крайнем случае обретешь неоценимый опыт, а в идеальном еще и мерч, строчку в портфолио и радость от того, что у тебя все получилось. Ближайшим крупным мероприятием, где можно понажимать на кнопки, мне видится Positive Hack Days 21–22 мая — встретимся там.

Веселых экспериментов!


(c) Сергей Собко, взял с хакер ру
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх