Friday, January 26, 2018

Encryption for MiFare Classic 1k cards

Today I want to talk about analyzing an RFID system for use in IoT projects. I recently submitted a talk to BSides Tampa 2018 which covers the research I have done on this topic.
I started a project about a year ago, which required RFID tags that were resistant to reading and modification(tag cloning was considered, but that is a story for another day.). Clearly what I needed was a card that supported encryption. A quick search through Amazon showed the majority of options supported DES or AES encryption. However the cheapest and most plentiful option were Mifare Classic 1k tags, which do not support either. Instead these types of cards use a proprietary encryption format which has long been broken and abandoned called CRYPTO1. When dealing with small scale prototypes and personal projects, I try to keep the budget to a minimum without sacrificing functionality. So I began to look at implementing AES-128 inside an Open Source, Python-Based, Mifare Classic reading/writing library in the hopes that others may be able to more easily apply more secure crypto while developing their own projects. The Mifare Classic 1k RFID tag is a small, low-energy, passive, RFID tag. It gets it's power inductively from the reader/writer while it is inside the effective range (usually a few centimeters). It contains a small on-board IC which handles the anti-collision, authentication handshake, and encryption on the tag side of the interaction. The tags also have 1k of storage memory. The memory is divided into 16 blocks (0-15). Each block is then divided into 4 rows (0-3) of 16 bytes each. The last row of each block is reserved for the Access Control Logic and the two crypto keys associated with each block. Finally, the 0th row of the 0th block is reserved for the manufacturer's data which includes the tag's UID). This means the actual usable amount of space is closer to 752 bytes
(15*(3*16)+(2*16) = 752)

You can squeeze a few more bytes out if you elect not to use the CRYPTO1 cipher. In this scenario, the specification says you may use half the bytes reserved for the second crypto key (Key B) for user-defined data. The first crypto key can be set, but never read so those bytes are lost for good. Furthermore, by messing with the crypto keys and ACL, it is possible to permanently lock a Mifare tag so that it may never be read or altered again. I decided to stick with the 752 byte limit to avoid accidentally locking a card.

Enhancing Data Privacy

Thinking about how AES-CBC and Base64 encoding work for a moment, we can calculate the length of the output message as dependent on the padded messages length. For example, A message that is between 240 bytes and 255 bytes will be padded up to 256 bytes. Why 240 bytes, you ask? Well, in the case where the the message length is an exact multiple of 16 an additional Null byte is added to meet AES-CBC's requirement for at least some padding to be applied. A 256 byte input text will produce a 364 byte, base64 encoded, cipher text. 543 bytes = 748 bytes after encryption and encoding (4bytes left over) and finally, 544 bytes = 768 bytes after encryption and encoding (to big). In fact there is an easy way to calculate the length of the resulting Encoded Cipher Text in two steps:
CipherLen = PlainTextLen + BlockLen - (PlainTextLen mod BlockLen) + IVLen
EncodedLen = (CipherLen + 2 - ((CipherLen + 2) mod 3)) / 3 * 4
Since AES-128 CBC uses 16 Bytes for both the block length and the IV length, the only variable in the first equation is the PlainText Length (in bytes). plugging the result into the second equation will yield the number of bytes for the expected output. CipherLen = 543 + 16 - (543 mod 16) + 16 = 560
EncodedLen = (560 + 2 - ((560 + 2) mod 3)) / 3 * 4 = 748

CipherLen = 544 + 16- (544 mod 16) + 16 = 576
EncodedLen = (576 + 2 - ((576+ 2) mod 3)) / 3 * 4 = 768

Analyzing the Data Object I wanted to encode was the next step. For this example I will use a hypothetical data structure: {
"sku":"000000000000",
"cid":"xxxxxxxxxxxxxx",
"ocode":"aaaaaaaaaaaa",
"dcode":"xxxxxxxxxx",
"wght":0.0,
"checker_id":"xxxxxxxxxx"
}


Once this object is serialized with pickle the total length is 228 bytes. That easily fits inside the 543 byte limit. In this case there is another bit of security we can add. By appending random bytes to fill up the rest of the plain text space we can help to obfuscate the length of the data a bit. I call this garbage the message mutex. The length of the mutex can be determined as 543-len(plain_text). After the mutex is appended to the plain-text that, plus a Data Encryption Key, is sent to the Encryption algorithm. After encryption, the length is 748 bytes as expected. This leaves us 4 Bytes in Sector 16 Block 2 (block_address 62) that we can use for whatever. In the next section I will cover what I decided to use them for.

Ensuring Data Integrity

Encryption is fine for privacy, but it does nothing for ensuring the integrity of the data. For that we need to hash the cipher text with a Keyed-Hashed Message Authentication Code (keyed-HMAC for short). We could use the same key we used to encrypt the data, but that is a bad practice. It is better to generate a new random key associated with the tag, and store it (encrypted with a third system-wide key called the Master Key) at some remotely-held database.The hashing algorithm takes the hashing key and appends it to the end of the data string. It then takes the SHA-256 hash of the newly created blob. This hash is stored alongside the account details associated with the tag and used for verification on future reads.
Now, what about those last 4 bytes in block address 62? Well, taking the first 4 bytes of the keyed-HMAC and adding them to the end of the tag allows an option to quickly check that the data has 'probably not changed' before doing an encrypted search to guarantee the data hash matches. In the database, I create an indexed field which contains a keyed-HMAC of the tag ID with the key being the base64 encoding of the 4 bytes. Verifying the integrity is a simple process:
  1. Read the entire tag into an array
  2. Slice off the last 4 bytes into a separate array. Create a second copy of this array to convert to a string (I use both forms in the rest of the steps)
  3. Convert the rest of the tag array and one of the 4 byte key arrays back to their string forms (base64 encoded strings).
  4. Take the keyed-HMAC of the tag's UID using the 4 byte key string. Try to use this to look up the tags details in the remote database. If a result is found the last 4 bytes match the tag UID as expected. If no result is found the tag UID or the 4 byte key must not match what we expected and we can exit out of the transaction (or permanently kill the card)
  5. Assuming card details are returned, one of the fields will contain the Hash key used during the previous tag write. This is used to take the keyed-HMAC of the tag data and verify it matches the one stored in the result provided by the DB

At this point we have authenticated the data on the tag has not been changed between reads. We can also be reasonably confident that the data couldn't be deciphered, even if the card had been read. This of course relies on the strength of the passwords used to encrypt the data. If you opt to encrypt your super-secret RFID cards with a weak password this method will only slow down the attackers effort to decrypt the information through brute-forcing the key. Next Let me discuss how we can actually get this data onto the tag so that I do not accidentally overwrite any block trailers.

Writing the tag

The next step was to take the combination of the cipher text, message mutex and 4 byte hash key (called the blob) and prepare it for writing out. The first step is to convert it to an array of bytes. Since all the the steps Encryption algorithm return base64 encoded strings, this is simple in Python:
def chunk_data(self, lst, chnk_sz):
 out = []
 for i in range(0, len(lst), chnk_sz):
  out.append(lst[i:i + chnk_sz])
 return out

data_buff = chunk_data(blob.encode("hex"), 2) # Returns array of hex bytes like ["de","ad","be","ef"] 

 
Finally, it's to write the buffer to the card. I wish it were as simple as writing the bytes out and being done with it. Unfortunately do to the memory construction discussed earlier, we have to dance around the memory space a bit. I am going to use block addresses to refer to the memory layout for this portion. The list of block addresses we want to skip is [0,3,7,11,15,...,63] For me it is easier to think of it as "If N mod(4) = 3, then skip" For all N between 1-63 inclusive. I automatically skip block address 0 and then implement the above mathematical logic in code: if (block_addr % 4 == 3):
  # These are the sector trailers, we don't want to overwrite
  block_addr += 1
self.rewrite(block_addr, block)

Conclusion

All of this can be handled easily using the Cipher class I added to the pi-rc522s Library at https://github.com/dreilly369/pi-rc522. I added some code to assist with the encryption and decryption which can be seen in the load() and dump_decrypted() functions inside util.py. Finally I added an example program which will encrypt an object to a card, and then on a subsequent read it recovers and decrypts the object. https://github.com/dreilly369/pi-rc522/blob/master/examples/CryptoExample.py
If you are going to be out at BSides Tampa 2018, come see the full talk which will cover this all in more detail plus some additional details about the project.

No comments:

Post a Comment