Реверс протоколу СКУД RS485 від Perco. Бережіть лінії своїх СКУД від вторгнення

Беручи участь останнім часом у різних цікавих проектах, виникла задачка альтернативного управління продуктом Електронна прохідна Perco KT02.3. Даний продукт є закінченим рішенням і не передбачає використання у складі інших систем СКУД, а також будь-якого вторгнення в своє середовище управління. Але, як мовиться в приказці, " «Можливо все! На неможливе просто потребує більше часу» © Ден Браун.

Як і що з цієї затії вийшло, читайте під катом.

Основний опис системи можна прочитати ось з цього документа.

Зупинимося на зовнішніх інтерфейсах системи:
Підтримує підключення по інтерфейсу RS-485 наступних пристроїв:
• до 8-ми контролерів замку PERCo-CL201 (контролер CL201 має вбудований зчитувач і забезпечує управління одним замком);
• табло системного часу PERCo-AU05
• картоприймач PERCo-IC02.1 (див. схему підключення в описі PERCo-IC02.)
Це означає, що в загальну шину даного турнікета може бути підключено достатню кількість периферійних систем, що забезпечують доступ в приміщення.
Інтерфейс зв'язку з ПК та іншими контролерами системи S-20 – Ethernet (забезпечується
підтримка стека протоколів TCP/IP (ARP, IP, ICMP, TCP, UDP, DHCP)).
Управляється через свій додаток, доступне для завантаження з сайту. Так само має дуже хитрий SDK, що закривається NDA.
«Поставка SDK передбачає підписання з зацікавленою стороною угоди про нерозголошення конфіденційної інформації та здійснюється безкоштовно.»
Так як ні схеми турнікета ні доступу до SDK у мене не було, озброївшись паяльником і трансивером RS485 ми почали вивчати як влаштований протокол.

Схема підключення зовнішніх пристроїв досить тривіальна.
image
Можна підключити:
  • РУ — радіопульт
  • ПДУ — пульт дистанційного керування
  • ДКЗП — Датчик контролю зони проходу
  • Сирену
  • до 8 замків PERCo-CL201
  • табло системного часу PERCo-AU05
Ніяких інших пристроїв підключити не можна.
Протокол Perco є закритим, але є опис годин PERCo-AU05, яке легко гуглится в мережі.

image

Дана картинка з опису це єдина згадка протоколу PERCo знайдене в мережі.
Відмінно. Шматок протоколу є, значить можна подивитися, що там бігає. Підключаємо RS485 до турнікету і дивимося.

Первинний дамп14:04:08 :: ['0xaa', '0x05', '0x8c', '0x04', '0x01', '0x01', '0x98', '0xfe']
14:04:08 :: ['0xaa', '0x25', '0x8c', '0x04', '0x01', '0x01', '0x19', '0x39']
14:04:08 :: ['0xaa', '0x45', '0x8c', '0x04', '0x01', '0x01', '0x99', '0x31']
14:04:09 :: ['0xaa', '0x65', '0x8c', '0x04', '0x01', '0x01', '0x18', '0xf6']
14:04:09 :: ['0xaa', '0x85', '0x8c', '0x04', '0x01', '0x01', '0x99', '0x20']
14:04:09 :: ['0xaa', '0xa5', '0x8c', '0x04', '0x01', '0x01', '0x18', '0xe7']
14:04:09 :: ['0xaa', '0xc5', '0x8c', '0x04', '0x01', '0x01', '0x98', '0xef']
14:04:10 :: ['0xaa', '0xe5', '0x8c', '0x04', '0x01', '0x01', '0x19', '0x28']
14:04:10 :: ['0xaa', '0x05', '0x1a', '0xff', '0xa4', '0xde']
14:04:10 :: ['0xaa', '0x25', '0x1a', '0xff', '0xa5', '0x14']
14:04:10 :: ['0xaa', '0x45', '0x1a', '0xff', '0xa5', '0x0a']
14:04:10 :: ['0xaa', '0x65', '0x1a', '0xff', '0xa4', '0xc0']
14:04:10 :: ['0xaa', '0x85', '0x1a', '0xff', '0xa5', '0x36']
14:04:11 :: ['0xaa', '0xa5', '0x1a', '0xff', '0xa4', '0xfc']
14:04:11 :: ['0xaa', '0xc5', '0x1a', '0xff', '0xa4', '0xe2']
14:04:11 :: ['0xaa', '0xe5', '0x1a', '0xff', '0xa5', '0x28']
14:04:11 :: ['0xaa', '0x01', '0x1a', '0xff', '0xe5', '0x1f', '0x7f', '0xa4']
14:04:11 :: ['0xaa', '0x01', '0x48', '0x04', '0xff', '0x00', '0xff', '0x6f', '0x60', '0xfe', '0x59']
14:04:11 :: ['0xaa', '0x01', '0xa8', '0x07', '0x01', '0x01', '0xff', '0x01', '0x01', '0xff', '0x01', '0x01', '0xff', '0x44', '0xc2', '0xff', '0xd1']
14:04:11 :: ['0xaa', '0x21', '0x1a', '0xff', '0xe4', '0xd5', '0x66', '0x64']
14:04:11 :: ['0xaa', '0x21', '0x48', '0x04', '0xff', '0x00', '0xff', '0x68', '0x00', '0xe7', '0x99']
14:04:11 :: ['0xaa', '0x21', '0xa8', '0x07', '0x01', '0x01', '0xff', '0x01', '0x01', '0xff', '0x01', '0x01', '0xff', '0xc5', '0x7d', '0xe6', '0x11']
14:04:11 :: ['0xaa', '0x02', '0x1a', '0xff', '0x15', '0x1f']
14:04:11 :: ['0xaa', '0x22', '0x1a', '0xff', '0x14', '0xd5']
14:04:11 :: ['0xaa', '0x04', '0x38', '0x34', '0x02', '0x11', '0x83', '0xfd']
14:04:11 :: ['0xaa', '0x01', '0x1b', '0x0f', '0xe4', '0xcb', '0xbe', '0x64']
14:04:11 :: ['0xaa', '0x01', '0x48', '0x02', '0x00', '0xff', '0xff', '0x1e', '0x28', '0xfe', '0x59']
14:04:11 :: ['0xaa', '0x21', '0x1b', '0x0f', '0xe5', '0x01', '0xa7', '0xa4']
14:04:11 :: ['0xaa', '0x21', '0x48', '0x02', '0x00', '0xff', '0xff', '0x19', '0x48', '0xe7', '0x99']
14:04:11 :: ['0xaa', '0x04', '0x38', '0x34', '0x02', '0x11', '0x83', '0xfd']
14:04:11 :: ['0xaa', '0x04', '0x38', '0x34', '0x02', '0x11', '0x83', '0xfd']
14:04:11 :: ['0xaa', '0x04', '0x38', '0x34', '0x02', '0x11', '0x83', '0xfd']
14:04:11 :: ['0xaa', '0x04', '0x38', '0x34', '0x02', '0x11', '0x83', '0xfd']
14:04:11 :: ['0xaa', '0x05', '0x04', '0x00']
14:04:11 :: ['0xaa', '0x01', '0x01', '0x0e', '0x10', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x88', '0x22', '0xf5']
14:04:11 :: ['0xaa', '0x01', '0x09', '0x3e', '0x69', '0x3e', '0x69']
14:04:11 :: ['0xaa', '0x01', '0x05', '0x16', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0xf2', '0x7a']
14:04:12 :: ['0xaa', '0x01', '0x1a', '0xff', '0xe5', '0x1f', '0x7f', '0xa4']
14:04:12 :: ['0xaa', '0x01', '0x48', '0x02', '0x00', '0xff', '0xff', '0x1e', '0x28', '0xfe', '0x59']
14:04:12 :: ['0xaa', '0x21', '0x01', '0x07', '0x10', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0xfb', '0x65']
14:04:12 :: ['0xaa', '0x21', '0x09', '0x27', '0xa9', '0x27', '0xa9']
14:04:12 :: ['0xaa', '0x21', '0x05', '0x4e', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0xf1', '0x6e']
14:04:12 :: ['0xaa', '0x21', '0x1a', '0xff', '0xe4', '0xd5', '0x66', '0x64']
14:04:12 :: ['0xaa', '0x21', '0x48', '0x02', '0x00', '0xff', '0xff', '0x19', '0x48', '0xe7', '0x99']
14:04:12 :: ['0xaa', '0x01', '0x01', '0x4f', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x34', '0x24']
14:04:12 :: ['0xaa', '0x01', '0x05', '0x40', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x11', '0x24']
14:04:12 :: ['0xaa', '0x21', '0x01', '0x2e', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0xe7', '0xe0']
14:04:12 :: ['0xaa', '0x21', '0x05', '0x2d', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x02', '0xdf']

В принципі взагалі незрозуміло, як це працює. Хто відправник, хто одержувач? Де що, чим терминируется? Просто потік якихось бінарних даних.
Провозившись зі знятими дампами кілька днів, я зрозумів, що «диявол сидить у деталях».
Вийшла наступна структура байт всередині пакетів для зчитувачів:
  • 1. 0xAA — код початку команди
  • 2. 0x[02][12] — ідентифікатор зчитувача
  • 3. 0x0[15] — код команди
  • якісь дані
  • контрольна сума CRC16
Що ж далі?
Хто це відправляє? Що з цього відповідь?
Виявилося, що все набагато хитріший, ніж ми звикли бачити у сесійних протоколах.
Для розуміння цього довелося підключити зчитувачі і контролер в розрив через два конвертора RS485.
Так от, насправді пакет складається з двох частин. Перша частина — це команда контролера, завжди починається з 0xAA, друга частина — це відповідь пристрою до якого належала команда. Даний відповідь має змінну довжину і закінчується контрольною сумою всього пакету.
В реальності сесія «контролер-зчитувач» виглядає ось так:
розділений дамп сесіїcntrler:['0xaa', '0x01', '0x1a', '0xff', '0xe5', '0x1f']
readers:['0x7f', '0xa4']
cntrler:['0xaa', '0x01', '0x48', '0x04', '0xff', '0x00', '0xff', '0x6f', '0x60']
readers:['0xfe', '0x59']
cntrler:['0xaa', '0x01', '0xa8', '0x07', '0x01', '0x01', '0xff', '0x01', '0x01', '0xff', '0x01', '0x01', '0xff', '0x44', '0xc2']
readers:['0xff', '0xd1']
cntrler:['0xaa', '0x21', '0x1a', '0xff', '0xe4', '0xd5']
readers:['0x66']
readers:['0x64']
cntrler:['0xaa', '0x21', '0x48', '0x04', '0xff', '0x00', '0xff', '0x68', '0x00']
readers:['0xe7']
readers:['0x99']
cntrler:['0xaa', '0x21', '0xa8', '0x07', '0x01', '0x01', '0xff', '0x01', '0x01', '0xff', '0x01', '0x01', '0xff', '0xc5', '0x7d']
readers:['0xe6', '0x11']
cntrler:['0xaa', '0x02', '0x1a', '0xff', '0x15', '0x1f']
cntrler:['0xaa', '0x02', '0x1a', '0xff', '0x15', '0x1f']
cntrler:['0xaa', '0x22', '0x1a', '0xff', '0x14', '0xd5']
cntrler:['0xaa', '0x22', '0x1a', '0xff', '0x14', '0xd5']
cntrler:['0xaa', '0x04', '0x38', '0x37', '0x2e', '0x11', '0x6f', '0x3d']
cntrler:['0xaa', '0x01', '0x1b', '0x0f', '0xe4', '0xcb']
readers:['0xbe']
readers:['0x64']
cntrler:['0xaa', '0x01', '0x48', '0x02', '0x00', '0xff', '0xff', '0x1e', '0x28']
readers:['0xfe', '0x59']
cntrler:['0xaa', '0x21', '0x1b', '0x0f', '0xe5', '0x01']
readers:['0xa7', '0xa4']
cntrler:['0xaa', '0x21', '0x48', '0x02', '0x00', '0xff', '0xff', '0x19', '0x48']
readers:['0xe7']
readers:['0x99']
cntrler:['0xaa', '0x04', '0x38', '0x37', '0x2e', '0x11', '0x6f', '0x3d']
cntrler:['0xaa', '0x04', '0x38', '0x37', '0x2e', '0x11', '0x6f', '0x3d']
cntrler:['0xaa', '0x04', '0x38', '0x37', '0x2e', '0x11', '0x6f', '0x3d']
cntrler:['0xaa', '0x04', '0x38', '0x37', '0x2e', '0x11', '0x6f', '0x3d']
cntrler:['0xaa', '0x05', '0x04', '0x00']
cntrler:['0xaa', '0x01', '0x01']
readers:['0x0f', '0x10', '0x00']
readers:['0x00', '0x00', '0x00']
readers:['0x00', '0x00', '0x00']
readers:['0x00', '0xfb', '0x30']
cntrler:['0xaa', '0x01', '0x09', '0x3e', '0x69']
readers:['0x3e', '0x69']
cntrler:['0xaa', '0x01', '0x05']
readers:['0x4b']
readers:['0x00', '0x00', '0x00']
readers:['0x00', '0x00', '0x00']
readers:['0x00', '0x00', '0x00']
readers:['0x60', '0xc1']
cntrler:['0xaa', '0x01', '0x1a', '0xff', '0xe5', '0x1f']
readers:['0x7f', '0xa4']
cntrler:['0xaa', '0x01', '0x48', '0x02', '0x00', '0xff', '0xff', '0x1e', '0x28']
readers:['0xfe', '0x59']
cntrler:['0xaa', '0x21', '0x01']
readers:['0x47']
readers:['0x10', '0x00', '0x00']
readers:['0x00', '0x00', '0x00']
readers:['0x00', '0x00', '0x00']
readers:['0xf9', '0xb1']

Розберемо деякі комбінації.
cntrler:['0xaa', '0x01', '0x1a', '0xff', '0xe5', '0x1f']
readers:['0x7f', '0xa4']
Контролер посилає команду '0x1A' для зчитувача з ідентифікатором '0x01' і даними '0xFF', на що зчитувач відповідає якимось кодом. Здавалося б «Ось! воно! бери і роби», ан ні.
В інструкції написано, що останні два байти пакета це контрольна сума всього пакету за винятком коду команди, за алгоритмом CRC16. Вважаємо CRC16 від ['0x01', '0x1A', '0xFF'] калькуляторі і отримуємо 0xE01A, що ніяк не сходиться з 0x1FE5. Виявляється доблесні розробники PERCo зробили невелику захист або від перешкод в лінії, або від таких як я ;)
Справа в тому що 0x1FE5, 0xE01A xor 0xFFFF і про це природно ніде не написано (див. мануал вище).
Отже, з пакетом від контролера все більш менш зрозуміло, що ж таке надіслав зчитувач з адресою 0x01?
Перебираючи дані всередині пакету від контролера і покроково вважаючи CRC16 виявилося, що відповідь зчитувача ['0x7f', '0xa4'] це контрольна сума другого і третього байта ['0x01', '0x1A'].
Таким чином зчитувач говорить контролеру, що він «живий».
Ініціалізаціяcntrler:['0xaa', '0x01', '0x48', '0x04', '0xff', '0x00', '0xff', '0x6f', '0x60']
readers:['0xfe', '0x59']
cntrler:['0xaa', '0x01', '0xa8', '0x07', '0x01', '0x01', '0xff', '0x01', '0x01', '0xff', '0x01', '0x01', '0xff', '0x44', '0xc2']
readers:['0xff', '0xd1']

Далі все так само. Команда закінчується CRC16 і CRC16 від другого і третього байта команди.

Зупинимося докладніше на адресації зовнішніх пристроїв.
адресація зовнішніх замків['0xaa', '0x05', '0x1a', '0xff', '0xa4', '0xde']
['0xaa', '0x25', '0x1a', '0xff', '0xa5', '0x14']
['0xaa', '0x45', '0x1a', '0xff', '0xa5', '0x0a']
['0xaa', '0x65', '0x1a', '0xff', '0xa4', '0xc0']
['0xaa', '0x85', '0x1a', '0xff', '0xa5', '0x36']
['0xaa', '0xa5', '0x1a', '0xff', '0xa4', '0xfc']
['0xaa', '0xc5', '0x1a', '0xff', '0xa4', '0xe2']
['0xaa', '0xe5', '0x1a', '0xff', '0xa5', '0x28']

Як видно з дампа, всі зовнішні замки мають бітову адресацію, звідси і виходить обмеження у 8 зовнішніх замків.
Після закінчення ініціалізації всіх доступних пристроїв, контролер на считывателях скидає індикацію
Скидання індикаціїcntrler:['0xaa', '0x01', '0x1b', '0x0f', '0xe4', '0xcb']
readers:['0xbe', '0x64']
cntrler:['0xaa', '0x01', '0x48', '0x02', '0x00', '0xff', '0xff', '0x1e', '0x28']
readers:['0xfe', '0x59']
cntrler:['0xaa', '0x21', '0x1b', '0x0f', '0xe5', '0x01']
readers:['0xa7', '0xa4']
cntrler:['0xaa', '0x21', '0x48', '0x02', '0x00', '0xff', '0xff', '0x19', '0x48']
readers:['0xe7', '0x99']

Команда контролера 0x1B резетит зчитувач, а команда ['0x48', lamp] запалює лампочку, де lamp має значення.
  • 0x01 — зелений
  • 0x02 — помаранчевий
  • 0x04 — червоний
Після ініціалізації зчитувачів, контролер ще раз перевіряє їх стан
Опитування стануcntrler:['0xaa', '0x01', '0x01']
readers:['0x0e', '0x10', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x88', '0x22', '0xf5']
cntrler:['0xaa', '0x01', '0x09', '0x3e', '0x69']
readers:['0x3e', '0x69']
cntrler:['0xaa', '0x01', '0x05']
readers:['0x16', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0xf2', '0x7a']
cntrler:['0xaa', '0x01', '0x1a', '0xff', '0xe5', '0x1f']
readers:['0x7f', '0xa4']
cntrler:['0xaa', '0x01', '0x48', '0x02', '0x00', '0xff', '0xff', '0x1e', '0x28']
readers:['0xfe', '0x59']
cntrler:['0xaa', '0x21', '0x01']
readers:['0x07', '0x10', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0xfb', '0x65']
cntrler:['0xaa', '0x21', '0x09', '0x27', '0xa9']
readers:['0x27', '0xa9']
cntrler:['0xaa', '0x21', '0x05']
readers:['0x4e', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0xf1', '0x6e']
cntrler:['0xaa', '0x21', '0x1a', '0xff', '0xe4', '0xd5']
readers:['0x66', '0x64']
cntrler:['0xaa', '0x21', '0x48', '0x02', '0x00', '0xff', '0xff', '0x19', '0x48']
readers:['0xe7', '0x99']

і запускає генератор опитування стану зчитувачів і замків.
Опитування зчитувачів відбувається 3 рази в секунду кожен.
А тепер починається найцікавіше.
На запит стану, зчитувач ДОПОВНЮЄ команду контролера даними з свого буфера, вважає CRC16 xor 0xFFFF і видає дані в канал зв'язку.
Розглянемо пакет відповіді зчитувача:
Порожній пакет: readers:['0x4e', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0xf1', '0x6e']
Пакет з картою: readers:['0x45', '0x40', '0x5d', '0x7a', '0x07', '0x00', '0x04', '0x00', '0x00', '0x00', '0xbb', '0x9d']
  • 1й байт це номер пакета, який обчислюється шляхом приросту до попереднього значення випадкового числа з діапазону від 1 до 15, після чого береться значення по модулю 79(0x4F)
  • 2й байт це стан зчитувача. Якщо там 0x00, то значить буфер зчитувача вже прочитаний контролером, якщо там 0x40, то значить в буфері є карта.
  • з 3го по 6 байт міститься код зчитаної карти.
  • 7 байті завжди живе цифра 4. Інші байти я не розбирав.
Якщо у контролера присутній прикладена карта, то контролер видає команду на зчитувач про прийняття коду картки, запалює зелену лампу і відкриває механізм проходу. Після закінчення часу очікування або при проході користувача механізм проходу закривається і стан зчитувача скидається в початковий. При цьому опитування інших пристроїв не припиняється.

А тепер, власне, що малося на увазі в заголовку про «Бережіть лінії своїх СКУД від вторгнення»?

Написаний мною на мові пітон перехоплювач дозволяє захопити управління СКУД PERCo в будь-якій точці магістралі RS485 і відстеживши карти на які турнікет видає дозвіл проходу, переривати передачу даних від зчитувача до контролера турнікета з базою валідних ключів, відкривати будь-які пристрої підключені до магістралі даних. При цьому «ліві» карти прикладаються до зчитувачі системи, можуть замінюватися на «валідні» і назад. Знявши дамп блоку ініціалізації і прокрутивши його назад в лінію можна емулювати як сам контролер, так і зчитувачі, що відкриває просто безмежні можливості для управління системою.

Так що «Бережіть лінії своїх СКУД від вторгнення» :)

PS: скрипти викладати не буду :-P

© Aborche 2016
Aborche

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.