By participating recently in various interesting projects, the problem of the alternative management of the
Perco product has
emerged. Electronic pass-through KT02.3 . This product is a complete solution and does not imply the use as part of other access control systems, as well as any intrusion into its control environment. But, as the saying goes,
“Everything is possible! The impossible just takes more time. ”(C) Dan Brown .
But we did it. How it all happened read under the cut.
The basic description of the system can be found
here from this document .
Let us dwell on the external interfaces of the system:
Supports connection via RS-485 interface to the following devices:
• up to 8 lock controllers PERCo-CL201 (the CL201 controller has a built-in reader and provides control of one lock);
• system time display PERCo-AU05
• card reader PERCo-IC02.1 (for wiring diagram, see description of PERCo-IC02.)
This means that a sufficient number of peripheral systems that provide access to the premises can be connected to the common bus of this turnstile.
Communication interface with PC and other controllers of the S-20 system - Ethernet (provided by
TCP / IP protocol stack support (ARP, IP, ICMP, TCP, UDP, DHCP)).
Managed through its application, available for download from the site. It also has a very tricky SDK, closed by the NDA.
“The delivery of the SDK provides for the signing of an agreement on non-disclosure of confidential information with the interested party and is free.”
Since neither the turnstile circuit nor access to the SDK was available to me, armed with a soldering iron and RS485 transceiver, we began to study how the protocol works.
')
The wiring of external devices is quite trivial.

You can connect:
- RU - radio control
- Remote control - remote control
- DKZP - Sensor of control of an area of ​​passage
- Siren
- up to 8 locks PERCo-CL201
- PERCo-AU05 system time display
No other devices can be connected.
The Perco protocol is closed, but there is a description of the PERCo-AU05 watch, which is easily googled on the network.

This picture from the description is the only mention of the PERCo protocol found on the network.
Fine. There is a piece of protocol, which means you can see what runs there. We connect RS485 to the turnstile and look.
Primary dump14: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 ']
In principle, it is generally not clear how this works. Who is the sender, who is the recipient? Where what, what is terminated? Just a stream of some binary data.
Having gone around with the dumps for several days, I realized that "the devil is sitting in the details."
The result was the following byte structure inside the reader packages:
- 1. 0xAA - the command start code
- 2. 0x [02] [12] - reader ID
- 3. 0x0 [15] - command code
- some data
- CRC16 checksum
What next?
Who sends this? What is the answer from this?
It turned out that everything is much more cunning than we used to see in session protocols.
To understand this, readers and a controller had to be connected to a break through two RS485 converters.
So, in fact, the package consists of two parts. The first part is the controller command, always starting with
0xAA , the second part is the device response to which the command
belongs . This response has a variable length and ends with the checksum of the entire packet.
In reality, the “controller-reader” session looks like this:
split session dumpcntrler: ['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']
Let's sort some combinations.
cntrler: ['0xaa', '0x01', '0x1a', '0xff', '0xe5', '0x1f']
readers: ['0x7f', '0xa4']
The controller sends the command
'0x1A' to the reader with the identifier
'0x01' and the data
'0xFF' , to which the reader responds with some code. It would seem “Here! it! take it and do it, but no.
The manual says that the last two bytes of the packet are the checksum of the entire packet with the exception of the command code, using the CRC16 algorithm. Counting CRC16 from
['0x01', '0x1A', '0xFF'] on the
calculator and we get
0xE01A , which does not converge with
0x1FE5 . It turns out the valiant developers of PERCo made a little protection either from line noise, or from people like me;)
The fact is that
0x1FE5 , this is
0xE01A xor 0xFFFF, and of course this is not written anywhere (see the manual above).
So, with the package from the controller, everything is more or less clear, what exactly is the reader with the address
0x01 sent?
Looking through the data inside the packet from the controller and incrementally counting the CRC16, it turned out that the reader's response
['0x7f', '0xa4'] is the checksum of the second and third byte
['0x01', '0x1A'] .
Thus, the reader tells the controller that it is “live”.
Initializationcntrler: ['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']
Then everything is the same. The command ends with CRC16 and CRC16 from the second and third byte of the command.
Let us dwell on the addressing of external devices.
external locking['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']]
As you can see from the dump, all external locks are bit-addressing, hence the limit of 8 external locks is obtained.
After the completion of the initialization of all available devices, the controller on the reader resets the indication
Reset Indicationcntrler: ['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']
The controller command
0x1B resets the reader, and the command
['0x48', lamp] lights the light bulb, where lamp has values.
- 0x01 - green
- 0x02 - orange
- 0x04 - red
After the readers are initialized, the controller checks their status again.
Polling statuscntrler: ['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']
and starts a generator of polling the status of readers and locks.
The reader is polled 3 times per second each.
And now the fun begins.
At the status request, the reader ADDITIONS the controller's command with data from its buffer, counts
CRC16 xor 0xFFFF and provides data to the communication channel.
Consider a reader response packet:
Empty package:
readers: ['0x4e', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0xf1', '0x6e']Package with map:
readers: ['0x45', '0x40', '0x5d', '0x7a', '0x07', '0x00', '0x04', '0x00', '0x00', '0x00', '0xbb' , '0x9d']- The 1st byte is the packet number, which is calculated by incrementing the previous value of a random number from the range from 1 to 15, after which the value is taken modulo 79 (0x4F)
- 2nd byte is the reader state. If there is 0x00 , then the reader's buffer is already read by the controller, if there is 0x40 , then there is a card in the buffer.
- From 3rd to 6th byte the code of the read card is placed.
- In 7 bytes, the number 4 always lives. I did not understand the remaining bytes.
If the controller has an attached card, then the controller issues a command to the reader to accept the card code, lights the green lamp and opens the passage mechanism. When the waiting time expires or when the user passes, the passage mechanism closes and the status of the reader is reset to its original state. In this survey of other devices does not stop.
And now, in fact, what was implied in the title about “Protect the lines of your ACS from invasion”?
The interceptor written by me in Python allows you to seize control of the PERCo ACS at any point of the RS485 trunk and track the cards to which the turnstile gives access authorization, interrupt data transfer from the reader to the turnstile controller with the base of valid keys, open any devices connected to the data highway. At the same time, the “left” cards applied to the readers of the system can be replaced by “valid” cards and vice versa. By removing the initialization block dump and scrolling it back into the line, you can emulate both the controller and readers, which opens up simply endless possibilities for managing the system.
So "Take care of the lines of your ACS from invasion" :)
PS: I will not upload scripts :-P
© Aborche 2016