Однажды у нас на предприятии встала задача о повышении уровня безопасности при передаче очень важных файлов. В общем, слово за слово, и пришли мы к выводу, что передавать надо с помощью 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 – не более чем флэшка с паролем и замысловатым интерфейсом.
Комментарии (52)
OLS
28.01.2016 10:58+3При выборе токена задавайте производителю самый первый вопрос — извлекаемое или неизвлекаемое хранение?
И у Аладдина и у Рутокена сейчас есть в прайсе и первого и второго вида, как для RSA так и для ГОСТ-а, в т.ч. нового (2012 года).
Просто необходимо выбирать для нужных целей нужные модели.Hormiga
28.01.2016 11:06С одной стороны да, такие решения есть, с другой — бремя выбора токена лежит на производителе ПО, а я пока что не видел ни одного ПО, в том числе и продвигаемого ЦБ, которое могло бы использовать токены JaCarta.
mexanism
28.01.2016 17:08Федеральная служба по регулированию алкогольного рынка РФ — egais.ru работает только с джакартой
duzorg
02.02.2016 12:11Но… Ключи для производителей на eToken и RuToken, подключится к ЕГАИС производителя (или как его называет ФСРАР — «Классический ЕГАИС») с помощью JaCarta нельзя.
nmk2002
28.01.2016 11:44пришли мы к выводу, что передавать надо с помощью scp
Интересно, почему выбрали scp? Рассматривали ли решения по управляемому файлообмену (MFT)?
Все эти ключи успешно прошли копирование и работали.
А как ключи на этих носителях были сгенерированы? использовали какой-то csp?Hormiga
28.01.2016 11:59Интересно, почему выбрали scp? Рассматривали ли решения по управляемому файлообмену (MFT)?
Ну, тогда стоят вопрос о том, чтобы сделать быстро и из того, что есть и знакомо.
Однако сейчас, после того, как я узнал об отсутствии сложностей на пути чтения с токена для злоумышленника, мы снова в поиске.
Сейчас по запросу «управляемый файлообмен MFT» гуглится в основном OpenTrust MFT, который лично мне показался оверкилом для цели передачи из А в Б.
А как ключи на этих носителях были сгенерированы? использовали какой-то csp?
Крипто-Ком: ключи сгенерированы банком, в дальнейшем при перегенерация идет в ActiveX библиотеке (которую я и рассматривал) и новый ключ кладется на токен (в моем примере — в папку DDDD). Также пробовал программу Admin-PKI.
Message-Pro: аналогично, банком.
Крипто-Про: ключи генерировались на сайте УЦ «Крипто-Про», насколько я понимаю, тоже посредством ActiveX или Java
Сигнатура и Верба: соответствующим ПО.
Kop3t3
28.01.2016 11:51>ни одно из СКЗИ, одобренных ФСБ для использования на территории РФ (вроде бы) не использует RSA
Если подсунуть libeTPkcs11.so или его виндовый аналог фаерфоксу или java keytool'у, то вроде можно сгенерировать RSA ключ. Он в таком случае будет неизвлекаемым или токен сгенерирует ГОСТ-овский ключ?
И есть ли эта замечательная библиотека под Linux (у меня стоит Safenet client, но такой библиотеки нет).
Также хотелось бы узнать, можно ли загружать свои java card апплеты на токены с поддержкой java. Интересная технология, хочется попробовать.Hormiga
28.01.2016 12:08+1Для работы с неизвлекаемыми ключами, а также апплетами у производителя, видимо, есть другие API, в то время как рассматриваемое в статье используется только для работы с файловой системой.
Если вы сгенерируете RSA ключ и поместите его в специальную область на токене, то токен сможет этим ключом генерировать подпись для ваших данных, но о ГОСТовских алгоритмах в случае eToken Pro речи не идет.
Disasm
28.01.2016 15:33В своё время брал eToken Java и нет, загружать туда апплеты нельзя. Техподдержка ответила, что ключи для загрузки они меняют на случайные при инициализации токена и нигде не хранят.
dmitrmax
28.01.2016 13:31Есть наработки по реверсу аппаратного ГОСТ-токена из комплекта CryptoPro Rutoken CSP. В кратце сделано практически всё, кроме того, что надо понять как преобразуется пароль от ключа в тот вид, который передаётся на токен. Если есть кто готов присоединиться, то Welcome. Конечная цель поддержка всякой аппаратной ГОСТ-криптотни под линуксом.
cryptoman
28.01.2016 14:01dev.rutoken.ru/pages/viewpage.action?pageId=18055184. Работает под Linux
dmitrmax
28.01.2016 14:08-1Во-первых, не хочется пользоваться закрытыми библиотеками. Во-вторых, с помощью openssl не возможно создать сертификат и/или запрос на него, который удовлетворял бы требованиям приказа ФСБ. Проблема в том, что приказ требует наличия доп.полей (таких как СНИЛС, ИНН и т.д.) хитрых типов, а openssl использует дефолтные. В-третьих, Рутокен ЭЦП это совсем не то же самое, что носитель из комплекта CryptoPro Rutoken CSP, второй имеет дополнительную функциональность и защиту каждого отдельного ключа отдельным паролем, а не одним ПИН-кодом на весь брелок. В-третьих, в перспективе планирую поддерждать ещё и УЭК.
cryptoman
28.01.2016 14:27+2Во-вторых, с помощью openssl не возможно создать сертификат и/или запрос на него, который удовлетворял бы требованиям приказа ФСБ. Проблема в том, что приказ требует наличия доп.полей (таких как СНИЛС, ИНН и т.д.) хитрых типов, а openssl использует дефолтные.
Возможно. Через API openssl. Через тулзу openssl возможно, начиная с версии openssl 1.1.0.
Мы сейчас готовим совместимую с этой версией сборку engine pkcs11_gost. Заодно в ней будет и поддержка новых криптографических ГОСТ-ов.
В-третьих, Рутокен ЭЦП это совсем не то же самое, что носитель из комплекта CryptoPro Rutoken CSP, второй имеет дополнительную функциональность и защиту каждого отдельного ключа отдельным паролем, а не одним ПИН-кодом на весь брелок.
Вообще говоря, в Рутокен ЭЦП есть поддержка Secure Messaging-протокола, обеспечивающего защиту канала между Рутокен ЭЦП и библиотекой pkcs11. Если интересны нюансы, то в личку.
dmitrmax
28.01.2016 14:58> Через API openssl.
Это да.
> Secure Messaging-протокола, обеспечивающего защиту канала между Рутокен ЭЦП и библиотекой pkcs11. Если интересны нюансы, то в личку.
А разве API PKCS #11 позволяет указывать что-то кроме ПИН-кода токена (административного и пользовательского)? Просто не очень понятно, как это заюзать. Либо тут уже библиотека должна через какой-то другой интерфейс запрашивать у пользователя пароль ключа, когда к нему идет обращение.
Greyushko
28.01.2016 14:29+1А что плохого в закрытых библиотеках, если ключ в любом случае не извлекается? Какой смысл в подмене своей библиотекой одной из частей программно-аппаратного комплекса? Конечно, дело благородное, но с чего вы внезапно взяли, что написанная вами открытая библиотека будет корректно работать с токеном с точки зрения безопасности? Желательно, не с точки зрения интуиции.
В частности, упомянутый ранее CryptoPro Rutoken CSP является полноценным ПАК, где токен и программные библиотеки — равнозначно важные сущности.
Про УЭК, кстати, интересно. Что значит, «поддержать»? Он ведь проводит ряд защищенных процедур на неопубликованных протоколах (это не значит, что они плохие/кривые/проприетарные), а затем включает шифрованный канал.
Совсем забыл небольшой вопрос: если вы «отцепляете» токен от СКЗИ, то зачем вообще будут использоваться ГОСТ-алгоритмы? Купите обычные западные токены и работайте спокойно с виндовыми провайдерами.dmitrmax
28.01.2016 14:40> Он ведь проводит ряд защищенных процедур на неопубликованных протоколах
Совершенно верно. Идея в том, чтобы это реверснуть. Это не так сложно, как может показаться на первый взгляд.
> Совсем забыл небольшой вопрос: если вы «отцепляете» токен от СКЗИ, то зачем вообще будут использоваться ГОСТ-алгоритмы?
Послушайте, требование использовать сертифицированные ФСБ СКЗИ — это больше требования для институциональных учреждений (банков, ведомств, мед.учреждений и т.д.) Для меня, как для конечного пользователя, доверие к себе многократно выше, чем доверие к конторе, которая сертифицировала что-то кому-то. Поэтому я вполне могу использовать любую реализацию ГОСТа, и это никак не обнаружит ни один мой контрагент.
> Купите обычные западные токены и работайте спокойно с виндовыми провайдерами.
Западные токены, к сожалению, не дают мне возможности использовать криптографию для нужд юридически значимых внутри России.Greyushko
28.01.2016 14:55+1> Совершенно верно. Идея в том, чтобы это реверснуть. Это не так сложно, как может показаться на первый взгляд.
Ого, ну, что называется, будет очень здорово, если получится. Но когда застрянете где-то, вспомните мои слова :)
> Для меня, как для конечного пользователя, доверие к себе многократно выше, чем доверие к конторе, которая сертифицировала что-то кому-то.
Конечно, выше. И это типичная проблема подавляющего большинства тех, кто пытается самостоятельно лезть в разработку криптографии. Не забывайте, пожалуйста, про закон Шнайера.
> Западные токены, к сожалению, не дают мне возможности использовать криптографию для нужд юридически значимых внутри России.
Осуществить юридически значимые нужды с ГОСТ-криптографией, но несертифицированной, да еще и на коленке сделанной, тоже весьма затруднительно. Приведу пример: для понимания базовых принципов криптоалгоритмов и прокачки навыка разрабоки я иногда даю своим студентам задания в духе «реализуй ГОСТ хххх-хх» или «встрой туда-то реализацию OpenSSL».
Хотите сказать, что их поделки, имеющие в коде функции типа gost_encrypt_megasecure, тоже применимы в юридически значимом документообороте?msuhanov
28.01.2016 15:45+2Хотите сказать, что их поделки, имеющие в коде функции типа gost_encrypt_megasecure, тоже применимы в юридически значимом документообороте?
Вы так говорите, будто подобные поделки не имеют сертификатов ФСТЭК и ФСБ.Greyushko
28.01.2016 16:18Я, конечно, не уверен, но не думаю, что мои студенты сразу после сдачи лаб отправляют их в органы :)
А если речь о промышленных разработках, то хотелось бы услышать более адекватные доводы, чем «всем хорошо известно».dmitrmax
28.01.2016 16:26+3> что мои студенты сразу после сдачи лаб отправляют их в органы :)
Разумеется, потому что у них нет ни лицензии на разработку СКЗИ, ни денег на их сертификацию ) А вот мой друг, будучи студентом, сидел и пилил openssl на поддержку ГОСТ, только не увас на лабах, а в лицензированной конторе в Питере. И сертифицировали же потом )
dmitrmax
28.01.2016 14:50> А что плохого в закрытых библиотеках, если ключ в любом случае не извлекается? Какой смысл в подмене своей библиотекой одной из частей программно-аппаратного комплекса?
Ещё один нюанс забыл. Смотрите, если вы пользовались хоть раз какими-то ГосУслугами или системой электронного ДО, где имеется авторизация или подпись документа по ключем, то должны были обратить внимание, что то, что плагин браузера посылает на подпись в токен, совершенно вам не видно. То есть вы вслепую подписываете что-то, надеясь на то, что это то же, что и на странице браузера. Это очень неправильно. Я хочу это ликвидировать, добавив возможность просмотреть подписываемое сторонним приложением, встроившись между плагином и библиотекой токена.Greyushko
28.01.2016 14:57> совершенно вам не видно
Для этого есть средства визуализации подписи (см. Rutoken PinPad или SafeTouch). Не нужно городить огороды. Да, в конце концов, если уж так хочется, можно поставить отладчик APDU-инструкций, ловить хэш, отправляющийся на подпись в токен, и его печатать пользователю.dmitrmax
28.01.2016 15:04> Для этого есть средства визуализации подписи (см. Rutoken PinPad или SafeTouch).
Они поддержаны в Linux? Что делать тем, у кого УЭК или такой носитель как у меня? Повесится от несовершенства мира?
> Да, в конце концов, если уж так хочется, можно поставить отладчик APDU-инструкций, ловить хэш, отправляющийся на подпись в токен, и его печатать пользователю.
А что собственно даст пользователю значения хэша? Да и какое-то решение из области заката солнца вручную. Неудобство использования инструментов защиты информации играет против их корректного применения. Описанная схема слишком сложна, чтобы средний пользователь мог её безопасно использовать.Greyushko
28.01.2016 16:01> Они поддержаны в Linux? Что делать тем, у кого УЭК или такой носитель как у меня? Повесится от несовершенства мира?
Вполне поддержаны. Насчет поддержки УЭК рутокен — точно нет, а для SafeTouch не вижу проблемы. Какой носитель у вас, я не знаю.
> А что собственно даст пользователю значения хэша?
Ну, если хотите, получайте это значение, считайте хэш от тех данных, что подписываете и сравнивайте: примерно так вся эта визуализация и работает.
> средний пользователь мог её безопасно использовать
С чего вы взяли, что к полученной системе будет применимо слово «безопасно»?dmitrmax
28.01.2016 16:20> Ну, если хотите, получайте это значение, считайте хэш от тех данных, что подписываете и сравнивайте: примерно так вся эта визуализация и работает.
Очень удобно, не правда ли? )
> С чего вы взяли, что к полученной системе будет применимо слово «безопасно»?
Я буду стремиться к тому, что к ней будет применимо слово «безопасней» по сравнению подписью чего-то вслепую. Кроме того, заставлять кого-либо пользоваться этим я не собираюсь.
Только не пытайтесь меня убедить, что сертификаторы ФСБ неимоверно крутые спецы, которые глазами анализируют исходники и ищут в них уязвимости и их сертификция — это клеймо безупречного качества. Я знаю как это делается. По крайней мере делалось некоторое время назад. Это очень формальный и практически бесполезный процесс, который стоит весьма хороших денег.
Ну и наконец, что конкретно вы предлагаете в условиях, когда производителю в основном пофиг на Linux. Тупо сидеть и ничего не делать? Прекрасный план!
Evengard
28.01.2016 14:17Саму дллку не выложите?
Hormiga
28.01.2016 14:37Пожалуйста.
Возможно, вам потребуется также eToken PKI Client вот отсюда.Evengard
28.01.2016 14:45Спасибо. Стоит SafeNet client, с ним не завелось, говорит об ошибке авторизации на токене — пин код даже не запрашивается. Возможно, оно с ним несовместимо… Вот только параллельно PKI Client не ставится.
Hormiga
28.01.2016 15:00Как я говорил, в скрипте нет уймы проверок, которые могли бы сказать подробнее.
Возможно не прошел Bind, а возможно, сопоставление Id. Попробуйте повыводить значение разных переменных в консоль с помощью ConsoleWrite($Var&@CRLF)Evengard
28.01.2016 15:37После установки PKI Client завелось.
Но спотыкнулось об ETDirEnumNext — ошибку здесь выдаёт. Не знаю правда какую.
И на ETFilesEnumNext тоже.Hormiga
28.01.2016 16:04Токен от какой СКЗИ пытаетесь копировать?
Evengard
28.01.2016 16:07Это не от СКЗИ, я пытаюсь считать данные корпоративного токена.
Я прогнал в цикле по всем возможным RootDir — такое ощущение что они все пустые.Hormiga
28.01.2016 16:14Возможно, он у вас использует тот самый неизвлекаемый RSA-ключ?
dmitrmax
28.01.2016 16:41На токенах с неизвлекаемым ключем всё равно RootDir обычно не пустой. Там как правило хранится сертификат, открытый ключ и какая-нибудь мета-информация типа имени контейнера или его ID. И файл, в который «записывают» подписываемый хэш, например.
Хотя безусловно, можно избежать концепции файлов полностью, используя полность проприетарный APDU-субпротокол.
SOb_S
28.01.2016 23:47Для копания в eToken была (может всё ещё есть, но, возможно, только у разработчиков) утилита eToken Editor.
Также с ключами работал OpenSC.
Пробовали такие?
Hormiga
29.01.2016 10:58Первая входит в пакет разработчика, поэтому у меня ее не было. Вторую не пробовал.
Hormiga
29.01.2016 11:03Кстати, глядя на ваш скриншот:
В коде часто встречалась строка «AKS ifdh» — так обозначаются смарт-карты, которые не брелки?SOb_S
29.01.2016 16:31В данном случае это точно были eToken. Этих идентификаторов могло быть больше, чем реальных устройств. Я для себя понимал это как аналог точки монтирования, чтобы она оставалась после отключения физического устройства и можно было на неё настроить ПО.
Evengard
29.01.2016 15:59А у вас не остался этот eToken Editor?
SOb_S
29.01.2016 16:39Даже если и лежит где-то в архивах, он с вероятностью 99% не работает с современными драйверами и устройствами. Оно должно быть в SDK.
Evengard
29.01.2016 17:00Всё же если остался скиньте плз, хочу попробовать. Желательно виндовую версию. Сам етокен достаточно старый, может быть и получится.
SOb_S
30.01.2016 23:39К сожалению не сохранилось.
Если вы что-то разрабатываете для организации или занимаетесь интеграцией, запросите у Aladdin SDK, кажется его бесплатно отдают (но не уверен, что всем).
amarao
31.01.2016 03:38Интересно, после этого компании закроются, или всем всё пофиг и «ключи безопаснее пароля, продолжайте кушать кактус»?
С другой стороны, возможность выковырять ключ открывает волшебные возможности к написанию софтового эмулятора, позволяющего засунуть всё в виртуалку и не париться.OLS
31.01.2016 21:16А что такого здесь сенсационного? Те, кто плотно занимается криптографией, знали этот факт с незапамятных времен. Никто никого не обманывает. Сотрудники обеих компаний честно отвечают на правильно поставленные вопросы.
amarao
01.02.2016 02:16Чем тогда токен отличается от дискетки с файлом и ради чего весь сыр-бор?
Greyushko
02.02.2016 17:31В данном конкретном случае наличием системы разграничения доступа. Потеря дискеты влечет компрометацию ключа, а для получения ключа с токена нужно еще подобрать пароль, на что даётся ограниченное число попыток.
0x3f00
> поэтому eToken – не более чем флэшка с паролем и замысловатым интерфейсом.
Меня это тоже удивило в свое время.