Чому всім можна, а мені не можна» або реверсим API і отримуємо дані з eToken

можна

Ідея видалася непоганою, але як це реалізувати? Тут я згадав, як одного разу в бухгалтерії не працював банк-клієнт, лаючись на відсутність бібліотеки з ім'ям etsdk.dll, що мене говорить, мене охопила цікавість і я поліз її колупати.

Взагалі компанія-розробник на своєму сайті розповсюджує SDK, але для цього треба пройти реєстрацію як компанія-розробник ПЗ, а це явно не я. На просторах інтернету документацію знайти не вдалося, але цікавість перемогла і я вирішив розібратися у всьому сам. Бібліотека – ось вона, є час, хто мене зупинить? Насамперед я запустив DLL Export Viewer від NirSoft, який показав мені пристойний список функцій, що експортуються бібліотекою. Список виглядає непогано, простежується логіка та послідовність дій під час роботи з токенами. Однак одного списку мало, потрібно зрозуміти які параметри, в якому порядку передавати та як отримувати результати.

можна

Тут-то і настав час згадати молодість і запустити OllyDbg версії 2.01, завантажити в нього бібліотеку ccom.dll криптосистеми Крипто-Ком, що використовується банк-клієнтом і використовує ту саму бібліотеку etsdk.dll, і почати розбиратися як саме вони це роблять.

Оскільки файлу, що виконується, немає, бібліотека завантажиться за допомогою loaddll.exe з комплекту Olly, тому про повноцінне налагодження ми можемо і не мріяти. По суті ми використовуватимемо відладчик як дизассемблер (так, є IDA, але з нею я ніколи не працював і взагалі вона платна).

чому

Усі функції використовують угоду про виклик cdecl. Це означає, що результат буде повертатися в регістрі EAX, а параметри передаватися через стек праворуч-ліворуч. Крім того, це означає, що всі параметри мають розмірність подвійного слова, а якщо не мають – розширюються до нього, що спростить нам життя.

Подивимося околиці виклику ETReadersEnumOpen:

Передається один параметр, що є покажчиком на якусь локальну змінну, а після виклику, якщо результат не дорівнює 0, управління передається на якийсь явно налагоджувальний код, а якщо дорівнює - йдемо далі (команда JGE передає управління якщо прапори ZF і OF рівні, а прапор OF команда TEST завжди скидає до 0). Таким чином, я укладаю наступний порядок: у функцію передається змінна за посиланням, в яку повернеться певний ідентифікатор перерахування, а як результат функція повертає код помилки або 0 якщо помилки немає.

Переходимо до ETReadersEnumNext:

всім

До неї передаються два параметри: значення змінної, отримане за допомогою ETReadersEnumOpen (ідентифікатор перерахування) та покажчик на локальну змінну, куди, очевидно, повертається чергове значення. Причому оскільки параметри передаються у порядку праворуч-ліворуч, саме перший параметр – ідентифікатор, а другий – покажчик результату. Код помилки так само повертається через EAX, причому, судячи з конструкції циклу, він використовується не тільки для повідомлення про помилку, але і для повідомлення про те, що більше перераховувати нічого.

З ETReadersEnumClose все ще простіше: в неї передається ідентифікатор перерахування, а результат нікого не хвилює.

Настав час перевірити наше уявлення про ці функції. Тут я змушений зробити невеликий ліричний відступ: річ у тому, що за професією я – сісадмін, і тому серйозні компілювані мови програмування – це не зовсім моя. По роботі мені більше потрібен Bash і Python під Linux, ну а якщо мені треба швидко щось зробити під Windows, я використовую AutoIt, що мені подобається.

Плюсами для мене є:

  • Неявне перетворення типів та недостатнєкількість представлених типів.
  • Відсутність записів (а також асоціативних масивів) та ОВП (взагалі воно є, але тільки для COM-об'єктів, так що ніби і немає).
Це відступ був до того, що приклади використання функцій ми будемо робити саме на AutoIt. Виклик функцій із зовнішніх бібліотек, у зв'язку з неявною типізацією в мові, виглядає дещо кострубато, але працює.

Приступимо: відверто кажучи, ми гадки не маємо, що і якого розміру повертають функції, тому віддаватимемо великий буфер для початку, і подивимося, що буде. Код для початку:

Виконавши його, отримуємо висновок типу:

Проганяємо кілька разів і бачимо, що змінюються лише перші 4 байти, значить, як ідентифікатор використовується 4-байтове ціле, а значить ми можемо трохи зачесати код виклику цієї функції до такого стану:

Подібні експерименти з функцією ETReadersEnumNext показали наступне: перші 260 байт буфера містять ім'я зчитувача та нулі. Послідовний виклик цієї функції перерахував мені всі зчитувачі в системі (наприклад, під ruToken їх створено заздалегідь три штуки). Зчитувачі під eToken створюються динамічно, залежно від числа підключених токенів і, найцікавіше, у них встановлений в один 261-й байт буфера, який, судячи з усього, вказує на сумісність зчитувача з нашою бібліотекою. Якщо вдивитися в дизассембльований код, то видно, що записи, у яких 261 байт дорівнює 0, не обробляються. Решта байти остаточно кілобайтного буфера в усіх зчитувачів рівні 0 і різняться.

Отже, зі зчитувачами розібралися, тепер треба зрозуміти, що далі. Оглянувши список функцій, я дійшов висновку, що послідовність виклику має бути такою: спочатку робимо bind потрібного зчитувача, на цьому етапіможемо дізнатися загальну інформацію про вставлений токен, потім робимо логін, і вже після цього отримуємо доступ до файлової системи. Таким чином, наступні на черзі функції ETTokenBind та ETTokenUnbind.

чому

ETTokenBind виглядає складно і незрозуміло, але, поколупавшись деякий час, я дійшов висновку, що функції передається два параметри, перший з якого - покажчик на буфер величиною 328 байт (0x0148), а другий - покажчик на рядок з ім'ям зчитувача. Шляхом експериментів було встановлено, що в перші чотири байти буфера повертається ідентифікатор (далі: ідентифікатор прив'язки). Для чого виділяється весь решта буфера – поки що загадка. З якими токенами я б не експериментував, інші 324 байти буфера залишалися заповнені нулями. Зазначений ідентифікатор, що логічно, успішно використовується як аргумент функцій ETTokenUnbind та ETTokenRebind.

всім

Наступна функція на черзі – ETRootDirOpen. Приймає три параметри: покажчик на результат, ідентифікатор прив'язки та константу. Функція має кілька особливостей.

Третє: значення, що повертається функцією, на скріншоті не видно, але повертається в середину того 328-байтного буфера, який виділявся раніше, з чого можна зробити висновок, що той буфер - структура, що зберігає різні ідентифікатори і дані, що стосуються токена.

Наступна група функцій: ETDirEnumOpen, ETDirEnumNext та ETDirEnumClose. Їх можна спробувати розплутати, не заглядаючи у код. Загалом вони повинні працювати так само, як ETReadersEnum*, з тією лише різницею, що в ETDirEnumOpen буде передаватися як параметр ще й ідентифікатор поточної папки. Перевіряємо – працює.

Група функцій ETFilesEnumOpen, ETFilesEnumNext і ETFilesEnumClose просто повинні працювати так само,проте перевірити це з упевненістю ми не можемо, т.к. в кореневій папці досліджуваного токена, судячи з усього, файлів немає, а це означає, що настав час йти вглиб дерева папок, функцією ETDirOpen.

можна

У даному API, схоже, намалювалася традиція, згідно з якою перший параметр використовується для повернення результату, тому припустимо, що це вірно і цього разу. Другий параметр, як бути переданим функції, проходить видозміни з допомогою команди MOVZX EDI,DI, тобто. слово розширюється до подвійного слова. Очевидно, це потрібно для того, щоб двобайтове ім'я папки передати у чотирибайтовому параметрі. Ну а третій параметр з логіки речей має бути ідентифікатором відкритої папки. Пробуємо – вийшло. ETDirClose вгадується без сюрпризів: 1 параметр – ідентифікатор папки.

Отже, ми дізналися достатньо, щоб перерахувати всі файли та папки на токені. Наступний простенький код саме це і зробить (опис виклику DllCall я тут не роблю – він буде для всіх функцій у тексті модуля наприкінці статті):

Результат у консолі:

можна

Якщо ми торкнулися ETFileGetInfo, треба відразу і реалізувати ETDirGetInfo: порядок параметрів той самий, тільки бере участь ідентифікатор папки, а чи не файла. Результат, що повертається: ім'я папки за ідентифікатором.

Остання функція, звернення якої реалізовано у цій бібліотеці – 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. Робимо висновок у тому, що довжина пароля відповідає політиці. Повторюємо із довгим паролем – відпрацьовує.

Тепер зводимо всі функції в один модуль і зберігаємо його - інклудити в інші проекти. Текст модуля вийшов таким:

Отже, ми можемо робити все, що захочемо із файловою системою токена. Щоб продемонструвати це, я написав простий скрипт, який копіюватиме вміст з одного токена на інший. Скрипт рівня «Proof-of-concept», тобто. тут не буде безліч перевірок, які повинні були б бути в «правильному» додатку, проте дозволить нам отримати другий токен, що діє.

можна

Але як так? Хіба не повинні ключі бути невилученими з токена? Відповідь криється в специфікаціях eToken: справа в тому, що ключ, що не видобувається, дійсно є, але служить він тільки для криптоперетворень за допомогою алгоритму RSA. Жодне з розглянутих СКЗІ… ні, ось так: жодне із СКЗІ, схвалених ФСБ длявикористання на території Україна (начебто) не використовує RSA, а всі вони використовують криптоперетворення на основі ГОСТ-*, тому eToken – не більше ніж флешка з паролем та хитромудрим інтерфейсом.

Ви можете допомогти і перевести небагато коштів на розвиток сайту