From 694215bc5375bd4d44a3c0d62d2aa7656b26e934 Mon Sep 17 00:00:00 2001 From: BlueChip Date: Thu, 21 Nov 2024 00:40:09 +0000 Subject: [PATCH] add KDF functionality to rf08s 'full' card recovery script --- client/pyscripts/fm11rf08_full.py | 75 +++++++++++++++++++++++--- client/pyscripts/fm11rf08s_recovery.py | 16 ++++-- 2 files changed, 81 insertions(+), 10 deletions(-) diff --git a/client/pyscripts/fm11rf08_full.py b/client/pyscripts/fm11rf08_full.py index e684f904d8..f5261dd0f3 100644 --- a/client/pyscripts/fm11rf08_full.py +++ b/client/pyscripts/fm11rf08_full.py @@ -142,7 +142,10 @@ def main(): lprint(f" Keys not loaded, use {s} to run recovery script [slow]", prompt="[" + color("!", fg="red") + "]") else: # FIXME: recovery() is only for RF08S. TODO for the other ones with a "darknested" attack - keyfile = recoverKeys() + keyfile = recoverKeys(uid=uid, kdf=[["Bambu v1", kdfBambu1]]) + if keyfile == False: + lprint("Script failed - aborting") + return key = loadKeys(keyfile) if key is not None: @@ -396,22 +399,34 @@ def loadKeys(keyfile): return key -def recoverKeys(): +def recoverKeys(uid, kdf=[[]]): """Run key recovery script""" badrk = 0 # 'bad recovered key' count (ie. not recovered) + keys = False + lprint(f"\nTrying KDFs:"); + for fn in kdf: + lprint(f" {fn[0]:s}", end='') + keys = fn[1](uid) + if keys != False: + lprint(" .. Success", prompt='') + break + lprint(" .. Fail", prompt='') + lprint("\nRunning recovery script, ETA: Less than 30 minutes") lprint('\n`-._,-\'"`-._,-"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,') + r = recovery(quiet=False, keyset=keys) + lprint('`-._,-\'"`-._,-"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,') + + if r == False: + return False - r = recovery(quiet=False) keyfile = r['keyfile'] rkey = r['found_keys'] # fdump = r['dumpfile'] # rdata = r['data'] - lprint('`-._,-\'"`-._,-"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,') - for k in range(0, 16+1): for ab in [0, 1]: if rkey[k][ab] == "": @@ -427,9 +442,57 @@ def recoverKeys(): lprint(f"[{kn}/", end='', prompt='') lprint("A]" if ab == 0 else "B]", end='', prompt='') if badrk > 0: - lprint() + lprint("", prompt='') return keyfile +def kdfBambu1(uid): + from Cryptodome.Protocol.KDF import HKDF + from Cryptodome.Hash import SHA256 + + # Generate all keys + try: + # extracted from Bambu firmware + salt = bytes([0x9a,0x75,0x9c,0xf2,0xc4,0xf7,0xca,0xff,0x22,0x2c,0xb9,0x76,0x9b,0x41,0xbc,0x96]) + keyA = HKDF(uid, 6, salt, SHA256, 16, context=b"RFID-A\0") + keyB = HKDF(uid, 6, salt, SHA256, 16, context=b"RFID-B\0") + except Exception as e: + print(f"{e}") + return False + + # --- Grab block 13 (in sector 3) --- + cmd = f"hf mf rdbl -c 0 --key {keyA[3].hex()} --blk 12" + #lprint(f" `{cmd}`", flush=True, log=False, end='') + for retry in range(5): + p.console(cmd) + + found = False + for line in p.grabbed_output.split('\n'): + if " | " in line and "# | s" not in line: + lsub = line[4:76] + found = True + if found: + break + if not found: + return False + + # --- Try to decode it as a bambu date string --- + try: + dl = bytes.fromhex(lsub[6:53]).decode('ascii').rstrip('\x00') + except Exception: + return False + + # dl 2024_03_22_16_29 + # yy y y m m d d h h m m + exp = r"20[2-3][0-9]_[0-1][0-9]_[0-3][0-9]_[0-2][0-9]_[0-5][0-9]" + if not re.search(exp, dl): + return False + + # --- valid date string, we are confident this is a bambu card --- + keys = [] + for i in range(0, 15+1): + keys.append([keyA[i].hex(), keyB[i].hex()]) + + return keys def verifyKeys(key): """Verify keys diff --git a/client/pyscripts/fm11rf08s_recovery.py b/client/pyscripts/fm11rf08s_recovery.py index 9f7270340b..9c7d7d8413 100755 --- a/client/pyscripts/fm11rf08s_recovery.py +++ b/client/pyscripts/fm11rf08s_recovery.py @@ -76,7 +76,7 @@ def color(s, fg=None): exit() -def recovery(init_check=False, final_check=False, keep=False, debug=False, supply_chain=False, quiet=True): +def recovery(init_check=False, final_check=False, keep=False, debug=False, supply_chain=False, quiet=True, keyset=False): def show(s='', prompt="[" + color("=", fg="yellow") + "] ", **kwargs): if not quiet: s = f"{prompt}" + f"\n{prompt}".join(s.split('\n')) @@ -94,7 +94,7 @@ def show(s='', prompt="[" + color("=", fg="yellow") + "] ", **kwargs): if uid is None: show("Card not found") - return + return False show("UID: " + color(f"{uid:08X}", fg="green")) def show_key(sec, key_type, key): @@ -106,6 +106,14 @@ def show_key(sec, key_type, key): save_path = prefs['file.default.dumppath'] + os.path.sep found_keys = [["", ""] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)] + + if keyset != False: + n = min(len(found_keys),len(keyset)) + show(f"{n} Key pairs supplied: ") + for i in range(0, n): + found_keys[i] = keyset[i] + show(f" Sector {i:2d} : A = {found_keys[i][0]:12s} B = {found_keys[i][1]:12s}") + if init_check: show("Checking default keys...") p.console("hf mf fchk") @@ -135,7 +143,7 @@ def show_key(sec, key_type, key): if (nonces_with_data == ""): show("Error getting nonces, abort.") - return + return False try: with open(nonces_with_data, 'r') as file: @@ -143,7 +151,7 @@ def show_key(sec, key_type, key): dict_nwd = json.load(file) except json.decoder.JSONDecodeError: show(f"Error parsing {nonces_with_data}, abort.") - return + return False nt = [["", ""] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)] nt_enc = [["", ""] for _ in range(NUM_SECTORS + NUM_EXTRA_SECTORS)]