Защо всеки може да го направи, но аз не мога“ или да обърне API и да получи данни от eToken

Идеята изглеждаше добра, но как да я реализираме? Тогава си спомних как веднъж клиентска банка не работеше в счетоводния отдел, кълнейки се в липсата на библиотека с говорещото име etsdk.dll, ме хвана любопитство и се качих да я избера.
По принцип компанията за разработка разпространява SDK на своя уебсайт, но за това трябва да се регистрирате като компания за разработка на софтуер и това явно не съм аз. Не беше възможно да намеря документация в интернет, но любопитството надделя и реших сам да разбера. Библиотека - ето я, има време, кой ще ме спре? Първо стартирах DLL Export Viewer на NirSoft, който ми показа приличен списък с функции, експортирани от библиотеката. Списъкът изглежда добре, може да се проследи логиката и последователността на действията при работа с токени. Един списък обаче не е достатъчен, трябва да разберете какви параметри, в какъв ред да преминете и как да получите резултатите.

Тогава беше време да си спомним младостта и да стартираме OllyDbg версия 2.01, да заредим в него библиотеката ccom.dll на криптосистемата Crypto-Com, използвана от клиентската банка и използвайки същата библиотека etsdk.dll, и да започнем да разбираме как точно го правят.
Тъй като няма изпълним файл, библиотеката ще бъде заредена с помощта на loaddll.exe от пакета Olly, така че не е нужно да мечтаем за пълноценно отстраняване на грешки. Всъщност ще използваме дебъгера като разглобител (да, има IDA, но никога не съм работил с него и като цяло е платен).

Всички функции използват конвенцията за извикване cdecl. Това означава, че резултатът ще бъде върнат в EAX регистъра, а параметрите ще бъдат предадени през стека отдясно наляво. В допълнение, това означава, че всички параметри имат размерността на двойна дума и ако нямат, те се разширяват до нея, което ще опрости живота ни.
Нека да разгледаме обкръжението на извикването на ETReadersEnumOpen:
Предава се един параметър, който е указател към някаква локална променлива и след извикването, ако резултатът не е равен на 0, контролът се прехвърля към някакъв очевидно код за отстраняване на грешки и ако е равен, продължаваме напред (командата JGE прехвърля контрола, ако флаговете ZF и OF са равни, а флагът OF винаги се нулира от командата TEST на 0). По този начин заключавам следния ред: променлива се предава на функцията по референция, в която ще бъде върнат определен идентификатор enum и в резултат на това функцията връща код за грешка или 0, ако няма грешка.
Нека да преминем към ETReadersEnumNext:

Към него се предават два параметъра: стойността на променливата, получена с помощта на ETReadersEnumOpen (идентификатор за изброяване) и указател към локална променлива, където очевидно се връща следващата стойност. Освен това, тъй като параметрите се предават в ред отдясно наляво, първият параметър е идентификаторът, а вторият е указателят на резултата. Кодът за грешка все още се връща чрез EAX и, съдейки по дизайна на цикъла, той се използва не само за съобщаване на грешка, но и за съобщаване, че няма нищо повече за изброяване.
С ETReadersEnumClose всичко е още по-просто: към него се предава идентификатор за изброяване, но никой не се интересува от резултата.
Време е да проверим разбирането си за тези функции. Тук съм принуден да направя малко лирично отклонение: факт е, че съм системен администратор по професия и следователно сериозните компилирани езици за програмиране не са моето нещо. За работа имам повече нужда от Bash и Python на Linux, но ако трябва бързо да създам нещо на Windows, използвам AutoIt, който обичам.
Плюсовете за мен са:
- Неявно преобразуване на типа и недостатъчноброя на представените видове.
- Липсата на записи (както и асоциативни масиви) и OOP (като цяло съществува, но само за COM обекти, така че някак не е).
Да започнем: честно казано, нямаме представа какво и какъв размер връщат функциите, така че ще дадем голям буфер за начало и ще видим какво ще се случи. Код за стартиране:
След като го изпълним, получаваме изход като този:
Пускаме го няколко пъти и виждаме, че само първите 4 байта се променят, което означава, че 4-байтово цяло число се използва като идентификатор, което означава, че можем да освежим малко кода за извикване на тази функция до това състояние:
Подобни експерименти с функцията ETReadersEnumNext показаха следното: първите 260 байта от буфера съдържат името на четеца и нули. Последователно извикване на тази функция изброи всички четци в системата за мен (например три от тях бяха създадени предварително за ruToken). Четците за eToken се създават динамично, в зависимост от броя на свързаните токени, и най-интересното е, че те имат 261-ви буферен байт, зададен на единица, което очевидно показва съвместимостта на четеца с нашата библиотека. Ако се вгледате внимателно в разглобения код, можете да видите, че записите, чийто 261-ви байт е 0, не се обработват. Всички останали байтове до края на килобайтовия буфер са равни на 0 за всички четци и не се различават.
И така, разбрахме читателите, сега трябва да разберем какво следва. След като разгледах списъка с функции, стигнах до извода, че последователността на извикванията трябва да бъде следната: първо свързваме желания четец, на този етапможем да намерим обща информация за вмъкнатия токен, след което влизаме и след това получаваме достъп до файловата система. По този начин следващите функции са 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 параметър е ID на папката.
И така, научихме достатъчно, за да изброим всички файлове и папки в токена. Следният прост код ще направи точно това (не правя описание на извикването на DllCall тук - ще бъде за всички функции в текста на модула в края на статията):
Резултат в конзолата:

Тъй като засегнахме ETFileGetInfo, трябва незабавно да внедрим ETDirGetInfo: редът на параметрите е същият, участва само идентификаторът на папката, а не файлът. Върнат резултат: име на папка по ID.
Последната функция, извикана от тази библиотека, е ETFileWrite. Необходими са 4 аргумента: идентификатор на файл, нула (експериментът показва, че това е отместване спрямо началото на файла), указател към буфер с данни и размера на данните. Важно е да запомните, че файлът не се разширява. Ако сумата от отместването и дължината на файла надвишава размера на файла, не се извършва запис, така че ако размерът на файла трябва да бъде променен, файлът ще трябва да бъде изтрит и пресъздаден с новия размер.
Освен това: ако си спомним таблицата за експортиране на библиотеката, тогава тя има още 5 функции, но тяхното извикване не е реализирано в тази библиотека, която работи с Crypto-Com CIPF. За наше щастие, същата банка разпространява и библиотеката Message-Pro CIPF - 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. Нито един от прегледаните CIPF ... не, така: нито един от CIPF, одобрен от FSB заизползване на територията на България (привидно) не използва RSA и всички те използват криптографски трансформации, базирани на GOST-*, така че eToken не е нищо повече от флашка с парола и сложен интерфейс.
Можете да помогнете и да прехвърлите средства за развитието на сайта