Фраза звучит несколько странно? Спасибо техническому прогрессу — не так давно «сфотографировать на телефон» звучало не менее странно.
В конце прошлого года я купил паяльную станцию, уже успевшую получить ярлык «народная». Её достоинства: удобные жала-картриджи T12, приличная мощность (до 72W в теории), быстрый нагрев (единицы секунд), невысокая цена. (Подробнее ознакомиться со станцией можно в этом шикарном обзоре)
Купил я самую последнюю версию hardware 2.1s, и немного расстроился, увидев что прошивка старая. Разумеется руки зачесались обновить. Зная что «сердцем» паяльной станции является STM32F103C8 (популярный микропроцессор ARM Cortex-M3 производства STMicroelectronics) — тем интереснее было покопаться, т.к. я когда-то уже моргал светодиодом на STM32F4Discovery.
Тут же были припаяны 4 провода SWD интерфейса, подключен программатор, залита прошивка.
И… Станция потребовала активацию!
Чтобы было понятнее, расскажу пару слов о разработчике этой станции. Вся информация почерпнута из интернета (и, частично, из самой прошивки). Железо и прошивку разработал некий китайский товарищ ZhongGuoxin (38827251@qq.com) приблизительно в 2014 году. Разработка активно шла в закрытой группе какого-то китайского форума. Видимо понимая тщётность попыток защитить разработку от клонирования своими «земляками», он сделал защиту в прошивке. Прошивка распространялась в виде бинарных модулей, и её мог залить в свежеспаянный клон любой желающий.
Почему именно так? Ведь есть гораздо более надёжные схемы, с распространением уже прошитых МК и обновлением зашифрованными BLOB-ами. Не знаю. Видимо тонкий баланс между затратами времени, удобством, популярностью и доходом. При старте прошивка генерирует коды ID1 и ID2, которые автор обменивает на ключ активации RG1 и RG2 за символическую сумму в 9 юаней (~80 рублей).
Но как можно защитить прошивку, если злоумышленник имеет к ней доступ? Сейчас узнаем.
К сожалению, моих кодов активации, честно купленных вместе с устройством, мне не досталось. Забегая вперед скажу, что помимо обновления затребовать эти коды может перепайка микропроцессора (если пожгли ему ноги) или замена EEPROM. И такие случаи уже были неоднократно.
Т.к. владею своим экземпляром я правомерно, то в полном соответствии с ст. 1280 ГК РФ, могу «осуществлять действия, необходимые для функционирования программы»
Прошивка была загружена в IDA. Очень быстро найдено место где вводится код активации. Чтобы убедиться что я на верном пути — я пропатчил аргумент у функции, которую назвал DisplayString. Залил пропатченную версию. Не стартует!
Ну ничего страшного, наверное неверно понял назначение функции. Откатываю изменения. Патчу выводимый текст. Не стартует!
Тааак. Есть проверка целостности. Нужен отладчик. И мы её поймаем аппаратной точкой останова! В качестве отладчиков перебрал несколько вариантов, остановился на связке IDA gdb + OpenOCD. Сам по себе отладчик в IDA неудобен и глюковат, но возможность интерактивно редактировать дизасм прямо в отладчике перевесила.
Запускаю OpenOCD, ставлю watchpoint на чтение изменённых байт кода. Запускаю. Ватчпоинт не срабатывает… Как же так? Ведь проверка целостности 100% присутствует!
И вот тут я заинтересовался по-настоящему.
Полный размер прошивки 75512 байт. Это довольно много. Код от данных для нас уже отделила IDA. Получилось 48128 байт кода и 27384 байта данных, без учёта маленьких пятен локальных данных, расположенных в коде между функциями — особенность ARM Thumb.
Теперь неплохо бы понять что эти данные означают.
Самое простое — текстовые данные. Названия пунктов меню, жал, копирайты. Их оказалось больше 6кб. Среди текстов меню попались байты, очень похожие на 32 битные адреса в сегменте FLASH. По этим адресам располагались объемные подпрограммы, содержащие в себе вызовы ранее найденной DisplayString. Значит это обработчики пунктов меню. Приличный объём кода опознан.
Продолжаем. Раз экран станции монохромный — логично предположить что графические данные будут представлены в битовом (монохромном) виде. Вспомнив детство с ZX-Spectrum — было решено отображать байты столбцами: так я в 90е искал графику в играх, она заметно отличалась от данных и кода.
Была написана программка viewbin на питоне. Для удобства каждый блок в 256 байт отделён от соседнего. Всего в одном столбце по вертикали располагается ровно килобайт (4 блока). Следующий столбец относится к следующему килобайту.
В правой части хорошо заметны графические закорючки. Но в картинки они складываются плохо. А если наклонить голову вправо? Сначала можно заметить шрифт 8*16 (x*y): первые 8 байт образуют верхнюю половину символа, вторые — его нижнюю половину.
Остальные символы строятся по тому же приципу. Большой шрифт имеет размер 16*32, глифы иероглифов 16*16
Плюс есть ещё несколько служебных символов (значок цельсия, термометр, стрелка, чекбоксы). Суммарно почти 16 кб занято графикой.
Итого из 27 кб данных удалось сходу распознать 22 кб. Неплохо.
Перейдём к коду — 48 килобайт тугого THUMB кода. Он действительно весьма компактный. Например часто встречающаяся операция v1 & (~v2) это всего одна инструкция «BTC».
МК, помимо ARM ядра, содержит кучу периферийных модулей. Всё это хозяйство управляется регистрами, отображёнными на память. Этих регистров тысячи. Они объёдинены в группы по функциональному признаку, каждая группа выровнена.
Плюс т.н. BitBanding — когда доступ к слову (32 bit) означает атомарные установку или сброс единственного бита. Нетрудно прикинуть, что один 32 разрядный регистр займёт 128 байт в области BitBand. Итого регистры занимают совершенно сумасшедший размер адресного пространства.
Работа с периферией заключается в записи или чтении этих регистров. Существует библиотека CMSIS от ARM, прикрывающая голые регистры тонким слоем СИ-шного кода и сопутствующие ей библиотеки от производителей (STM32F10x_StdPeriph_Driver в нашем случае) для периферии.
Из Си кода работа с периферией выглядит как изменение именованного регистра периферии.
В ассемблере же эта работа выглядит как несколько уровней косвенной адресации. Обычных регистров CPU. Достали r1 из r2, по нему другой r0, а потом ещё раз r0 по смещению 0x0C. Что это — неясно совершенно.
После ввода регистрационного кода вызывается несколько функций. Очищается экран. Выводится надпись (на китайском!). После чего вызывается ещё несколько функций. Сверяются два значения. И выводится либо одно, либо другое сообщение (на китайском!). После чего станция виснет.
Предположив что это и есть проверка кода я заменил условие. Ничего не изменилось. Поставил точки останова на вызываемых функциях и их внутренностях. Они вызываются из большого числа мест совершенно с разыми аргументами.
Понятнее не стало. Возвращаемся к началу. Так почему же не сработал WatchPoint подсчёта контроля целостности?
Я догадывался что причина в DMA (Direct Memory Access) контроллере. Тем более что STM32 имеет аппаратный модуль подсчёта CRC — даёшь ему задачу, и он сам читает байты из памяти через DMA, инкрементирует адрес, и считает контрольную сумму. Достаточно лишь дождаться окончания выполнения и прочитать результат из регистра.
Но чтобы исследовать работу с периферией надо корректно определить все регистры по их номерам. Регистров тысячи, их числовая форма скрыта от программиста слоем библиотечного кода. В дизассемблере нашёл константу — поищи что это за регистр. Заниматься прыжками datasheet-дизассемблер мне быстро наскучило. И я написал питон-скрипт для IDA (ссылка на GitHub), который генерирует основную массу регистров. А заодно определяет таблицу векторов прерываний, даёт имена обработчикам, оформляет точку старта и создаёт сегмент SRAM.
Стало чуточку понятнее.
Чтобы восстановить логику работы я предпочитаю начинать с маленьких функций. Они легче для понимания, а полезные ещё и часто используются.
Функции, использующие много битовой арифметики, были определены как Floating Point с помощью «грубой силы»: указатель команд ставился на начало функции. В регистры заносилось HEX представление плавающей точки (например числа «100.0» и «30.0»). (Ссылка на удобный Online конвертер). Результат функции конвертировался обратно в плавающий вид. Если на выходе имеем что-то осмысленное, типа «70.0» или «3.3333» — то смело можем давать имя. Оказалось что часть функций работает с float, часть c double.
Мне очень повезло, и для работы с периферией автор использовал стандартую библиотеку, СИ-код сопоставлялся с дизассемблером идеально.
Споткнулся я на нескольких noreturn функциях, на входе в которые lr указывал «вникуда», имея значение 0xFFFFFFFF, а стек был заполнен странными константами навроде 0x12121212, 0x09090909. Гугл настаивал что это FreeRTOS (Что?!
Операционная система реального времени в… ПАЯЛЬНИКЕ?).
Но вот с исходниками бинарный код сопоставить не получилось. Я проверил 10 версий, от 10.0.0 (2018) до 3.2.4 (2005). Везде сходство было лишь поверхностным. Поэтому ограничился именованием таких фукнций как TaskXXXXX, а общий код, который передавал им управление, как TaskCreate.
Среди опознанных функции стандартной библиотеки оказались DMAInit, DMACmd, DMAGetCurrentDataCounter. Проблема заключалась в том, что вызывались они слишком уж много и часто. А я ленивый.
Поэтому я написал питон API для работы с функциями OpenOCD: Ссылка на GitHub. Команды шлются OpenOCD серверу по telnet протоколу. В качестве примера использования в каталоге «examples/dbgbot» лежит код робота, речь о котором пойдёт ниже.
На основе этого API я сделал робота, исследующего вызовы DMA. Робот реагирует на инициализацию DMA операции в функции DMA_Init. Протоколирует адреса периферии, памяти, направление передачи, количество и размер передаваемых элементов, а так же опциональный признак автоинкремента адресов источника и приёмника.
Но кроме этого я хотел видеть получаемые данные. Например если запрошен рассчёт CRC — то хочу знать что именно там посчиталось. Для этого необходимо пройти всю цепочку вызовов: Init -> Command «enable» -> Wait. Т.е. нужна машина состояний. Которая динамически устанавливает и снимает точки останова в зависимости положения между состояниями.
Для Wait надо ждать когда обнулится аппаратный счётчик. (Ещё есть сигнализация прерываниями, но это не наш случай.) Ждать надо продолжая исполнять команды, т.к. JTAG отладка полностью останавливает ядро микропроцессора.
Проблема оказалась в том, что автор не заморачивался с ожиданием, если давал DMA команду на небольшой объём данных — к тому моменту когда они ему становились нужны чип уже всё сделает. Для меня же это решение означало что я не могу надеяться на вызов DMAGetCurrentDataCounter для отслеживания окончания передачи. И когда этими данными начнут пользоваться я тоже не знал — если получать их слишком поздно они уже могут быть испорчены работой остального кода.
Поэтому пришлось применить эвристику: если размер передачи > 32 байт — будет использована явная проверка. Иначе после выдачи команды на трансфер я пошагово исполню столько инструкций, сколько байт запрошено на передачу.
Это не единственный «хак» — изучив весь код ожиданий, я пришёл к выводу, что он всегда занимает 4 байта после вызова DMAGetCurrentDataCounter.
Поэтому после первого срабатывания этой точки останова — получаем адрес возврата и переставляем точку останова на 4 байта за ним. Ожидание завершено!
Видео работы робота с анализом DMA операций:
Функций, относящихся с посчёту CRC оказалось 8 (ВОСЕМЬ!). Ни одна не содержит параметры в явном виде, используются витиеватые вычисления. Часть из этих функций для рассчёта использует рекурсию, что дополнительно усложняет понимание.
Из дампа, сделанного роботом, отлично видно что константа (я назвал её MasterCRC) копируется из Flash в RAM, потом из RAM в RAM. Считается CRC на две половинки флеша: до этой констаны и после этой константы до самого конца, т.е. даже на незанятую прошивкой область. А это значит что если чип перед прошивкой предварительно не очистить — контрольная сумма не сойдётся!
Полный подсчёт CRC выполняется 4 раза. Ужас! Потом считается CRC на CRC. И CRC на CRC на CRC. Ужас! Сильно в манипуляции с CRC я вникать не стал.
Кроме подсчёта CRC и странных перемещений из памяти в память в логе оказалось получение уникального ID чипа. С помощью DMA. Это очень подозрительно, т.к. ID чипа задаётся при произодстве, он уникален для каждого кристалла — прекрасный кандидат на роль источника данных для формирования регкода.
Подозрение подтвердилось — вызовы вернули мои ID1 и ID2 коды.
Определяя функции из стандартной библиотеки периферии я заметил обработку странной таблицы по адресу 0x800C23C. В ней описывались состояния каждого из 16 пинов всех 7 GPIO портов (вызовы — жуткий копипаст). Стало интересно какие пины для чего используются.
Обратите внимание на подключение EEPROM Flash. Линии I2C заведены на ноги, где нет аппаратного I2C. Странное решение, не правда ли?
Ну тем лучше. Я никогда не работал с I2C. А тут представилась такая возможность пощупать его на уровне ручного управления сигналами на ножках МК! Скачал доку с сайта TI, нарисовал таблицу состояний, набросал код для робота. Чтобы он по изменениям пинов раскодировал передачу.
И тут меня ждал облом. EEPROM не останавливается отладчиком. И хотя теоретически она могла бы ждать мастера, на практике всё разваливается напрочь.
Ок, зная работу протокола даём имена функциям, поднимаясь уровень за уровнём до грозди функций Flash_ReadData / Flash_WriteData, малоотличающихся друг от друга (копипаст?)
Смотрю на ссылки — чтение и запись вызывается из места ввода кода. Интересно! Оказывается 4 байтовый ответный код после ввода пишется по флеш. А потом читается оттуда. Ожидание этих операций я и принял за проверку. А два разных сообщения на китайском — если прочитали тоже что и записали и если нет (аппаратная ошибка?).
Получается что после ввода код не проверяется. Он проверяется всегда при старте станции. И при ошибке проверки вместо основной RTOS задачи паяльника создаётся другая RTOS задача, которая занимается вводом кода и записью его во флеш. Чтож, логично.
Кроме того в соседнх флеш-функциях происходит чтение-запись солидного куска данных. Размером 0x3AE байт. Похоже на настройки. После их чтения вызывается функция вычисления кода по буферу с данными. До зубовного скрежета напоминающую функцию вычисления ID1 и ID2. И даже таблица с данными ещё одна, точно такая же. Зачем использовать такой код для проверки целостности настроек? Взял бы лучше CRC! Постойте, а что если…? Загоняю в гугл константы из таблицы — точно. Это и есть CRC. Точнее CRC16_CCIT. Две копии одной и той же функции. Две копии таблиц. Чтобы враг не догадался.
Итак, мы контролируем чтение из флеша. «reset init». Точка останова быстро приводит нас месту где читается свежезаписанный ответ. Ставим watchpoint на прочитанные данные. «resume» Станция как ни в чём ни бывало показывет интерфейс ввода кода.
Опять DMA? Смотрим лог. Сразу после копирования прочитанного кода из флеш происходит два копирования блоков по 32 байта. Смотрим дизассемблер по адресу возврата из DMA операции. В коде видим после копирования 32 байт вызов странной функции с кучей xor внутри. Ставим точку останова.
При вызове функции видим буфер с какими-то данными, в последних 4 байтах которого лежат уже знакомые ID1 и ID2 станции. На выходе из функции видим 16 битное число. Ещё один вызов — буфер с другими данными, в конце так же лежат ID1 и ID2.
Осталось понять что именно вычисляет эта загадочная функция. А делает она вот что:
Такое ощущение что оригинальную таблицу нарезали на кусочки по 8 элементов, перемешали, и опять склеили. Например 1-ый кусочек стал 0-ым. 5-ый никуда не делся. А на месте 4-го теперь 31-ый. Пишем короткий код для сопоставления, и вот нужные перестановки для сборки этой странной таблицы: 1, 2, 3, 4, 31, 5, 6, 7, 8, 9, 10, 11, 12, 0, 13, 25, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 26, 27, 28, 29, 30.
Переписываем на Python. Проверяем — работает. Для отладки я попросил несколько ребят из интернетов, имеющих такие же станции, дать мне их ID и проверить сгенерированные RG. Проверили ещё на 5 станциях. Результат правильный.
А если бы защита оказалась более простой? Наверное ввёл бы код, и забыл про неё…
Пишите в комментариях, интересно ли вам узнать продолжение истории: реверс алгоритмов работы паяльной станции и ход разработки моего варианта.
P.S.: Специально для этой статьи переписал код генерации ключей на javascript. Online версия, там же есть ссылка на Python версию. Если объяснения были где-то непонятны — можно почерпнуть детали из кода. Ну и, конечно, оживить вашу собственную станцию, заблокированную обновлением или ремонтом железа.
В конце прошлого года я купил паяльную станцию, уже успевшую получить ярлык «народная». Её достоинства: удобные жала-картриджи T12, приличная мощность (до 72W в теории), быстрый нагрев (единицы секунд), невысокая цена. (Подробнее ознакомиться со станцией можно в этом шикарном обзоре)
Купил я самую последнюю версию hardware 2.1s, и немного расстроился, увидев что прошивка старая. Разумеется руки зачесались обновить. Зная что «сердцем» паяльной станции является STM32F103C8 (популярный микропроцессор ARM Cortex-M3 производства STMicroelectronics) — тем интереснее было покопаться, т.к. я когда-то уже моргал светодиодом на STM32F4Discovery.
Тут же были припаяны 4 провода SWD интерфейса, подключен программатор, залита прошивка.
И… Станция потребовала активацию!
Чтобы было понятнее, расскажу пару слов о разработчике этой станции. Вся информация почерпнута из интернета (и, частично, из самой прошивки). Железо и прошивку разработал некий китайский товарищ ZhongGuoxin (38827251@qq.com) приблизительно в 2014 году. Разработка активно шла в закрытой группе какого-то китайского форума. Видимо понимая тщётность попыток защитить разработку от клонирования своими «земляками», он сделал защиту в прошивке. Прошивка распространялась в виде бинарных модулей, и её мог залить в свежеспаянный клон любой желающий.
Почему именно так? Ведь есть гораздо более надёжные схемы, с распространением уже прошитых МК и обновлением зашифрованными BLOB-ами. Не знаю. Видимо тонкий баланс между затратами времени, удобством, популярностью и доходом. При старте прошивка генерирует коды ID1 и ID2, которые автор обменивает на ключ активации RG1 и RG2 за символическую сумму в 9 юаней (~80 рублей).
Но как можно защитить прошивку, если злоумышленник имеет к ней доступ? Сейчас узнаем.
К сожалению, моих кодов активации, честно купленных вместе с устройством, мне не досталось. Забегая вперед скажу, что помимо обновления затребовать эти коды может перепайка микропроцессора (если пожгли ему ноги) или замена EEPROM. И такие случаи уже были неоднократно.
Т.к. владею своим экземпляром я правомерно, то в полном соответствии с ст. 1280 ГК РФ, могу «осуществлять действия, необходимые для функционирования программы»
Первый блин комом
Прошивка была загружена в IDA. Очень быстро найдено место где вводится код активации. Чтобы убедиться что я на верном пути — я пропатчил аргумент у функции, которую назвал DisplayString. Залил пропатченную версию. Не стартует!
Ну ничего страшного, наверное неверно понял назначение функции. Откатываю изменения. Патчу выводимый текст. Не стартует!
Тааак. Есть проверка целостности. Нужен отладчик. И мы её поймаем аппаратной точкой останова! В качестве отладчиков перебрал несколько вариантов, остановился на связке IDA gdb + OpenOCD. Сам по себе отладчик в IDA неудобен и глюковат, но возможность интерактивно редактировать дизасм прямо в отладчике перевесила.
Запускаю OpenOCD, ставлю watchpoint на чтение изменённых байт кода. Запускаю. Ватчпоинт не срабатывает… Как же так? Ведь проверка целостности 100% присутствует!
И вот тут я заинтересовался по-настоящему.
Исследуем данные
Полный размер прошивки 75512 байт. Это довольно много. Код от данных для нас уже отделила IDA. Получилось 48128 байт кода и 27384 байта данных, без учёта маленьких пятен локальных данных, расположенных в коде между функциями — особенность ARM Thumb.
Теперь неплохо бы понять что эти данные означают.
Самое простое — текстовые данные. Названия пунктов меню, жал, копирайты. Их оказалось больше 6кб. Среди текстов меню попались байты, очень похожие на 32 битные адреса в сегменте FLASH. По этим адресам располагались объемные подпрограммы, содержащие в себе вызовы ранее найденной DisplayString. Значит это обработчики пунктов меню. Приличный объём кода опознан.
Продолжаем. Раз экран станции монохромный — логично предположить что графические данные будут представлены в битовом (монохромном) виде. Вспомнив детство с ZX-Spectrum — было решено отображать байты столбцами: так я в 90е искал графику в играх, она заметно отличалась от данных и кода.
Была написана программка viewbin на питоне. Для удобства каждый блок в 256 байт отделён от соседнего. Всего в одном столбце по вертикали располагается ровно килобайт (4 блока). Следующий столбец относится к следующему килобайту.
В правой части хорошо заметны графические закорючки. Но в картинки они складываются плохо. А если наклонить голову вправо? Сначала можно заметить шрифт 8*16 (x*y): первые 8 байт образуют верхнюю половину символа, вторые — его нижнюю половину.
Остальные символы строятся по тому же приципу. Большой шрифт имеет размер 16*32, глифы иероглифов 16*16
Плюс есть ещё несколько служебных символов (значок цельсия, термометр, стрелка, чекбоксы). Суммарно почти 16 кб занято графикой.
Итого из 27 кб данных удалось сходу распознать 22 кб. Неплохо.
Перейдём к коду — 48 килобайт тугого THUMB кода. Он действительно весьма компактный. Например часто встречающаяся операция v1 & (~v2) это всего одна инструкция «BTC».
Что такое ARM Cortex M3
МК, помимо ARM ядра, содержит кучу периферийных модулей. Всё это хозяйство управляется регистрами, отображёнными на память. Этих регистров тысячи. Они объёдинены в группы по функциональному признаку, каждая группа выровнена.
Плюс т.н. BitBanding — когда доступ к слову (32 bit) означает атомарные установку или сброс единственного бита. Нетрудно прикинуть, что один 32 разрядный регистр займёт 128 байт в области BitBand. Итого регистры занимают совершенно сумасшедший размер адресного пространства.
Работа с периферией заключается в записи или чтении этих регистров. Существует библиотека CMSIS от ARM, прикрывающая голые регистры тонким слоем СИ-шного кода и сопутствующие ей библиотеки от производителей (STM32F10x_StdPeriph_Driver в нашем случае) для периферии.
Из Си кода работа с периферией выглядит как изменение именованного регистра периферии.
В ассемблере же эта работа выглядит как несколько уровней косвенной адресации. Обычных регистров CPU. Достали r1 из r2, по нему другой r0, а потом ещё раз r0 по смещению 0x0C. Что это — неясно совершенно.
Исследуем код
После ввода регистрационного кода вызывается несколько функций. Очищается экран. Выводится надпись (на китайском!). После чего вызывается ещё несколько функций. Сверяются два значения. И выводится либо одно, либо другое сообщение (на китайском!). После чего станция виснет.
Предположив что это и есть проверка кода я заменил условие. Ничего не изменилось. Поставил точки останова на вызываемых функциях и их внутренностях. Они вызываются из большого числа мест совершенно с разыми аргументами.
Понятнее не стало. Возвращаемся к началу. Так почему же не сработал WatchPoint подсчёта контроля целостности?
Я догадывался что причина в DMA (Direct Memory Access) контроллере. Тем более что STM32 имеет аппаратный модуль подсчёта CRC — даёшь ему задачу, и он сам читает байты из памяти через DMA, инкрементирует адрес, и считает контрольную сумму. Достаточно лишь дождаться окончания выполнения и прочитать результат из регистра.
Но чтобы исследовать работу с периферией надо корректно определить все регистры по их номерам. Регистров тысячи, их числовая форма скрыта от программиста слоем библиотечного кода. В дизассемблере нашёл константу — поищи что это за регистр. Заниматься прыжками datasheet-дизассемблер мне быстро наскучило. И я написал питон-скрипт для IDA (ссылка на GitHub), который генерирует основную массу регистров. А заодно определяет таблицу векторов прерываний, даёт имена обработчикам, оформляет точку старта и создаёт сегмент SRAM.
Стало чуточку понятнее.
Чтобы восстановить логику работы я предпочитаю начинать с маленьких функций. Они легче для понимания, а полезные ещё и часто используются.
Функции, использующие много битовой арифметики, были определены как Floating Point с помощью «грубой силы»: указатель команд ставился на начало функции. В регистры заносилось HEX представление плавающей точки (например числа «100.0» и «30.0»). (Ссылка на удобный Online конвертер). Результат функции конвертировался обратно в плавающий вид. Если на выходе имеем что-то осмысленное, типа «70.0» или «3.3333» — то смело можем давать имя. Оказалось что часть функций работает с float, часть c double.
Мне очень повезло, и для работы с периферией автор использовал стандартую библиотеку, СИ-код сопоставлялся с дизассемблером идеально.
Споткнулся я на нескольких noreturn функциях, на входе в которые lr указывал «вникуда», имея значение 0xFFFFFFFF, а стек был заполнен странными константами навроде 0x12121212, 0x09090909. Гугл настаивал что это FreeRTOS (Что?!
Операционная система реального времени в… ПАЯЛЬНИКЕ?).
Но вот с исходниками бинарный код сопоставить не получилось. Я проверил 10 версий, от 10.0.0 (2018) до 3.2.4 (2005). Везде сходство было лишь поверхностным. Поэтому ограничился именованием таких фукнций как TaskXXXXX, а общий код, который передавал им управление, как TaskCreate.
Исследуем работу периферии или Автоматизация отладки
Среди опознанных функции стандартной библиотеки оказались DMAInit, DMACmd, DMAGetCurrentDataCounter. Проблема заключалась в том, что вызывались они слишком уж много и часто. А я ленивый.
Поэтому я написал питон API для работы с функциями OpenOCD: Ссылка на GitHub. Команды шлются OpenOCD серверу по telnet протоколу. В качестве примера использования в каталоге «examples/dbgbot» лежит код робота, речь о котором пойдёт ниже.
На основе этого API я сделал робота, исследующего вызовы DMA. Робот реагирует на инициализацию DMA операции в функции DMA_Init. Протоколирует адреса периферии, памяти, направление передачи, количество и размер передаваемых элементов, а так же опциональный признак автоинкремента адресов источника и приёмника.
Но кроме этого я хотел видеть получаемые данные. Например если запрошен рассчёт CRC — то хочу знать что именно там посчиталось. Для этого необходимо пройти всю цепочку вызовов: Init -> Command «enable» -> Wait. Т.е. нужна машина состояний. Которая динамически устанавливает и снимает точки останова в зависимости положения между состояниями.
Для Wait надо ждать когда обнулится аппаратный счётчик. (Ещё есть сигнализация прерываниями, но это не наш случай.) Ждать надо продолжая исполнять команды, т.к. JTAG отладка полностью останавливает ядро микропроцессора.
Проблема оказалась в том, что автор не заморачивался с ожиданием, если давал DMA команду на небольшой объём данных — к тому моменту когда они ему становились нужны чип уже всё сделает. Для меня же это решение означало что я не могу надеяться на вызов DMAGetCurrentDataCounter для отслеживания окончания передачи. И когда этими данными начнут пользоваться я тоже не знал — если получать их слишком поздно они уже могут быть испорчены работой остального кода.
Поэтому пришлось применить эвристику: если размер передачи > 32 байт — будет использована явная проверка. Иначе после выдачи команды на трансфер я пошагово исполню столько инструкций, сколько байт запрошено на передачу.
Это не единственный «хак» — изучив весь код ожиданий, я пришёл к выводу, что он всегда занимает 4 байта после вызова DMAGetCurrentDataCounter.
Поэтому после первого срабатывания этой точки останова — получаем адрес возврата и переставляем точку останова на 4 байта за ним. Ожидание завершено!
Лог DMA операций
??? DMA LR:0x08000abf P:0x4001244c *(00000000) (=>) M:0x20000224 [0x000f] P2.M2+
-------------------------------------------------------------------------------------
MasterCRCtoRAM
M2M DMA LR:0x08002e81 P:0x080125bc *(145eac33) (=>) M:0x200000f0 [0x0002] P2+M2+
-------------------------------------------------------------------------------------
MasterCRCtoRAM
M2M DMA LR:0x08002e81 P:0x080125bc *(145eac33) (=>) M:0x20000020 [0x0002] P2+M2+
-------------------------------------------------------------------------------------
Calc_CRC_0_Wrap (Begin)
M2M DMA LR:0x08005001 P:0x200000f0 *(145eac33) (=>) M:0x20000010 [0x0002] P2+M2+
-------------------------------------------------------------------------------------
Calc_CRC_0
CRC DMA LR:0x080050d3 P:0x40023008 (<=) M:0x2000085c *(00000001) [0x0001] P4.M4.
CRC DMA LR:0x08005127 P:0x40023000 (<=) M:0x20000010 *(145eac33) [0x0002] P4.M4.
CRC DMA LR:0x08005127 P:0x40023000 (<=) M:0x20000010 *(145eac33) [0x0002] P4.M4.
CRC DMA LR:0x08005177 P:0x40023000 *(abb10a5e) (=>) M:0x20000010 [0x0001] P4.M4.
-------------------------------------------------------------------------------------
Calc_CRC_0_Wrap (End)
M2M DMA LR:0x08005043 P:0x200000f0 *(145eac33) (=>) M:0x20000014 [0x0002] P2+M2+
-------------------------------------------------------------------------------------
Calc_CRC_1
CRC DMA LR:0x08005203 P:0x40023008 (<=) M:0x20000804 *(00000001) [0x0001] P4.M4.
CRC DMA LR:0x08005257 P:0x40023000 (<=) M:0x20000014 *(145eac33) [0x0006] P4.M4.
CRC DMA LR:0x08005257 P:0x40023000 (<=) M:0x20000014 *(145eac33) [0x0006] P4.M4.
CRC DMA LR:0x08005257 P:0x40023000 (<=) M:0x20000014 *(145eac33) [0x0006] P4.M4.
CRC DMA LR:0x080052a7 P:0x40023000 *(4859bef2) (=>) M:0x20000014 [0x0001] P4.M4.
-------------------------------------------------------------------------------------
Calc_CRC_2
CRC DMA LR:0x08002359 P:0x40023008 (<=) M:0x20000954 *(00000001) [0x0001] P4.M4.
CRC DMA LR:0x080023ad P:0x40023000 (<=) M:0x08000000 [0x496f] P4.M4+
CRC DMA LR:0x08002411 P:0x40023000 (<=) M:0x080125c0 [0x3690] P4.M4+
CRC DMA LR:0x08002463 P:0x40023000 *(145eac33) (=>) M:0x2000001c [0x0001] P4.M4.
CRC DMA LR:0x080024b5 P:0x40023000 *(145eac33) (=>) M:0x20000958 [0x0001] P4.M4.
-------------------------------------------------------------------------------------
Calc_CRC_3
CRC DMA LR:0x08002f07 P:0x40023008 (<=) M:0x2000095c *(00000001) [0x0001] P4.M4.
CRC DMA LR:0x08002f5d P:0x40023000 (<=) M:0x08000000 [0x496f] P4.M4+
CRC DMA LR:0x08002fbf P:0x40023000 (<=) M:0x080125c0 [0x3690] P4.M4+
CRC DMA LR:0x08003011 P:0x40023000 *(145eac33) (=>) M:0x20000030 [0x0001] P4.M4.
-------------------------------------------------------------------------------------
Calc_CRC_4
CRC DMA LR:0x08002c17 P:0x40023008 (<=) M:0x20000854 *(00000001) [0x0001] P4.M4.
CRC DMA LR:0x08002c6b P:0x40023000 (<=) M:0x20000030 *(145eac33) [0x0002] P4.M4.
CRC DMA LR:0x08002c6b P:0x40023000 (<=) M:0x20000030 *(145eac33) [0x0002] P4.M4.
CRC DMA LR:0x08002cbb P:0x40023000 *(abb10a5e) (=>) M:0x20000030 [0x0001] P4.M4.
-------------------------------------------------------------------------------------
Calc_CRC_5
CRC DMA LR:0x080030bb P:0x40023008 (<=) M:0x2000095c *(00000001) [0x0001] P4.M4.
CRC DMA LR:0x08003111 P:0x40023000 (<=) M:0x08000000 [0x496f] P4.M4+
CRC DMA LR:0x08003173 P:0x40023000 (<=) M:0x080125c0 [0x3690] P4.M4+
CRC DMA LR:0x080031c5 P:0x40023000 *(145eac33) (=>) M:0x20000034 [0x0001] P4.M4.
-------------------------------------------------------------------------------------
Calc_CRC_6
CRC DMA LR:0x08002d47 P:0x40023008 (<=) M:0x200007fc *(00000001) [0x0001] P4.M4.
CRC DMA LR:0x08002d9b P:0x40023000 (<=) M:0x20000034 *(145eac33) [0x0006] P4.M4.
CRC DMA LR:0x08002d9b P:0x40023000 (<=) M:0x20000034 *(145eac33) [0x0006] P4.M4.
CRC DMA LR:0x08002d9b P:0x40023000 (<=) M:0x20000034 *(145eac33) [0x0006] P4.M4.
CRC DMA LR:0x08002deb P:0x40023000 *(4859bef2) (=>) M:0x20000034 [0x0001] P4.M4.
-------------------------------------------------------------------------------------
Calc_CRC_2
CRC DMA LR:0x08002359 P:0x40023008 (<=) M:0x20000c64 *(00000001) [0x0001] P4.M4.
CRC DMA LR:0x080023ad P:0x40023000 (<=) M:0x08000000 [0x496f] P4.M4+
CRC DMA LR:0x08002411 P:0x40023000 (<=) M:0x080125c0 [0x3690] P4.M4+
CRC DMA LR:0x08002463 P:0x40023000 *(145eac33) (=>) M:0x2000001c [0x0001] P4.M4.
CRC DMA LR:0x080024b5 P:0x40023000 *(145eac33) (=>) M:0x20000c68 [0x0001] P4.M4.
-------------------------------------------------------------------------------------
Calc_CRC_7
CRC DMA LR:0x08007a7d P:0x40023008 (<=) M:0x200008a0 *(00000001) [0x0001] P4.M4.
CRC DMA LR:0x08007acd P:0x40023000 (<=) M:0x200000fc *(abcdfedc) [0x0076] P4.M4.
CRC DMA LR:0x08007acd P:0x40023000 (<=) M:0x200000fc *(abcdfedc) [0x0076] P4.M4.
CRC DMA LR:0x08007b1f P:0x40023000 *(80a34f2a) (=>) M:0x200000fc [0x0001] P4.M4.
-------------------------------------------------------------------------------------
DMACopyWord_0
M2M DMA LR:0x08002b09 P:0x200000fc *(80a34f2a) (=>) M:0x20000000 [0x0002] P2+M2+
-------------------------------------------------------------------------------------
DMACopyWord_1
M2M DMA LR:0x08002b91 P:0x200000fc *(80a34f2a) (=>) M:0x200000f4 [0x0002] P2+M2+
-------------------------------------------------------------------------------------
Get_DesignID_0
M2M DMA LR:0x080029f3 P:0x1ffff7e8 (=>) M:0x200021c0 [0x0006] P2+M2+
0x200021c0 56 ff 76 06 51 80 48 54 | 38 18 10 87 V.v.Q.HT8 ...
-------------------------------------------------------------------------------------
DMACopyWord_2
M2M DMA LR:0x08002a7f P:0x20000000 *(80a34f2a) (=>) M:0x200001b0 [0x0002] P2+M2+
-------------------------------------------------------------------------------------
Get_DesignID_1
M2M DMA LR:0x08001c61 P:0x1ffff7e8 (=>) M:0x20002dc0 [0x0006] P2+M2+
0x20002dc0 56 ff 76 06 51 80 48 54 | 38 18 10 87 V.v.Q.HT8 ...
M2M DMA LR:0x08001cc5 P:0x200001b0 *(80a34f2a) (=>) M:0x20002db8 [0x0002] P2+M2+
-------------------------------------------------------------------------------------
Calc_CRC_7_Wrap (Begin)
M2M DMA LR:0x08001d99 P:0x200030b8 *(5a92dc90) (=>) M:0x200030c4 [0x0002] P2+M2+
M2M DMA LR:0x08001dd5 P:0x0800f4bb (=>) M:0x20003098 [0x0010] P2+M2+
0x20003090 | 00 04 04 44 c4 4f 44 44 . ..D.ODD
0x200030a0 c4 24 24 2f b4 24 04 04 | 00 40 44 24 24 15 0c 04 .$$/.$... @D$$...
0x200030b0 fe 04 0c 15 24 24 44 40 | ....$$D@
M2M DMA LR:0x08001e2f P:0x0800f4fb (=>) M:0x20003098 [0x000e] P2+M2+
0x20003090 | 00 10 60 02 8c 00 00 fe . .`.....
0x200030a0 92 92 92 92 92 fe 00 00 | 00 04 04 7e 01 40 7e 42 ......... ..~.@~B
0x200030b0 42 7e 42 7e | B~B~
-------------------------------------------------------------------------------------
Calc_CRC_7
CRC DMA LR:0x08007a7d P:0x40023008 (<=) M:0x20002f84 *(00000001) [0x0001] P4.M4.
CRC DMA LR:0x08007acd P:0x40023000 (<=) M:0x200030c8 *(a7894d6e) [0x0076] P4.M4.
CRC DMA LR:0x08007acd P:0x40023000 (<=) M:0x200030c8 *(a7894d6e) [0x0076] P4.M4.
CRC DMA LR:0x08007b1f P:0x40023000 *(fab893e0) (=>) M:0x200030c8 [0x0001] P4.M4.
-------------------------------------------------------------------------------------
Calc_CRC_7_Wrap (End)
M2M DMA LR:0x08001e81 P:0x200030c8 *(fab893e0) (=>) M:0x200000f8 [0x0002] P2+M2+
M2M DMA LR:0x08001ebd P:0x200030c8 *(fab893e0) (=>) M:0x200030bc [0x0002] P2+M2+
-------------------------------------------------------------------------------------
DMACopy_32bytes_0
M2M DMA LR:0x08001b73 P:0x0800f4bb (=>) M:0x20002778 [0x0010] P2+M2+
0x20002770 | 00 04 04 44 c4 4f 44 44 . ..D.ODD
0x20002780 c4 24 24 2f b4 24 04 04 | 00 40 44 24 24 15 0c 04 .$$/.$... @D$$...
0x20002790 fe 04 0c 15 24 24 44 40 | ....$$D@
-------------------------------------------------------------------------------------
DMACopy_32bytes_1
M2M DMA LR:0x08001be7 P:0x0800f4fb (=>) M:0x20002778 [0x0010] P2+M2+
0x20002770 | 00 10 60 02 8c 00 00 fe . .`.....
0x20002780 92 92 92 92 92 fe 00 00 | 00 04 04 7e 01 40 7e 42 ......... ..~.@~B
0x20002790 42 7e 42 7e 42 42 7e 40 | B~B~BB~@
-------------------------------------------------------------------------------------
Calc_CRC_7
CRC DMA LR:0x08007a7d P:0x40023008 (<=) M:0x200026bc *(00000001) [0x0001] P4.M4.
CRC DMA LR:0x08007acd P:0x40023000 (<=) M:0x200027a4 *(a7894d6e) [0x0076] P4.M4.
CRC DMA LR:0x08007acd P:0x40023000 (<=) M:0x200027a4 *(a7894d6e) [0x0076] P4.M4.
CRC DMA LR:0x08007b1f P:0x40023000 *(fab893e0) (=>) M:0x200027a4 [0x0001] P4.M4.
-------------------------------------------------------------------------------------
DMACopy_32bytes_0
M2M DMA LR:0x08001b73 P:0x0800f4bb (=>) M:0x20002a80 [0x0010] P2+M2+
0x20002a80 00 04 04 44 c4 4f 44 44 | c4 24 24 2f b4 24 04 04 ...D.ODD. $$/.$..
0x20002a90 00 40 44 24 24 15 0c 04 | fe 04 0c 15 24 24 44 40 .@D$$.... ...$$D@
-------------------------------------------------------------------------------------
DMACopy_32bytes_1
M2M DMA LR:0x08001be7 P:0x0800f4fb (=>) M:0x20002a80 [0x0010] P2+M2+
0x20002a80 00 10 60 02 8c 00 00 fe 92 | 92 92 92 92 fe 00 00 ..`...... .......
0x20002a90 00 04 04 7e 01 40 7e 42 42 | 7e 42 7e 42 42 7e 40 ...~.@~BB ~B~BB~@
-------------------------------------------------------------------------------------
Calc_CRC_7
CRC DMA LR:0x08007a7d P:0x40023008 (<=) M:0x200029c4 *(00000001) [0x0001] P4.M4.
CRC DMA LR:0x08007acd P:0x40023000 (<=) M:0x20002aac *(a7894d6e) [0x0076] P4.M4.
CRC DMA LR:0x08007acd P:0x40023000 (<=) M:0x20002aac *(a7894d6e) [0x0076] P4.M4.
CRC DMA LR:0x08007b1f P:0x40023000 *(fab893e0) (=>) M:0x20002aac [0x0001] P4.M4.
Видео работы робота с анализом DMA операций:
Функций, относящихся с посчёту CRC оказалось 8 (ВОСЕМЬ!). Ни одна не содержит параметры в явном виде, используются витиеватые вычисления. Часть из этих функций для рассчёта использует рекурсию, что дополнительно усложняет понимание.
Из дампа, сделанного роботом, отлично видно что константа (я назвал её MasterCRC) копируется из Flash в RAM, потом из RAM в RAM. Считается CRC на две половинки флеша: до этой констаны и после этой константы до самого конца, т.е. даже на незанятую прошивкой область. А это значит что если чип перед прошивкой предварительно не очистить — контрольная сумма не сойдётся!
Полный подсчёт CRC выполняется 4 раза. Ужас! Потом считается CRC на CRC. И CRC на CRC на CRC. Ужас! Сильно в манипуляции с CRC я вникать не стал.
DMA приносит первый успех
Кроме подсчёта CRC и странных перемещений из памяти в память в логе оказалось получение уникального ID чипа. С помощью DMA. Это очень подозрительно, т.к. ID чипа задаётся при произодстве, он уникален для каждого кристалла — прекрасный кандидат на роль источника данных для формирования регкода.
Любопытная информация: оказывается 12 байтовый ID чипа содержит внутри себя номер партии, номер пластины (вафли), и даже x-y координаты чипа на этой вафле!Анализ кода после получения ID чипа показал, что дважды вызывается одна и та же функция, сначала над первыми 6 байтами ID, а затем над вторыми. Функция обрабатывает переданный буфер, используя байты как индекс в 256-значном массиве, ксоря и сдвигая результаты.
Подозрение подтвердилось — вызовы вернули мои ID1 и ID2 коды.
Ногодрыжество
Определяя функции из стандартной библиотеки периферии я заметил обработку странной таблицы по адресу 0x800C23C. В ней описывались состояния каждого из 16 пинов всех 7 GPIO портов (вызовы — жуткий копипаст). Стало интересно какие пины для чего используются.
Сопоставив код и схему из обзора нарисовал вот такую таблицу
подтяжка:
IPU — push supply (умолчание, если не указано иное)
IPD — push ground
AIN — analog in
Out_OD — Out Open-drain
Out_PP — Out Push-pull
частота 50mhz (умолчание, если не указано иное)
Какие-то настройки в порту GPIO_C видимо остались мусором от старых версий/прототипов. Разводка поменялась, порт вообще отключили, а скорость зачем-то меняется.
IPU — push supply (умолчание, если не указано иное)
IPD — push ground
AIN — analog in
Out_OD — Out Open-drain
Out_PP — Out Push-pull
частота 50mhz (умолчание, если не указано иное)
Какие-то настройки в порту GPIO_C видимо остались мусором от старых версий/прототипов. Разводка поменялась, порт вообще отключили, а скорость зачем-то меняется.
Обратите внимание на подключение EEPROM Flash. Линии I2C заведены на ноги, где нет аппаратного I2C. Странное решение, не правда ли?
Ну тем лучше. Я никогда не работал с I2C. А тут представилась такая возможность пощупать его на уровне ручного управления сигналами на ножках МК! Скачал доку с сайта TI, нарисовал таблицу состояний, набросал код для робота. Чтобы он по изменениям пинов раскодировал передачу.
И тут меня ждал облом. EEPROM не останавливается отладчиком. И хотя теоретически она могла бы ждать мастера, на практике всё разваливается напрочь.
Ок, зная работу протокола даём имена функциям, поднимаясь уровень за уровнём до грозди функций Flash_ReadData / Flash_WriteData, малоотличающихся друг от друга (копипаст?)
Смотрю на ссылки — чтение и запись вызывается из места ввода кода. Интересно! Оказывается 4 байтовый ответный код после ввода пишется по флеш. А потом читается оттуда. Ожидание этих операций я и принял за проверку. А два разных сообщения на китайском — если прочитали тоже что и записали и если нет (аппаратная ошибка?).
Получается что после ввода код не проверяется. Он проверяется всегда при старте станции. И при ошибке проверки вместо основной RTOS задачи паяльника создаётся другая RTOS задача, которая занимается вводом кода и записью его во флеш. Чтож, логично.
Кроме того в соседнх флеш-функциях происходит чтение-запись солидного куска данных. Размером 0x3AE байт. Похоже на настройки. После их чтения вызывается функция вычисления кода по буферу с данными. До зубовного скрежета напоминающую функцию вычисления ID1 и ID2. И даже таблица с данными ещё одна, точно такая же. Зачем использовать такой код для проверки целостности настроек? Взял бы лучше CRC! Постойте, а что если…? Загоняю в гугл константы из таблицы — точно. Это и есть CRC. Точнее CRC16_CCIT. Две копии одной и той же функции. Две копии таблиц. Чтобы враг не догадался.
Последние шаги
Итак, мы контролируем чтение из флеша. «reset init». Точка останова быстро приводит нас месту где читается свежезаписанный ответ. Ставим watchpoint на прочитанные данные. «resume» Станция как ни в чём ни бывало показывет интерфейс ввода кода.
Опять DMA? Смотрим лог. Сразу после копирования прочитанного кода из флеш происходит два копирования блоков по 32 байта. Смотрим дизассемблер по адресу возврата из DMA операции. В коде видим после копирования 32 байт вызов странной функции с кучей xor внутри. Ставим точку останова.
При вызове функции видим буфер с какими-то данными, в последних 4 байтах которого лежат уже знакомые ID1 и ID2 станции. На выходе из функции видим 16 битное число. Ещё один вызов — буфер с другими данными, в конце так же лежат ID1 и ID2.
Любопытная информация: данные в каждом из 32-байтных буферов это изображения соответствующего иероглифа. Последние 4 байта изображения замененяются в буфере на ID1+ID2. Что это за иероглифы? Может быть имя разработчика? Не знаю, я не силён в китайском.А что если результат функции и есть искомый код-ответ? Запускаем станцию, вводим полученные в отладчике числа, рестарт — и код принят!
Осталось понять что именно вычисляет эта загадочная функция. А делает она вот что:
- кучу XOR между явно заданными константами. Просто взял финальное значение. Назовём его «manyXorValue».
- вычисляет CRC16_CCIT от начала самой себя. Также берём уже посчитанное значение. Назовём его «xorCodeCrc16».
- считает CRC16, полагая в качестве начального значения результат (manyXorValue xor xorCodeCrc16), а вместо уже знакомых двух копий CRC16 таблицы ещё одну таблицу. Очень похожую, но всё-таки другую.
Такое ощущение что оригинальную таблицу нарезали на кусочки по 8 элементов, перемешали, и опять склеили. Например 1-ый кусочек стал 0-ым. 5-ый никуда не делся. А на месте 4-го теперь 31-ый. Пишем короткий код для сопоставления, и вот нужные перестановки для сборки этой странной таблицы: 1, 2, 3, 4, 31, 5, 6, 7, 8, 9, 10, 11, 12, 0, 13, 25, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 26, 27, 28, 29, 30.
Переписываем на Python. Проверяем — работает. Для отладки я попросил несколько ребят из интернетов, имеющих такие же станции, дать мне их ID и проверить сгенерированные RG. Проверили ещё на 5 станциях. Результат правильный.
Заключение
- Основой защиты оказался запутанный и сложный код большого объёма (не считал точно, но на вскидку его 10-15%), густо замешанный на работе с железом. Не удивлюсь если многопоточность RTOS тоже играет важную роль. Благо я не собирался понять все тонкости реализации.
- Станция заработала с более новой прошивкой. Наконец-то собрал для неё шикарную алюминиевую ручку с Tao. Спаял этой же станцией.
UPDATE: В комментариях уже дважды спросили про эту ручку. Вот отличный обзор на неё. - Узнал массу новой информации про ARM Cortex M3 и периферию STM32.
- Откровенно разочаровался качеством китайской прошивки. Буду делать открытую, переписанную с нуля.
А если бы защита оказалась более простой? Наверное ввёл бы код, и забыл про неё…
Пишите в комментариях, интересно ли вам узнать продолжение истории: реверс алгоритмов работы паяльной станции и ход разработки моего варианта.
P.S.: Специально для этой статьи переписал код генерации ключей на javascript. Online версия, там же есть ссылка на Python версию. Если объяснения были где-то непонятны — можно почерпнуть детали из кода. Ну и, конечно, оживить вашу собственную станцию, заблокированную обновлением или ремонтом железа.