Reverse Engineering RFID Reader

From Dallas Makerspace
Jump to: navigation, search

Model

http://www.kawamall.com/pd_3x_lckombopu.cfm

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 (2014-04-20)

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

General Notes

The protocol seems to include some fixed length strings (padded with zero's). All the commands seem to be six characters (OPDOOR, GETINF, ADDLST, DELLST so far). Packets seem to have a rolling XOR checksum in many cases. In some cases, the packet seems to start with a length prefix, which is not included in the checksum.

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 (NOTE: \x4F thru \x52 is ASCII for "OPDOOR", so that little section including the \x00 looks like a null terminated string)
  * 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 (NOTE: as above, "OPDOOR" is in there)
  * 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" (NOTE: like above, \x47 thru \x46 is "GETINF")
 * 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" (NOTE: \x47 thru \x46 is "GETINF")
 * 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 ("OPDOOR" in there)
  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 ("OPDOOR" in there)
  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.)

Current Scripts

Get Log

 import binascii
 from socket import *
 import sys
 
 logfile = open('door.log', 'a')
 
 HOST = 'xxx.xxx.xxx.xxx' # IP of the reader
 PORT = 1868
 ADDR = (HOST, PORT)
 
 passwd = binascii.a2b_hex("A5xxxxxx") # 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
 recordcount = int(recordcountstr, 16)                   # 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))
         if not s1:
                 print "s1 is blank"
                 break
         elif len(s1) < 8:                                                       # get bytes again if length is incorrect
                 logfile.write("Error - Too Few Bytes In S1: " + s1 + "\n")      # add mystery bytes to the log
                 s1 = binascii.b2a_hex(c.recv(5))                                # not sure why it returns a single byte of different information
         if not s1:
                 print "s1 is blank on 2nd attempt"
                 break
 
         s2 = binascii.b2a_hex(c.recv(2))
         if not s2:
                 print "s2 is blank"
                 break
 
         print s1
         print s2
 
         if not s1[8:10]:                                        # sometimes hours is blank
                 hours = "00"
         else:
                 hours =  str(int(s1[8:10], 16)).zfill(2)
 
         minutes = str(int(s2[:2], 16)).zfill(2)
         timestr = hours + ":" + minutes
 
         if s1[:6] == "fafafa":                                  # Record type check - Date record
                 logfile.write("New Date record\n")
                 datestr = str(int(s1[6:8], 16)).zfill(2)                # year
                 datestr += "-" + str(int(s1[8:10], 16)).zfill(2)        # month
                 datestr += "-" + str(int(s2[:2], 16)).zfill(2)          # day
 
                 logfile.write("Date string: " + datestr + "\n")
 
         if s1[:2] == "00":                                              # Record type check - Entry record
                 logfile.write("New Entry record\n")
                 rfid = str(int(s1[2:8], 16))
 
                 logfile.write("ID: " + rfid + " entered at " + timestr + "\n")
                 keepgoing = keepgoing - 1
 
         if s1[:2] == "90":                                              # Record type check - Soft open
                 logfile.write("New Soft open record\n")
                 logfile.write("Soft open at " + timestr + "\n")
 
                 keepgoing = keepgoing - 1
 
         if s1[:2] == "30":                                              # Record type check - Unknown key record
                 logfile.write("New Unknown Entry Attempt record\n")
                 rfid = str(int(s1[2:8], 16))
                 logfile.write("Unknown ID: " + rfid + " attempted entry at " + timestr + "\n")
 
                 keepgoing = keepgoing - 1
 
         c.send(ack)
 
 c.close()
 logfile.close()

Add

 import sys
 print sys.argv
 import binascii
 from socket import *
 
 HOST = 'xxx.xxx.xxx.xxx' # IP of the reader
 PORT = 1868
 ADDR = (HOST, PORT)
 
 c = socket(AF_INET, SOCK_STREAM)
 c.connect((ADDR))
 
 #ask user for input
 rawID = raw_input("Enter ID (without leading zeros): ")
 formattedID = str(("{0:x}".format(int(rawID))).zfill(8))
 
 #print formattedID
 #print (formattedID == "00434B23")
 
 if len(formattedID) > 8:
     print("Error: Too many digits in ID")
     sys.exit(0)
 
 s1 = "A5xxxxxx" # Passwd string - A5 and the 6 digit password
 
 s2 = "09" + "A84144444C535400" + "A2" # Command to add an user, (4144444C5354 is ASCII for "ADDLST")
 #ends with checksum A2
 
 s3 = "55" + formattedID + "E0E0" + "888888" + "3232323232323232323232" + "3131313131313131" + "E3FC" + "00" # Hex value of key
 #Description of each part of the string:
 #1. "55" U (for User)
 #2. the user's ID
 #3. which door to grant access, E0EE is just door 1, E0E0 is both doors
 #4. "888888" ???
 #5. 11 character max field showing user's "code", possibly for employee ID
 #6. 8 character max field for user's name
 #7. "E3FC" ???
 #This key usually ends with a checksum value which is generated later in this code
 
 
 s4 = "00" # Appears to be an acknowledgement string
 
 # function to change final hex value to correct checksum
 def addChecksum( hexStr ):
     xor = 0
     for i in range(0, len(hexStr)-1):
         xor = xor ^ ord(hexStr[i])
     return hexStr[0:-1] + chr(xor)
 
 # Convert to binary / hex (yes, there are a ton of ways to do this)
 b1 = binascii.a2b_hex(s1)
 #b2 = "\t" + addChecksum(binascii.a2b_hex(s2))
 b2 = binascii.a2b_hex(s2)
 b3 = addChecksum(binascii.a2b_hex(s3))
 b4 = binascii.a2b_hex(s4)
 
 print (binascii.hexlify(b1))
 print (binascii.hexlify(b2))
 print (binascii.hexlify(b3)) # debug
 print (binascii.hexlify(b4))
 
 c.settimeout(10)
 c.send(b1) # Send passwd string
 c.send(b2) # send add user command
 c.recv(1) # Wait for acknowledgement (\x26)
 c.send(b3) # Send key
 c.send(b4) # Send acknowledgement
 while True:
         x = c.recv(1)
         if len(x) == 0:
                 break
 c.close()

Remove

import binascii
from socket import *
import sys
 
def removeUser(ADDR, formattedID):
	c = socket(AF_INET, SOCK_STREAM)
	c.connect((ADDR))
 
	s1 = "A5XXXXXX" # Passwd string - A5 and the 6 digit password
 
	s2 = "09" + "A944454C4C535400" + "AF" # Command to remove an user 
	# "44454C4C5354" means DELLST (like delete list)
	#s2 = "09A8" + "4144444C5354" + "00A2" # 00A2 may specify the door #
	#erase 2nd user 09:a9:44:45:4c:4c:53:54:00:af
	#erase 3rd user 09:a9:44:45:4c:4c:53:54:00:af
 
	s3 = formattedID # Hex value of ID
	#s3 = "005E4440" # different id example
 
	#function to generate the checksum of s3
	def addChecksum( hexStr ):
		xor = 0
		for i in range(0, len(hexStr)):
			xor = xor ^ ord(hexStr[i])
		return chr(xor)
 
	#s4 = "4C" # Appears to be an acknowledgement string
	#erase 2nd user 00:45:69:b6 - 9A
	#erase 3rd user 00:5e:44:40 - 5A
 
	# 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)
	b4 = addChecksum(binascii.a2b_hex(s3)) # assign checksum to variable
 
	c.settimeout(10)
	c.send(b1) # Send passwd string
	c.send(b2) # send open door command
	c.recv(1) # Wait for acknowledgement (\x26)
	c.send(b3) # Send key
	c.send(b4) # Send checksum
	response = ""
	while True:
		x = c.recv(1)
		if len(x) == 0:
			break
		else:
			if (x == "5"):
				response = "Response Received - User Not Registered"
	c.close()
	return response
 
HOST = '10.0.0.XXX' # IP of the reader
PORT = XXXX
ADDR = (HOST, PORT)
 
#ask user for input
#rawID = raw_input("Enter ID (without leading zeros): ")
rawID = sys.argv[1]
formattedID = str(("{0:x}".format(int(rawID))).zfill(8))
 
if len(formattedID) > 8:
	print("Error: Too many digits in ID")
	sys.exit(0)
 
# try to terminate the user up to 5 times
userRemoved = 0
for x in range(0,4):
	result = removeUser(ADDR, formattedID)
	if result == "Response Received - User Not Registered":
		userRemoved = 1
		break
 
if userRemoved == 1:
	print("User Removed Successfully")
else:
	print("Failed To Remove User")