Pull to refresh

Реверс протокола СКУД RS485 от Perco. Берегите линии своих СКУД от вторжения

Reading time10 min
Views19K
Участвуя последнее время в разных интересных проектах, возникла задачка альтернативного управления продуктом 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. Cчитаем 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
Tags:
Hubs:
Total votes 10: ↑9 and ↓1+8
Comments5

Articles