Привет, Хабр!
Однажды, у нас на предприятии встала задача о повышении уровня безопасности при передаче ОЧЕНЬ ВАЖНЫХ ФАЙЛОВ. В общем, слово за слово, и пришли мы к выводу, что передавать надо с помощью scp, а закрытый ключ сертификата для авторизации хранить на брелке типа eToken, благо их у нас накопилось определенное количество.
Идея показалась неплохой, но как это реализовать? Тут я вспомнил, как однажды в бухгалтерии не работал банк-клиент, ругаясь на отсутствие библиотеки с говорящим именем etsdk.dll, меня охватило любопытство и я полез ее ковырять.
Вообще, компания-разработчик на своем сайте распространяет SDK, но для этого надо пройти регистрацию как компания-разработчик ПО, а это явно не я. На просторах интернета документацию найти не удалось, но любопытство одержало верх и я решил разобраться во всём сам. Библиотека – вот она, время есть, кто меня остановит?
Первым делом я запустил DLL Export Viewer от NirSoft, который показал мне приличный список функций, экспортируемых библиотекой. Список выглядит неплохо, прослеживается логика и последовательность действий при работе с токенами. Однако одного списка мало, нужно понять какие параметры, в каком порядке передавать и как получать результаты.
Тут-то и пришла пора вспомнить молодость и запустить OllyDbg версии 2.01, загрузить в него библиотеку ccom.dll криптосистемы Крипто-Ком, используемой банк-клиентом и использующей ту самую библиотеку etsdk.dll, и начать разбираться как именно они это делают.
Поскольку исполняемого файла нет, библиотека загрузится с помощью loaddll.exe из комплекта Olly, поэтому о полноценной отладке мы можем и не мечтать. По сути мы будем использовать отладчик как дизассемблер (да, есть IDA, но с ней я никогда не работал и вообще она платная).
Вызываем контекстное меню и выбираем Search for > All intermodular calls, упорядочиваем результат по имени и ищем функции, начинающиеся на ET*, и не находим. Это значит, что библиотека подключается динамически, поэтому в том же списке мы ищем вызовы GetProcAddress, просматриваем их и с определенной попытки натыкаемся на попытку узнать адрес функции ETReadersEnumOpen, а присмотревшись чуть дальше видим загрузку в память адресов всех функций из библиотеки etsdk.dll.
Неплохо. Полученные адреса функций сохраняются в память командами типа MOV DWORD PTR DS:[10062870],EAX, выделяем каждую такую команду, вызываем контекстное меню и выбираем Find references to > Address constant. В открывшемся окне будут показаны текущая команда и все места вызова функции. Пройдемся по ним и проставим комментарий с именем вызываемой функции – этим мы облегчим себе дальнейшую жизнь.
Пришло время выяснить, как правильно вызывать эти функции. Начнем с начала и изучим получение информации о считывателях. Переходим к месту вызова функции ETReadersEnumOpen и, благодаря оставленным комментариям, видим, что ETReadersEnumOpen, оба ETReadersEnumNext и ETReadersEnumClose сосредоточились в одной функции – очевидно, она, среди прочего, занимается получением списка считывателей.
Все функции используют соглашение о вызове cdecl. Это значит, что результат будет возвращаться в регистре EAX, а параметры передаваться через стек справа-налево. Кроме того, это значит, что все параметры имеют размерность двойного слова, а если не имеют – расширяются до него, что упростит нам жизнь.
Посмотрим окрестности вызова ETReadersEnumOpen:
Передается один параметр, представляющий собой указатель на некую локальную переменную, а после вызова, если результат не равен 0, управление передается на некий явно отладочный код, а если равен – идем дальше (команда JGE передает управление если флаги ZF и OF равны, а флаг OF команда TEST всегда сбрасывает в 0). Таким образом, я заключаю следующий порядок: в функцию передается переменная по ссылке, в которую вернется некий идентификатор перечисления, а как результат функция возвращает код ошибки или 0 если ошибки нет.
Переходим к ETReadersEnumNext:
В нее передаются два параметра: значение переменной, полученное с помощью ETReadersEnumOpen (идентификатор перечисления) и указатель на локальную переменную, куда, очевидно, возвращается очередное значение. Причем поскольку параметры передаются в порядке справа-налево, именно первый параметр – идентификатор, а второй – указатель результата. Код ошибки все так же возвращается через EAX, причем, судя по конструкции цикла, он используется не только для сообщения об ошибке, но и для сообщения о том, что больше перечислять нечего.
С ETReadersEnumClose все еще проще: в нее передается идентификатор перечисления, ну а результат никого не волнует.
Пришло время проверить наше представление об этих функциях. Тут я вынужден сделать небольшое лирическое отступление: дело в том, что по профессии я – сисадмин, и поэтому серьезные компилируемые языки программирования – это не совсем мое. По работе мне больше нужен Bash и Python под Linux, ну а если мне надо быстро что-нибудь сваять под Windows, я использую полюбившийся мне AutoIt.
Плюсами для меня являются:
- мобильность (интерпретатор и редактор скриптов полностью portable),
- простая работа с GUI,
- возможность, если недостаточно функционала, подключать внешние библиотеки (знаю, что тривиально для языка программирования, но не так уж тривиально для скриптового языка),
- возможность скомпоновать скрипты в исполняемые файлы.
Минусы:
- Неявное преобразование типов и недостаточное количество представленных типов.
- Отсутвие записей (а также ассоциативных массивов) и ООП (вообще оно есть, но только для COM-объектов, так что как бы и нету).
Это отступление было к тому, что примеры использования функций мы будем ваять именно на AutoIt. Вызов функций из внешних библиотек, в связи с неявной типизацией в языке, выглядит несколько коряво, но работает.
Приступим: откровенно говоря, мы понятия не имеем, что и какого размера возвращают функции, поэтому будем отдавать большой буфер для начала, и посмотрим, что будет. Код для начала:
Dim $ETSdkDll=DllOpen('etsdk.dll') Dim $buf=DllStructCreate('BYTE[32]') Func PrintBuf($buf) For $i=1 To DllStructGetSize($buf) ConsoleWrite(Hex(DllStructGetData($buf,'buf',$i),2)&' ') Next ConsoleWrite(@CRLF) EndFunc ConsoleWrite('Buffer before: ') PrintBuf($buf) $result=DllCall($ETSdkDll,'DWORD','ETReadersEnumOpen', _ 'PTR',DllStructGetPtr($buf) _ ) ConsoleWrite('Buffer after: ') PrintBuf($buf) ConsoleWrite('Return value: '&$result[0]&@CRLF)
Выполнив его, получаем вывод типа такого:
Buffer before: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 Buffer after: 44 6F C8 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 Return value: 0
Прогоняем несколько раз и видим, что меняются только первые 4 байта, значит, в качестве идентификатора используется 4-байтовое целое, а значит мы можем немного причесать код вызова этой функции до такого состояния:
Func ETReaderEnumOpen() Local $id=DllStructCreate('DWORD') Local $result=DllCall($ETSdkDll,'DWORD','ETReadersEnumOpen', _ 'PTR',DllStructGetPtr($id) _ ) Return $result[0]?0:DllStructGetData($id,1) EndFunc
Подобные эксперименты с функцией ETReadersEnumNext показали следующее: первые 260 байт буфера содержат имя считывателя и нули. Последовательный вызов этой функции перечислил мне все считыватели в системе (например, под ruToken их создано заранее три штуки). Считыватели под eToken создаются динамически, в зависимости от числа подключенных токенов и, самое интересное, у них установлен в еденицу 261-й байт буфера, который, судя по всему, указывает на совместимость считывателя с нашей библиотекой. Если вглядеться в дизассемблированный код, то видно, что записи, у которых 261-й байт равен 0, не обрабатываются. Все остальные байты до конца килобайтного буфера у всех считывателей равны 0 и не различаются.
Итак, со считывателями разобрались, теперь надо понять что дальше. Осмотрев список функций, я пришел к выводу, что последовательность вызова должна быть следующей: сначала делаем bind нужного считывателя, на этом этапе можем узнать общую информацию о вставленном токене, потом делаем логин, и уже после этого получаем доступ к файловой системе. Таким образом, следующие на очереди функции ETTokenBind и ETTokenUnbind.
ETTokenBind выглядит сложно и непонятно, но, поковырявшись некоторое время, я пришел к выводу, что функции передается два параметра, первый из который – указатель на буфер величиной 328 байт (0x0148), а второй – указатель на строку с именем считывателя. Путем экспериментов было установлено, что в первые четыре байта буфера возвращается идентификатор (далее: идентификатор привязки). Для чего выделяется весь остальной буфер – пока загадка. С какими токенами я бы не экспериментировал, остальные 324 байта буфера оставались заполнены нулями. Указанный идентификатор, что логично, успешно используется как аргумент функций ETTokenUnbind и ETTokenRebind.
Следующая функция на очереди – ETRootDirOpen. Принимает три параметра: указатель на результат, идентификатор привязки и константу. У функции есть несколько особенностей.
Первое: возвращаемый результат этой функции проверяется не только на равенство нулю (успех), но и на равенство младших двух байт числу 0x6982, и в случае, если результат равен этому числу, управление передается функции, которая впоследствии вызывает ETTokenLogin, а потом еще раз пытается вызвать ETRootDirOpen. Отсюда можно заключить, что 0x6982 – код ошибки, означающий «Требуется авторизация». Забегая вперед скажу, что все остальные функции, работающие с файлами и папками, устроены так же.
Второе: в качестве одного из параметров эта функция принимает константу 0xF007. Вызовов с другими константами в коде нет. Возможно, эта константа как-то характеризует информацию, записанную на токен (множество корневых папок?). Я попробовал пройти брутфорсом по всем значениям двухбайтовой константы и токен откликнулся только на значения 0x0001, 0xF001-0xF00B (авторизацию, кстати, ни разу не попросил). Позже я выяснил, что на свежеинициализированном токене доступны те же папки. Подумав над этим некоторое время, я пришел к выводу, что по замыслу разработчика, разные корневые папки используются для разных целей, и где-то прописано, что 0xF007 – для ключей.
Третье: значение, возвращаемое функцией, на скриншоте не видно, но возвращается в середину того 328-байтного буфера, который выделялся ранее, из чего можно сделать вывод, что тот буфер – структура, хранящая самые разные идентификаторы и данные, касающиеся рассматриваемого токена.
Раз уж пошла попытка авторизации, время разобраться с ней. Функция ETTokenLogin получает два параметра: идентификатор привязки и указатель на буфер. Сначала я думал, что буфер используется для вывода какого-то результата, однако экспериметы показали, что используется следующий алгоритм: если указатель нулевой или указывает на пустую строку, библиотека рисует интерфейсное окно с запросом пароля, если же он указывает на непустую строку – эта строка используется как пароль. ETTokenLogout воспринимает всего один параметр: идентификатор привязки.
Следующая группа функций: ETDirEnumOpen, ETDirEnumNext и ETDirEnumClose. Их можно попробовать распутать, не заглядывая в код. В общем и целом они должны работать так же, как ETReadersEnum*, с той лишь разницей, что в ETDirEnumOpen будет передаваться в качестве параметра еще и идентификатор текущей папки. Проверяем – работает.
Группа функций ETFilesEnumOpen, ETFilesEnumNext и ETFilesEnumClose просто обязаны работать так же, однако проверить это с уверенностью мы пока не можем, т.к. в корневой папке исследуемого токена, судя по всему, файлов нет, а это значит, что пора уходить вглубь дерева папок, функцией ETDirOpen.
В данном API, похоже, нарисовалась традиция, согласно которой, первый параметр используется для возврата результата, поэтому предположим, что это верно и в этот раз. Второй параметр, прежде чем быть переданным функции, проходит видоизменения с помощью команды MOVZX EDI,DI, т.е. слово расширяется до двойного слова. Очевидно, это нужно для того, чтобы двухбайтовое имя папки передать в четырехбайтовом параметре. Ну а третий параметр по логике вещей должен быть идентификатором открытой папки. Пробуем – получилось. ETDirClose угадывается без сюрпризов: 1 параметр – идентификатор папки.
Итак, мы узнали достаточно, чтобы перечислить все файлы и папки на токене. Следующий простенький код именно это и сделает (описание вызова DllCall я тут не делаю – оно будет для всех функций в тексте модуля в конце статьи):
Func PrintDir($Id,$Prefix) Local $EnumId=ETDirEnumOpen($Id) While 1 Local $dir=ETDirEnumNext($EnumId) If @error Then ExitLoop ConsoleWrite($Prefix&'(dir)'&Hex($dir,4)&@CRLF) Local $DirId=ETDirOpen($dir,$Id) PrintDir($DirId,$Prefix&@TAB) ETDirClose($DirId) WEnd ETDirEnumClose($EnumId) $EnumId=ETFilesEnumOpen($Id) While 1 Local $file=ETFilesEnumNext($EnumId) If @error Then ExitLoop ConsoleWrite($Prefix&'(file)'&Hex($file,4)&@CRLF) WEnd ETFilesEnumClose($EnumId) EndFunc Local $EnumId=ETReaderEnumOpen() If $EnumId Then While 1 Local $reader=ETReaderEnumNext($EnumId) If @error Then ExitLoop If Not $reader[1] Then ContinueLoop Local $BindId=ETTokenBind($reader[0]) ConsoleWrite($reader[0]&':'&@CRLF) ETTokenLogin($BindId,'123456') Local $DirId=ETRootDirOpen($BindId) PrintDir($DirId,@TAB) ETDirClose($DirId) WEnd EndIf ETReaderEnumClose($EnumId)
Результат в консоли:
Aladdin Token JC 0: (dir)1921 (dir)DDDD (file)0002 (file)0003 (file)0004 (file)0001 (file)A001 (file)B001 (file)C001 (file)AAAA (file)D001
Отлично!
Чтож, мы научились открывать и просматривать папки, пора научиться открывать и читать файлы. ETFileOpen принимает 3 параметра, поэтому для начала пробуем сделать так же, как и для ETDirOpen: результат, имя файла, идентификатор папки и обламываемся: разработчики поменяли местами последние два параметра. Ну хоть ETFileClose работает без сюрпризов.
ETFileRead. Самая страшная функция из всех, т.к. воспринимает аж 5 параметров. Куда столько? Попробуем перечислить что нам нужно: откуда читать (файл), куда читать (буфер), сколько читать и начиная откуда читать. Попробуем разобраться что да как:
Как видно, третий параметр, передаваемый в функцию ETFileRead всегда равен 0xFFFF, поэтому я склонен считать, что это – длина считываемого куска данных. Остальные 4 параметра приходят в функцию, названную мной FileReadHere извне в том же порядке. Ниже на рисунке окрестности вызова этой функции. Значение первого параметра берется из памяти по адресу ESI+8. Указатель на этот адрес используется в функции FileOpenHere (названа по тому же принципу) и туда, очевидно, записан идентификатор открытого файла. Второй параметр равен нулю, поэтому его назначаем ответственным за точку начала чтения файла. Третий параметр (четвертый для ETFileRead) какой-то мутный, поэтому его назначим указателем на буфер-результат. Пятый параметр необычен совсем. В него помещается слово из адреса ESI+12, расширяясь до двойного слова – это необычно, т.к. пока что все смещения, которые я видел, были кратны 4 (12 не кратно 4, потому что это 0x12, т.е. 18 в десятичной). Адрес ESI+10 нигде в окрестностях не упоминается, а вот ESI+0C передается в FileGetInfoHere, поэтому придется сначала разобраться с функцией ETFileGetInfo. Она простая, первый параметр – идентификатор файла, второй – указатель на буфер результата. После вызова в буфере меняются 1, 2, 3, 7 и 8 байты. Забегая вперед, скажу, что выяснится, что последние два байта – размер файла. Именно это значение передается в функцию ETFileRead и в функцию, инициализирующую выходной буфер для нее. Первые два байта результата ETFileGetInfo оказались именем файла. Значение третьего я не понял, но он был установлен в 1 только у одного файла на токене. Таким образом, вырисовывается следующий порядок параметров: идентификатор файла, точка начала чтения, максимальное количество считывемых байт, указатель на буфер, размер буфера.
Раз уж мы затронули ETFileGetInfo, надо бы сразу и реализовать ETDirGetInfo: порядок параметров тот же, только участвует идентификатор папки, а не файла. Возвращаемый результат: имя папки по идентификатору.
На этом мы закончили читать с токена, пришло время писать на токен. Начнем с того, чтобы создать папку. Параметры функции ETDirCreate: указатель для результата (очевидно, после создания папка откроется и сюда вернется идентификатор), имя папки, идентификатор родительской папки и 0. Четвертый параметр жестко прописан в коде и я так и не понял, на что он влияет. Папки успешно создаются при любом его значении. ETDirDelete принимает всего 1 параметр, поэтому это, очевидно, идентификатор открытой папки. ETFileCreate воспринимает пять параметров: указатель на результат, аналогично ETDirCreate, идентификатор папки, имя файла, размер файла и пятый параметр. Если пятый параметр установить в ненулевое значение, то при последующем вызове ETFileGetInfo для этого файла, третий байт результата (тот самый, непонятный) будет установлен в 1. Подумав, я провел эксперимент и убедился, что когда атрибут установлен, для доступа к файлу необходимо ввести пароль, если нет, то это не обязательно. Забавно, что на токене, с которым я экспериментировал, такой файл оказался всего один. Надеюсь, что все остальные файлы зашифрованы на ключе из этого. ETFileDelete работает без сюрпризов, аналогично ETDirDelete.
Последняя функция, обращение к которой реализовано в этой библиотеке – ETFileWrite. Принимает 4 аргумента: идентификатор файла, ноль (эксперимент показывает, что это смешение относительно начала файла), указатель на буфер с данными и размер данных. При этом важно помнить, что файл не расширяется. Если сумма смещения и длины файла превышает размер файла, запись не происходит, поэтому если размер файла требуется изменить, файл придется удалять и создавать заново с новым размером.
Далее: если вспомнить таблицу экспорта библиотеки, то в ней есть еще 5 функций, однако их вызов не реализован в данной библиотеке, работающей с СКЗИ Крипто-Ком. На наше счастье, тот же банк распространяет также и библиотеку для работы с СКЗИ Message-Pro – mespro2.dll, которая также может работать с токенами и в ней есть немного больше, а именно – вызов ETTokenLabelGet.
На скриншоте видно, что есть два вызова функции, различающиеся тем, что в первом случае второй параметр равен нулю, а во втором – какому-то числу. Третий параметр всегда указатель, поэтому предположим, что это результат, а первый – было бы логично предположить, что идентификатор связки с токеном. Пробуем запустить с нулем в качестве второго параметра – первые 4 байта в буфере изменились на значение 0x0000000A, т.е. 10, а это как раз длина имени «TestToken» с нулевым байтом в конце. Но если по указателю в третий параметр возврачается двойное слово, получается, указатель на буфер нужного размера надо передавать во второй параметр. Посему заключаем такой порядок: первый раз запускаем функцию так, что второй параметр – нулевой указатель, а третий – указатель на двойное слово. Потом инициализируем буфер нужного размера и запускаем функцию второй раз, при этом второй параметр – указатель на буфер.
Но вызов еще 4 функций не реализован и тут, поэтому их реализацию я получил брутфорсом и интуицией: я обнаружил, что если вызываемой функции передать слишком мало параметров, это вызывает критическую ошибку при выполнении программы, это позволяет экспериментально подобрать количество параметров оставшихся функций:
ETTokenIDGet: 3
ETTokenMaxPinGet: 2
ETTokenMinPinGet: 2
ETTokenPinChange: 2
ETTokenIDGet принимает слишком много параметров для возврата какого-то простого значения, поэтому запустим ее так же, как и ETTokenGetLabel – получается с первой попытки и возвращает строку с номером, написанным на боку токена.
ETTokenMaxPinGet и ETTokenMinPinGet, как раз наоборот, имеют количество параметров, идеальное для возврата однго числового значения. Пробуем первый параметр – идентификатор связки, второй – указатель на число. В результате получаем максимальную и минимально возможные длины пароля, заданные в настройках токена.
ETTokenPinChange, исходя из названия, служит для смены пароля на токен, соответственно, должен бы принимать только идентификатор связки и указатель на строку с новым паролем. Пробуем первый раз, получаем код ошибки 0x6982, который, как мы знаем, означает необходимость выполнить логин на токен. Логично. Повторяем с логином и коротким паролем – получаем ошибку 0x6416. Делаем вывод о том, что длина пароля не соответствует политике. Повторяем с длинным паролем – отрабатывает.
Теперь сводим все функции в один модуль и сохраняем его – будем инклудить в другие проекты. Текст модуля получился такой:
;Func ETReadersEnumOpen() ;Func ETReadersEnumNext($EnumId) ;Func ETReadersEnumClose($EnumId) ;Func ETTokenBind($ReaderName) ;Func ETTokenRebind($BindId) ;Func ETTokenUnbind($BindId) ;Func ETTokenLogin($BindId,$Pin='') ;Func ETTokenPinChange($BindId,$Pin) ;Func ETTokenLogout($BindId) ;Func ETRootDirOpen($BindId,$Dir=0xF007) ;Func ETDirOpen($Dir,$DirId) ;Func ETDirCreate($Dir,$DirId) ;Func ETDirGetInfo($DirId) ;Func ETDirClose($DirId) ;Func ETDirDelete($DirId) ;Func ETDirEnumOpen($DirId) ;Func ETDirEnumNext($EnumId) ;Func ETDirEnumClose($EnumId) ;Func ETFileOpen($File,$DirId) ;Func ETFileCreate($File,$DirId,$Size,$Private=0) ;Func ETFileGetInfo($FileId) ;Func ETFileRead($FileId) ;Func ETFileWrite($FileId,$Data,$Pos=0) ;Func ETFileClose($FileId) ;Func ETFileDelete($FileId) ;Func ETFilesEnumOpen($DirId) ;Func ETFilesEnumNext($EnumId) ;Func ETFilesEnumClose($EnumId) ;Func ETTokenLabelGet($BindId) ;Func ETTokenIDGet($BindId) ;Func ETTokenMaxPinGet($BindId) ;Func ETTokenMinPinGet($BindId) Const $ET_READER_NAME=0 Const $ET_READER_ETOKEN=1 Const $ET_FILEINFO_NAME=0 Const $ET_FILEINFO_PRIVATE=1 Const $ET_FILEINFO_SIZE=2 Dim $ETSdkDll=DllOpen('etsdk.dll') Func ETReadersEnumOpen() Local $Out=DllStructCreate('DWORD') Local $CallRes=DllCall($ETSdkDll,'WORD','ETReadersEnumOpen', _ 'PTR',DllStructGetPtr($Out) _ ) Return $CallRes[0] _ ?SetError($CallRes[0],0,False) _ :DllStructGetData($Out,1) EndFunc Func ETReadersEnumNext($EnumId) Local $Reader=DllStructCreate('CHAR name[260]; BYTE etoken;') Local $CallRes=DllCall($ETSdkDll,'WORD','ETReadersEnumNext', _ 'DWORD',$EnumId, _ 'PTR',DllStructGetPtr($Reader) _ ) Local $Result[2]=[ DllStructGetData($reader,'name'), _ DllStructGetData($reader,'etoken')] Return $CallRes[0] _ ?SetError($CallRes[0],0,False) _ :$Result EndFunc Func ETReadersEnumClose($EnumId) Local $CallRes=DllCall($ETSdkDll,'WORD','ETReadersEnumClose', _ 'DWORD',$EnumId _ ) Return $CallRes[0] _ ?SetError($CallRes[0],0,False) _ :True EndFunc Func ETTokenBind($ReaderName) Local $In=DllStructCreate('BYTE['&(StringLen($ReaderName)+1)&']') Local $Out=DllStructCreate('DWORD') DllStructSetData($In,1,$ReaderName) Local $CallRes=DllCall($ETSdkDll,'WORD','ETTokenBind', _ 'PTR',DllStructGetPtr($Out), _ 'PTR',DllStructGetPtr($In) _ ) Return $CallRes[0] _ ?SetError($CallRes[0],0,False) _ :DllStructGetData($Out,1) EndFunc Func ETTokenRebind($BindId) Local $CallRes=DllCall($ETSdkDll,'WORD','ETTokenRebind', _ 'DWORD',$BindId _ ) Return $CallRes[0] _ ?SetError($CallRes[0],0,False) _ :True EndFunc Func ETTokenUnbind($BindId) Local $CallRes=DllCall($ETSdkDll,'WORD','ETTokenUnbind', _ 'DWORD',$BindId _ ) Return $CallRes[0] _ ?SetError($CallRes[0],0,False) _ :True EndFunc Func ETTokenLogin($BindId,$Pin='') Local $In=DllStructCreate('BYTE['&(StringLen($Pin)+1)&']') DllStructSetData($In,1,$Pin) Local $CallRes=DllCall($ETSdkDll,'WORD','ETTokenLogin', _ 'DWORD',$BindId, _ 'PTR',DllStructGetPtr($In) _ ) Return $CallRes[0] _ ?SetError($CallRes[0],0,False) _ :True EndFunc Func ETTokenPinChange($BindId,$Pin) Local $In=DllStructCreate('CHAR['&(StringLen($Pin)+1)&']') DllStructSetData($In,1,$Pin) Local $CallRes=DllCall($ETSdkDll,'WORD','ETTokenPinChange', _ 'DWORD',$BindId, _ 'PTR',DllStructGetPtr($In) _ ) Return $CallRes[0] _ ?SetError($CallRes[0],0,False) _ :True EndFunc Func ETTokenLogout($BindId) Local $CallRes=DllCall($ETSdkDll,'WORD','ETTokenLogout', _ 'DWORD',$BindId _ ) Return $CallRes[0] _ ?SetError($CallRes[0],0,False) _ :True EndFunc Func ETRootDirOpen($BindId,$Dir=0xF007) Local $Out=DllStructCreate('DWORD') Local $CallRes=DllCall($ETSdkDll,'WORD','ETRootDirOpen', _ 'PTR',DllStructGetPtr($Out), _ 'DWORD',$BindId, _ 'DWORD',$Dir _ ) Return $CallRes[0] _ ?SetError($CallRes[0],0,False) _ :DllStructGetData($Out,1) EndFunc Func ETDirOpen($Dir,$DirId) Local $Out=DllStructCreate('DWORD') Local $CallRes=DllCall($ETSdkDll,'WORD','ETDirOpen', _ 'PTR',DllStructGetPtr($Out), _ 'DWORD',$Dir, _ 'DWORD',$DirId _ ) Return $CallRes[0] _ ?SetError($CallRes[0],0,False) _ :DllStructGetData($Out,1) EndFunc Func ETDirCreate($Dir,$DirId) Local $Out=DllStructCreate('DWORD') Local $CallRes=DllCall($ETSdkDll,'WORD','ETDirCreate', _ 'PTR',DllStructGetPtr($Out), _ 'DWORD',$Dir, _ 'DWORD',$DirId, _ 'DWORD',0 _ ) Return $CallRes[0] _ ?SetError($CallRes[0],0,False) _ :DllStructGetData($Out,1) EndFunc Func ETDirGetInfo($DirId) Local $Out=DllStructCreate('BYTE[8]') Local $CallRes=DllCall($ETSdkDll,'WORD','ETDirGetInfo', _ 'DWORD',$DirId, _ 'PTR',DllStructGetPtr($Out) _ ) Return $CallRes[0] _ ?SetError($CallRes[0],0,False) _ :DllStructGetData($Out,1) EndFunc Func ETDirClose($DirId) Local $CallRes=DllCall($ETSdkDll,'WORD','ETDirClose', _ 'DWORD',$DirId _ ) Return $CallRes[0] _ ?SetError($CallRes[0],0,False) _ :True EndFunc Func ETDirDelete($DirId) Local $CallRes=DllCall($ETSdkDll,'WORD','ETDirDelete', _ 'DWORD',$DirId _ ) Return $CallRes[0] _ ?SetError($CallRes[0],0,False) _ :True EndFunc Func ETDirEnumOpen($DirId) Local $Out=DllStructCreate('DWORD') Local $CallRes=DllCall($ETSdkDll,'WORD','ETDirEnumOpen', _ 'PTR',DllStructGetPtr($Out), _ 'DWORD',$DirId _ ) Return $CallRes[0] _ ?SetError($CallRes[0],0,False) _ :DllStructGetData($Out,1) EndFunc Func ETDirEnumNext($EnumId) Local $Out=DllStructCreate('DWORD') Local $CallRes=DllCall($ETSdkDll,'WORD','ETDirEnumNext', _ 'DWORD',$EnumId, _ 'PTR',DllStructGetPtr($Out) _ ) Return $CallRes[0] _ ?SetError($CallRes[0],0,False) _ :DllStructGetData($Out,1) EndFunc Func ETDirEnumClose($EnumId) Local $CallRes=DllCall($ETSdkDll,'WORD','ETDirEnumClose', _ 'DWORD',$EnumId _ ) Return $CallRes[0] _ ?SetError($CallRes[0],0,False) _ :True EndFunc Func ETFileOpen($File,$DirId) Local $Out=DllStructCreate('DWORD') Local $CallRes=DllCall($ETSdkDll,'WORD','ETFileOpen', _ 'PTR',DllStructGetPtr($Out), _ 'DWORD',$DirId, _ 'DWORD',$File _ ) Return $CallRes[0] _ ?SetError($CallRes[0],0,False) _ :DllStructGetData($Out,1) EndFunc Func ETFileCreate($File,$DirId,$Size,$Private=0) Local $Out=DllStructCreate('DWORD') Local $CallRes=DllCall($ETSdkDll,'WORD','ETFileCreate', _ 'PTR',DllStructGetPtr($Out), _ 'DWORD',$DirId, _ 'DWORD',$File, _ 'DWORD',$Size, _ 'DWORD',$Private _ ) Return $CallRes[0] _ ?SetError($CallRes[0],0,False) _ :DllStructGetData($Out,1) EndFunc Func ETFileGetInfo($FileId) Local $Out=DllStructCreate('WORD name;WORD private;WORD;WORD size') Local $CallRes=DllCall($ETSdkDll,'WORD','ETFileGetInfo', _ 'DWORD',$FileId, _ 'PTR',DllStructGetPtr($Out) _ ) Local $Result[3]=[ DllStructGetData($Out,'name'), _ DllStructGetData($Out,'private'), _ DllStructGetData($Out,'size')] Return $CallRes[0] _ ?SetError($CallRes[0],0,False) _ :$Result EndFunc Func ETFileRead($FileId) Local $FileInfo=ETFileGetInfo($FileId) If @error Then Return SetError(@error,0,False) Local $Out=DllStructCreate('BYTE ['&$FileInfo[$ET_FILEINFO_SIZE]&']') Local $CallRes=DllCall($ETSdkDll,'WORD','ETFileRead', _ 'DWORD',$FileId, _ 'DWORD',0, _ 'DWORD',0xFFFF, _ 'PTR',DllStructGetPtr($Out), _ 'DWORD',$FileInfo[$ET_FILEINFO_SIZE] _ ) Return $CallRes[0] _ ?SetError($CallRes[0],0,False) _ :DllStructGetData($Out,1) EndFunc Func ETFileWrite($FileId,$Data,$Pos=0) $Data=Binary($Data) Local $DataSize=BinaryLen($Data) Local $In=DllStructCreate('BYTE['&$DataSize&']') DllStructSetData($In,1,$Data) Local $CallRes=DllCall($ETSdkDll,'WORD','ETFileWrite', _ 'DWORD',$FileId, _ 'DWORD',$Pos, _ 'PTR',DllStructGetPtr($In), _ 'DWORD',$DataSize _ ) Return $CallRes[0] _ ?SetError($CallRes[0],0,False) _ :True EndFunc Func ETFileClose($FileId) Local $CallRes=DllCall($ETSdkDll,'WORD','ETFileClose', _ 'DWORD',$FileId _ ) Return $CallRes[0] _ ?SetError($CallRes[0],0,False) _ :True EndFunc Func ETFileDelete($FileId) Local $CallRes=DllCall($ETSdkDll,'WORD','ETFileDelete', _ 'DWORD',$FileId _ ) Return $CallRes[0] _ ?SetError($CallRes[0],0,False) _ :True EndFunc Func ETFilesEnumOpen($DirId) Local $Out=DllStructCreate('DWORD') Local $CallRes=DllCall($ETSdkDll,'WORD','ETFilesEnumOpen', _ 'PTR',DllStructGetPtr($Out), _ 'DWORD',$DirId _ ) Return $CallRes[0] _ ?SetError($CallRes[0],0,False) _ :DllStructGetData($Out,1) EndFunc Func ETFilesEnumNext($EnumId) Local $Out=DllStructCreate('DWORD') Local $CallRes=DllCall($ETSdkDll,'WORD','ETFilesEnumNext', _ 'DWORD',$EnumId, _ 'PTR',DllStructGetPtr($Out) _ ) Return $CallRes[0] _ ?SetError($CallRes[0],0,False) _ :DllStructGetData($Out,1) EndFunc Func ETFilesEnumClose($EnumId) Local $CallRes=DllCall($ETSdkDll,'WORD','ETFilesEnumClose', _ 'DWORD',$EnumId _ ) Return $CallRes[0] _ ?SetError($CallRes[0],0,False) _ :True EndFunc Func ETTokenLabelGet($BindId) Local $Out1=DllStructCreate('DWORD') Local $CallRes=DllCall($ETSdkDll,'WORD','ETTokenLabelGet', _ 'DWORD',$BindId, _ 'PTR',0, _ 'PTR',DllStructGetPtr($Out1) _ ) If $CallRes[0] Then Return SetError($CallRes[0],0,False) Local $Out2=DllStructCreate('CHAR['&DllStructGetData($Out1,1)&']') $CallRes=DllCall($ETSdkDll,'WORD','ETTokenLabelGet', _ 'DWORD',$BindId, _ 'PTR',DllStructGetPtr($Out2), _ 'PTR',DllStructGetPtr($Out1) _ ) Return $CallRes[0] _ ?SetError($CallRes[0],0,False) _ :DllStructGetData($Out2,1) EndFunc Func ETTokenIDGet($BindId) Local $Out1=DllStructCreate('DWORD') Local $CallRes=DllCall($ETSdkDll,'WORD','ETTokenIDGet', _ 'DWORD',$BindId, _ 'PTR',0, _ 'PTR',DllStructGetPtr($Out1) _ ) If $CallRes[0] Then Return SetError($CallRes[0],0,False) Local $Out2=DllStructCreate('CHAR['&DllStructGetData($Out1,1)&']') $CallRes=DllCall($ETSdkDll,'WORD','ETTokenIDGet', _ 'DWORD',$BindId, _ 'PTR',DllStructGetPtr($Out2), _ 'PTR',DllStructGetPtr($Out1) _ ) Return $CallRes[0] _ ?SetError($CallRes[0],0,False) _ :DllStructGetData($Out2,1) EndFunc Func ETTokenMaxPinGet($BindId) Local $Out=DllStructCreate('DWORD') Local $CallRes=DllCall($ETSdkDll,'WORD','ETTokenMaxPinGet', _ 'DWORD',$BindId, _ 'PTR',DllStructGetPtr($Out) _ ) Return $CallRes[0] _ ?SetError($CallRes[0],0,False) _ :DllStructGetData($Out,1) EndFunc Func ETTokenMinPinGet($BindId) Local $Out=DllStructCreate('DWORD') Local $CallRes=DllCall($ETSdkDll,'WORD','ETTokenMinPinGet', _ 'DWORD',$BindId, _ 'PTR',DllStructGetPtr($Out) _ ) Return $CallRes[0] _ ?SetError($CallRes[0],0,False) _ :DllStructGetData($Out,1) EndFunc
Итак, мы можем делать все, что захотим с файловой системой токена. Чтобы продемонстрировать это, я написал простенький скрипт, который будет копировать содержимое с одного токена на другой. Скрипт уровня «Proof-of-concept», т.е. тут не будет уймы проверок, которые должны были бы быть в «правильном» приложении, однако позволит нам получить второй действующий токен.
#include <etsdk.au3> #include <GUIConstantsEx.au3> #include <StaticConstants.au3> #NoTrayIcon Opt('MustDeclareVars',1) Opt('GUIOnEventMode',1) Opt('GUIDataSeparatorChar',@LF) Const $Title='eToken Copy' Const $GUISize[2]=[250,100] Dim $SrcCtrl,$DstCtrl,$ListTimer Func TokenCopyDir($SrcId,$DstId) Local $Name,$SrcSubId,$DstSubId,$SrcInfo,$SrcData ; Проход по папкам с рекурсией Local $EnumId=ETDirEnumOpen($SrcId) While 1 $Name=ETDirEnumNext($EnumId) If @error Then ExitLoop $SrcSubId=ETDirOpen($Name,$SrcId) $DstSubId=ETDirOpen($Name,$DstId) If @error Then $DstSubId=ETDirCreate($Name,$DstId) EndIf TokenCopyDir($SrcSubId,$DstSubId) ETDirClose($SrcSubId) ETDirClose($DstSubId) WEnd ETDirEnumClose($EnumId) ; Проход по файлам $EnumId=ETFilesEnumOpen($SrcId) While 1 $Name=ETFilesEnumNext($EnumId) If @error Then ExitLoop $SrcSubId=ETFileOpen($Name,$SrcId) $SrcInfo=ETFileGetInfo($SrcSubId) $DstSubId=ETFileOpen($Name,$DstId) If Not @error Then ETFileDelete($DstSubId) EndIf $DstSubId=ETFileCreate($Name,$DstId,$SrcInfo[$ET_FILEINFO_SIZE],$SrcInfo[$ET_FILEINFO_PRIVATE]) ETFileWrite($DstSubId,ETFileRead($SrcSubId)) ETFileClose($SrcSubId) ETFileClose($DstSubId) WEnd ETFilesEnumClose($EnumId) EndFunc Func TokenCopy() Local $Src=GUICtrlRead($SrcCtrl) Local $Dst=GUICtrlRead($DstCtrl) If $Src=='' Or $Dst=='' Then MsgBox(0x10,$Title,'Не все поля заполнены') Return False EndIf ; Из выбранного поля получаем номер токена $Src=StringMid($Src,StringLen($Src)-8,8) $Dst=StringMid($Dst,StringLen($Dst)-8,8) If $Src==$Dst Then MsgBox(0x10,$Title,'Нельзя выбрать один и тот же токен') Return False EndIf ; Подключаемся к токенам Local $SrcBindId=False,$DstBindId=False Local $EnumId=ETReadersEnumOpen() While 1 Local $Reader=ETReadersEnumNext($EnumId) If @error Then ExitLoop If Not $Reader[$ET_READER_ETOKEN] Then ContinueLoop Local $BindId=ETTokenBind($Reader[$ET_READER_NAME]) If ETTokenIDGet($BindId)==$Src Then $SrcBindId=$BindId ElseIf ETTokenIDGet($BindId)==$Dst Then $DstBindId=$BindId Else ETTokenUnbind($BindId) EndIf WEnd ETReadersEnumClose($EnumId) If Not ETTokenLogin($SrcBindId) Then MsgBox(0x10,$Title,'Ошибка авторизации на токене-источнике') Return False EndIf If Not ETTokenLogin($DstBindId) Then MsgBox(0x10,$Title,'Ошибка авторизации на токене-назначении') Return False EndIf ; Запуск копирования TokenCopyDir(ETRootDirOpen($SrcBindId),ETRootDirOpen($DstBindId)) ETTokenUnbind($SrcBindId) ETTokenUnbind($DstBindId) MsgBox(0x40,$Title,'Копирование завершено') EndFunc Func GetTokenList() Local $Reader, $BindId, $Result='' Local $EnumId=ETReadersEnumOpen() While 1 $Reader=ETReadersEnumNext($EnumId) If @error Then ExitLoop If Not $Reader[$ET_READER_ETOKEN] Then ContinueLoop $BindId=ETTokenBind($Reader[$ET_READER_NAME]) $Result&=@LF&ETTokenLabelGet($BindId)&' ('&ETTokenIDGet($BindId)&')' ETTokenUnbind($BindId) WEnd ETReadersEnumClose($EnumId) Return $Result EndFunc Func UpdateTokenList() Local $Tokens=GetTokenList() GUICtrlSetData($SrcCtrl,$Tokens,GUICtrlRead($SrcCtrl)) GUICtrlSetData($DstCtrl,$Tokens,GUICtrlRead($DstCtrl)) EndFunc Func onClose() Exit EndFunc Func GUIInit() GUICreate($Title,$GUISize[0],$GUISize[1],(@DesktopWidth-$GUISize[0])/2,(@DesktopHeight-$GUISize[1])/2) GUISetOnEvent($GUI_EVENT_CLOSE,'onClose') GUICtrlCreateLabel('Источник:',8,8,64,-1,$SS_RIGHT) GUICtrlCreateLabel('Назначение:',8,32,64,-1,$SS_RIGHT) $SrcCtrl=GUICtrlCreateCombo('',76,6,$GUISize[0]-84,-1) $DstCtrl=GUICtrlCreateCombo('',76,30,$GUISize[0]-84,-1) GUICtrlCreateButton('Копировать',8,54,$GUISize[0]-16,$GUISize[1]-62) GUICtrlSetOnEvent(-1,'TokenCopy') GUISetState(@SW_SHOW) EndFunc GUIInit() UpdateTokenList() $ListTimer=TimerInit() While 1 ; Обновление списка токенов раз в 3 секунды If TimerDiff($ListTimer)>3000 Then UpdateTokenList() $ListTimer=TimerInit() EndIf Sleep(100) WEnd
Я попробовал все СКЗИ, до которых смог дотянуться: Крипто-Ком, Крипто-Про, Message-Pro, Сигнатура и даже Верба. Все эти ключи успешно прошли копирование и работали.
Но как же так? Разве не должны ключи быть неизвлекаемыми с токена? Ответ кроется в спецификациях eToken: дело в том, что неизвлекаемый ключ действительно есть, но служит он только для криптопреобразований с помощью алгоритма RSA. Ни одно из рассмотренных СКЗИ… нет, вот так: ни одно из СКЗИ, одобренных ФСБ для использования на территории РФ (вроде бы) не использует RSA, а все они используют криптопреобразования на основе ГОСТ-*, поэтому eToken – не более чем флэшка с паролем и замысловатым интерфейсом.
ссылка на оригинал статьи https://habrahabr.ru/post/276057/
Добавить комментарий