In a previous post, we saw how to implement a transparent reader with the Flipper Zero. What if we take the same concept but this time to implement a transparent card emulator? We could use our Flipper Zero like a cannon to attack digital fortresses, such as readers or smartphones, by sending erroneous requests. Malformed commands, commands not expected in the lifecycle, fuzzing, buffer overflow—the sky is the limit!
1 - Context
Just like with the transparent card reader, I want to communicate with the Flipper using its serial CLI from my computer. The computer handles all the logic, meaning it decides what response to give depending on the command, using a Python script, for example.
Now, regarding the implementation of the card emulator commands, it's essentially a kind of mirror mode compared to the reader:
- We need to detect when the RF field is activated by the terminal.
- We need to detect when the RF field is deactivated by the terminal.
- We need to be able to receive/send bits to the terminal.
- We need to be able to receive/send bytes to the terminal.
Except there's a small detail that complicates things. Remember that during card/reader communication, it's the reader that acts as the master, meaning it's the one that initiates communication and sends commands.
So, if we're creating a card emulator, it must be waiting for events from the reader. You can think of it like a server, with the reader acting as the client. We'll need to code this into the Flipper Zero.
Alright, first of all, let’s do a quick recap of the communication exchanges between a reader and a card using ISO 14443-A.
2 - Communication exchanges between a reader and a card using ISO 14443-A
Here is a diagram that summarizes the main exchanges between a reader and a card communicating via ISO 14443-A.
+----------------+ +----------------+
| Reader | | Card |
+----------------+ +----------------+
| |
Field activation |
| |
| --- REQA (Request Command Type A) -------------> |
| 26 |
| |
| <------------ ATQA (Answer to Request Type A) ---|
| 04 00
| |
| --- ANTICOLLISION Command ---------------------->|
| |
| <------------ UID (Unique Identifier) -----------|
| |
| --- SELECT [UID] Command ----------------------->|
| |
| <------------ SAK (Select Acknowledge) ----------|
| |
| --- RATS (Request for Answer To Select) -------->|
| E0 50 BC A5 |
| |
| <------------ ATS (Answer To Select) ------------|
| 0A 78 80 82 02 20 63 CB A3 A0 92 43 |
| |
| ---- [Opt] PPS (Proto and Parameter Selection) ->|
| D0 73 87 |
| |
| <------------ [PPS Response] --------------------|
| D0 73 87 |
| |
| --- TPDU [Encapsulated APDU Command] ----------->|
| 0200A404000E325041592E5359532E444446303100E042 |
| |
| <------------ TPDU [Encapsulated APDU Response] -|
| 00a404000e325041592e5359532e444446303100 |
Now the question is, "How do we implement all of this on the Flipper?"
4 - Flipper Zero implementation
As in my previous article, I will continue to expand the file applications/main/nfc/nfc_cli.c
(see the file on my branch ).
First, a quick hardware point. For NFC management, the Flipper Zero uses the ST25R3916 chip. This is great because it allows us to create both a contactless reader and a card emulator. The chip automatically handles sending the commands involved from field activation to anticollision. All we need to do is specify the ATQA, SAK, UID, and its length that we want to send back.
The Flipper provides the function furi_hal_nfc_iso14443a_listener_set_col_res_data
to handle all of this.
That's why I added 3 commands to the Flipper's NFC CLI to configure these elements:
set_atqa
set_sak
set_uid
And just before starting the emulation, we'll call furi_hal_nfc_iso14443a_listener_set_col_res_data
with these parameters.
if(g_NfcTech == FuriHalNfcTechIso14443a) {
furi_hal_nfc_iso14443a_listener_set_col_res_data(g_uid, g_uid_len, g_atqa, g_sak);
fdt = ISO14443_3A_FDT_LISTEN_FC;
}
Next, setting the Flipper Zero to card emulator mode is done using the function furi_hal_nfc_set_mode
. This time, we specify the mode FuriHalNfcModeListener
, and for the technologies, we use the standard values: FuriHalNfcTechIso14443a
, FuriHalNfcTechIso14443b
, and FuriHalNfcTechIso15693
.
Finally, to start the emulation, I implemented the command run_emu
, which will initiate an infinite loop waiting for a nearby reader. Event monitoring is handled by the function furi_hal_nfc_listener_wait_event
.
FuriHalNfcEvent event = furi_hal_nfc_listener_wait_event(100);
Next, the event
can take several values depending on what has been detected:
-
FuriHalNfcEventFieldOn
indicates that a field activation has been detected. -
FuriHalNfcEventFieldOff
indicates that the field has been turned off. - The most important event is
FuriHalNfcEventRxEnd
, which indicates that a command from the terminal has been received. At this point, we need to send our response. Again, it's important to note that all the handling of command sending, up to and including anticollision, is done automatically. So, we can basically start processing a command likeselect
, for example.
while(true) {
FuriHalNfcEvent event = furi_hal_nfc_listener_wait_event(100);
if(event == FuriHalNfcEventTimeout) {
if(cli_cmd_interrupt_received(cli)) {
break;
}
}
if(event & FuriHalNfcEventAbortRequest) {
break;
}
if(event & FuriHalNfcEventFieldOn) {
printf("on\r\n");
}
if(event & FuriHalNfcEventFieldOff) {
furi_hal_nfc_listener_idle();
printf("off\r\n");
}
if(event & FuriHalNfcEventListenerActive) {
// Nothing
}
if(event & FuriHalNfcEventRxEnd) {
5 - Handling the reception of the command and sending the response
Now, let's see how to handle the reception of the command and sending the response.
if(event & FuriHalNfcEventRxEnd) {
furi_hal_nfc_timer_block_tx_start(fdt);
rx_bits = 0;
furi_hal_nfc_listener_rx(rx_data, rx_data_size, &rx_bits);
if((rx_bits / 8) != 0) {
for(size_t i = 0; i < (rx_bits / 8); i++) {
printf("%02X", rx_data[i]);
}
printf("\r\n");
if(nfc_emu_get_resp(cli, rx_cmd))
break;
}
while(furi_hal_nfc_timer_block_tx_is_running()) {
}
FuriHalNfcError r = furi_hal_nfc_listener_tx(rx_data, bit_buffer_get_size(rx_cmd));
if(r != FuriHalNfcErrorNone) {
printf("error\r\n");
}
}
- Data reception is handled via
furi_hal_nfc_listener_rx(rx_data, rx_data_size, &rx_bits);
. We display the received data using aprintf
, which sends the response to the terminal connected to the Flipper. An important thing to understand is that as soon as we receive the command, we must respond very quickly. This means we cannot manually write the response in the shell—it will be too late. This is why the only way to communicate with the Flipper is by using a Python script with a dispatcher that specifies which response to give for each received command. -
Then, the terminal sends a response that we retrieve using the function
nfc_emu_get_resp(cli, rx_cmd)
. This part is a bit tricky because, in a shell command, you don’t typically have a back-and-forth exchange. So, I use the functioncli_getc(cli)
to read a character.- Sometimes, I get an unwanted character
0xA
. If it's the first character received, I skip it, as I read character by character. - The first character indicates whether the Flipper Zero should calculate and add the CRC to the command itself (
0x31
means yes, otherwise no). - Then, I read the characters of the response in hexadecimal string format. When we receive the character
0xA
, it indicates the reception is complete.
- Sometimes, I get an unwanted character
Finally, we convert the hexadecimal string into a
uint8_t
array usingunhexify(tmp, (uint8_t*)bit_buffer_get_data(rx_data), len);
.If necessary, we add a CRC using
add_crc
.Lastly, we can send the response to the reader using:
FuriHalNfcError r = furi_hal_nfc_listener_tx(rx_data, bit_buffer_get_size(rx_cmd));
.
And now, how do we go about validating all of this?
6 - Card emulation validation
6.1 - How it started ... (Hydra NFC v2)
Well, we could use our transparent reader from the previous post to validate our emulator. So, we would need two Flipper Zeros... which I don’t have. However, I do have a Hydra NFC v2, which allows for a transparent reader setup.
I just need to use a script from pynfc.
import time
from pynfcreader.devices.hydra_nfc_v2 import HydraNFCv2
from pynfcreader.sessions.iso14443.iso14443a import Iso14443ASession
hydra_nfc = HydraNFCv2(port="", debug=False)
hn = Iso14443ASession(drv=hydra_nfc, block_size=120)
hn.connect()
hn.field_off()
time.sleep(0.1)
hn.field_on()
time.sleep(0.1)
hn.polling()
#
hn._send_tpdu(bytes.fromhex("00 A4 04 00 0E 32 50 41 59 2E 53 59 53 2E 44 44 46 30 31 00"))
It’s very practical because it allows us to send commands one by one to validate everything:
- Sending the REQA
- Anticollision
- Select
- PPS
- Sending a TPDU
6.2 - How it finished... (PC/SC reader).
However, in reality, communications are a bit more complicated. So, I used a PC/SC reader, the ACR122U, to send/receive a full APDU command, in combination with a Python script (using pyscard ) to make a real-world test.
In my case, I simply select the PPSE application.
import sys
from smartcard.System import readers
from smartcard.Exceptions import NoCardException
from smartcard.util import toHexString
# APDU SELECT PPSE command
SELECT_PPSE = [0x00, 0xA4, 0x04, 0x00, 0x0E, 0x32, 0x50, 0x41, 0x59, 0x2E, 0x53, 0x59, 0x53, 0x2E, 0x44, 0x44, 0x46,
0x30, 0x31, 0x00]
def send_select_ppse():
# Get the list of available card readers
available_readers = readers()
if len(available_readers) == 0:
print("No card reader available.")
sys.exit(1)
# Use the first reader found
reader = available_readers[0]
print(f"Reader found: {reader}")
# Connect to the card
connection = reader.createConnection()
try:
connection.connect()
atr = connection.getATR() # Get the ATR
atr_hex = ' '.join(f'{byte:02X}' for byte in atr) # Convert the ATR to hexadecimal
print(f"ATR: {atr_hex}")
# Send the SELECT PPSE command
print(f"Sending APDU SELECT PPSE command: {toHexString(SELECT_PPSE)}")
response, sw1, sw2 = connection.transmit(SELECT_PPSE)
# Display the card's response
print(f"Response: {toHexString(response)}")
print(f"Status: {sw1:02X} {sw2:02X}")
except NoCardException:
print("No card was detected.")
sys.exit(1)
if __name__ == "__main__":
send_select_ppse()
So now, the card emulator needs to handle many more events. Therefore, I created a Python script below to manage this case. There’s a lot to explain, such as the different types of TPDU (i-block, r-block, s-block), but that will be in a future blog post.
import time
from pynfcreader.sessions.iso14443.tpdu import Tpdu
from pynfcreader.devices import flipper_zero
from pynfcreader.sessions.iso14443.iso14443a import Iso14443ASession
fz = flipper_zero.FlipperZero("", debug=False)
fz.connect()
fz.set_mode_emu_iso14443A()
def process_apdu(cmd: str):
print(f"apdu {cmd}")
if cmd == "00a404000e325041592e5359532e444446303100":
rapdu = "6F57840E325041592E5359532E4444463031A545BF0C42611B4F07A0000000421010500243428701019F2808400200000000000061234F07A0000000041010500A4D4153544552434152448701029F280840002000000000009000"
else:
rapdu = "6F00"
return rapdu
class Emu(Iso14443ASession):
def __init__(self, cid=0, nad=0, drv=None, block_size=16, process_function=None):
Iso14443ASession.__init__(self, cid, nad, drv, block_size)
self._addCID = False
self.drv = self._drv
self.process_function = process_function
def run(self):
self.drv.start_emulation()
print("...go!")
self.low_level_dispatcher()
def low_level_dispatcher(self):
capdu = bytes()
ats_sent = False
iblock_resp_lst = []
while 1:
r = fz.emu_get_cmd()
rtpdu = None
print(f"tpdu < {r}")
if r == "off":
print("field off")
elif r == "on":
print("field on")
ats_sent = False
else:
tpdu = Tpdu(bytes.fromhex(r))
if (tpdu.tpdu[0] == 0xE0) and (ats_sent is False):
rtpdu, crc = "0A788082022063CBA3A0", True
ats_sent = True
elif tpdu.r:
print("r block")
if r == "BA00BED9":
rtpdu, crc = "BA00", True
elif r[0:2] in ["A2", "A3", "B2", "B3"]:
rtpdu, crc = iblock_resp_lst.pop(0).hex(), True
elif tpdu.s:
print("s block")
elif tpdu.i:
print("i block")
capdu += tpdu.get_inf_field()
if tpdu.is_chaining() is False:
rapdu = self.process_function(capdu.hex())
capdu = bytes()
iblock_resp_lst = self.chaining_iblock(data=bytes.fromhex(rapdu))
rtpdu, crc = iblock_resp_lst.pop(0).hex(), True
print(f">>> rtdpu {rtpdu}\n")
fz.emu_send_resp(rtpdu.encode(), crc)
emu = Emu(drv=fz, process_function=process_apdu)
emu.run()
With this, it works very well, and the emulation is extremely stable. I can place or remove the Flipper from the reader and send the commands multiple times, and it works every time. Once again, the Flipper has an excellent implementation of its NFC layer, and its API allows for a lot of functionality with minimal effort in the implementation.
Below, you have a sample of the output from the Python script.
...go!
tpdu < off
field off
tpdu < on
field on
tpdu < E050BCA5
>>> rtdpu 0A788082022063CBA3A0
tpdu < BA00BED9
r block
>>> rtdpu BA00
tpdu < BA00BED9
r block
>>> rtdpu BA00
tpdu < BA00BED9
r block
>>> rtdpu BA00
tpdu < BA00BED9
r block
>>> rtdpu BA00
tpdu < 0200A404000E325041592E5359532E444446303100E042
i block
apdu 00a404000e325041592e5359532e444446303100
>>> rtdpu 126f57840e325041592e5359532e444446
tpdu < A36FC6
r block
>>> rtdpu 133031a545bf0c42611b4f07a000000042
tpdu < A2E6D7
r block
>>> rtdpu 121010500243428701019f280840020000
tpdu < A36FC6
r block
>>> rtdpu 130000000061234f07a000000004101050
tpdu < A2E6D7
r block
>>> rtdpu 120a4d4153544552434152448701029f28
tpdu < A36FC6
r block
>>> rtdpu 030840002000000000009000
tpdu < BA00BED9
r block
>>> rtdpu BA00
6.3 A little bit of Proxmark as well
Using the Proxmark 3 was useful for debugging communication in sniffing mode: I placed it between the reader and the card (which could be a genuine card or the Flipper), and I was able to check the data exchanges.
# sniffing
hf 14a sniff
# Exchange decoding
hf list
What's next?
Good, what's next?
- First, I could give more explanations about the card emulation Python script.
- Also, I should implement a way to stop the card emulation when a button is pressed, because currently the event-waiting loop never finishes. The only way to exit is to restart the Flipper.
- Also, we could do some fun stuff by using both a transparent reader and a card emulator at the same time, for instance, to perform a man-in-the-middle attack and modify the communication live!
Top comments (0)