Взлом аппаратного ключа методом veni, vidi, vici

в 13:08, , рубрики: Без рубрики
Взлом аппаратного ключа методом veni, vidi, vici - 1

К одному очень дорогому оборудованию для работы управляющей программы нужен аппаратный ключ с зашитой датой, указывающей, когда право использования оборудования кончается. За ключи исправно платили заграничному вендору, но после санкций это стало невозможным и оборудование стало простаивать. Важно, что интернет не использовался для активации ключа. Значит всё необходимое в ключ зашито. Если корпус ключа открыть, то видна одна микросхема FT232R с небольшой обвязкой.

Задача: Сделать так, чтобы можно было пользоваться оборудованием. Дистрибутив софта, требующего ключ, есть. Работает он под Windows. Просроченный ключ есть. Оборудованию около 10 лет.

Ниже описан путь решения со всеми ошибками.

Стоит ли браться

Какие есть компетенции: 15+ лет опыта в области разработки программируемого железа и сопутствующего софта. Опыт реверса плат. Знаю неплохо Си/C++ и Python. Ключи никогда не взламывал. Владею навыком поиска информации, который позволяет мне решать задачи, которые я раньше не решал.

Анализируем: Микросхему FT232R я знаю, как USB-UART переходник от компании FTDI, но на всякий случай гуглим её. Видим, что у неё есть EEPROM. Оборудование старое. Значит могли просто записать на EEPROM дату окончания действия ключа. Для взлома будет достаточно просниффить протокол USB с помощью USBlyzer, например. Далее научиться эмулировать ключ и подобрать способ хранения даты, обойдясь без дизассемблирования. Стоит взяться, посмотреть внимательнее и справиться. То есть прийти, увидеть и победить.

Первоначальный план оказался ошибочен. Об этом ниже.

Начинаем работу без выезда на установку

У нас есть дистрибутив и стоит посмотреть на структуру его зависимостей и проверить, что связь с FTDI там используется. Инструмент для этого я не помню. Гуглим "DLL dependency". Первая ссылка ведет на Dependency walker. По названию вроде то, что нужно. Скачиваем, запускаем, указываем исполнимый файл. Получаем несколько ошибок и предупреждение. Игнорируем, чтобы не тратить на них время.

Ошибка при запуске Dependency walker

Ошибка при запуске Dependency walker
Ошибки внутри Dependency walker
Ошибки внутри Dependency walker

Dependency walker какой-то старый и поиск по дереву зависимостей DLL в нём не работает. То есть приходится искать нужную библиотеку FTDI глазами. После 5 минут прокликивания находим нужное и всего в одном месте.

Найдена зависимость от FTDI

Найдена зависимость от FTDI

Гипотеза подтвердилась, место общения с микросхемой FT232R найдено, сниффер USB распакован и нужен только доступ к оборудованию.

Если быть внимательным

Обратите внимание на FTCHIPID.DLL. Название должно было насторожить, но в тот момент не насторожило.
Уже позже я стал гуглить "FTDI ChipID" и первая ссылка была https://ftdichip.com/software-examples/ftdichip-id/ При первом прочтении документа я увидел, что вся суть ChipID в уникальном серийнике FT232R:

The examples posted on this page demonstrate the use of the FTDIChip-ID™ feature of the FT232R and FT245R devices. The FTDIChip-ID™ is a permanent, unique number programmed into each IC during production that cannot be changed by the end user. Since each number is unique, applications can be tied to specific devices, providing a dongle feature. The USB-Key is ideal for developers wishing to utilise only the FTDIChip-ID™ security features without a UART or FIFO interface.
An application note is available on the FTDIChip-ID™ on our Application Notes page. In order to extract the FTDIChip-ID™ from a FT232R and FT245R device, the FTChipID DLL is required and D2XX drivers must be installed.

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

This web application example lists all of the FT232R and FT245R based devices connected to the client PC and allows the user to select which devices to register with a server. The registration works by reading the unique FTDIChip-ID™ from the device, encrypting it with date and time information and then writing the encrypted data back into the onboard EEPROM user area.
The encrypted FTDIChip-ID™ information is decrypted and checked against the permanent FTDIChip-ID™ to verify the registration. If the decrypted FTDIChip-ID™ does not match, then the device is not registered.

Похоже, что именно эта технология и использовалась. Благо можно скачать и посмотреть пример применения. Но вообще криптография это целая область и FTDI вполне по силам сделать её по-настоящему, так что просто записать в EEPROM дату на годик побольше не поможет. Нужен будет алгоритм кодирования. Отложим пока этот вопрос.

План "Б"

Разумно подготовить альтернативный план, если эмулятор аппаратного ключа не заработает. А именно пропатчить софт. Нужно как-то подлезть к DLL и подизассемблировать. Благо еще в школе увлекался ассемблерами. Но опыта дизассемблирования нет. Нужен хороший инструмент. Начинаем гуглить DLL hack. Вторая ссылка в видео смотрится хорошо (Hacking a game with DLL injection [Game Hacking 101]): https://www.youtube.com/watch?v=KCtLiBnlpk4

Потратив 10 минут на просмотр становится понятно, что есть хороший софт binary ninja. Ну что ж, мы других не знаем. Смотрим что там с лицензией и ценой. 300 долларов... Жаба душит, да и платить за границу в условиях санкций не выйдет. Значит нужен другой инструмент. Гуглим binary ninja alternative. Как обычно, нам поможет первая же ссылка. Читаем там про первые 3 примера и внимание привлекает IDA, где в комментах написано, что есть Freeware модификация. Качаем и ставим.

Смотрим, что это за зверь. Начало вроде простое: открой бинарный файл и кликай Next. Затем появляется дизассемблер, что-то продолжает работать и вдруг бах - графовое представление выполнения:

Скриншот IDA

Скриншот IDA

Это же праздник какой-то. Поиск по именам функций работает. При клике на любую метку мы попадаем на её код или значение. Все ветвления это какая-то форма jmp, а все вызовы это call. Дальше, правда пока непонятно, но всё равно результат прекрасный.

А можно ли тут патчить? Гуглим IDA patching. Какая ссылка нам подходит? Правильно, первая же. Например, можно найти точку условного ветвления, где один путь выполнения показывает ошибку проверки ключа, а второй путь выполняется для валидного ключа и заменить в точке ветвления условный jump, например jnz, на безусловный (jmp). jnz это "jump if not zero" (тут надо иметь какой-то бэкграунд в ассемблере), а jmp это безусловный jump. Как только делаем замену, то сразу перестраивается граф - удобно. IDA сыпет какими-то сообщениями, что патчинг что-то там рассинхронизирует, но это мы игнорируем. Берём старый файл и пропатченный файл и делаем бинарное сравнение в любом софте - видим, что действительно пару байт изменились.

Проверяем, можно ли тут дебажить. Да, можно. Есть точки останова, есть просмотр состояния памяти, регистров. В общем, есть всё необходимое. Пробуем запустить отладку софта, но он жалуется на отсутствие аппаратуры, для управления которой он создан. При этом он всё равно доходит до проверки ключа и выдаёт ошибку, что ключ не найден. Откладываем дальнейшие эксперименты до выезда, чтобы иметь гарантированно правильное аппаратное окружение.

На выезде

На месте выясняется, что единственный аппаратный ключ трогали и сломали. Использовали FTProg, но в режиме чтения (со слов оператора). Теперь ключ определяется, как FTDI устройство, он есть в диспетчере устройств. Но софт реагирует на него так же, как на его отсутствие: пишет, что ключ не найден. Ключу вскрывали корпус. Может быть это аппаратная поломка?

Внешний вид печатной платы ключа

Внешний вид печатной платы ключа

На всякий случай прозваниваем ключ. Видим, что схема подключения эквивалентна рекомендуемой из даташита на FT232R.

Схема подключения FT232R из даташита

Схема подключения FT232R из даташита

Дополнительно к рекомендуемой схеме есть 2 конденсатора на линиях USB D+/D-, соединяющие их с землёй. Ну это не может влиять на распознавание ключа, ведь связь по USB работает, а USB для связи требует только D+/D- сигналы. Значит если ключ и сломали, то скорее всего переписали EEPROM. Других экземпляров ключей нет. Печально, ведь посмотреть раскодировку из EEPROM даже просроченной даты теперь не получится. Запускаем USBlyzer, чтобы просниффить шину USB во время работы софта. Видим, что софт после запуска шлёт какие-то пакеты. Но пересылаемых данных очень мало. Вряд ли он успевает вычитать существенную часть EEPROM. Почему сломался ключ остаётся загадкой. Без корректно определяемого ключа задача заметно усложняется.

Попытка 1 - мне повезёт

У нас же был план "Б". Поменять путь выполнения в программе от ведущего к ошибке на путь, ведущий дальше. То есть пропатчить место, где проверяется ключ.
Ставим IDA на компьютер, идущий в составе установки и получаем ошибку запуска. Как же так, ведь на моём компьютере всё работало. Оказывается IDA с седьмой версии может работать только на 64битных операционных системах, а оборудование-то старое и система там 32битная. Нда, поотлаживать не выйдет. Разве что деградировать на IDA 6, которое, как удалось нагуглить, работало на x86 архитектурах. Но это как-то не комильфо. Пробуем поискать 32битный дебаггер и находим OllyDbg. Скачиваем, ставим, запускаем. Ой, как-то всё не так удобно:

OllyDbg скриншот

OllyDbg скриншот

OllyDbg не впечатлил. Ну хорошо, а если пропатчить на одном компьютере с IDA, а запустить на другом? Начнём поиск места с текста сообщения об ошибке. Софт пишет ошибку Dongle Not Present. Выполняем поиск в IDA по этой фразе и находим текстовые блоки:

Текст "Dongle not present" в дизассемблере exe файла

Текст "Dongle not present" в дизассемблере exe файла

Ага, тут несколько разных сообщений об ошибке ключа. Кликаем и просим IDA найти ссылки на эти строки. Получаем код:

Код, выдающий "Dongle not present"

Код, выдающий "Dongle not present"

В зависимости от результата вычитания из регистра eax единицы (sub eax, 1) мы получаем либо одно, либо другое сообщение об ошибке. На языке Си это эквивалент if (some_var == 1) {} else {}. Если посмотреть по графу чуть выше, то вызывается внешняя функция validation, от которой начинаются ответвления к разным сообщениям об ошибках и завершения функции, но есть один путь выполнения дальше. Заменяем разные jnz и jz на jmp по-очереди, чтобы не было перехода на плохие сообщения об ошибках ключа, копируем пропатченный файл на компьютер установки и ... ничего. Софт определяет невалидность ключа, затем, как мы и хотели, проскакивает вывод сообщения об ошибке ключа, продолжает выполнение, но дальше вылетает. А иногда выводит сообщения, которых выводить не должен. То, что наивный метод оказался неудачным вообще-то логично, так как в компьютерной программе, я нагло заменял логику поведения, что могло привести как минимум к использованию неинициализированных переменных.

Значит надо заменять аккуратно, не игнорируя логику программы, а каким-то образом изучив её. Путь в софте верхнего уровня только что не удался. Путь от аппаратуры требует наличия работающего ключа. Но где-то в середине есть библиотеки FTDI и их API открыт. Преимущество этого места в том, что там точно не должно быть много кода для анализа, так как микросхема простая. В предыдущих просмотренных видео встречался термин DLL injection. Значит надо заменить ответы от библиотеки FTDI на такие, которые удовлетворят проверку. А подбирать мы их будем в отладчике. У нас новый план.

План "В"

Если чего-то не знаешь, то сядь и выучи. Нам вроде нужно сделать DLL injection (на самом деле нет). Набираем в гугле Dll injection IDA, первая ссылка и попадаем на видео "DLL Injection with Ghidra / IDA Pro Tutorial #2". О, есть еще какая-то Ghidra. Гуглим IDA vs Ghidra и находим статью на хабре (тут по второй ссылке https://habr.com/ru/post/480824/). Ага, значит это конкуренты и обе есть с бесплатными версиями. Ну и хорошо. Может пригодится. Смотрим найденный Tutorial #1 и #2 от Juan Sacco. Оказывается можно в IDA декомпилировать в Си по кнопке F5 (для декомпилирования 32бит приложений нужна платная IDA Pro)! Можно на точке останова писать код на питоне, который будет по ходу выполнения заменять нужные регистры! Можно раскрашивать граф выполнения, давать имена меткам. Круто. С умением отладки продвинулись, а с задачей нет.

Автоматизация

А всё-таки каким наиболее быстрым способом мы хотим перехватывать вызовы к библиотеке FTDI? Вчитываемся в DLL injection и понимаем, что это про присоединение к работающему процессу (статья на хабре https://habr.com/ru/post/73324/). DLL hijacking звучит логичней - это про вызов подменной библиотеки вместо исходной. Можно ли как-то автоматизировать? Набираем в гугле довольно кривой запрос: dll copygen for hijack. Первые две ссылки про защиту, а вот третья про Exploiting DLL Hijacking by DLL Proxying Super Easily. "Супер просто" это нам подходит. Там же мы вычитываем, что есть методика DLL Proxying, когда все вызовы подменной библиотеки перенаправляются на настоящую библиотеку, а возвращаемые значения от настоящей библиотеки проходят через подменную в приложение, которое подменную библиотеку импортировало. Это тоже способ, но нам бы автоматически сгенерировать проект. Мы же ленивые. Кроме того, для сборки собственной подменной DLL нужно будет вспомнить правила согласования динамической линковки, всякие stdcall, cdecl и так далее. Желательно, чтобы это всё настроили за нас. Гуглим dll auto proxy, первая ссылка и находим ProxiFy (https://www.codeproject.com/Articles/1179147/ProxiFy-Automatic-Proxy-DLL-Generation). Проект дан в виде проекта для Visual Studio. Его еще надо собрать, чтобы получить работающую программу-генератор. Ну, студия у нас есть. Открываем и ... Ошибка сборки - нужны Microsoft tools 140. У меня 2013 студия и в ней версия Microsoft tools 120. Пробуем заменить в файле проекта все зависимости с 140 на 120 с помощью Find and Replace. Получаем ошибки компиляции. Ладно, поставим Visual Studio 2019, который услужливо предлагает Windows 10. Тут и Community edition есть. Полчаса ожидания на скачивание и установку и ... снова куча ошибок. Теперь об отсутствии string.h, stdio.h и других стандартных библиотек. В итоге дело было в установке компонента Universal Windows Platform development. Предыдущие студии без него как-то обходились...

Недостающий компонент vs2019

Недостающий компонент vs2019

Наверное есть и другие автоматические средства генерации прокси DLL. Ищем и находим еще пару проектов: https://github.com/maluramichael/dll-proxy-generator, который сделан на основе Proxify (и важные вещи в документации опущены) и https://github.com/zeroKilo/ProxyDllMaker, написанный на C# и вообще без документации. Спасибо, но это еще хуже.
Ладно, ставим Visual Studio 2015, который использовал Kristoffer Blasiak (об этом есть упоминание на странице ProxiFy). Еще полчаса ожидания, проект открывается и высыпает ошибки:

Ошибки сборки debug режима Proxify

Ошибки сборки debug режима Proxify

Каким-то образом догадываемся быстро попробовать сборку Release. В ней ошибок нет, ура! Хотя для сборки Debug режима достаточно было выключить поддержку Unicode и добавить пару библиотек в зависимости линкера. Видимо, Кристофер поленился все сборочные сценарии настроить. Итак, запускаем ProxiFy:

ProxiFy заработал и просит библиотеку, которую будем проксировать

ProxiFy заработал и просит библиотеку, которую будем проксировать

Можем сгенерировать код для ftd2xx.dll. Получается что-то такое:

ProxiFy сделал cpp и def файлы для сборки проксирующей библиотеки FTDI

ProxiFy сделал cpp и def файлы для сборки проксирующей библиотеки FTDI

Создаём новый проект Win32, указываем, что это DLL. Называем его FTD2XX, чтобы все имена были, как у заменяемой библиотеки (это было важной догадкой, иначе не заработало бы). Код собирается после отключения Unicode, но после подмены dll на сгенерированную и переименовании оригинальной dll с добавлением подчёркивания (ftd2xx_dll), как того требует документация ProxiFy, код программы вылетает. Тут вчитываемся в документацию к ProxiFy и понимаем, что дело может быть в ordinals, которые ProxiFy как раз не поддерживает (об этом пишет автор).

В ProxiFy не работают ordinals

В ProxiFy не работают ordinals

ordinals это способ имепортировать функции из DLL не по именам, а по номерам. Для этого в проект добавляется def файл, который связывает имена и номера функций.
В нашем FTD2XX.def файле FT_Close имеет ordinal 1, а в dependency walker мы видим:

Настоящие ordinals в DLL FTD2XX

Настоящие ordinals в DLL FTD2XX

То есть настоящий ordinal для FT_Close это 2. В результате у нас вызывались не те функции в прокси-dll, что хочет импортирующая программа, поехал стек и всё сломалось. Можно поискать генератор def файла с ordinals, но в итоге по запросу dll proxy ordinals нашёлся еще один генератор: https://github.com/kevinalmansa/DLL_Wrapper с подробной документацией и поддержкой ordinals. Написан на С++.

Короткий вывод - нет, это не работает. Подробности под катом

Делаем git clone, открываем 2015 студией, ... Теперь нужен Microsoft tools 14.1. Гррх. Открываем в 2019 студии, выбираем апгрейд на Microsoft tools 14.2 и получаем странную ошибку, что не найден precompiled headers (PCH file). Это файл, где скомпилирован код, который редко меняется, что позволяет ускорить сборку. Но я же делаю сборку Rebuild all, так создай же этот файл. Гуглим, находим (https://stackoverflow.com/questions/6096384/how-to-fix-pch-file-missing-on-build), что можно установить опцию использовать precompiled headers, а можно их создавать. Переключаем на создание. В общем, итог в том, что собраться оно собралось, но требует файл конфигурации, где указано какие функции проксировать. Файл конфигурации бракует, хотя формат правильный. Гори ты синим огнём, такая автоматизация.

Ладно, доделаем руками

Дареному коню в зубы не смотрят, но я разочарован. Ладно, вернёмся к ProxiFy. 76 функций это не так много. Напишем все ordinals в def файле руками по данным dependency walker.

Вписал ordinals в def файл FTD2XX руками

Вписал ordinals в def файл FTD2XX руками

Но при сборке и подмене dll софт опять вылетает. Хватит гадать - посмотрим на вызовы проксирующей библиотеки через IDA в режиме отладки. Ставим точку останова на первом вызове FTDI библиотеки, который называется FT_OpenEx. Заходим внутрь и видим, что вызов FT_OpenEx кидает нас на FT_Purge. А это же соседние указатели в списке, который строит ProxiFy (27 и 28 индекс).

Заполнение массива указателей с нулевого индекса

Заполнение массива указателей с нулевого индекса

А как Proxify делает вызовы проксируемых функций?

Вызов функций по массиву указателей с единичного индекса

Вызов функций по массиву указателей с единичного индекса

Так, товарищ Kristoffer Blasiak, а как это у тебя вообще работало? Заполняем указатели с нулевого индекса, а используем с первого, разве так можно? Меняю все указатели на единичку выше, не забывая расширить на единичку и размер массива указателей p[]. Снова ныряем в IDA и видим, что вызов FT_OpenEx через наше прокси-dll попадает на FT_OpenEx метку внутри оригинальной FTD2XX.dll. Далее софт быстро падает...

Если мы приняли вызов в проксирующей dll, а дальше в ассемблере передали его на нужную метку в оригинальной библиотеке, то по сути мы ничего сломать не могли, верно? Погуглив "calling conventions", я нашёл страницу википедии (https://en.wikipedia.org/wiki/X86_calling_conventions), где написано много подробностей, но если упрощать, то все параметры передаются в стеке, а возвращаемое значение функции кладётся в EAX регистр. Давайте проверим это в IDA.

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

Момент вызова FT_OpenEx из исходной программы

Момент вызова FT_OpenEx из исходной программы

Проверяем, какой формат вызова у FT_OpenEx (открытый API это большое подспорье).

FT_STATUS FT_OpenEx (PVOID pvArg1, DWORD dwFlags, FT_HANDLE ftHandle)
Parameters
*
pvArg1
Meaning depends on dwFlags, but it will normally be interpreted as a pointer to a null terminated string.
* dwFlags
FT_OPEN_BY_SERIAL_NUMBER, FT_OPEN_BY_DESCRIPTION or FT_OPEN_BY_LOCATION.
* ftHandle
Pointer to a variable of type FT_HANDLE where the handle will be stored. This handle must be used to access the device.

Видим, что в стеке передаётся указатель на адрес 5A17E000 - это pvArg1. Далее идёт 1 - это FT_OPEN_BY_SERIAL_NUMBER. Наконец, идёт адрес в стеке, куда запишут ftHandle. Заходим в отладчике в вызов проксирующего FT_OpenEx и видим лишний код, работающий с ebp и esp (регистры стека).

Момент вызова FT_OpenEx внутри нашей проксирующей функции

Момент вызова FT_OpenEx внутри нашей проксирующей функции
push ebp <- этого мы не писали
mov ebp, esp <- этого мы не писали
jmp off_59D953F8 <- это наш код
pop ebp <- этого мы не писали

Заходим в вызов реального FT_OpenEx и видим, что весь стек ebp у нас сдвинут на 2 слова, где 0x5A172F40 это адрес возврата (так и должно быть), а 0x004FECC0 это бяка из-за добавленного компилятором кода (push ebp). Спасибо средствам автоматизации. Опять же непонятно, как это могло работать у автора. Разве что все его функции не имели принимаемых параметров и ничего не читали из стека? Какую опцию отключить, чтобы убрать нагенерированный код, нагуглить не удалось. Пришло время жестких хаков. Значит нам подменяют epb, старое значение кладут в стек, а после прыжка ebp восстанавливают? Ну так в ассемблерную вставку напишем pop ebp перед прыжком jmp. А далее, чтобы в стеке было слово, которое будет считано через сгенерированный pop, снова добавим push ebp в стек. Получится вот так:

Добавка push и pop для нейтрализации лишнего кода

Добавка push и pop для нейтрализации лишнего кода

Компилятор нас предупреждает:

warning C4731: 'PROXY_FT_OpenEx': frame pointer register 'ebp' modified by inline assembly code

Снова подменяем ftd2xx на проксированный вызов и приложение начинает работать, как без подмены! Ура. Хотя IDA показывает, что наши игры со стеком привели к тому, что возврат из ftd2xx идёт в обход нашей проксирующей библиотеки. Это ожидаемо, так как для операции возврата retn используется адрес возврата, который хранится в стеке, а стек мы оставили без изменений и там хранится адрес возврата сразу в validation.

Получившаяся процедура проксирования с возвратом напрямую

Получившаяся процедура проксирования с возвратом напрямую

То есть таким проксированием можно вмешиваться в передаваемые параметры, но не в возвращаемое значение. Для нормального проксирования нужно генерировать функции с тем же списком параметров, как у функции в проксируемой библиотеке, а затем докладывать копии параметров в стек и делать вызов функции. Генерируемый Proxify код void PROXYsmth (void) не подойдёт.

Это неудача, но не окончательная. Теперь мы хотя бы можем сделать средства логирования вызовов библиотеки ftd2xx и ftchipid. Дописываем в нашу проксирующую библиотеку открытие файла лога и записи туда фактов вызова библиотеки ftd2xx. Делаем то же самое для ftchipid. Добавляем из документации FTDI получаемые параметры для интересующих нас функций. Например для чтения из EEPROM было так:

	void PROXY_FT_EE_UARead() {
		__asm
		 {
			pop ebp
			jmp p[9 * 4]
			push ebp
		}
	}

Стало так:

	FTD2XX_API
		FT_STATUS WINAPI PROXY_FT_EE_UARead(
			FT_HANDLE ftHandle,
			PUCHAR pucData,
			DWORD dwDataLen,
			LPDWORD lpdwBytesRead
		) {
		fprintf(pLog, "FT_EE_UARead(%p, %p, %lu, %p)n", (void*)ftHandle, (void*)pucData, dwDataLen, (void*)lpdwBytesRead);
		fflush(pLog);
		__asm
		 {
			pop ebp
			jmp p[9 * 4]
			push ebp
		}
	}

В итоге мы получаем вот такие логи:

FT_OpenEx(7871E000, 1, 0077EE94)
pArg1 = ********
FT_SetTimeouts(03430508, 500, 500)
FT_EE_UASize(03430508, 0077EE88)
FT_EE_UARead(03430508, 0F56F5E0, 30, 0077EE6C)
FT_Close(03430508)

Здесь значения адресов в памяти не интересны, так как они каждый новый запуск будут новые. Разве что можно пользоваться ими, пока идёт текущая отладка. Зато интересен порядок вызова, а также передаваемые параметры, не являющиеся адресами.

Промежуточные итоги

Мы попробовали зайти с трёх сторон:

  1. Сверху, через сам софт, который без ключа не работает. Наивный патч обхода ошибок проверки ключа сделал программу нерабочей.

  2. Снизу, через эмуляцию ключа. Единственный ключ сломали чьи-то кривые руки и отлаживать там нечего.

  3. Середина - ftd2xx.dll. Мы не можем управлять данными в вызовах, но можем их хорошо логировать вместе с получаемыми параметрами.

Давайте вспомним, что мы выяснили:

  1. Есть функция validation, от возвращаемого результата которой зависит вывод ошибки ключа или её отсутствие.

  2. Внутри функции validation вызываются библиотеки FTDI: ftd2xx и ftchipid и ответов этих библиотек достаточно, чтобы признать ключ валидным или нет.

  3. Мы умеем записывать порядок вызова функций ftd2xx и ftchipid с передаваемыми параметрами.

  4. FTDI предлагает использовать несменяемый серийник ключа, чтобы шифровать дату. Если так и сделали, то дело может быть плохо.

Погружаемся глубже

Взлом аппаратного ключа методом veni, vidi, vici - 24

Попытаемся сделать то, чего изначально хотелось избежать: анализа кода по дизассемблеру. Давайте посмотрим в IDA какие функции ftd2xx и ftchipid импортирует библиотека с функцией validation.

Импорт функций для validation

Импорт функций для validation

Это совсем немного.

Открываем дизассемблер IDA и делаем поиск FT_OpenEx. Находим его использование в функции validation. К счастью все обращения к FTDI умещаются в 2 страницы кода и больше нигде в программе обращений нет. То есть мы нашли самое узкое место, где можно попытаться сделать патч. Да и документация на API ftd2xx.dll открытая. Вот начало использования:

Основной код взаимодействия с FTDI в validation

Основной код взаимодействия с FTDI в validation

Начинается всё с попытки открытия FT_OpenEx. Хорошо видно, что в стек перед вызовом кладутся 3 параметра. Также вспомним, что в EAX регистре можно посмотреть возвращаемое значение по правилам stdcall. По нашему proxy-DLL-логгеру и документации на эту функцию мы знаем, что указывается серийник, который нужно открыть. Это не неизменяемый ChipID серийник, а тот, который можно прописать с помощью FTProg - утилиты от FTDI. Смотрим какой серийник нужен (замазан красным), зашиваем его с помощью FTProg и вместо "Dongle Not Present" получаем ошибку "Dongle Expired". Наконец-то какое-то продвижение.

Дальше в коде достаётся уникальный ChipID, устанавливаются таймауты (по описанию функции она к нашему делу отношения не имеет и можно её игнорировать), опрашивается размер свободной памяти и эта память вычитывается. Всё это было видно в логе нашей proxy DLL чуть выше.

Также видны 2 условных перехода, идущие после обращений к функциям ftd2xx и test eax,eax. То есть просто смотрится возвращаемое значение и если ошибка, то аварийное завершение. После вычитывания памяти EEPROM идёт следующий блок кода:

Обработка EEPROM

Обработка EEPROM

По сути блок разбит на 3 части. В верхней идёт подготовка к вызову субрутины sub_10001520, укладываются в стек её параметры. Одним из параметров идёт указатель на вычитанные из EEPROM данные. Далее идёт вызов этой субрутины. В третьей части идёт подготовка к вызову localtime64. Скорее всего это подготовка к сравнению времени на ключе и времени на компьютере. Заглянем в субрутину sub_10001520:

Декодирование EEPROM

Декодирование EEPROM

Она оказалась небольшой. Виден один небольшой цикл и подготовка к выходу из функции. Взглянув на этот код, я не могу и близко объяснить суть цикла, но в пошаговом отладчике стало быстро понятно, что:

  1. Цикл имеет итераций около длины памяти EEPROM.

  2. В нём по байтам читается один буфер и пишется другой буфер.

  3. Читается тот буфер, что считался из EEPROM.

  4. Одним из параметров вызова является уникальный ChipID.

Похоже, что мы нашли дешифровку данных! Где-то через час сидения с листочком и ручкой стал понятен алгоритм шифрования:

  1. В качестве ключа взять второй байт из уникального ChipID. (Почему второй? Security through obscurity?)

  2. Выполнить XOR ключа с очередным байтом из данных EEPROM и сохранить получившийся байт в буфер результата.

  3. Результат XOR использовать, как новое значение ключа.

  4. Повторять с пункта 2, пока не кончатся данные.

Старый добрый XOR из 20 века, рад встретить тебя, а не AES какой-нибудь. Быстро набросал программку для записи данных в EEPROM так, чтобы атакуемый софт их дешифровал, как мне надо. Шифрование выглядит вот так:

void encode(char b, unsigned char *bufin, unsigned char *bufout, unsigned int buflen)
	{
		for (unsigned int i = 0; i < buflen; i++)
		{
			bufout[i] = b ^ bufin[i];
			b = bufout[i];
		}
	}

Далее в IDA удалось понять какие байты отвечают за дату. Естественно, дата хранится в unixtime и сравнивается с результатом localtime. Удалось установить дату истечения лицензии в будущем, проверка ключа в отладчике пошла по пути, выглядящим, как успешный и ...

Победа?

И я получил ту же ошибку, что и раньше, когда данные в EEPROM были невалидные - “Dongle Expired”. Функция validation получила правильную дату из ключа, закончила какое либо общение с ключом, но на поздних стадиях работы приняла решение, выдать код просроченного ключа. Что ж, смотрим что она там делает.

Код ниже пугал своей запутанностью. Выглядело это вот так:

После кода проверки даты наступает такая путаница, что даже погружаться не хочется

После кода проверки даты наступает такая путаница, что даже погружаться не хочется

Понять что-то из этого я не смог, как и проследить в отладке какую-то логику в выполнении кода. Но я заметил lpFileName, а также по ходу нескольких последующих довольно неинтересных страниц кода последовательность:

CreateFileA
WriteFile
FlushFileBuffers
Здесь наверное что-то важное
DeleteFileA

Скорее всего тут создаётся файл, а затем удаляется. Выделим самый интересный фрагмент: сразу после записи, но до удаления.

Место между концом записи в файл и его удалением

Место между концом записи в файл и его удалением

Хорошо, что IDA берёт на себя подсветку синтаксиса, подсказки из открытых API и многое другое. Внешний вызов VerifyLicense3 прямо бросается в глаза. Смотрим Imports в IDA, что он импортируется из некой flexlm.dll

Найдена связка с FlexLM

Найдена связка с FlexLM

Гуглим flexlm и в третьей ссылке находим его описание:

FLEXlm software is a prominent license management solution that enables software vendors to impose restrictions on the number of software seats available to their customers. FLEXlm supports different licensing policies such as Floating (aka Concurrent) and Node Locked licenses. This type of software system is also referred to as DRM (Digital Rights Management) Solutions .

Куда же я вляпался

Оказывается помимо аппаратного ключа, о котором я думал всё время, есть еще полноценное программное решение для цифровой защиты. Несколько страниц кода, которые я опустил, подготавливали для него файл, записывая туда фрагменты лицензии, а затем передавали файл на верификацию. На меня напало ощущение, что чем глубже копаешь, так больше понимаешь, что зря полез заниматься не своим делом. Но терять все наработки, особенно после успеха с дешифровкой ключа, не хотелось. Раз есть какое-то средство защиты, то можно посмотреть, как оно работает.

  1. Первый путь был посмотреть созданный файл лицензии до того, как его стирают. Файл оказался многострочный с непонятной информацией. Документацию на правила составления файла лицензии FlexLM (LM это сокращение от license manager) я не нашёл. Снова тупик.

  2. Второй путь был поотлаживать flexlm.dll и попытаться увидеть, что там проверяется. Я просто устал нажимать на Step over. Библиотека оказалась большой. Тупик.

  3. Третий путь был поискать какой-то готовый хак для FlexLM, но ничего не гуглилось.

  4. Наконец, я попробовал снова наивный подход, когда мы убеждаем софт, что функция вернула не значение ошибки, а значение корректной верификации. Ранее мы это делали заменой jz на jnz. Здесь не видно очевидных условных переходов, разделяющих успешную и неуспешную валидацию. Какой результат от функции validate должен получиться. мы знаем. Несколькими запусками отладки по регистрам, по командам удалось найти место в оставшейся странице кода после VerifyLicense3, где nz можно пропатчить на z, чтобы уже функция validate возвращала значение успешной валидации. При запуске софта была получена снова та же ошибка - "Dongle Expired", но уже из другого места, где очень похожим образом вызывается VerifyLicense3. В IDA есть инструмент, который показывает все использования операнда, которых оказалось всего два:

Оказывается есть 2 вызова операнда VerifyLicense3

Оказывается есть 2 вызова операнда VerifyLicense3

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

Veni, Vidi, Vici

Эта статья описывает успех в задаче, где изначально у меня не было опыта. Найденный путь не обязательно самый короткий, а у хакеров какие-то мои открытия могут вызвать улыбку, но это статья про путь новичка. В статье больше трёх четвертей занимает описание неудачных путей поиска, потому, что так и было в реальности. Упорство творит чудеса. В статье много раз упоминается слово "погуглить". Я искренне советую пользоваться частыми запросами в поисковые системы интернета, советую подбирать формулировку и терминологию поискового запроса. Пример подбора показан на DLL injection -> hijacking -> proxying. Назовём этот приём высокочастотным гуглингом. Я признаю, что без фундаментальных знаний о jmp, о стеке, о регистрах, о схемотехнике, о программировании, о dll - ни гуглёж, ни упорство не поможет. Не будешь понимать то, о чём читаешь; не будешь понимать, как зайти со стороны, если проблема не решается в лоб.

Но если у тебя есть фундаментальные знания, упорство, умение искать информацию и умение осваивать новые инструменты, то даже в новой (и смежной) для тебя области можно прийти, увидеть и победить.

Автор:
dummywhite

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js