3 # ------------------------------------------------------------------------------
14 from fm11rf08s_recovery
import recovery
16 author
= "@csBlueChip"
19 # Copyright @csBlueChip
21 # This program is free software: you can redistribute it and/or modify
22 # it under the terms of the GNU General Public License as published by
23 # the Free Software Foundation, either version 3 of the License, or
24 # (at your option) any later version.
26 # This program is distributed in the hope that it will be useful,
27 # but WITHOUT ANY WARRANTY; without even the implied warranty of
28 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29 # GNU General Public License for more details.
31 # See LICENSE.txt for the text of the license.
33 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
35 # The original version of this script can be found at:
36 # https://github.com/csBlueChip/Proxmark_Stuff/tree/main/MiFare_Docs/Fudan_RF08(S)/PM3_Script
37 # The original version is released with an MIT Licence.
38 # Or please reach out to me [BlueChip] personally for alternative licenses.
41 # optional color support .. `pip install ansicolors`
43 from colors
import color
44 except ModuleNotFoundError
:
45 def color(s
, fg
=None):
51 """Print and Log: init globals
63 def startlog(uid
, dpath
, append
=False):
64 """Print and Log: set logfile and flush logbuffer
73 logfile
= f
"{dpath}hf-mf-{uid.hex().upper()}-log.txt"
75 with
open(logfile
, 'w'):
78 with
open(logfile
, 'a') as f
:
83 def lprint(s
='', end
='\n', flush
=False, prompt
="[" + color("=", fg
="yellow") + "] ", log
=True):
91 s
= f
"{prompt}" + f
"\n{prompt}".join(s
.split('\n'))
92 print(s
, end
=end
, flush
=flush
)
96 if logfile
is not None:
97 with
open(logfile
, 'a') as f
:
111 p
= pm3
.pm3() # console interface
119 # No logfile name yet
120 lprint("Fudan FM11RF08[S] full card recovery")
121 lprint("\nDump folder... " + color(f
"{dpath}", fg
="yellow"))
123 # FIXME: script is announced as for RF08 and for RF08S but it comprises RF32N key
124 # and if RF08 is supported, all other NXP/Infineon with same backdoor can be treated
125 # by the same script (once properly implemented, see other FIXME)
126 bdkey
, blk0
= getBackdoorKey()
129 uid
= getUIDfromBlock0(blk0
)
130 startlog(uid
, dpath
, append
=False)
132 fudanValidate(blk0
, args
.validate
)
135 keyfile
= f
"{dpath}hf-mf-{uid.hex().upper()}-key.bin"
137 # FIXME: nr of sectors depend on the tag. RF32N is 40, RF32 is 64, RF08 is 16, RF08S is 16+1
138 # Currently loadKeys is hardcoded for RF08S
139 if args
.force
or (key
:= loadKeys(keyfile
)) is None:
140 if args
.recover
is False:
141 s
= color("--recover", fg
="yellow")
142 lprint(f
" Keys not loaded, use {s} to run recovery script [slow]", prompt
="[" + color("!", fg
="red") + "]")
144 # FIXME: recovery() is only for RF08S. TODO for the other ones with a "darknested" attack
145 keyfile
= recoverKeys()
146 key
= loadKeys(keyfile
)
149 ret
, mad
, key
= verifyKeys(key
)
151 if args
.nokeys
is False:
152 s
= color("--nokeys", fg
="yellow")
153 lprint(f
" Use {s} to keep going past this point", prompt
="[" + color("!", fg
="red") + "]")
156 # FIXME: nr of blocks depend on the tag. RF32 is 256, RF08 is 64, RF08S is 64+8
157 # Currently readBlocks is hardcoded for RF08S
158 data
, blkn
= readBlocks(bdkey
, args
.fast
)
159 data
= patchKeys(data
, key
)
161 dump18
= diskDump(data
, uid
, dpath
) # save it before you do anything else
165 # FIXME: nr of blocks depend on the tag. RF32 is 256, RF08 is 64, RF08S is 64+8,
166 # Currently dumpAcl is hardcoded for RF08S
169 if (mad
is True) or (args
.mad
is True):
172 if (args
.bambu
is True) or (detectBambu(data
) is True):
181 """Get PM3 preferences
186 p
.console("prefs show --json")
187 prefs
= json
.loads(p
.grabbed_output
)
188 dpath
= prefs
['file.default.dumppath'] + os
.path
.sep
193 """Assert python version"""
194 required_version
= (3, 8)
195 if sys
.version_info
< required_version
:
196 print(f
"Python version: {sys.version}")
197 print(f
"The script needs at least Python v{required_version[0]}.{required_version[1]}. Abort.")
203 """Parse the CLi arguments"""
204 parser
= argparse
.ArgumentParser(description
='Full recovery of Fudan FM11RF08* cards.')
206 parser
.add_argument('-n', '--nokeys', action
='store_true', help='extract data even if keys are missing')
207 parser
.add_argument('-r', '--recover', action
='store_true', help='run key recovery script if required')
208 parser
.add_argument('-f', '--force', action
='store_true', help='force recovery of keys')
209 parser
.add_argument('-b', '--bambu', action
='store_true', help='force Bambu tag decode')
210 parser
.add_argument('-m', '--mad', action
='store_true', help='force M.A.D. decode')
211 parser
.add_argument('-v', '--validate', action
='store_true', help='check Fudan signature (requires internet)')
212 parser
.add_argument('--fast', action
='store_true', help='use ecfill for faster card transactions')
214 args
= parser
.parse_args()
216 if args
.force
is True:
221 def getBackdoorKey():
223 [=] # | sector 00 / 0x00 | ascii
224 [=] ----+-------------------------------------------------+-----------------
225 [=] 0 | 5C B4 9C A6 D2 08 04 00 04 59 92 25 BF 5F 70 90 | \\........Y.%._p.
231 # FM11RF08S FM11RF08 FM11RF32
232 dklist
= ["A396EFA4E24F", "A31667A8CEC1", "518b3354E760"]
234 lprint("\nTrying known backdoor keys...")
238 cmd
= f
"hf mf rdbl -c 4 --key {k} --blk 0"
239 lprint(f
"\n`{cmd}`", end
='', flush
=True)
241 for line
in p
.grabbed_output
.split('\n'):
242 if " | " in line
and "# | s" not in line
:
245 s
= color('ok', fg
='green')
246 lprint(f
" ( {s} )", prompt
='')
249 s
= color('fail', fg
='yellow')
250 lprint(f
" ( {s} ) [{res}]", prompt
='')
253 lprint("\n Unknown key, or card not detected.", prompt
="[" + color("!", fg
="red") + "]")
258 def getUIDfromBlock0(blk0
):
259 """Extract UID from block 0"""
260 uids
= blk0
[0:11] # UID string : "11 22 33 44"
261 uid
= bytes
.fromhex(uids
.replace(' ', '')) # UID (bytes) : 11223344
265 def decodeBlock0(blk0
):
266 """Extract data from block 0"""
268 lprint(" UID BCC ++----- RF08 ID -----++")
269 lprint(" ! ! SAK !! !!")
270 lprint(" ! ! ! ATQA !! Fudan Sig !!")
271 lprint(" !---------. !. !. !---. VV .---------------. VV")
272 # 0 12 15 18 24 27 45
274 # 00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF
275 lprint(f
" Block 0 : {blk0}")
277 # --- decode block 0 ---
279 uid
= getUIDfromBlock0(blk0
)
280 bcc
= int(blk0
[12:14], 16) # BCC
281 chk
= 0 # calculate checksum
285 sak
= int(blk0
[15:17], 16) # SAK
286 atqa
= int(blk0
[18:23].replace(' ', ''), 16) # 0x7788
288 fida
= int(blk0
[24:26], 16) # Fudan ID 0x88
289 fidb
= int(blk0
[45:47], 16) # Fudan ID 0xFF
290 # fid = (fida<<8)|fidb # Fudan ID 0x88FF
292 hash = blk0
[27:44] # Fudan hash "99 AA BB CC DD EE"
294 type = f
"[{fida:02X}:{fidb:02X}]" # type/name
296 if fida
== 0x01 or fida
== 0x03 or fida
== 0x04:
297 type += " - Fudan FM11RF08S"
300 if fida
== 0x01 or fida
== 0x02 or fida
== 0x03:
301 type += " - Fudan FM11RF08"
303 elif fidb
== 0x91 or fidb
== 0x98:
304 type += " - Fudan FM11RF08 (never seen in the wild)"
307 type += " - Unknown (please report)"
309 # --- show results ---
316 desc
= f
"fail. Expected {chk:02X}"
317 lprint(f
" UID/BCC : {uid.hex().upper()}/{bcc:02X} - {desc}")
320 desc
= "NXP MIFARE TNP3xxx 1K"
322 desc
= "NXP MIFARE CLASSIC 1k | Plus 1k | Ev1 1K"
324 desc
= "NXP MIFARE Mini 0.3k"
326 desc
= "NXP MIFARE Plus 2k"
328 desc
= "NXP MIFARE Classic 4k | Plus 4k | Ev1 4k"
331 lprint(f
" SAK : {sak:02X} - {desc}")
332 lprint(f
" ATQA : {atqa:04X}") # show ATQA
333 lprint(f
" Fudan ID : {type}") # show type
334 lprint(f
" Fudan Sig: {hash}") # show ?Partial HMAC?
337 def fudanValidate(blk0
, live
=False):
338 """Fudan validation"""
339 url
= "https://rfid.fm-uivs.com/nfcTools/api/M1KeyRest"
340 hdr
= "Content-Type: application/text; charset=utf-8"
341 post
= f
"{blk0.replace(' ', '')}"
343 lprint(f
"\n Validator:\n`wget -q -O -"
344 f
" --header=\"{hdr}\""
345 f
" --post-data \"{post}\""
350 # Warning, this import causes a "double free or corruption" crash if the script is called twice...
351 # So for now we limit the import only when really needed
354 except ModuleNotFoundError
:
355 s
= color("not found", fg
="red")
356 lprint(f
"Python module 'requests' {s}, please install!", prompt
="[" + color("!", fg
="red") + "] ")
358 lprint("\nCheck Fudan signature (requires internet)...")
360 headers
= {"Content-Type": "application/text; charset=utf-8"}
361 resp
= requests
.post(url
, headers
=headers
, data
=post
)
363 if resp
.status_code
!= 200:
364 lprint(f
"HTTP Error {resp.status_code} - check request not processed")
367 r
= json
.loads(resp
.text
)
368 if r
['data'] is not None:
369 desc
= f
" {{{r['data']}}}"
372 lprint(f
"The man from Fudan, he say: {r['code']} - {r['message']}{desc}")
374 s
= color('--validate', fg
="yellow")
375 lprint(f
'\n Use {s} to perform Fudan signature check automatically', prompt
='[?]')
378 def loadKeys(keyfile
):
379 """Load keys from file
381 If keys cannot be loaded AND --recover is specified, then run key recovery
383 key
= [[b
'' for _
in range(2)] for _
in range(17)] # create a fresh array
385 lprint("\nLoad keys from file... " + color(f
"{keyfile}", fg
="yellow"))
388 with (open(keyfile
, "rb")) as fh
:
390 for sec
in range((16+2)-1):
391 key
[sec
][ab
] = fh
.read(6)
400 """Run key recovery script"""
401 badrk
= 0 # 'bad recovered key' count (ie. not recovered)
403 lprint("\nRunning recovery script, ETA: Less than 30 minutes")
405 lprint('\n`-._,-\'"`-._,-"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,')
407 r
= recovery(quiet
=False)
408 keyfile
= r
['keyfile']
409 rkey
= r
['found_keys']
410 # fdump = r['dumpfile']
413 lprint('`-._,-\'"`-._,-"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,')
415 for k
in range(0, 16+1):
417 if rkey
[k
][ab
] == "":
419 lprint("Some keys were not recovered: ", end
='')
421 lprint(", ", end
='', prompt
='')
427 lprint(f
"[{kn}/", end
='', prompt
='')
428 lprint("A]" if ab
== 0 else "B]", end
='', prompt
='')
444 lprint("Checking keys...")
446 for sec
in range(0, 16+1): # 16 normal, 1 dark
456 cmd
= f
"hf mf rdbl -c {ab} --key {key[sec][ab].hex()} --blk {bn}"
457 lprint(f
" `{cmd}`", end
='', flush
=True)
459 res
= p
.console(cmd
, capture
=False)
460 lprint(" " * (3-len(str(bn
))), end
='', prompt
='')
462 s
= color("ok", fg
="green")
463 lprint(f
" ( {s} )", end
='', prompt
='')
465 s
= color("fail", fg
="red")
466 lprint(f
" ( {s} )", end
='', prompt
='')
470 # check for Mifare Application Directory
471 if (sec
== 0) and (ab
== 0) \
472 and (key
[0][0] == b
'\xa0\xa1\xa2\xa3\xa4\xa5'):
474 lprint(" - MAD Key", prompt
='')
476 lprint("", prompt
='')
479 s
= color(f
'{badk}', fg
="red")
480 e
= "s exist" if badk
!= 1 else " exists"
481 lprint(f
" {s} bad key{e}", prompt
="[" + color("!", fg
="red") + "]")
485 lprint("All keys verified")
489 lprint("MAD key detected")
494 def readBlocks(bdkey
, fast
=False):
496 Read all block data - INCLUDING advanced verification blocks
498 [=] # | sector 00 / 0x00 | ascii
499 [=] ----+-------------------------------------------------+-----------------
500 [=] 0 | 5C B4 9C A6 D2 08 04 00 04 59 92 25 BF 5F 70 90 | \\........Y.%._p.
506 blkn
= list(range(0, 63 + 1)) + list(range(128, 135 + 1))
508 lprint("\nLoad blocks {0..63, 128..135}[64 + 8 = 72] from the card")
512 # Try fast dump first
513 # The user uses keyhole #0 (-a)
514 # The vendor uses keyhole #1 (-b)
515 # The thief uses keyhole #4 (backdoor)
517 cmd
= f
"hf mf ecfill -c 4 --key {bdkey}"
518 lprint(f
"`{cmd}`", flush
=True, log
=False)
520 for line
in p
.grabbed_output
.split('\n'):
523 lprint(f
"`{cmd}`", flush
=True, log
=False)
525 for line
in p
.grabbed_output
.split('\n'):
526 if " | " in line
and "sec | blk | data" not in line
:
529 blkn_todo
= list(range(128, 135+1))
533 cmd
= f
"hf mf rdbl -c 4 --key {bdkey} --blk {n}"
534 lprint(f
" `{cmd}`", flush
=True, log
=False, end
='')
536 for retry
in range(5):
540 for line
in p
.grabbed_output
.split('\n'):
541 if " | " in line
and "# | s" not in line
:
548 s
= color("ok", fg
="green")
550 data
.append(f
"{n:3d} | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- | ----------------")
552 s
= color("fail", fg
="red")
554 lprint(" " * (3 - len(str(n
))), flush
=True, log
=False, end
='', prompt
='')
555 lprint(f
' ( {s} )', flush
=True, log
=False, prompt
='')
557 s
= color("ok", fg
="green")
559 s
= color("fail", fg
="red")
561 lprint(f
'Loading ( {s} )', log
=False)
565 def patchKeys(data
, key
):
566 """Patch keys in to data
567 3 | 00 00 00 00 00 00 87 87 87 69 00 00 00 00 00 00 | .........i......
569 lprint("\nPatching keys in to data")
571 for sec
in range(0, 16 + 1):
572 blk
= (sec
* 4) + 3 # find "trailer" for this sector
574 if key
[sec
][0] == b
'':
575 keyA
= "-- -- -- -- -- -- "
577 kstr
= key
[sec
][0].hex()
578 keyA
= "".join([kstr
[i
:i
+2] + " " for i
in range(0, len(kstr
), 2)])
580 if key
[sec
][1] == b
'':
581 keyB
= "-- -- -- -- -- -- "
583 kstr
= key
[sec
][1].hex()
584 keyB
= "".join([kstr
[i
:i
+2] + " " for i
in range(0, len(kstr
), 2)])
586 data
[blk
] = data
[blk
][:6] + keyA
+ data
[blk
][24:36] + keyB
589 data
[blk
] = data
[blk
][:6] + "-- -- -- -- -- -- " + data
[blk
][24:36] + "-- -- -- -- -- --"
593 def dumpData(data
, blkn
):
596 lprint("===========")
598 lprint("===========")
608 lprint(f
"{sec:2d}:{data[cnt]}")
610 lprint(f
" :{data[cnt]}")
613 if (cnt
% 4 == 0) and (n
!= blkn
[-1]): # Space between sectors
617 def detectBambu(data
):
618 """Let's try to detect a Bambu card by the date strings..."""
620 dl
= bytes
.fromhex(data
[12][6:53]).decode('ascii').rstrip('\x00')
622 ds
= bytes
.fromhex(data
[13][6:41]).decode('ascii').rstrip('\x00')
627 # dl 2024_03_22_16_29
628 # yy y y m m d d h h m m
629 exp
= r
"20[2-3][0-9]_[0-1][0-9]_[0-3][0-9]_[0-2][0-9]_[0-5][0-9]"
631 if re
.search(exp
, dl
) and (ds
== dls
):
632 lprint("\nBambu date strings detected.")
635 lprint("\nBambu date strings not detected.")
640 """Dump bambu details
642 https://github.com/Bambu-Research-Group/RFID-Tag-Guide/blob/main/README.md
646 3 | 00 00 00 00 00 00 87 87 87 69 00 00 00 00 00 00 | .........i......
650 lprint("===========")
652 lprint("===========")
654 lprint("Decompose as Bambu tag .. ", end
='')
656 MaterialVariantIdentifier_s
= bytes
.fromhex(data
[1][6:29]).decode('ascii').rstrip('\x00')
657 UniqueMaterialIdentifier_s
= bytes
.fromhex(data
[1][30:53]).decode('ascii').rstrip('\x00') # [**] 8not16
659 FilamentType_s
= bytes
.fromhex(data
[2][6:53]).decode('ascii').rstrip('\x00')
661 DetailedFilamentType_s
= bytes
.fromhex(data
[4][6:53]).decode('ascii').rstrip('\x00')
663 Colour_rgba
= int(data
[5][6:17].replace(' ', ''), 16)
664 SpoolWeight_g
= int(data
[5][21:23] + data
[5][18:20], 16)
665 Block5_7to8
= data
[5][24:29]
666 FilamentDiameter_mm
= struct
.unpack('f', bytes
.fromhex(data
[5][30:41].replace(' ', '')))[0]
667 Block5_12to15
= data
[5][42:50]
669 DryingTemperature_c
= int(data
[6][9:11] + data
[6][6:8], 16)
670 DryingTime_h
= int(data
[6][15:17] + data
[6][12:14], 16)
671 BedTemperatureType_q
= int(data
[6][21:23] + data
[6][18:20], 16)
672 BedTemperature_c
= int(data
[6][27:29] + data
[6][24:26], 16)
673 MaxTemperatureForHotend_c
= int(data
[6][33:35] + data
[6][30:32], 16)
674 MinTemperatureForHotend_c
= int(data
[6][39:41] + data
[6][36:38], 16)
675 Block6_12to15
= data
[6][42:50]
677 # XCamInfo_x = bytes.fromhex(data[8][6:41].replace(' ', ''))
678 XCamInfo_x
= data
[8][6:41]
679 NozzleDiameter_q
= struct
.unpack('f', bytes
.fromhex(data
[8][42:53].replace(' ', '')))[0]
681 # TrayUID_s = bytes.fromhex(data[9][6:53]).decode('ascii').rstrip('\x00') #[**] !ascii
682 TrayUID_s
= data
[9][6:53]
684 Block10_0to3
= data
[10][6:17]
685 SpoolWidth_um
= int(data
[10][21:23] + data
[14][18:20], 16)
686 Block10_6to15
= data
[10][24:50]
688 ProductionDateTime_s
= bytes
.fromhex(data
[12][6:53]).decode('ascii').rstrip('\x00')
690 ShortProductionDateTime_s
= bytes
.fromhex(data
[13][6:53]).decode('ascii').rstrip('\x00')
692 # Block14_0to3 = data[14][6:17]
693 FilamentLength_m
= int(data
[14][21:23] + data
[14][18:20], 16)
694 # Block14_6to15 = data[14][24:51]
696 # (16blocks * 16bytes = 256) * 8bits = 2048 bits
705 Hash
.append(data
[b
][6:53])
707 lprint("[offset:length]", prompt
='')
709 lprint(f
" [ 0: 8] MaterialVariantIdentifier_s = \"{MaterialVariantIdentifier_s}\"")
710 lprint(f
" [ 8: 8] UniqueMaterialIdentifier_s = \"{UniqueMaterialIdentifier_s}\"")
712 lprint(f
" [ 0:16] FilamentType_s = \"{FilamentType_s}\"")
714 lprint(f
" [ 0:16] DetailedFilamentType_s = \"{DetailedFilamentType_s}\"")
716 lprint(f
" [ 0: 4] Colour_rgba = 0x{Colour_rgba:08X}")
717 lprint(f
" [ 4: 2] SpoolWeight_g = {SpoolWeight_g}g")
718 lprint(f
" [ 6: 2] Block5_7to8 = {{{Block5_7to8}}}")
719 lprint(f
" [ 8: 4] FilamentDiameter_mm = {FilamentDiameter_mm}mm")
720 lprint(f
" [12: 4] Block5_12to15 = {{{Block5_12to15}}}")
722 lprint(f
" [ 0: 2] DryingTemperature_c = {DryingTemperature_c}^C")
723 lprint(f
" [ 2: 2] DryingTime_h = {DryingTime_h}hrs")
724 lprint(f
" [ 4: 4] BedTemperatureType_q = {BedTemperatureType_q}")
725 lprint(f
" [ 6: 2] BedTemperature_c = {BedTemperature_c}^C")
726 lprint(f
" [ 8: 2] MaxTemperatureForHotend_c = {MaxTemperatureForHotend_c}^C")
727 lprint(f
" [10: 2] MinTemperatureForHotend_c = {MinTemperatureForHotend_c}^C")
728 lprint(f
" [12: 4] Block6_12to15 = {{{Block6_12to15}}}")
730 lprint(f
" [ 0:12] XCamInfo_x = {{{XCamInfo_x}}}")
731 lprint(f
" [12: 4] NozzleDiameter_q = {NozzleDiameter_q:.6f}__")
733 # lprint(f" [ 0:16] TrayUID_s = \"{TrayUID_s}\"")
734 lprint(f
" [ 0:16] TrayUID_s = {{{TrayUID_s}}} ; not ASCII")
736 lprint(f
" [ 0: 4] Block10_0to3 = {{{Block10_0to3}}}")
737 lprint(f
" [ 4: 2] SpoolWidth_um = {SpoolWidth_um}um")
738 lprint(f
" [ 6:10] Block10_6to15 = {{{Block10_6to15}}}")
740 lprint(f
" [ 0:16] ProductionDateTime_s = \"{ProductionDateTime_s}\"")
742 lprint(f
" [ 0:16] ShortProductionDateTime_s = \"{ShortProductionDateTime_s}\"")
744 lprint(f
" [ 0: 4] Block10_0to3 = {{{Block10_0to3}}}")
745 lprint(f
" [ 4: 2] FilamentLength_m = {FilamentLength_m}m")
746 lprint(f
" [ 6:10] Block10_6to15 = {{{Block10_6to15}}}")
747 lprint(f
"\n Blocks {hblk}:")
748 for i
in range(0, len(hblk
)):
749 lprint(f
" [ 0:16] HashBlock[{i:2d}] = {{{Hash[i]}}} // #{hblk[i]:2d}")
751 except Exception as e
:
753 lprint(f
"Failed: {e}")
756 # +=============================================================================
759 # ,-------------------.
760 # ( 2.2 : ACCESS BITS )
761 # `-------------------'
763 # The Access bits on both (used) Sectors is the same: 78 77 88
765 # Let's reorganise that according to the official spec Fig 9.
767 # ========== ===========
768 # 78 77 88 --> 78 87 87
769 # ab cd ef --> cb fa ed
771 # The second nybble of each byte is the inverse of the first nybble.
772 # It is there to trap tranmission errors, so we can just ignore it/them.
774 # So our Access Control value is : {c, f, e} == {7, 8, 8}
776 # Let's convert those nybbles to binary
780 # |||| ...and transpose them:
782 # |||`--- 100 - Block 0 Access bits
783 # ||`---- 100 - Block 1 Access bits
784 # |`----- 100 - Block 2 Access bits
785 # `------ 011 - Block 3 Access bits [Sector Trailer]
787 # Now we can use the lookup table [Table 3] to work out what we can do
788 # with the Sector Trailer (Block(S,3)):
790 # | Key A | | Access Bits | | Key B |
791 # | read ¦ write | | read ¦ write | | read ¦ write |
792 # +------¦-------+ +------¦-------+ +------¦-------+
793 # 000 : | -- ¦ KeyA | | KeyA ¦ -- | | KeyA ¦ KeyA |
794 # 001 : | -- ¦ KeyA | | KeyA ¦ KeyA | | KeyA ¦ KeyA | Transport Mode
795 # 010 : | -- ¦ -- | | KeyA ¦ -- | | KeyA ¦ -- |
797 # 011 : | -- ¦ KeyB | | A+B ¦ KeyB | | -- ¦ KeyB | <-- Our Card!
799 # 100 : | -- ¦ KeyB | | A+B ¦ -- | | -- ¦ KeyB |
800 # 101 : | -- ¦ -- | | A+B ¦ KeyB | | -- ¦ -- |
801 # 110 : | -- ¦ -- | | A+B ¦ -- | | -- ¦ -- | }__
802 # 111 : | -- ¦ -- | | A+B ¦ -- | | -- ¦ -- | } The Same!?
804 # Our card uses 011, for (both of) the (used) Sector Trailer(s). So:
805 # Both Key A and Key B can READ the Access Bits
806 # Key B can (additionally) WRITE to Key A, Key B (itself), and the Access Bits
808 # Then we can do a similar lookup for the 3 data Blocks (in this Sector)
809 # This time using [Table 4]
812 # | read ¦ write | Inc ¦ Dec |
813 # +------¦-------+------¦------+
814 # 000 : | A+B ¦ A+B | A+B ¦ A+B | Transport Mode
815 # 001 : | A+B ¦ -- | -- ¦ A+B |
816 # 010 : | A+B ¦ -- | -- ¦ -- |
817 # 011 : | KeyB ¦ KeyB | -- ¦ -- |
819 # 100 : | A+B ¦ KeyB | -- ¦ -- | <-- Our Card!
821 # 101 : | KeyB ¦ -- | -- ¦ -- |
822 # 110 : | A+B ¦ KeyB | KeyB ¦ A+B |
823 # 111 : | -- ¦ -- | -- ¦ -- |
825 # Our card uses 100, for all of the (used) Sectors. So:
826 # Both Key A and Key B can READ the Block
827 # Only Key B can WRITE to the Block
828 # The block cannot be used as a "counter" because:
829 # Neither key can perform increment nor decrement commands
832 # IF YOU PLAN TO CHANGE ACCESS BITS, RTFM, THERE IS MUCH TO CONSIDER !
833 # ==============================================================================
837 6 18 24 27 30 33 42 53
839 3 | 00 00 00 00 00 00 87 87 87 69 00 00 00 00 00 00 | .........i......
842 aclkh
= [] # key header
843 aclk
= [""] * 8 # key lookup
844 aclkx
= [] # key output
847 lprint("=====================")
848 lprint(" Access Control List")
849 lprint("=====================")
852 aclkh
.append(" _______________________________________________________ ")
853 aclkh
.append("| | Sector Trailers |")
854 aclkh
.append("| |----------------------------------------------|")
855 aclkh
.append("| Sector |____Key_A_____||_Access_Bits__||____Key_B_____|")
856 aclkh
.append("| | read ¦ write || read ¦ write || read ¦ write |")
857 aclkh
.append("|--------+------¦-------++------¦-------++------¦-------|")
858 # "| xx | -- ¦ KeyA || KeyA ¦ -- || KeyA ¦ KeyA |"
859 aclk
[0] = "| -- ¦ KeyA || KeyA ¦ -- || KeyA ¦ KeyA | [000]" # noqa: E222
860 aclk
[1] = "| -- ¦ KeyA || KeyA ¦ KeyA || KeyA ¦ KeyA | [001]" # noqa: E222
861 aclk
[2] = "| -- ¦ -- || KeyA ¦ -- || KeyA ¦ -- | [010]" # noqa: E222
862 aclk
[3] = "| -- ¦ KeyB || A+B ¦ KeyB || -- ¦ KeyB | [011]" # noqa: E222
863 aclk
[4] = "| -- ¦ KeyB || A+B ¦ -- || -- ¦ KeyB | [100]" # noqa: E222
864 aclk
[5] = "| -- ¦ -- || A+B ¦ KeyB || -- ¦ -- | [101]" # noqa: E222
865 aclk
[6] = "| -- ¦ -- || A+B ¦ -- || -- ¦ -- | [110]" # noqa: E222 # yes, the same!?
866 aclk
[7] = "| -- ¦ -- || A+B ¦ -- || -- ¦ -- | [111]" # noqa: E222 # ...
868 acldh
= [] # data header
869 acld
= [""] * 8 # data lookup
870 acldx
= [] # data output
872 acldh
.append(" _____________________________________ ")
873 acldh
.append("| | Data Blocks |")
874 acldh
.append("| |-----------------------------|")
875 acldh
.append("| Block | Data || Counter |")
876 acldh
.append("| | read ¦ write || Inc ¦ Dec |")
877 acldh
.append("|-------+------¦-------++------¦------+")
878 # "| xxx | A+B ¦ A+B || A+B ¦ A+B | "
879 acld
[0] = "| A+B ¦ A+B || A+B ¦ A+B | [000]" # noqa: E222
880 acld
[1] = "| A+B ¦ -- || -- ¦ A+B | [001]" # noqa: E222
881 acld
[2] = "| A+B ¦ -- || -- ¦ -- | [010]" # noqa: E222
882 acld
[3] = "| KeyB ¦ KeyB || -- ¦ -- | [011]" # noqa: E222
883 acld
[4] = "| A+B ¦ KeyB || -- ¦ -- | [100]" # noqa: E222
884 acld
[5] = "| KeyB ¦ -- || -- ¦ -- | [101]" # noqa: E222
885 acld
[6] = "| A+B ¦ KeyB || KeyB ¦ A+B | [110]" # noqa: E222
886 acld
[7] = "| -- ¦ -- || -- ¦ -- | [111]" # noqa: E222
890 # --- calculate the ACL indices for each sector:block ---
896 sec
= sn
if sn
< 16 else sn
- 16
901 r0
= ((c
& (2**0)) << 2) |
((f
& (2**0)) << 1) |
((e
& (2**0)) ) # noqa: E202
902 r1
= ((c
& (2**1)) << 1) |
((f
& (2**1)) ) |
((e
& (2**1)) >> 1) # noqa: E202
903 r2
= ((c
& (2**2)) ) |
((f
& (2**2)) >> 1) |
((e
& (2**2)) >> 2) # noqa: E202
904 r3
= ((c
& (2**3)) >> 1) |
((f
& (2**3)) >> 2) |
((e
& (2**3)) >> 3) # noqa: E221
905 idx
[sec
] = [r0
, r1
, r2
, r3
]
907 # --- build the ACL conversion table ---
911 sec
= sn
if sn
< 16 else sn
- 16
914 aclkx
.append(f
"| {sn:2d} " + aclk
[idx
[sec
][bn
% 4]]
915 + f
" {{{d[24:32]}}} -> {{{d[27]}{d[31]}{d[30]}}}")
917 acldx
.append(f
"| {bn:3d} " + acld
[idx
[sec
][bn
% 4]])
919 # --- print it all out ---
926 lprint(" | | ¦ || ¦ || ¦ |")
937 lprint(" | | ¦ || ¦ |")
941 def diskDump(data
, uid
, dpath
):
943 dump18
= f
'{dpath}hf-mf-{uid.hex().upper()}-dump18.bin'
945 lprint(f
'\nDump card data to file... ' + color(dump18
, fg
='yellow'))
949 with
open(dump18
, 'wb') as f
:
953 b
= bytes
.fromhex(d
[6:53].replace(' ', '').replace('--', 'FF'))
956 lprint('Bad data exists, and has been saved as 0xFF')
958 s
= color('ok', fg
='green')
959 lprint(f
' Save file operations ( {s} )', prompt
='[+]')
961 except Exception as e
:
962 s
= color('fail', fg
='red')
963 lprint(f
' Save file operations: {e} ( {s} )', prompt
='[!]')
976 lprint("====================================")
977 lprint(" MiFare Application Directory (MAD)")
978 lprint("====================================")
981 cmd
= f
"hf mf mad --force --verbose --file {dump18}"
982 lprint(f
"`{cmd}`", log
=False)
984 lprint('\n`-._,-\'"`-._,-"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,\n')
988 for line
in p
.grabbed_output
.split('\n'):
989 lprint(line
, prompt
='')
991 lprint('`-._,-\'"`-._,-"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,')
994 if __name__
== "__main__":