fm11rf08s_full pip8 style

This commit is contained in:
Philippe Teuwen 2025-03-02 16:41:14 +01:00
commit beeec2385c

View file

@ -1,4 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""This script recovers Fudan FM11RF08S cards, including functionalities for Bambu tags decoding."""
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Imports # Imports
@ -43,12 +44,13 @@ try:
from colors import color from colors import color
except ModuleNotFoundError: except ModuleNotFoundError:
def color(s, fg=None): def color(s, fg=None):
"""Return the string as such, without color."""
_ = fg _ = fg
return str(s) return str(s)
def initlog(): def initlog():
"""Print and Log: init globals """Print and Log: init globals.
globals: globals:
- logbuffer (W) - logbuffer (W)
@ -61,7 +63,7 @@ globals:
def startlog(uid, dpath, append=False): def startlog(uid, dpath, append=False):
"""Print and Log: set logfile and flush logbuffer """Print and Log: set logfile and flush logbuffer.
globals: globals:
- logbuffer (RW) - logbuffer (RW)
@ -81,13 +83,12 @@ globals:
def lprint(s='', end='\n', flush=False, prompt="[" + color("=", fg="yellow") + "] ", log=True): def lprint(s='', end='\n', flush=False, prompt="[" + color("=", fg="yellow") + "] ", log=True):
"""Print and Log """Print and Log.
globals: globals:
- logbuffer (RW) - logbuffer (RW)
- logfile (R) - logfile (R)
""" """
s = f"{prompt}" + f"\n{prompt}".join(s.split('\n')) s = f"{prompt}" + f"\n{prompt}".join(s.split('\n'))
print(s, end=end, flush=flush) print(s, end=end, flush=flush)
@ -102,7 +103,7 @@ globals:
def main(): def main():
"""== MAIN == """== MAIN ==.
globals: globals:
- p (W) - p (W)
@ -143,7 +144,7 @@ globals:
else: else:
# FIXME: recovery() is only for RF08S. TODO for the other ones with a "darknested" attack # FIXME: recovery() is only for RF08S. TODO for the other ones with a "darknested" attack
keyfile = recoverKeys(uid=uid, kdf=[["Bambu v1", kdfBambu1]]) keyfile = recoverKeys(uid=uid, kdf=[["Bambu v1", kdfBambu1]])
if keyfile == False: if keyfile is False:
lprint("Script failed - aborting") lprint("Script failed - aborting")
return return
key = loadKeys(keyfile) key = loadKeys(keyfile)
@ -181,7 +182,7 @@ globals:
def getPrefs(): def getPrefs():
"""Get PM3 preferences """Get PM3 preferences.
globals: globals:
- p (R) - p (R)
@ -193,7 +194,7 @@ globals:
def checkVer(): def checkVer():
"""Assert python version""" """Assert python version."""
required_version = (3, 8) required_version = (3, 8)
if sys.version_info < required_version: if sys.version_info < required_version:
print(f"Python version: {sys.version}") print(f"Python version: {sys.version}")
@ -203,7 +204,7 @@ def checkVer():
def parseCli(): def parseCli():
"""Parse the CLi arguments""" """Parse the CLi arguments."""
parser = argparse.ArgumentParser(description='Full recovery of Fudan FM11RF08S cards.') parser = argparse.ArgumentParser(description='Full recovery of Fudan FM11RF08S cards.')
parser.add_argument('-n', '--nokeys', action='store_true', help='extract data even if keys are missing') parser.add_argument('-n', '--nokeys', action='store_true', help='extract data even if keys are missing')
@ -222,15 +223,15 @@ def parseCli():
def getBackdoorKey(): def getBackdoorKey():
"""Find backdoor key r"""Find backdoor key.
[=] # | sector 00 / 0x00 | ascii [=] # | sector 00 / 0x00 | ascii
[=] ----+-------------------------------------------------+----------------- [=] ----+-------------------------------------------------+-----------------
[=] 0 | 5C B4 9C A6 D2 08 04 00 04 59 92 25 BF 5F 70 90 | \\........Y.%._p. [=] 0 | 5C B4 9C A6 D2 08 04 00 04 59 92 25 BF 5F 70 90 | \........Y.%._p.
globals: globals:
- p (R) - p (R)
""" """
# FM11RF08S FM11RF08 FM11RF32 # FM11RF08S FM11RF08 FM11RF32
dklist = ["A396EFA4E24F", "A31667A8CEC1", "518b3354E760"] dklist = ["A396EFA4E24F", "A31667A8CEC1", "518b3354E760"]
@ -259,14 +260,14 @@ globals:
def getUIDfromBlock0(blk0): def getUIDfromBlock0(blk0):
"""Extract UID from block 0""" """Extract UID from block 0."""
uids = blk0[0:11] # UID string : "11 22 33 44" uids = blk0[0:11] # UID string : "11 22 33 44"
uid = bytes.fromhex(uids.replace(' ', '')) # UID (bytes) : 11223344 uid = bytes.fromhex(uids.replace(' ', '')) # UID (bytes) : 11223344
return uid return uid
def decodeBlock0(blk0): def decodeBlock0(blk0):
"""Extract data from block 0""" """Extract data from block 0."""
lprint() lprint()
lprint(" UID BCC ++---- RF08* ID -----++") lprint(" UID BCC ++---- RF08* ID -----++")
lprint(" ! ! SAK !! !!") lprint(" ! ! SAK !! !!")
@ -346,7 +347,7 @@ def decodeBlock0(blk0):
def fudanValidate(blk0, live=False): def fudanValidate(blk0, live=False):
"""Fudan validation""" """Fudan validation."""
url = "https://rfid.fm-uivs.com/nfcTools/api/M1KeyRest" url = "https://rfid.fm-uivs.com/nfcTools/api/M1KeyRest"
hdr = "Content-Type: application/text; charset=utf-8" hdr = "Content-Type: application/text; charset=utf-8"
post = f"{blk0.replace(' ', '')}" post = f"{blk0.replace(' ', '')}"
@ -387,7 +388,7 @@ def fudanValidate(blk0, live=False):
def loadKeys(keyfile): def loadKeys(keyfile):
"""Load keys from file """Load keys from file.
If keys cannot be loaded AND --recover is specified, then run key recovery If keys cannot be loaded AND --recover is specified, then run key recovery
""" """
@ -408,15 +409,15 @@ If keys cannot be loaded AND --recover is specified, then run key recovery
def recoverKeys(uid, kdf=[[]]): def recoverKeys(uid, kdf=[[]]):
"""Run key recovery script""" """Run key recovery script."""
badrk = 0 # 'bad recovered key' count (ie. not recovered) badrk = 0 # 'bad recovered key' count (ie. not recovered)
keys = False keys = []
lprint(f"\nTrying KDFs:"); lprint("\nTrying KDFs:")
for fn in kdf: for fn in kdf:
lprint(f" {fn[0]:s}", end='') lprint(f" {fn[0]:s}", end='')
keys = fn[1](uid) keys = fn[1](uid)
if keys != False: if len(keys) > 0:
lprint(" .. Success", prompt='') lprint(" .. Success", prompt='')
break break
lprint(" .. Fail", prompt='') lprint(" .. Fail", prompt='')
@ -427,7 +428,7 @@ def recoverKeys(uid, kdf=[[]]):
r = recovery(quiet=False, keyset=keys) r = recovery(quiet=False, keyset=keys)
lprint('`-._,-\'"`-._,-"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,') lprint('`-._,-\'"`-._,-"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,')
if r == False: if r is False:
return False return False
keyfile = r['keyfile'] keyfile = r['keyfile']
@ -453,7 +454,21 @@ def recoverKeys(uid, kdf=[[]]):
lprint("", prompt='') lprint("", prompt='')
return keyfile return keyfile
def kdfBambu1(uid): def kdfBambu1(uid):
"""Derive keys from a given UID using the Bambu HKDF algorithm and validates the card data.
This function generates two keys (keyA and keyB) using the Bambu HKDF algorithm with a predefined salt and context.
It then attempts to read block 13 from sector 3 of the card using keyA. If successful, it decodes the data
and checks if it matches a specific date format. If the data is valid, it returns a list of derived keys.
Args:
uid (bytes): The UID of the card.
Returns:
list: A list of derived keys if the card data is valid.
bool: False if any step in the process fails.
"""
from Cryptodome.Protocol.KDF import HKDF from Cryptodome.Protocol.KDF import HKDF
from Cryptodome.Hash import SHA256 from Cryptodome.Hash import SHA256
@ -502,13 +517,13 @@ def kdfBambu1(uid):
return keys return keys
def verifyKeys(key): def verifyKeys(key):
"""Verify keys """Verify keys.
globals: globals:
- p (R) - p (R)
""" """
badk = 0 badk = 0
mad = False mad = False
@ -563,12 +578,11 @@ globals:
def readBlocks(bdkey, fast=False): def readBlocks(bdkey, fast=False):
""" r"""Read all block data - INCLUDING advanced verification blocks.
Read all block data - INCLUDING advanced verification blocks
[=] # | sector 00 / 0x00 | ascii [=] # | sector 00 / 0x00 | ascii
[=] ----+-------------------------------------------------+----------------- [=] ----+-------------------------------------------------+-----------------
[=] 0 | 5C B4 9C A6 D2 08 04 00 04 59 92 25 BF 5F 70 90 | \\........Y.%._p. [=] 0 | 5C B4 9C A6 D2 08 04 00 04 59 92 25 BF 5F 70 90 | \........Y.%._p.
globals: globals:
- p (R) - p (R)
@ -634,7 +648,8 @@ globals:
def patchKeys(data, key): def patchKeys(data, key):
"""Patch keys in to data """Patch keys in to data.
3 | 00 00 00 00 00 00 87 87 87 69 00 00 00 00 00 00 | .........i...... 3 | 00 00 00 00 00 00 87 87 87 69 00 00 00 00 00 00 | .........i......
""" """
lprint("\nPatching keys in to data") lprint("\nPatching keys in to data")
@ -662,7 +677,7 @@ def patchKeys(data, key):
def dumpData(data, blkn): def dumpData(data, blkn):
"""Dump data""" """Dump data."""
lprint() lprint()
lprint("===========") lprint("===========")
lprint(" Card Data") lprint(" Card Data")
@ -708,7 +723,7 @@ def detectBambu(data):
def dumpBambu(data): def dumpBambu(data):
"""Dump bambu details """Dump bambu details.
https://github.com/Bambu-Research-Group/RFID-Tag-Guide/blob/main/README.md https://github.com/Bambu-Research-Group/RFID-Tag-Guide/blob/main/README.md
@ -833,14 +848,14 @@ https://github.com/Bambu-Research-Group/RFID-Tag-Guide/blob/main/README.md
# The Access bits on both (used) Sectors is the same: 78 77 88 # The Access bits on both (used) Sectors is the same: 78 77 88
# Let's reorganise that according to the official spec Fig 9. # Let's reorganize that according to the official spec Fig 9.
# Access C1 C2 C3 # Access C1 C2 C3
# ========== =========== # ========== ===========
# 78 77 88 --> 78 87 87 # 78 77 88 --> 78 87 87
# ab cd ef --> cb fa ed # ab cd ef --> cb fa ed
# The second nybble of each byte is the inverse of the first nybble. # The second nybble of each byte is the inverse of the first nybble.
# It is there to trap tranmission errors, so we can just ignore it/them. # It is there to trap transmission errors, so we can just ignore it/them.
# So our Access Control value is : {c, f, e} == {7, 8, 8} # So our Access Control value is : {c, f, e} == {7, 8, 8}
@ -903,7 +918,7 @@ https://github.com/Bambu-Research-Group/RFID-Tag-Guide/blob/main/README.md
# IF YOU PLAN TO CHANGE ACCESS BITS, RTFM, THERE IS MUCH TO CONSIDER ! # IF YOU PLAN TO CHANGE ACCESS BITS, RTFM, THERE IS MUCH TO CONSIDER !
# ============================================================================== # ==============================================================================
def dumpAcl(data): def dumpAcl(data):
"""Dump ACL """Dump ACL.
6 18 24 27 30 33 42 53 6 18 24 27 30 33 42 53
| | | | | | | | | | | | | | | |
@ -1010,10 +1025,10 @@ def dumpAcl(data):
def diskDump(data, uid, dpath): def diskDump(data, uid, dpath):
"""Full Dump""" """Full Dump."""
dump18 = f'{dpath}hf-mf-{uid.hex().upper()}-dump18.bin' dump18 = f'{dpath}hf-mf-{uid.hex().upper()}-dump18.bin'
lprint(f'\nDump card data to file... ' + color(dump18, fg='yellow')) lprint('\nDump card data to file... ' + color(dump18, fg='yellow'))
bad = False bad = False
try: try:
@ -1037,12 +1052,11 @@ def diskDump(data, uid, dpath):
def dumpMad(dump18): def dumpMad(dump18):
"""Dump MAD """Dump MAD.
globals: globals:
- p (R) - p (R)
""" """
lprint() lprint()
lprint("====================================") lprint("====================================")
lprint(" MiFare Application Directory (MAD)") lprint(" MiFare Application Directory (MAD)")