SecureCard
SecureCard writeup or how i learned to stop redstone flux and love the radio frequency⌗
i started working a fulltime job 1.5 years ago and that more or less coincided with my ctf team merging into a mega team and ever since then i haven’t been playing ctfs a lot
i love ctfs because i get to learn stuff, do stuff with my friends, make new friends. this challenge has helped achieve all of those goals and i’m so happy to be back.
initial observations⌗
we’re given a trace file. i’ve already seen how to open those from the crapto challenge.
i tried all of the possible interpretations supported by pm3, and the card seemed to be a mifare desfire, as only that had annotations for everything in the trace. here’s what it looked like:
after figuring this out, i did some googling, and apparently mifare is owned by a company called nxp who more or less have a monopoly in the rfid space and famously keep a lot of information / data sheets confidential and proprietary and sue people and stuff
but luckily, there’s an open source, lgpl licensed implementation available at https://github.com/nfc-tools/libfreefare. we can use their code as reference.
so what’s happening in this trace?⌗
first i tried just interpreting this as ascii to see if there would be any flags / keys / anything interesting, like with the crapto challenge which basically already ended at this point; but we weren’t so lucky this time. “read data” seemed especially suspicious but it appeared to be encrypted.
as i knew nothing about rfid when i was doing this challenge, i wanted to understand the entire trace to figure out what’s wrong with the encryption, as google suggested that the desfire card was not “hackable” like the classic.
as it appeared to be the most unique/googleable annotation near the start, i started by searching “ANTICOLL-2” on google
then of course i purchased the international standard because paywalling international standards is a very good idea and it’s not like they should be public if we expect the public to follow them (but hey at least they’re findable on google unlike nxp docs loll)
but yes, the idea here is that there can be multiple cards near the reader at the same time, and the reader wants to be able to pick the one it wants to talk to, so there’s an anti collision sequence, documentation was not the best experience to read but 14443-3 describes this bit:
where we send a wake up signal to wake up the rfid tags (they need the reader for power and all), then ask all tags nearby to send us their id (0x93), the tag in question sends one that starts with 0x88 and we select that, receive a selection acknowledgement (sak) but the 0x88 means the full id’s gonna take at least one more round (up to two), so we ask for the second part of every tag’s id (0x95), the tag send the rest of its id this time not starting with a 0x88 so we stop after selecting it and receiving another sak
and the standard points to 14443-4 for what’s next, another 170 eur down the drain (totes), which basically only explains this bit in our trace:
where we aren’t, in fact, the rats, but it means Request Answer To Select, and we’re setting Frame Size for proximity coupling Device Integer to 1, which will set the Frame Size for proximity coupling Device to 24 bytes. we will receive a Answer to select in response, where everything seems to be non standard and we’re using it in combination with the sak to understand what tag it is and therefore what protocol to speak. documentation for this bit is available at https://www.nxp.com/docs/en/application-note/AN10833.pdf but we had already figured out that it would be desfire.
desfire time⌗
I noticed that in the rest of the trace, every request and its response started with either a 0a00 or a 0b00 which is the prologue field according to iso 14443-4, alternating bit 1 is the block number which may be used for sequencing and ended with a two byte crc and quickly started ignoring them
aside from those, pm3 annotations are already somewhat helpful in explaining what these desfire specific commands do - we select an application first, aa is Aes Authenticate, af is the “Auth Frame” or the next frame in the aes authentication, then we get a file’s settings and read what’s probably the same file’s data
here’s the proxmark annotations with some guesses and the sequence/crc bytes removed
Rdr | 5a 37 13 00
SELECT APPLICATION -> assuming 5a is the command, the argument seems to be little endian 0x1337 :)
Tag | 00
OKAY
Rdr | aa 00
AUTH AES (keyNo 0)
Tag | af a7 18 45 be 52 8a 7e 8e 08 16 3d 06 3d 95 42 aa
AUTH FRAME
Rdr | af 2c 2a bd a6 a1 f9 df f5 0b 87 37 6c 30 57 5b c3 0e 62 4f cd f6 6f 04 0a 3c a1 65 47 47 e2 81 47
AUTH FRAME
Tag | 00 60 f9 01 97 5a 30 25 78 5c 0d 43 70 8a de 38 b2
OKAY / AUTH FINISH
Rdr | f5 01
GET FILE SETTINGS -> assuming f5 is the command, maybe we're asking for file 01?
Tag | 00 00 03 00 00 19 00 00 18 6b 65 df 80 ba c2 87
FILE SETTINGS
Rdr | bd 01 00 00 00 00 00 00
READ DATA -> assuming bd is the command, we have a bunch of null bytes and one 01 which is likely the file, which we got the settings for
Tag | 00 4c be b5 2c 49 15 35 0e af b5 dc fc a9 52 d9 50 99 4c 12 a1 cf 07 09 82 33 99 57 b4 40 a1 0a 36
OKAY / FILE CONTENTS
now, we have to understand how the authentication works, how to parse the file settings, and figure out how we might decrypt the file contents even though we don’t have the aes key
so how the authentication works is: an aes key is pre-shared between the tag and the reader, they want to make sure they both have the correct key without sending it over, and establish a session key for use within this session.
how they go about it is, both sides create a 16 byte random, rnda for the reader and rndb for the tag, the tag encrypts its random, sends it over, the reader decrypts the random, rotates it left one byte, prepends its random, encrypts it, sends it over, the tag decrypts it, makes sure the rotated rndb matches, then rotates rnda, encrypts it, sends it over, the reader checks the rotated rnda, and the authentication is over
they then create a session key using both randoms that both sides now know, where it equals to a[:4] + b[:4] + a[12:16] + b[12:16]
it is worth noting: challenge description mentions something may not be set: what if this is the aes key?
i first decrypt the E(rndb) with aes key and iv both set to 16 null bytes, and get something random as expected,
then i decrypt E(rnda+rndb1) where i saw it was a very suspicious b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16'
. turns out this is a totally-not-random random set by proxmark (guess a tag could know it was talking to a proxmark this way? hmm), as set in https://github.com/RfidResearchGroup/proxmark3/blob/d41c025f5cc7d539c4afc943784b8ec911d272d2/client/src/mifare/desfirecore.c#L1102
here’s python code that does the authentication sequence and prints out the session key:
from Crypto.Cipher import AES
key = b'\x00'*16
iv = b'\x00'*16
encrypt = AES.new(key, AES.MODE_CBC, iv=iv)
decrypt = AES.new(key, AES.MODE_CBC, iv=iv)
b = b'Nq\xb5\x0c\xf7Z\xde\xe4\xda;\x11=\x0e\x81k8'
card_encrypted_b = encrypt.encrypt(b)
assert card_encrypted_b == bytes.fromhex('a7 18 45 be 52 8a 7e 8e 08 16 3d 06 3d 95 42 aa')
assert b == decrypt.decrypt(card_encrypted_b)
a = b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16'
reader_encrypted_a_plus_b1 = encrypt.encrypt(a + b[1:]+ bytes([b[0]]))
assert reader_encrypted_a_plus_b1 == bytes.fromhex('2c 2a bd a6 a1 f9 df f5 0b 87 37 6c 30 57 5b c3 0e 62 4f cd f6 6f 04 0a 3c a1 65 47 47 e2 81 47')
a_plus_b1 = decrypt.decrypt(reader_encrypted_a_plus_b1)
b1 = a_plus_b1[16:]
assert b == bytes([b1[-1]])+b1[:-1]
card_encrypted_a1 = encrypt.encrypt(a[1:] + bytes([a[0]]))
assert card_encrypted_a1 == bytes.fromhex('60 f9 01 97 5a 30 25 78 5c 0d 43 70 8a de 38 b2')
a1 = decrypt.decrypt(card_encrypted_a1)
assert a == bytes([a1[-1]])+a1[:-1]
session_key = a[:4] + b[:4] + a[12:16] + b[12:16]
print("session key:",session_key.hex())
at this point i thought i had solved the challenge and tried to decrypt the file at the end, but came across a very interesting issue:
>>> session = AES.new(session_key, AES.MODE_CBC, iv=b'\x00'*16)
>>> maybe_flag = session.decrypt(bytes.fromhex('4c be b5 2c 49 15 35 0e af b5 dc fc a9 52 d9 50 99 4c 12 a1 cf 07 09 82 33 99 57 b4 40 a1 0a 36'))
>>> print(maybe_flag)
b'\t1\x99L;\x0cz\x868x^\x91\xfa\xfb*\xf8pl1c4t3d}V\xb7\xd5S\x80\x00\x00'
that very much appears to be the second part of the flag.
teammate already got very close to guessing the right flag, but we weren’t so lucky
of course, this seems like it’s the first block being wrong but the second block being right, which probably means we have the correct key but the wrong iv.
in the authentication code, one of the last things libfreefare does is cmac_generate_subkeys(MIFARE_DESFIRE(tag)->session_key);
- could this have anything to do with what’s happening?
turns out, the answer is yes, and libfreefare calls mifare_cryto_preprocess_data
for every command before sending it. what this does is, if authenticated with aes it will cmac (weird mac function that uses aes no one else seems to use) every request even if the signature is not being appended. the iv is reset after authentication, but never again, so we will have to cmac all of the requests and responses until the flag is sent.
i tried to implement cmac myself, was actually messing up what i was putting in cmac, but decided to use libfreefare directly in order to be sure.
one thing that cost me several hours was this: apparently the requests (reader->tag) are cmac’ed as they are, but the responses (tag->reader) have their status code, which are actually sent first, put to the end when cmac’ing.
with the very cursed (printf %s and xxd to read memory? lmao) code above, i manually cmac’ed each request and response, setting the iv to the last one each time, until i cmac’ed the last command which was the READ DATA
, and used that cmac as the iv for aes decryption of the flag:
>>> session = AES.new(session_key, AES.MODE_CBC, iv=bytes.fromhex('6d52 ed2a 407e 1cb7 5c27 6fa4 a598 1a95'))
>>> flag = session.decrypt(bytes.fromhex('4c be b5 2c 49 15 35 0e af b5 dc fc a9 52 d9 50 99 4c 12 a1 cf 07 09 82 33 99 57 b4 40 a1 0a 36'))
>>> print(flag)
b'dctf{rf1d_15_c0mpl1c4t3d}V\xb7\xd5S\x80\x00\x00'