NFC Card Emulation

NFC and RFID are widespread technologies commonly used in services like contactless payments, item scanning, and security keycards. Over the years many technologies using NFC have been reverse engineered or hacked, resulting in tools like the proxmark or flipper zero. NXP, one of the big keycard manufacturers, has had multiple generations of their Mifare security cards turned inside out. In this article, we’ll go over how keycard emulation is performed with a PN532.

Special thanks to the RFIDIOt repo and Salvador Mendoza!

Here’s the repo:https://github.com/m5kro/acr122u-emulation/

Connection

Let’s start with how the computer and PN532 communicate. There are multiple ways for this to happen but the 2 common ones are through a separate reader chip or the i2c bus. In this example, I’ll be using an acr122u reader/writer that works over USB but if your device supports i2c (like a Raspberry Pi) you can buy the standalone module on Amazon. The entire chain of communication will look something like this:

Computer → Controller → PN532 → Target Reader

and the opposite direction for responses.

Commands and information will be sent from the computer in individual bytes. This sounds complicated at first but you’re using bytes as commands (APDU), not converting commands into bytes, if that makes any sense. So it only takes about 2 to 3 bytes per command, making it much easier to work with.

There’s a great Python library called Pyscard that will help us establish a link with the controller using PCSC. Do note that this will not work on Windows as the Windows drivers require a card to be detected by the reader before anything can happen. For Linux users please install PCSC and blacklist the pn533 and pn533_usb drivers before continuing.

To start a connection, we’ll import the library and use the following code to detect and select the acr122u:

# Import pyscard
from smartcard.System import readers
from smartcard.CardConnection import CardConnection
from smartcard.scard import SCARD_SHARE_DIRECT
def main():
    # Get the list of available readers
    reader_list = readers()
    if not reader_list:
        print("No readers found")
        return
    # Use the first available reader
    reader = reader_list[0]
    print(f"Using reader: {reader}")
    # Create a connection to the reader
    connection = reader.createConnection()
    try:
        # Connect in direct mode with raw protocol
        connection.connect(protocol=CardConnection.RAW_protocol, mode=SCARD_SHARE_DIRECT)
        print("Connected to reader")
    except Exception as e:
        print(f"Error: {e}")
main()

Controller Commands

Now that we have a connection, we need to ensure the controller is responding to our commands. If you are following along with a different reader or using i2c, these commands may look different. For the acr122u all of the commands were found in the controller’s user manual, or the RFIDIOT repository. We’ll test by disabling auto polling, turning the LED orange, and getting the controller firmware revision.

Here are the commands in Python:

# These should go within the try section of the previous connection code

ACS_DISABLE_AUTO_POLL = ['ff', '00', '51', '3f', '00']
ACS_LED_ORANGE = ['ff', '00', '40', '0f', '04', '00', '00', '00', '00']
ACS_GET_READER_FIRMWARE = ['ff', '00', '48', '00', '00']

def to_bytes(hex_list):
# Convert a list of hex strings to a list of bytes
return [int(byte, 16) if isinstance(byte, str) else byte for byte in hex_list]

def send_apdu(connection, apdu_hex):
# Send an APDU command and print the response
apdu = to_bytes(apdu_hex)
print(f"Sending APDU: {apdu}")
try:
response, sw1, sw2 = connection.transmit(apdu)
print(f"Response: {response}")
print(f"Status words: {sw1:02X} {sw2:02X}")

if not response:
print(f"No data returned, but status words: {sw1:02X} {sw2:02X}")
return None, sw1, sw2

return response, sw1, sw2

except Exception as e:
print(f"Exception during APDU transmission: {e}")
return None, None, None

# Begin controller test commmands

# Send the disable auto poll command
_, sw1, sw2 = send_apdu(connection, ACS_DISABLE_AUTO_POLL)
if sw1 == 0x90:
print("Auto poll disabled successfully")
else:
print("Failed to disable auto poll")
return

# Send the LED orange command
_, sw1, sw2 = send_apdu(connection, ACS_LED_ORANGE)
if sw1 == 0x90:
print("LED set to orange successfully")
else:
print("Failed to set LED to orange")
return

# Send the get reader firmware command
response, sw1, sw2 = send_apdu(connection, ACS_GET_READER_FIRMWARE)
response.append(sw1)
response.append(sw2)
try:
firmware_version = ''.join(chr(b) for b in response)
print(f"Reader firmware version: {firmware_version}")
except Exception as e:
print(f"Failed to get reader firmware: {e}")
return

# End controller test commands

For most commands, a status code of 90 followed by another byte will be given. Others will have specific responses, such as the get reader firmware command, where the status words (sw1 and sw2) are part of the response. Some of you may have also noticed the exception part of the send_apdu() function. This is to account for empty responses which will happen later.

PN532 Commands

Now it’s time to start sending commands to the PN532. Since the controller is still in between our computer and the chip, we’ll need to wrap the instructions. For the acr122u this will be 4 bytes followed by a byte containing the length of the PN532 command. This limits the PN532 command to 255 bytes but as we’ll see later this should be sufficient for our purposes.

To test this we’ll ask for the chip’s revision and firmware number:

ACS_DIRECT_TRANSMIT = ['ff', '00', '00', '00']
GET_PN532_FIRMWARE = ['d4', '02']

def pn532_print_firmware(data):
# Print the PN532 firmware information Thanks to RFIDIOT for this code :)
if data[:2] != PN532_OK:
print(' Bad data from PN532:', data)
else:
print(' IC:', data[2])
print(' Rev: %d.%d' % (data[3] >> 4, data[3] & 0x0F))
print(' Support:', end=' ')
support = data[4]
spacing = ''
for n in PN532_FUNCTIONS.keys():
if support & n:
print(spacing + PN532_FUNCTIONS[n])
spacing = ' '

# Send the direct transmit command with GET_PN532_FIRMWARE
full_command = ACS_DIRECT_TRANSMIT + [len(GET_PN532_FIRMWARE)] + GET_PN532_FIRMWARE
response, sw1, sw2 = send_apdu(connection, full_command)
if sw1 == 0x90:
pn532_print_firmware(response)
else:
print("Failed to get PN532 firmware")
return

If the output shows readable data then we are ready to begin proper card emulation.

Emulation

Note: I’m still learning/researching this part so some information may be incorrect. If you are an expert please feel free to correct me in the comments.

To start emulation, we need to give the PN532 some starting identification data. According to the NXP documentation, there are a few things we need:

  1. mode — 00 for passive, 01 DEP only, 02 PICC only
  2. sens_res — response to a SENS_REQ, 0400 or 0800
  3. nfcid1t — UID, 3 byte identifier for Mifare
  4. sel_res — Communication abilities, 20 PICC, 40 DEP, 60 DEP and PICC
  5. nfcid2t — 16 byte identification for FeliCa, here is first 8 bytes
  6. pad — second 8 bytes for FeliCa
  7. system_code — FeliCa system code, depends on card type
  8. nfcid3t — FeliCa ID for p2p communication
  9. general_bytes length
  10. general_bytes — Extra required information in p2p communication
  11. historical_bytes length
  12. historical_bytes — Manufacture info and more custom information

Once we have this information we can tell the PN532 to use it by sending the tg init as target command.

TG_INIT_AS_TARGET = ['d4', '8c']

# Arguments for TG_INIT_AS_TARGET
mode = '00' # 00 = Passive Only 01 = DEP Only 02 = PICC Only
sens_res = '0400' # 0400 or 0800 try both
nfcid1t = '000000'
sel_res = '20' # 40 = DEP 60 = DEP and PICC 20 = PICC
nfcid2t = '0000000000000000'
pad = '0000000000000000'
system_code = '0000'
nfcid3t = '00000000000000000000'
general_bytes = ''
historical_bytes = ''

init_as_target_command = TG_INIT_AS_TARGET + to_bytes([mode]) + to_bytes(sens_res) + to_bytes(nfcid1t) + to_bytes(sel_res) + to_bytes(nfcid2t) + to_bytes(pad) + to_bytes(system_code) + to_bytes(nfcid3t) + [len(to_bytes(general_bytes))] + to_bytes(general_bytes) + [len(to_bytes(historical_bytes))] + to_bytes(historical_bytes)
init_as_target_command = ACS_DIRECT_TRANSMIT + [len(init_as_target_command)] + init_as_target_command

# Send the direct transmit command with TG_INIT_AS_TARGET
response, sw1, sw2 = send_apdu(connection, init_as_target_command)
if sw1 == None:
print("TG_INIT_AS_TARGET command sent successfully")
else:
print("Failed to send TG_INIT_AS_TARGET command")
return

Now here comes the part where I haven’t completely figured out. Remember the send_apdu() function? Notice how it doesn’t quit if it runs into an exception, but just returns nothing. This is because I can’t seem to get the PN532 to return anything for tg init as target. According to NXP, the chip should return D5 8D along with the information given. However, despite initiating correctly (I tested with my phone’s NFC reader), nothing is returned, and pyscard gets confused.

Assuming that’s the intended result, we can communicate with the reader by sending tg get data and tg set data.

TG_GET_DATA = ['d4', '86']
TG_SET_DATA = ['d4', '8e']
ISO_OK = ['90', '00']

# Send the direct transmit command with TG_GET_DATA
full_command = ACS_DIRECT_TRANSMIT + [len(TG_GET_DATA)] + TG_GET_DATA
response, sw1, sw2 = send_apdu(connection, full_command)
if sw1 == 0x90:
print(f"TG_GET_DATA returned: {response}")
else:
print("Failed to get data with TG_GET_DATA")
return

# Send the direct transmit command with TG_SET_DATA and ISO_OK
tg_set_data_command = TG_SET_DATA + ISO_OK
full_command = ACS_DIRECT_TRANSMIT + [len(tg_set_data_command)] + tg_set_data_command
response, sw1, sw2 = send_apdu(connection, full_command)
if sw1 == 0x90:
print("TG_SET_DATA command sent successfully")
else:
print("Failed to send TG_SET_DATA command")

tg get data returns the information received by the reader, tg set data sets the response that will be given. If implemented correctly, this is how we can get full card emulation and communication.

Conclusion

This is only part 1 of the project. In part 2, I will hopefully have all the issues figured out and have Mifare ultralight emulation working (maybe even Mifare Classic). It is of course, easier to clone cards to a magic card. However, with this setup, you can do more than copy a card, you can change up the data to find exploits, or even write your own communications method.


Posted

in

,

by

Comments

Leave a Reply