Reverse Engineering RFID Reader
From Dallas Makerspace
(Redirected from ReverseEngineeringRFIDReader)
Contents |
Reverse Engineering RFID Reader / Door lock
This page contains the assorted details discovered in reverse engineering the RFID reader / door lock. Details will be sparse as we discover more but we've made interesting progress so far.
Current Progress (2011-02-28)
- We can send a door unlock sequence over ethernet from any platform running Python (with a socket library)
- We can send a "perpetual unlock" sequence as per above
- Looks like we can use the tags along with several of the Parallax RFID readers for any other purpose we see fit
- Wrote a quick & dirty iPhone app to be tested (*See notes below for security considerations!)
- We can parse the entry log format for standard tag reads, unknown tag reads, and "Soft Open" calls (ie, the door unlock sequence mentioned above)
- Written Python tool to query reader for log entries, parse them, and display in text form.
- Python logging code is largely working and logging information to text files and to mongodb for further processing.
- Seeing some odd behaviors - the record counter isn't perfect and occasionally the byte values are off. Also seeing individual entries occasionally missing 2 hex values.
- The record counter has an option to handle overflows - if it's greater than 0xff (255) entries, then the 2nd byte is a value greater than the first byte. ie, 1c1d means 283 (28 + 255) entries. Determined this from some parsed text logging. Need to figure out algorithm for how many are actually there. Something like (byte2-byte1)*255 + byte1... Ok, that was easier than it seems - which leads to a theoretical max of 65535 entry logs. Guessing it's considerably less than that.
- The missing values appear to be a networking issue, likely a bug in the python code. May add a small delay in record gathering to see if that eases the issue.
To Do
- Write general parsing code for log formats
- Possibly test / configure "Hard open" or the "Stay open" command available in the software
- Work on server code for interacting with the door in general (db logging, connectivity with LDAP, etc)
- Determine which server to run python log reader on
Details
Reader
- Link to Hardware
- Runs a version of SMC Embedded according to nmap
- TCP Port 1868 is the listening / control port on the RFID reader
- To send an open door command, the communication looks like:
* Client - Connect to 1868 * Client - Send "\xA5<passwd in hex>" (For example, with a password of 123456, you would send "\xA5\x12\x34\x56") * Client - Send "\x09\xB1\x4F\x50\x44\x4F\x4F\x52\x00\xB8 * Server - Send "\x26" * Client - Send "\x00" * Client - Send "\x00" * There's some further handshake communication afterwards, but it's not required to signal the relay to open.
- To send a "perpetual" open door command, the communication looks like:
* Client - Connect to 1868 * Client - Send "\xA5<passwd in hex>" (For example, with a password of 123456, you would send "\xA5\x12\x34\x56") * Client - Send "\x09\xB1\x4F\x50\x44\x4F\x4F\x52\x00\xB8 * Server - Send "\x26" * Client - Send "\xf0" * Client - Send "\xf0" * There's some further handshake communication afterwards, but it's not required to signal the relay to open. * To re-lock the door after a "perpetual" open, send the open once sequence
- The reader does not automatically send the RFID tag data elsewhere so it must be polled to get access information. Initial decodes below...
- "GetInfo" - the interaction between the software and the door reader for log reading:
* Client - Connect to TCP 1868 * Client - Send "\xA<passwd in hex>" * Client - Send "\x09\xA7\x47\x45\x54\x49\x4E\x46\x00\xB0" * Server - Send "\x26" * Client - Send "\x00" * Client - Send "\x00" * Server - Send "\x26\x06\x06" * Client - Send "\x26" * Server - Send "\x00" * Server - Send "\xNN\xNN" where NN is the number of Entry records in hex for the logs to be sent - *DATE RECORDS DO NOT COUNT TOWARDS THIS* - Discovered some instances where this assumption is not correct. Debugging... * Client - Send "\x26" * Client - Send "\xFA" * Server - Send "\xFA\xFA\xFA\xYY\xMM" * Server - Send "\xDD\xNN" where YY is 2 digit Year, MM is 2 digit Month, DD is 2 digit Day, all in Hex. NN is an unknown byte value. This will be referred to as the Date record. * Client - Send "\x26" * Client - Send "\xFA" * Server - Send "\xV1\xV2\xV3\xV4\xHH" * Server - Send "\xMM\xNN" where V1-V4 are the RFID bytes in Hex, HH is the Hour in hex, MM is the minute value in hex, and NN is an unknown byte value. This will be referred to as a valid Entry record. Have discovered instances where the \xHH byte is missing or relocated. Possibly a short rfid tag. * What happens next depends on several factors as follows... * If there are no more records, the client sends "\x26" and the connection is finished. * If there are more Entry records on the same day they continue the sequence mentioned until there are no more Entries or the Entries occur on a new date. * If the entries occur on a new date, you see a new Date record, then at least one Entry record, then it continues as described above. * Manual "Soft Open" commands are represented by the Server sending * Server - Send "\x90\xF2\xF2\xF2\xHH" * Server - Send "\xMM\xNN" where the \x90\xF2\xF2\xF2 is a literal string, and HH, MM, and NN are as described above. * Unidentified keys are recorded as * Server - Send "\x30\xV1\xV2\xV3\xHH" * Server - Send "\xMM\xNN" where V1-4, HH, MM, and NN are described as above.
Special cases
- No records logged - the exchange looks like the following:
* Client - Connect to TCP 1868 * Client - Send "\xA<passwd in hex>" * Client - Send "\x09\xA7\x47\x45\x54\x49\x4E\x46\x00\xB0" * Server - Send "\x26" * Client - Send "\x00" * Client - Send "\x00" * Server - Send "\x26\x06\x06" * Client - Send "\x26" * Server - Send "\x00" * Server - Send "\x00\x00" (ie, No records) * Client - Send "\x26"
Log Decode Notes
- Date records do not count towards the record counting. This is important to keep track of whether to end the connection or ask for another (ie, the \xFA)
- During custom code testing, we determined that if you fail to acknowledge the receipt of the data, it is stored on the log reader until the next read. It's unknown at this point if it stores all the data since the last successful read or just the failed records (ie, if a couple records are properly ack'd but then parsing fails before final acknowledgment.)
Native control software
- Called "Batman!"
- Looks to be an app running on top of Visual FoxPro
- That somehow does TCP communication (rather impressive in itself...) But - it uses TCP packets for strict communication rather than relying on stream format. ie, the client will send data in 3 separate packets...
- As imagined, it's flat out awful
- If run on Windows 7 (and likely Vista), it requires "Run as Administrator"
PCAPs
In these caps, 192.168.1.68 is the door reader itself, 192.168.1.100 is a Windows XP VM running the "Batman" software, and 192.168.1.200 is a Mac OSX Snow Leopard system running the Python script.
- Other pcaps are available to members on an as needed basis due to sensitive information contained within. Please contact Mike Metzger if you'd like to see these / work with them.
Python scripts
These are really raw right now, just used as proof of concept...
- Open door
import binascii from socket import * HOST = '192.168.1.68' # IP of the reader PORT = 1868 ADDR = (HOST, PORT) c = socket(AF_INET, SOCK_STREAM) c.connect((ADDR)) s1 = "A5888888" # Passwd string - A5 and the 6 digit password s2 = "09B14F50444F4F5200B8" # Command to "open door" - activates relay s3 = "00" # Appears to be an acknowledgement string # Convert to binary / hex (yes, there are a ton of ways to do this) b1 = binascii.a2b_hex(s1) b2 = binascii.a2b_hex(s2) b3 = binascii.a2b_hex(s3) c.send(b1) # Send passwd string c.send(b2) # send open door command c.recv(1) # Wait for acknowledgement (\x26) c.send(b3) # Send \x00 c.send(b3) # Send \x00 c.close()
- "Perpetual" Open door
import binascii from socket import * HOST = '192.168.1.68' # IP of the reader PORT = 1868 ADDR = (HOST, PORT) c = socket(AF_INET, SOCK_STREAM) c.connect((ADDR)) s1 = "A5888888" # Passwd string - A5 and the 6 digit password s2 = "09B14F50444F4F5200B8" # Command to "open door" - activates relay s3 = "f0" # Appears to be an acknowledgement string # Convert to binary / hex (yes, there are a ton of ways to do this) b1 = binascii.a2b_hex(s1) b2 = binascii.a2b_hex(s2) b3 = binascii.a2b_hex(s3) c.send(b1) # Send passwd string c.send(b2) # send open door command c.recv(1) # Wait for acknowledgement (\x26) c.send(b3) # Send \xf0 c.send(b3) # Send \xf0 c.close()
- Debug proof of concept log reader
import binascii
from socket import *
import sys
HOST = '192.168.0.68' # IP of the reader
PORT = 1868
ADDR = (HOST, PORT)
passwd = binascii.a2b_hex("A5888888") # Password string - A5 and the 6 digit password
getinfo = binascii.a2b_hex("09A7474554494E4600B0") # GETINF command
ack = binascii.a2b_hex("26") # Seems to be an acknowledgment byte
nulbyte = binascii.a2b_hex("00") # Null byte value
getnext = binascii.a2b_hex("FA") # Appears to be a "get next" command
sendinginfo = binascii.a2b_hex("260606") # Used at the start of the log send by the server / door reader
c = socket(AF_INET, SOCK_STREAM)
c.connect(ADDR)
c.send(passwd)
c.send(getinfo)
c.recv(1) # Should be equal to ack
c.send(nulbyte)
c.send(nulbyte)
c.recv(3) # Should be equal to getnext
c.send(ack)
c.recv(1) # Should be equal to nulbyte
recordcountstr = binascii.b2a_hex(c.recv(2)) # Get the record count, convert back to hex
print recordcountstr
recordcount = int(recordcountstr[:2], 16) # Get one byte, convert to decimal, maintain recordcount
print recordcount
if recordcount == 0:
print "No records available to download"
c.send(ack)
c.close()
sys.exit()
c.send(ack)
keepgoing = recordcount
datestr = ""
while (keepgoing):
c.send(getnext)
s1 = binascii.b2a_hex(c.recv(5))
s2 = binascii.b2a_hex(c.recv(2))
print s1
print s2
if s1[:6] == "fafafa": # Record type check - Date record
print "New Date record"
datestr += str(int(s1[6:8], 16))
datestr += "-" + str(int(s1[8:10], 16))
datestr += "-" + str(int(s2[:2], 16))
print "Date string: ", datestr
if s1[:2] == "00": # Record type check - Entry record
print "New Entry record"
rfid = str(int(s1[2:8], 16))
timestr = str(int(s1[8:10], 16)) + ":" + str(int(s2[:2], 16))
print "ID: " + rfid + " entered at " + timestr
keepgoing = keepgoing - 1
if s1[:2] == "90": # Record type check - Soft open
print "New Soft open record"
timestr = str(int(s1[8:10], 16)) + ":" + str(int(s2[:2], 16))
print "Soft open at " + timestr
keepgoing = keepgoing - 1
if s1[:2] == "30": # Record type check - Unknown key record
print "New Unknown Entry Attempt record"
rfid = str(int(s1[2:8], 16))
timestr = str(int(s1[8:10], 16)) + ":" + str(int(s2[:2], 16))
print "Unknown ID: " + rfid + " attempted entry at " + timestr
keepgoing = keepgoing - 1
c.send(ack)
c.close()
Notes
- If you pick one of these up, do not connect it to any kind of public network. Given that the password is 6 digits of 0-9 with no encryption / challenge-response / etc, it would be pretty easy to brute force through the 1 million possible combinations and get the latch to release.
- To put it in perspective, RFID can also be spoofed, but has a 32-40 bit id, giving you between 4.2 and about 16.8 billion possible combinations (of course, this is part of the reason people are concerned about RFID for tracking purposes.)