Сбербанк или туда и обратно

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.


ГЛАВА 1. Нежданные гости


Все началось в то злополучное утро, когда Project Manager сообщил, что сроки реализации проекта должны быть быстро и решительно сокращены на месяц. Точнее говоря проект должен быть готов через 4 дня. Нет, наш PO не зверь, и ничуть не похож на сову (разве что чуть-чуть на ворона), просто так сложилось. Ну раз надо, так надо, тем более что команде (а я являюсь ведущим разработчиком команды «С») было обещано что-то вкусное. На часах и календаре был четверг, 11:00, к понедельнику проект должен быть готов.

Для начала, чем мы вообще занимаемся. Мы занимаемся автоматизацией кинотеатров — автоматическим и дистанционным управлением оборудования, автоматизацией кинопоказа, мониторингом, видеопанелями, а теперь еще и терминалами продажи билетов и бара. Конкретно последнему пункту и посвящена данная статья.

Сам проект, который нужно было завершить до понедельника представляет из себя некую прослойку между основным сервером на Scala и железным терминалом оплаты VeriFone VX 820 (на самом деле терминалов больше, но для примера возьмем только его). Понятно, что просто так проводить через него транзакции нам никто не даст, поэтому используются утилиты и библиотеки Сбербанка/Arcus и UCS. Таким образом схема работы в итоге должна быть следующей:



Внешне он выглядит вот так:



Также данная подсистема должна использоваться на стандартных кассовых машинах которые все видели в любом кинотеатре у кассиров.

Согласно внутренней традиции каждый проект нашей команды мы называем именем из древнескандинавской мифологии, для данной подсистемы было выбрано имя Gefjon — Имя богини плодородия и изобилия (неплохое название для сервера оплаты, разве нет? Ну и легенда о быках отрезающих остров идеально ложится на текущую архитектуру, отрезая работу с оборудованием от высокоуровневого языка).

Формат входящих и выходящих сообщений — HTTP сервер с JSON нагрузкой. Это оптимальный компромисс между Scala, которой сложно опуститься до вычленения бинарных данных из socket-потоков и C, которому трудно подняться до передачи объектов через сеть. Возможных операций, которыми необходимо оперировать не так много: оплата, отмена, возврат, разные типы отчетов, открытие сервисного меню и ping. С виду ничего сложного. Так как банковских систем целых три (а в последствии ожидается пополнение семейства), то было решено разделить проект на компоненты:



Зеленым покрашены блоки, которые нам нужно было сделать, синим — те, которые нельзя поменять и которые предоставляет банк.

Так как основные проблемы возникли только с ПО от Сбербанка, то статья в целом будет посвящена подводным камням, которые мы пересчитали своей ладьей.

ГЛАВА 2. Баранье жаркое



(фото: heaclub.ru)

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

        char buf[BUF_KB * 2];
        char * null;
        char * grep;
 
#ifdef _WIN32_WINNT
        char * ptr;
        null = "nul";
        grep = "findstr";
#else
        null = "/dev/null";
        grep = "grep";
#endif
        sprintf(buf, "%s %"PRIi32"= %sops.ini >%s 2>%s || "
                     "echo %"PRIi32"=9,6,PINPAD_TEST >> %sops.ini",
               grep,
               TERM_ARCUS_TEST_PINPAD,
               TERM_PATH,
               null,
               null,
               TERM_ARCUS_TEST_PINPAD,
               TERM_PATH);
 
#ifdef _WIN32_WINNT
        ptr = buf;
        while (*ptr)
        {
            if (*ptr == '/')
                *ptr = '\\';
            ptr++;
        }
#endif

Понятное дело, что для Production варианта это не годилось, поэтому нужно было по сути написать все заново.

Каждый банк, который предоставляет библиотеки для работы с терминалом обычно предоставляет два варианта подключения: через функции библиотеки (.so/.dll) или посредством готовой утилиты, которой всего-то нужно передать два значения — тип операции и сумму (когда нужно). В теории ничего сложного, всего-то

char buff[100];
sprintf(buffer, "%d %d", atoi(argv[1]), atoi(argv[2]));
system(buffer);

Результат операции при этом будет помещен в файл «e», а слип-чек — в файл «p». Просто отправим эти файлы на stdout с преобразованием в JSON, чтобы HTTP-сервер просто отправил их наверх как payload без размышлений о том, что там.

Но эта статья не вышла бы, если бы все было так просто.

ГЛАВА 4. Через гору и под горой


Первоначальный вариант реализации представлял из себя простой вызов приложения — HTTP-сервер вызывал нужную обертку с унифицированными параметрами (например X-отчет это 4), а утилита например gfj_pilot запускала sb_pilot с параметром, который требовался для это операции (например X-отчет это 9). Затем утилита-обертка читала из е-файла результат операции (например 2000 — «отказ оплаты, повторите операцию») и преобразовывала в универсальную ошибку (например 3 — «Ошибка чтения или процессинга карты/счета, повторите операцию»). После этого файл «p» преобразовывался в base64 для избежания ломания форматирования и отсылался вместе с результатом в stdout в виде JSON.

Все это прекрасно работало, пока в один прекрасный момент нам не сообщили, что…

… это не работает под Windows.



Ну точнее у самого Windows проблем нет (кроме того, что слип генерируется в кодировке Cp-1251, а консоль работает в CP866). Просто не генерировался «е» файл. Запустили банковскую утилиту напрямую:

C:\banks\sber\sb_pilot>dir
 Том в устройстве C не имеет метки.
 Серийный номер тома: B401-6B9D
 
 Содержимое папки C:\banks\sber\sb_pilot
 
04.02.2019  12:28    <DIR>          .
04.02.2019  12:28    <DIR>          ..
31.01.2019  17:12            10 832 F12X24.BIN
31.01.2019  17:12           128 000 gate.dll
31.01.2019  17:12            72 192 loadparm.exe
31.01.2019  17:12            36 204 OPT0.R
31.01.2019  17:12            20 716 OPT1.R
31.01.2019  17:12             1 806 OPT3.R
31.01.2019  17:12           388 608 pilot_nt.dll
31.01.2019  23:06               463 pinpad.ini
31.01.2019  17:12            91 136 posScheduler.exe
31.01.2019  17:12               418 printers.ini
01.02.2019  16:51            91 646 sbkernel1902.log
31.01.2019  17:12           653 312 sbrf.dll
31.01.2019  17:12           840 192 SBRFCOM.dll
31.01.2019  17:12         3 142 656 sb_kernel.dll
01.02.2019  16:51                 9 SESS.D
01.02.2019  16:51               715 SPLC.D
31.01.2019  17:12            72 192 upwin.exe
              20 файлов      5 659 718 байт
               2 папок  37 567 004 672 байт свободно
 
# Отправляем команду оплаты (1) на 10 рублей (1000 копеек)
C:\banks\sber\sb_pilot>loadparm.exe 1 1000
 
C:\banks\sber\sb_pilot>dir
 Том в устройстве C не имеет метки.
 Серийный номер тома: B401-6B9D
 
 Содержимое папки C:\banks\sber\sb_pilot
 
04.02.2019  12:28    <DIR>          .
04.02.2019  12:28    <DIR>          ..
04.02.2019  12:28               216 commerr.log
31.01.2019  17:12            10 832 F12X24.BIN
31.01.2019  17:12           128 000 gate.dll
31.01.2019  17:12            72 192 loadparm.exe
31.01.2019  17:12            36 204 OPT0.R
31.01.2019  17:12            20 716 OPT1.R
31.01.2019  17:12             1 806 OPT3.R
01.02.2019  18:51             1 349 p
31.01.2019  17:12           388 608 pilot_nt.dll
31.01.2019  23:06               463 pinpad.ini
31.01.2019  17:12            91 136 posScheduler.exe
31.01.2019  17:12               418 printers.ini
04.02.2019  12:28            92 218 sbkernel1902.log
31.01.2019  17:12           653 312 sbrf.dll
31.01.2019  17:12           840 192 SBRFCOM.dll
31.01.2019  17:12         3 142 656 sb_kernel.dll
01.02.2019  16:51                 9 SESS.D
01.02.2019  16:51               715 SPLC.D
31.01.2019  17:12            72 192 upwin.exe
              19 файлов      5 659 029 байт
               2 папок  37 567 008 768 байт свободно
 
C:\banks\sber\sb_pilot>

Действительно, «e»-файла нет. Камень в сторону Сбербанка #1. Пишем письмо в сбербанк (впоследствии получили ответ, что так и должно быть), а так как времени на переписку нет и надо запускаться вот прям уже, ищем обходные пути получения результата.

04.02 12:28:55 SBKRNL: Failed to open device \\.\COM1, err 2
04.02 12:28:56 SBKRNL: Failed to open device \\.\COM1, err 2
04.02 12:28:56 SBKRNL: Result  = 0
04.02 12:28:56 GATE: unlock:'00000054'
04.02 12:28:56 GATE: lock:'00000054' 'UPOSWINMUTEX2'
04.02 12:28:56 GATE: unlock:'00000054'
04.02 12:28:56 LOADPARM: Unloading GATE.DLL...
04.02 12:28:56 GATE: SB_KERNEL.DLL is unloaded
04.02 12:28:56 LOADPARM: GATE.DLL unloaded

Ага, результат можно получить из лога sbkernelГГММ.log. Неудобно, плюс нет хеша карты чтобы впоследствии прикрутить «Спасибо» от сбербанка. Не годится.

Придется подключаться к библиотеке pilot_nt.dll и импортировать из нее функции. Все бы ничего, но… Камень в сторону Сбербанка #2: под Linux такой библиотеки нет, придется создавать два разных приложения под разные платформы — для linux вызывать утилиту sb_pilot (аналог loadparm.exe, кстати камень #3 за разное название утилиты под разными платформами), под windows подключаться к библиотеке pilot_nt.dll.

ГЛАВА 5. Загадки в темноте


На часах 19:00.

Сбербанк — компания крупная, большинство программных решений производятся по ГОСТам и формальным документам. Залезаем в каталог, который поставляет Сбербанк вместе с библиотеками:

Sberbank$ ls -l Docs
итого 30160
drwx------ 2 alex alex    4096 янв 17 19:31 FAQ
-rw-rw-r-- 1 alex alex 3398465 май  9  2018 Базовая настройка UPOS для автономного решения (АР).docx
-rw-rw-r-- 1 alex alex 1182078 май  9  2018 Базовая настройка UPOS для ИКР.docx
-rw-rw-r-- 1 alex alex  853504 май  9  2018 Версии и изменения.doc
drwx------ 3 alex alex    4096 янв 31 17:11 Для разработчиков ПО ККМ
-rw-rw-r-- 1 alex alex 5280787 май  9  2018 Загрузка ПО в POS-терминалы.docx
-rw-rw-r-- 1 alex alex 1149640 май  9  2018 Коды ошибок.docx
drwx------ 2 alex alex    4096 май 28  2018 Настройка UPOS
drwx------ 2 alex alex    4096 май 28  2018 Настройка кассовых программ
-rw-rw-r-- 1 alex alex 3451601 май  9  2018 Определение схемы автономного решения (АР).docx
-rw-rw-r-- 1 alex alex 1956196 май  9  2018 Определение схемы ИКР.docx
-rw-rw-r-- 1 alex alex 1043161 май  9  2018 Памятка по настройке функции ОПЛАТА авиабилетов (Аэрофлот)_(ИКР).docx
-rw-rw-r-- 1 alex alex 4348157 май  9  2018 Параметры POS-терминалов.docx
-rw-rw-r-- 1 alex alex 3970267 май  9  2018 Подключение отдельных функций.docx
drwx------ 3 alex alex    4096 май 28  2018 Руководства пользователя
-rw-rw-r-- 1 alex alex 2644702 май  9  2018 Руководство по настройке POS-терминалов.docx
drwx------ 2 alex alex    4096 май 28  2018 Сопроводительная документация
-rw-rw-r-- 1 alex alex 1558211 май  9  2018 Схема содержания документов.png

Куча добра, однако нас интересует только каталог для разработчиков:

Sberbank$ ls -l Docs/Для\ разработчиков\ ПО\ ККМ/
итого 8704
-rw-rw-r-- 1 alex alex   47105 май  9  2018 1C.docx
-rw-rw-r-- 1 alex alex    1824 май  9  2018 cardtype.h
-rw-rw-r-- 1 alex alex 2590378 май  9  2018 cr_ttk_protocol_ru.rtf
-rw-rw-r-- 1 alex alex     208 май  9  2018 deprtmnt.h
-rw-rw-r-- 1 alex alex   16681 май  9  2018 errors.h
drwx------ 6 alex alex    4096 май 28  2018 examples
-rw-rw-r-- 1 alex alex   58575 май  9  2018 gate.h
-rw-rw-r-- 1 alex alex    4218 май  9  2018 paramsln.h
-rw-rw-r-- 1 alex alex   61693 май  9  2018 pilot_nt.h
-rw-rw-r-- 1 alex alex   28160 май  9  2018 ReadTrack2.doc
-rw-rw-r-- 1 alex alex    7417 май  9  2018 sbkernel.h
-rw-rw-r-- 1 alex alex  144896 май  9  2018 sb_pilot.doc
-rw-rw-r-- 1 alex alex 3525323 май  9  2018 Интеграция с ККМ через ole-объект sbrf.dll.rtf
-rw-rw-r-- 1 alex alex   46683 май  9  2018 Интеграция с ККМ через библиотеку gate.dll.chi
-rw-rw-r-- 1 alex alex  255414 май  9  2018 Интеграция с ККМ через библиотеку gate.dll.chm
-rw-rw-r-- 1 alex alex  814653 май  9  2018 Интеграция с ККМ через библиотеку gate.dll.pdf
-rw-rw-r-- 1 alex alex   41618 май  9  2018 Интеграция с ККМ через библиотеку pilot_nt.chi
-rw-rw-r-- 1 alex alex  241716 май  9  2018 Интеграция с ККМ через библиотеку pilot_nt.chm
-rw-rw-r-- 1 alex alex  968753 май  9  2018 Интеграция с ККМ через библиотеку pilot_nt.pdf
-rw-rw-r-- 1 alex alex      81 май  9  2018 Подтипы пинпадов.txt

Много макулатуры, на всякий случай еще раз перечитаем pilot_nt, из которой узнаем следующее:
Таблица 1. Поддерживаемые sb_pilot ОС.
ОС Разрядность Имя модуля
Windows 32 sb_pilot.exe
Linux 32 sb_pilot
DOS 16 sb_pilot.exe

Оказывается утилита под windows должна все-таки называться sb_pilot. Что ж, камень в сторону Сбербанка #4 за несоответствие собственной документации.
Передача результатов работы программы.

По окончании работы программы формируются два текстовых файла — файл обмена и файл чека.

Первый имеет имя e и предназначен для передачи вызывающей программе параметров совершенной операции. Первая строка в этом файле содержит код результата операции, и через запятую – поясняющее текстовой сообщение. Код 0 означает успешное проведение платежа, любое другое значение – отказ или невозможность проведения платежа.




Лениво кидаем еще один камень и начинаем изучать документацию на подключение библиотеки напрямую.
Порядок вызова функций библиотеки

При оплате (возврате) покупки по банковской карте кассовая программа должна вызвать из библиотеки Сбербанка функцию card_authorize(), заполнив поля TType и Amount и указав нулевые значения в остальных полях. По окончании работы функции необходимо проанализировать поле RCode. Если в нем содержится значение «0» или «00», авторизация считается успешно выполненной, в противном случае отклоненной. Кроме этого, необходимо проверить значение поля Check.

Если оно не равно NULL, его необходимо отправить на печать (в нефискальном режиме) и затем
удалить вызовом функции GlobalFree(). При закрытии смены кассовая программа должна вызвать из библиотеки Сбербанка функцию close_day(), заполнив поле TType = 7 и указав нулевые значения в остальных полях. По окончании работы функции необходимо проверить значение поля Check.

Если поле Check не равно NULL, его необходимо отправить на печать (в нефискальном режиме) и после этого удалить вызовом функции GlobaFree().
Звучит несложно, даже хэдер файл предоставлен. Что ж, подключаем его, компилируем и…

$ cat main.c && i686-w64-mingw32-gcc main.c -o main.a
#include "pilot_nt.h"
 
int main(void) {
    return 0;
}
 
 
In file included from main.c:1:0:
pilot_nt.h:525:3: error: unknown type name ‘auth_answer’
   auth_answer   ans;                     /**< [in, out]  �������� ��������� ��������. ��. ::auth_answer */
   ^
pilot_nt.h:544:3: error: unknown type name ‘auth_answer’
   auth_answer   ans;               /**< [in, out]  �������� ��������� ��������. ��. ::auth_answer */
   ^
pilot_nt.h:567:3: error: unknown type name ‘auth_answer’
   auth_answer   ans;               /**< [in, out]  �������� ��������� ��������. ��. ::auth_answer */
   ^
pilot_nt.h:590:3: error: unknown type name ‘auth_answer’
   auth_answer   ans;              /**< [in, out]  �������� ��������� ��������. ��. ::auth_answer */
   ^
pilot_nt.h:627:3: error: unknown type name ‘auth_answer’
   auth_answer   ans;              /**< [in, out]  �������� ��������� ��������. ��. ::auth_answer */
   ^
pilot_nt.h:668:3: error: unknown type name ‘auth_answer’
   auth_answer   ans;               /**< [in, out]  �������� ��������� ��������. ��. ::auth_answer */

Эммм… Что? Открываем pilot_nt.h:

#ifdef __cplusplus
extern "C"{
#endif
<...>
/**
 * Основные параметры операции
 * Структура, используемая для описания операции и получения результатов выполнения операции.
 */
struct auth_answer{
   int TType;             /**< [in] тип транзакции. см ::OpetationTypes */
   unsigned long Amount;  /**< [in] сумма в копейках                    */
   char RCode[3];         /**< [out] код результата авторизации         */
   char AMessage[16];     /**< [out] текст результата авторизации       */
   int  CType;            /**< [in,out] тип карты                       */
   char* Check;           /**< [out] образ чека, должен освобождаться GlobalFree в вызывающей программе */
};
<...>
struct auth_answer7{
  auth_answer auth_answ;           /**< [in, out]  Основные параметры операции. См. ::auth_answer */ <---- THIS
  char   AuthCode[MAX_AUTHCODE];  /**< [out] Код авторизации. 7 байт.                            */
  char   CardID [CARD_ID_LEN];     /**< [out] Идентификатор карты. 25 байт.                       */
  int    SberOwnCard;              /**< [out] Флаг принадлежности карты Сбербанку                 */
};

Сразу, не глядя камень за комментарии на русском в кодировке CP1251.

Ну и самый серьезный камень: дорогие разработчики на С++. Если вы пишете extern «C» — это означает, что код внутри блока должен компилироваться С-компилятором. Если вы НЕ сделали `typedef` структуры, то при каждом ее упоминании в качестве указания типа необходимо писать ключевое слово `struct`.

Патчим файл для разработчиков, подставляя везде, где нужно слово `struct`. Линкуемся с библиотекой `pilot_nt.dll`. Победа, не? Запускаем наше приложение.

ГЛАВА 6. Из огня да в полымя


Ну вы поняли, да? Приложение просто падает. Сразу, до main. Медитируем, добавляем NIH-аналог функции errno для windows: GetLastError (камень #3 в сторону Microsoft, первые два за кодировки).

C:\banks\sber\WIN>sb_pilot.exe 1 1000
E: !g_sblibrary (0xc0000096)

0xc0000096? А разве GetLastError не должна возвращать адекватный код ошибки?
For a complete list of error codes provided by the operating system, see System Error Codes.
Ага, открываем статью по ссылке:
The following topics provide lists of system error codes. These values are defined in the WinError.h header file.

  • System Error Codes (0-499) (0x0-0x1f3)
  • System Error Codes (500-999) (0x1f4-0x3e7)
  • System Error Codes (1000-1299) (0x3e8-0x513)
  • System Error Codes (1300-1699) (0x514-0x6a3)
  • System Error Codes (1700-3999) (0x6a4-0xf9f)
  • System Error Codes (4000-5999) (0xfa0-0x176f)
  • System Error Codes (6000-8199) (0x1770-0x2007)
  • System Error Codes (8200-8999) (0x2008-0x2327)
  • System Error Codes (9000-11999) (0x2328-0x2edf)
  • System Error Codes (12000-15999) (0x2ee0-0x3e7f)
Отлично, мы получили незадокументированную ошибку, кидаем камень и открываем всезнающий google:

  • forum.vingrad.ru/forum/topic-346194/kw-dll-loadlibrary-%D0%BE%D1%82%D0%BB%D0%B0%D0%B4%D0%BA%D0%B0.html
  • bbs.csdn.net/topics/80078275
  • forums.codeguru.com/showthread.php?179566-0xC0000096-Privileged-Instruction
  • www.unknowncheats.me/forum/general-programming-and-reversing/97763-privileged-instruction-error.html
  • cboard.cprogramming.com/windows-programming/146130-prallel-port-programming.html
  • computer-programming-forum.com/82-mfc/dc2481c0ecead2f2.htm

Суть ошибки сводится к тому, что какая-то подпрограмма использует одну из инструкций

  • _inp()
  • _inpw()
  • _inpd()
  • _outp()
  • _outpw()
  • _outpd()

Использование которых запрещено под NT-ядрами, так как они пытаются работать с параллельным портом напрямую. Судя по всему этот код вызывается в инициализаторе библиотеки, т.е. библиотека при старте хочет опросить порты на наличие устройств, но NT-ядро требует работы через драйвер.

Безвыходная ситуация?

ГЛАВА 8. Пауки и мухи


22:00. На всякий случай возникает идея проверить, что это не из-за того, что мы используем кросскомпиляцию с Linux с помощью mingw. Параллельно понимаем, что Сбербанк поставляет только 32хбитное приложение, поэтому слинковаться с 64хбитным приложением не выйдет, ну ладно, но все равно запустим камень в сторону Сбербанка за 32-only версию в 2019м году.

Дано: установленная в virtualbox windows 7;
Необходимо: установить Visual Studio и скопилировать MVP.

Заходим на сайт Microsoft, качаем Visual Studio 2017. Берем лицензию сообщества, так как мы берем ее для проверки, для бизнеса лицензия будет куплена, если взлетит.
Скачиваем несколько сотен мегабайт и…

Видим, что наша версия ОС (Windows 7) не поддерживается.

Ок, идем на всякие непотребные сайты, ищем Visual Studio 2008, скачиваем несколько сотен мегабайт заново и…

Получаем iso файл.

Ладно, попытаемся установить Daemon Tools 10 (так как это та версия, которую предлагает сайт), чтобы вставить этот виртуальный диск.

Запускаем скачанный бинарь. Осечка, требуется .NET Framework 4.5, скачиваем, ставим.
Запускаем скачанный бинарь, установка началась, загрузчик говорит что ему нужна 4.5.2, скачиваем, ставим.
Запускаем скачанный бинарь, установка началась, загрузчик говорит что никуда не поедет, пока мы не поставим обновление безопасности KB3033929, скачиваем, ставим.

И получаем оплеуху от Microsoft в виде сообщения:



Яростно кидаем очень острый камень в сторону Microsoft, качаем с торрентов старый Daemon Tools, успешно распаковываем Visual Studio, устанавливаем, наконец-таки (00:00) компилируем MVP, получаем такую же ошибку. Что ж, хорошая была версия, но не срослось.

ГЛАВА 11. На пороге


Пишем второму программисту, который в этот момент в срочном порядке допиливает сервер и процедуру регистрации. Он вспоминает, есть гит-репозиторий, который на NT-подключает эту библиотеку и работает с ней.

Подозрительно глядя на репозиторий скачиваем его, компилируем и запускаем. Работает.



Еще более подозрительно смотрим на код. Код идентичен, разве что написан на C++ а не С.
Понимаем, что язык тут не причем. Смотрим библиотеки сбербанка, которые тянет за собой код.
Видим последний коммит.

И вот тут нас поджидает очередной сюрприз.

Оказывается, что версии библиотеки Сбербанка могут быть разными. Последний коммит увеличивает версию с 23 до 27й. Копируем себе на тестовый компьютер версию из гита — РАБОТАЕТ!

Проверяем все архивы, которые присылал Сбербанк, сравниваем версии и строим табличку:
Версия Работает
26.0.15 — Основная нет
27.4.12 — Из репозитория да
23.0.13 — Из репозитория да
29.0.9 — Самая свежая от СБ да
23.0.13 — С патчем для системы «Криптера» да

Отлично, вот теперь заживем. На тех системах где стоит 26 обновим до 29 или 27 и все взлетит.
Кидаем камень #9 в сторону Сбербанка за то, что сломали поведение на NT системах.

ГЛАВА 12. Что ждало их внутри


Не хватает «е» файла? Не беда, берем патченные заголовочники, динамически линкуемся с библиотекой чтобы корректно вернуть ошибку, пишем код, который просто запишет код возврата из функции в файл «е», назовем бинарь sb_pilot.exe и…

Работать-то оно работает.

Вот только на версии для системы «Криптера» не создается «р» файл.

Грустно смотрим на капающую по костяшкам кровь и на вмятину в стене.

Для начала, что такое система «Криптера».

Cryptera — это датская компания, выпускающая шифрующее оборудование/оборудование безопасности/ключи и пр. Думаю, что вы все видели один из экземпляров их продукции:



Так вот Сбербанк использует их криптомодуль для пинпадов и выпускает специальную «патченную» библиотеку, в которой, как мы уже поняли, не создается файл «р». Пишем по этому поводу в Сбербанк и через несколько дней получим ответ, что «под оригинальной системой файл „р“ будет создаваться, а под патченной на Криптеру — нет». Выдадим им камень # 10 через несколько дней, ведь работать нужно уже сейчас.

К счастью, или к несчастью, но функции, которые мы используем для проведения операций возвращают уже упомянутую структуру:

struct auth_answer{
   int TType;             /**< [in] тип транзакции. см ::OpetationTypes */
   unsigned long Amount;  /**< [in] сумма в копейках                    */
   char RCode[3];         /**< [out] код результата авторизации         */
   char AMessage[16];     /**< [out] текст результата авторизации       */
   int  CType;            /**< [in,out] тип карты                       */
   char* Check;           /**< [out] образ чека, должен освобождаться GlobalFree в вызывающей программе */
};

О, отлично, чек уже есть, можем сами сохранить его в файл или сразу вывести в JSON…

printf("%s\n", answer.Check);

И получаем падение приложение из-за обращения по невалидному указателю.

ГЛАВА 14. Огонь и вода


4:00. Выполняем Сету Бандха Сарвангасану чтобы успокоиться, и внимательно читаем мануал:
[out] образ чека, должен освобождаться GlobalFree в вызывающей программе
Что это нам дает? Очень многое. Во-первых то, что раз указатель требует очистки с помощью GlobalFree то его саллоцировали с помощью GlobalAlloc. Следовательно она выдает не указатель на память, как было в 16битной версии, а номер объекта с семантически объявленым типом HGLOBAL, который можно скормить в функции GlobalSize чтобы получить размер выделенного блока и GlobalLock чтобы заблокировать кусок памяти, но получить оригинальный указатель. Кстати, камень #6 в сторону Microsoft за NIH malloc и free, которые есть в стандартной библиотеке.

printf("%s\n", GlobalLock(answer.Check));

И все равно получаем падение. Окей, а что показывае GlobalSize? Ноль? Как-то странно.

Проверяем другие функции, которые тоже должны отдавать слип — видим ту же картину.

В голову приходит разве что самостоятельно сгенерировать слип по тем данным, которые может выдать самая крутая функция оплаты (да, у Сбербанка функции именуются card_authorize2..14, камень кидать за это не буду):

struct auth_answer14 {
  auth_answer   ans;               /**< [in, out]  Основные параметры операции. См. ::auth_answer */
  char   AuthCode[MAX_AUTHCODE];  /**< [out] Код авторизации. 7 байт.              */
  char   CardID[CARD_ID_LEN];      /**< [out] Идентификатор карты. 25 байт. Для международных карт все символы, кроме первых 6 и последних 4, будут заменены символами ‘*’.*/
  int    ErrorCode;                /**< [out] Код ошибки.                                         */
  char   TransDate[TRANSDATE_LEN]; /**< [out] Дата и время операции                               */
  int    TransNumber;              /**< [out] Номер операции за опер. день, см. номер на чеке     */
  int    SberOwnCard;              /**< [out] Флаг принадлежности карты Сбербанку                 */
  char   Hash[CARD_HASH_LEN];      /**< [in, out] хеш SHA1 от номера карты, в формате ASCII с нулевым байтом в конце. 40 байт.*/
  char   Track3[CARD_TRACK3_LEN];  /**< [out] третья дорожка карты*/
  DWORD  RequestID;                /**< [in,out] Уникальный номер операции. Только PCI DSS решения.*/
  DWORD  Department;              /**< [in] Порядковый номер отдела от 0 до 14-ти, включительно.
                                            При установке номера отдела в 0xFFFFFFFF, номер отдела
                                            будет запрошен через интерфейс терминала после вставки карты.
                                            Если номер отдела будет указан вне настроенного диапазона,
                                            то терминал вернет код ошибки 4191. */
  char   RRN[MAX_REFNUM];          /**< [in,out] Номер ссылки операции, присвоенный хостом. Используется
                                                для операций возврат, множественной авторизации и завершения расчета.
                                                Содержит уникальный 12-значный ссылочный номер.
                                                При предавторизации это поле является выходным
                                                (его заполняет библиотека pilot_nt.dll), а при
                                                завершении расчета – входным (значение должно
                                                быть заполнено вызывающей программой; оно должно
                                                совпадать со значением, возвращенным при предавторизации).*/
  DWORD  CurrencyCode;             /**< [in] Международный код валюты (810, 643, 840, 978 и т.д.) */
  char   CardEntryMode;            /**< [out] Способ чтения карты ('D'-магн.полоса, 'M'-ручной ввод, 'C'-чип, 'E'-бесконтакт EMV, 'R'-бесконтакт magstripe, 'F'-fallback)*/
  char   CardName[MAX_CARD_NAME_LEN]; /**< [out] Название типа карты */
  char   AID[MAX_AID_ASCII_LEN];   /**< [out] Application ID чиповой карты (уже в виде ASCIIZ-строки)*/
  char   FullErrorText[MAX_FULL_ERROR_TEXT]; /**< [out] Полный текст сообщения об ошибке*/
  DWORD  GoodsPrice;                /**< [in] Цена за единицу товара, коп (34.99->3499)*/
  DWORD  GoodsVolume;               /**< [in] Количество товара, в тыс. долях (30.234->30234)*/
  char   GoodsCode[MAX_GOODS_CODE+1]; /**< [in] Код товара во внешней системе.*/
  char   GoodsName[MAX_GOODS_NAME]; /**< [in] Наименование товара во внешней системе. Внимание! В структуре auth_answer14 название товара на один символ короче чем в gate.dll TGoodsData. Зафиксируем эту ошибку как стандарт*/
};
 
 
/** @brief Выполнение операций по картам
 *  @param[in] track2 данные дорожки карты с магнитной полосой. Если NULL, то будет предложено считать карту.
 *  @param[in,out] auth_answer см. ::auth_answer14
 *  @param[in,out] payinfo Информация для платежной системы
 *  @return int Код ошибки.
 */
PILOT_NT_API int  card_authorize14(
  char *track2,
  struct auth_answer14 *auth_answer,
  struct payment_info_item *payinfo
);

Пробуем подбирать поля… Выясняем, что от счастья нас отделяло всего одно — Фамилия и Имя носителя карты. Без них слип не считается законным:
Реквизиты: идентификатор банкомата, электронного терминала или другого технического средства, предназначенного для совершения операций с использованием платежных карт; вид операции; дата совершения операции; сумма операции; валюта операции; сумма комиссионного вознаграждения код авторизации; реквизиты платежной карты.
Жаль, но сформировать законный слип с теми данными, что у нас есть не получится.

Покопаемся в документации еще раз.

Находим пример, который Сбербанк поставляет в каталоге «examples»

std::cout << "Authorization completion finished with code '" << result << "'" << std::endl;
 
std::ofstream file(CHEQUE_FILENAME);
file << argument.auth_answ.Check;
file.close();
 
if (argument.auth_answ.Check) {
  std::cout << "Cheque saved to file " << CHEQUE_FILENAME << std::endl;
  //GlobaFree(argument.auth_answ.Check);
}

Просто выводится текст, находящийся по указателю. Но ведь мы уже убедились, что так оно не работает… На всякий случай скомпилируем их пример и запустим. Вылет на строчке `file << argument.auth_answ.Check;`, что ж, Сбербанк, держите камень #11 за неработающие примеры.

7:00. Уже можно писать разработчикам другой обертки, которая несколько лет назад была написана на Delphi. Получаем ответ, что у них все работает. Ищем основу их обертки и находим на github:

TAuthAnswer = packed record
  TType: integer;
  Amount: UINT; // IN Сумма операции в копейках
  Rcode: array [0 .. 2] of AnsiChar;
  AMessage: array [0 .. 15] of AnsiChar;
  CType: integer;
  Check: PAnsiChar;
end;
 
 
    Result := Func(nil, @FAuthAnswer);
    FLastError := Result;
    FCheque := PAnsiChar(FAuthAnswer.Check);

Простое преобразование типа в указатель без каких-либо вызовов функций.

Начинаем подозревать злых духов.

ГЛАВА 17. Гроза разразилась


Люди начинают возвращаться в офис, сочувственно кивая головой. PO выглядит не очень веселым узнав последние новости.

Тут вспоминается одна деталь. Когда мы выводили поля структуры #14 чтобы увидеть их значения то один байт каждой строки был отрезан. С одной стороны это, с другой
Внимание! В структуре auth_answer14 название товара на один символ короче чем в gate.dll TGoodsData. Зафиксируем эту ошибку как стандарт
Может это все же связано с…

Страшная догадка осеняет мозг словно молния. Объявим структуру как

typedef struct __attribute__((packed)) {
   int TType;             /**< [in] тип транзакции. см ::OpetationTypes */
   unsigned long Amount;  /**< [in] сумма в копейках                    */
   char RCode[3];         /**< [out] код результата авторизации         */
   char AMessage[16];     /**< [out] текст результата авторизации       */
   int  CType;            /**< [in,out] тип карты                       */
   char* Check;           /**< [out] образ чека, должен освобождаться GlobalFree в вызывающей программе */
};

И…

Ничего не меняется.

Все так же Size = 0, Все так же Lock = NULL.

Боль.

Тлен.

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

    u32 i;
    for (i = 0; i < sizeof(answ); i++) {
        printf("%02x ", *((u8 *)&answ + i));
    }
    printf("\n");
 
 
C:\banks\sber\sb_pilot>sb_pilot.exe 1 1000
01 00 00 00 e8 03 00 00 30 00 00 ce e4 ee e1 f0 e5 ed ee 00 00 00 00 00 00 00 00 02 00 00 00 f8 6c 7a 00 00

`30 00 00 ce` — а это значит, что Сбербанк все же использует Packed структуры. Вот только в хэдерах об этом нет ни слова. Поэтому не работают примеры, поэтому не получается получить указатель на текст в конце — ведь он битый из-за сдвига на 1 байт. Огромный и колючий камень в сторону Сбербанка!

И тут в глаза бросился один мааааленький нюанс. 4 + 4 + 3 + 16 + 4 + 4 = 35. А тут 36 байт, Обеликс.

Раз тут 36 байт, значит компилятор все еще выравнивает структуру. Значит между RCode и AMessage все еще вставлен дополнительный байт. Но почему? Ведь мы указали `__packed__`!

ГЛАВА 18. Обратный путь


Причины того, что выравнивание все еще включено появились в 2012м году: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=52991. Починен баг только в GCC 8 (камень за 6 лет забагованности!), обновиться на который пока нет возможности. К счастью существует workaround:

-mno-ms-bitfields

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



Слип! Родненький! Я по тебе скучал, даже не буду ругаться из-за кракозябр, камень за это я уже кидал.

И скормим, наконец, Майкрософт камень, за то, что GlobalSize/Lock выдают нули на невалидные указатели.

ГЛАВА 19. Последняя глава


Чтобы максимально снизить количество ifdef'ов для прослойки для sb_pilot мы написали отдельное приложение, которое полностью имитирует linux-версию sb_pilot. Таким образом оставив код прослойки #1 прежним, оставив лишь одно условие:

#if defined(BXI_OS_GLX)
#define GFJ_PILOT_EXECUTABLE "./sb_pilot"
#elif defined(BXI_OS_WIN)
#define GFJ_PILOT_EXECUTABLE "./sb_pilot.exe"
#endif



Итоги сражения:

  • Сбербанк: 12 камней
  • Майкрософт: 7 камней
  • GCC: 1 камень

Ачивка-воспоминание на нашу командную доску:

Источник: https://habr.com/ru/post/451188/


Интересные статьи

Интересные статьи

Я давно знаком с Битрикс24, ещё дольше с 1С-Битрикс и, конечно же, неоднократно имел дела с интернет-магазинами которые работают на нём. Да, конечно это дорого, долго, местами неуклюже...
Почти два года назад, под Новый год, у меня зазвонил телефон. После пары дежурных фраз «как сам, как дети» меня позвали работать в Германию. Я крепко призадумался. С о...
Люди пользуются кнопкой возврата на предыдущую страницу в браузере очень часто — возможно, чаще, чем вы думаете. А если так, то зачем сразу выбрасывать страницу из памяти браузера, а спустя секун...
Как широко известно, с 1 января 2017 года наступает три важных события в жизни интернет-магазинов.
Периодически мне в разных вариантах задают вопрос, который «в среднем» звучит так: «что лучше: заказать интернет-магазин на бесплатной CMS или купить готовое решение на 1С-Битрикс и сделать магазин на...