RFID - Followup on Vingcard
Few times ago I have published an article about two RFID locks that I encountered while traveling and a rough blackbox analysis of these two technologies. Unfortunately, back then, I only had few samples of key cards regarding Vingcard’s locks and that led me to take false assumptions.
But I was lucky enough very recently as to meet this lock once more. And because it was a three weeks stay, it was pretty easy to purposely tell the reception that my card was not working anymore, a couple of times, in order to have them reprogram it (yay, I’m a bad guy!). The purpose here was, first, to check what values can change over time (they usually encode the duration of the stay instead of the checkout timestamp) and secondly, to ensure that there is not a kind of timestamp-dependant key.
Just as a quick recap, last time I unveiled that Vingcard’s locks rely on Mifare Ultralight which is a simple 64-byte memory storage without any access control or cryptography embedded. Part of the memory was simply XORed with a 1 byte key that I supposed to be derived from at least the serial of the tag. At the end of the XORed block, we found a 4-byte block that might be a checksum of the protected zone but unfortunately, I wasn’t able to find the algorithm nor the data involved in its computation.
Right now, my sample collection is worth 24 cards, covering 3 hotels around the world with at least 2 rooms in each of them and different stays going from 1 day up to 3 weeks.
First thing you want to do when you are at least as lazy as me, is to write some scripts that will help you narrowing your analysis. The first script I wrote replaced the protected block with its unXORed version so I only get cleartext data.
Then, my second step was to write another Python script that will take a bunch of files as input and will output a sort of byte mask to unveil which bytes remain constant through the given samples. I also added an option to print this mask with a given amount of bytes per line. This may ease the analysis as Mifare Ultralight works on 4-bytes blocks.
Here is the output when this script is ran against the full sample set I have (a question mark means that the value is changing and this script does the comparison against nibbles):
04 ?? ?? ??
?? ?? 2? 8?
?? ?? 00 00
?? 0? ?? ??
06 0A 00 21
00 00 00 00
00 00 00 90
?1 ?? ?? ?2
?? ?? ?? ??
0? 00 ?? ??
?? ?? ?? ?0
FF 00 ?? ??
?? ?? 00 00
00 00 00 00
00 00 00 00
?? ?? ?? ??
As expected the last four bytes are varying (it is supposed to be a checksum) and first 16 bytes are also varying (those are read-only zone that are set by the manufacturer at production time). The first byte is not varying but it is expected as it represents the chip manufacturer (here, 0x04 stands for NXP semiconductors).
In such kind of locks, apart from your room number (which might be encoded as a floor number and a room number) and the duration of your stay, you can expect to find a hotel identification code or building code. This is to prevent a key from one building to open another door in another building for very big hotels. In order to determine where this code is stored on those tags, we run again the script to compute the mask. But we only run it against the 16 tags I have dumped on the last hotel I was staying at. As they cover a bunch of rooms for different duration, the mask might reveal easily the hotel id:
04 ?? ?? ??
?? ?? 2? 8?
?? ?? 00 00
?? 0? ?? ??
06 0A 00 21
00 00 00 00
00 00 00 90
E1 A5 71 ?2
?? ?? ?? ??
00 00 ?? ??
?? ?? ?? 00
FF 00 00 3F
00 00 00 00
00 00 00 00
00 00 00 00
?2 ?? ?2 ??
Bingo! Indeed, if you compare carefully the last two computed masks, there are 3 bytes that are now constant (offset 0x1C): 0xE1A571. That should be the identification code for the hotel/building.
As we obviously have two sections in that dump (header/footer and then the XORed block), we will first leverage our knowledge to guess the structure of the first section (header + footer).
First, let’s go back to a simple bin-diffing process like we did on the first post but against the card that I got reprogrammed (same hotel, same room but duration of the stay should lessen).
Here is a capture of vbindiff against two unXORed dumps:
Interestingly, only 3 bytes changed within the protected section but there is absolutely no difference in the header/footer part! Regarding the last four bytes that were supposed to be a checksum, it is either computed only against the header or it is not a checksum! As we already took the assumption that offset 0x20 encodes the room ID, the byte at offset 0x27 might be part of duration of the stay.
Let’s verify the second assumption about the XOR key by running vbindiff against the raw dumps:
Damned! The XOR key is different too despite the header/footer being exactly the same. So it can’t be derived from it. My guess is now that it is just a sort of random key, computed at programming time, probably indicated at offset 0x1A, even though it might be bruteforced by the reader. But such attack would imply delay in accepting the key card so it doubt a bruteforce attack is involved here.
With those new findings, there does not seem to have any dependency between the two sections. We can then analyze them separately.
As I haven’t found a more convenient way to do it, I’m using scapy to display the file structure I have guessed so far:
In the above picture, you can see that the protected_data is still encrypted. But if I ask scapy to create a graph out of that specific field, everything is automatically decrypted:
The Raw payload that it still present is just the remaining constant bytes of the protected_data. As we previously saw with the mask generated earlier, those bytes remain constant across the whole sample set, so I didn’t bother dissecting them with scapy.
Please keep in mind that the field names above are still guesses and I can be wrong. Specially, I don’t know the exact field length of hotel_id, key_id, duration and room_id.
This is all for this post because I won’t have enough time right now to refine the cross-analysis over the 24 dumps I have. Should any of my readers come with better ideas or proofs that some of my guesses are wrong don’t hesitate to let a comment or to send me an email. If you have dumps that you can share, please throw me an email too :)
For those who are interested, you can find the scapy layer I wrote to dissect the dumps on my usual Bitbucket repository. Here is a sample of how to use it (you must have the Vingcard.py file in your current working directory before launching Python):
$ python
>>> from Vingcard import *
WARNING: No route found for IPv6 destination :: (no default route?)
>>> f=open("dump.hex", "rb")
>>> p=Vingcard(f.read())
>>> f.close()
>>> p.show()
One thing I didn’t mentioned in my previous article about Vingcard’s technology is that, in a full deployment, the individual locks communicate with a Zigbee gateway on each floor and those gateways are connected back to the reception, giving them a real-time view of what’s happening. Unfortunately, I doubt that hotels are going to buy the full package or that the reception will do something if the console shows something weird is happening.
Even though I am pretty sure that Zigbee antennas were deployed on the last hotel I was staying at (there was white dome antennas every 10 meters on every floors), it was abroad so I hadn’t brought my Api-mote board to play around with that signal. But if someone starts to analyze this signal, please, again, leave a comment below or send me an email!