3 # ------------------------------------------------------------------------------
14 from fm11rf08s_recovery
import recovery
16 # ------------------------------------------------------------------------------
17 # Revision log & Licence
18 # ------------------------------------------------------------------------------
20 1.2.0 - BC - Proxmark3 Submission
24 # Copyright @csBlueChip
26 # This program is free software: you can redistribute it and/or modify
27 # it under the terms of the GNU General Public License as published by
28 # the Free Software Foundation, either version 3 of the License, or
29 # (at your option) any later version.
31 # This program is distributed in the hope that it will be useful,
32 # but WITHOUT ANY WARRANTY; without even the implied warranty of
33 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
34 # GNU General Public License for more details.
36 # See LICENSE.txt for the text of the license.
38 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
40 # The original version of this script can be found at:
41 # https://github.com/csBlueChip/Proxmark_Stuff/tree/main/MiFare_Docs/Fudan_RF08(S)/PM3_Script
42 # The original version is released with an MIT Licence.
43 # Or please reach out to me [BlueChip] personally for alternative licenses.
46 # optional color support .. `pip install ansicolors`
48 from colors
import color
49 except ModuleNotFoundError
:
50 def color(s
, fg
=None):
55 # +=============================================================================
58 # ==============================================================================
59 def startlog(uid
, append
=False):
62 logfile
= f
"{dpath}hf-mf-{uid:08X}-log.txt"
64 with
open(logfile
, 'w'):
68 # +=========================================================
69 def lprint(s
, end
='\n', flush
=False):
70 print(s
, end
=end
, flush
=flush
)
72 if logfile
is not None:
73 with
open(logfile
, 'a') as f
:
77 # ++============================================================================
80 # >> p. [console handle]
82 # ==============================================================================
88 p
= pm3
.pm3() # console interface
95 print(f
"{prompt} Fudan FM11RF08[S] full card recovery")
98 print(f
"{prompt} Dump folder: {dpath}")
108 keyfile
= f
"{dpath}hf-mf-{uid:08X}-key.bin"
111 if args
.force
is False and loadKeys() is True:
114 if args
.recover
is False:
115 lprint(f
"{prompt} * Keys not loaded, use --recover to run recovery script [slow]")
118 if loadKeys() is True:
122 if verifyKeys() is False:
123 if args
.nokeys
is False:
124 lprint(f
"{prompt} ! Use --nokeys to keep going past this point")
130 diskDump() # save it before you do anything else
138 if (args
.bambu
is True) or (detectBambu() is True):
142 lprint(f
"{prompt} Tadah!")
147 # +=============================================================================
148 # Get PM3 preferences
150 # ==============================================================================
154 p
.console("prefs show --json")
155 prefs
= json
.loads(p
.grabbed_output
)
156 dpath
= prefs
['file.default.dumppath'] + os
.path
.sep
159 # +=============================================================================
160 # Assert python version
161 # ==============================================================================
163 required_version
= (3, 8)
164 if sys
.version_info
< required_version
:
165 print(f
"Python version: {sys.version}")
166 print(f
"The script needs at least Python v{required_version[0]}.{required_version[1]}. Abort.")
171 # +=============================================================================
172 # Parse the CLi arguments
174 # ==============================================================================
178 parser
= argparse
.ArgumentParser(description
='Full recovery of Fudan FM11RF08* cards.')
180 parser
.add_argument('-n', '--nokeys', action
='store_true', help='extract data even if keys are missing')
181 parser
.add_argument('-r', '--recover', action
='store_true', help='run key recovery script if required')
182 parser
.add_argument('-f', '--force', action
='store_true', help='force recovery of keys')
183 parser
.add_argument('-b', '--bambu', action
='store_true', help='force Bambu tag decode')
184 parser
.add_argument('-v', '--validate', action
='store_true', help='check Fudan signature (requires internet)')
186 args
= parser
.parse_args()
188 if args
.force
is True:
192 # +=============================================================================
196 # [=] # | sector 00 / 0x00 | ascii
197 # [=] ----+-------------------------------------------------+-----------------
198 # [=] 0 | 5C B4 9C A6 D2 08 04 00 04 59 92 25 BF 5F 70 90 | \........Y.%._p.
199 # ==============================================================================
204 # FM11RF08S FM11RF08 FM11RF32
205 dklist
= ["A396EFA4E24F", "A31667A8CEC1", "518b3354E760"]
208 print(f
"{prompt} Trying known backdoor keys...")
212 cmd
= f
"hf mf rdbl -c 4 --key {k} --blk 0"
213 print(f
"{prompt} `{cmd}`", end
='', flush
=True)
214 res
= p
.console(f
"{cmd}")
215 for line
in p
.grabbed_output
.split('\n'):
216 if " | " in line
and "# | s" not in line
:
222 print(f
" - fail [{res}]")
226 print(f
"{prompt} ! Unknown key, or card not detected.")
230 # +=============================================================================
231 # Extract data from block 0
234 # ==============================================================================
239 # We do this early so we can name the logfile!
240 uids
= blk0
[0:11] # UID string : "11 22 33 44"
241 uid
= int(uids
.replace(' ', ''), 16) # UID (value) : 0x11223344
242 startlog(uid
, append
=False)
245 lprint(f
"{prompt} UID BCC ++----- RF08 ID -----++")
246 lprint(f
"{prompt} ! ! SAK !! !!")
247 lprint(f
"{prompt} ! ! ! ATQA !! Fudan Sig !!")
248 lprint(f
"{prompt} !---------. !. !. !---. VV .---------------. VV")
249 # 0 12 15 18 24 27 45
251 # 00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF
252 lprint(f
"{prompt} Block 0 : {blk0}")
254 # --- decode block 0 ---
256 bcc
= int(blk0
[12:14], 16) # BCC
257 chk
= 0 # calculate checksum
258 for h
in uids
.split():
261 sak
= int(blk0
[15:17], 16) # SAK
262 atqa
= int(blk0
[18:23].replace(' ', ''), 16) # 0x7788
264 fida
= int(blk0
[24:26], 16) # Fudan ID 0x88
265 fidb
= int(blk0
[45:47], 16) # Fudan ID 0xFF
266 # fid = (fida<<8)|fidb # Fudan ID 0x88FF
268 hash = blk0
[27:44] # Fudan hash "99 AA BB CC DD EE"
270 type = f
"[{fida:02X}:{fidb:02X}]" # type/name
272 if fida
== 0x01 or fida
== 0x03 or fida
== 0x04:
273 type += " - Fudan FM11RF08S"
276 if fida
== 0x01 or fida
== 0x02 or fida
== 0x03:
277 type += " - Fudan FM11RF08"
279 elif fidb
== 0x91 or fidb
== 0x98:
280 type += " - Fudan FM11RF08 (never seen in the wild)"
283 type += " - Unknown (please report)"
285 # --- show results ---
289 lprint(f
"{prompt} UID/BCC : {uid:08X}/{bcc:02X} - ", end
='')
293 lprint(f
"fail. Expected {chk:02X}")
295 lprint(f
"{prompt} SAK : {sak:02X} - ", end
='')
297 lprint("NXP MIFARE TNP3xxx 1K")
299 lprint("NXP MIFARE CLASSIC 1k | Plus 1k | Ev1 1K")
301 lprint("NXP MIFARE Mini 0.3k")
303 lprint("NXP MIFARE Plus 2k")
305 lprint("NXP MIFARE Classic 4k | Plus 4k | Ev1 4k")
309 lprint(f
"{prompt} ATQA : {atqa:04X}") # show ATQA
310 lprint(f
"{prompt} Fudan ID : {type}") # show type
311 lprint(f
"{prompt} Fudan Sig: {hash}") # show ?Partial HMAC?
312 lprint(f
"{prompt} Dark Key : {dkey}") # show key
315 # +=============================================================================
318 # ==============================================================================
320 # Warning, this import causes a "double free or corruption" crash if the script is called twice...
321 # So for now we limit the import only when really needed
325 url
= "https://rfid.fm-uivs.com/nfcTools/api/M1KeyRest"
326 hdr
= "Content-Type: application/text; charset=utf-8"
327 post
= f
"{blk0.replace(' ', '')}"
330 lprint(f
"{prompt} Validator: `wget -q -O -"
331 f
" --header=\"{hdr}\""
332 f
" --post-data \"{post}\""
338 lprint(f
"{prompt} Check Fudan signature (requires internet)...")
340 headers
= {"Content-Type": "application/text; charset=utf-8"}
341 resp
= requests
.post(url
, headers
=headers
, data
=post
)
343 if resp
.status_code
!= 200:
344 lprint(f
"{prompt} HTTP Error {resp.status_code} - check request not processed")
347 r
= json
.loads(resp
.text
)
348 lprint(f
"{prompt} The man from Fudan, he say: {r['code']} - {r['message']}", end
='')
349 if r
['data'] is not None:
350 lprint(f
" {{{r['data']}}}")
355 lprint(f
"{prompt} ...Use --validate to perform Fudan signature check automatically")
358 # +=============================================================================
359 # Load keys from file
360 # If keys cannot be loaded AND --recover is specified, then run key recovery
363 # ==============================================================================
370 key
= [[b
'' for _
in range(2)] for _
in range(17)] # create a fresh array
373 lprint(f
"{prompt} Load Keys from file: |{keyfile}|")
376 with (open(keyfile
, "rb")) as fh
:
378 for sec
in range((16+2)-1):
379 key
[sec
][ab
] = fh
.read(6)
387 # +=============================================================================
388 # Run key recovery script
390 # ==============================================================================
394 badrk
= 0 # 'bad recovered key' count (ie. not recovered)
397 lprint(f
"{prompt} Running recovery script, ETA: Less than 30 minutes")
400 lprint(f
'{prompt} `-._,-\'"`-._,-"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,')
402 r
= recovery(quiet
=False)
403 keyfile
= r
['keyfile']
404 rkey
= r
['found_keys']
405 # fdump = r['dumpfile']
408 lprint(f
'{prompt} `-._,-\'"`-._,-"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,')
410 for k
in range(0, 16+1):
412 if rkey
[k
][ab
] == "":
414 lprint(f
"{prompt} Some keys were not recovered: ", end
='')
422 lprint(f
"[{kn}/", end
='')
423 lprint("A]" if ab
== 0 else "B]", end
='')
428 # +=============================================================================
432 # ==============================================================================
439 lprint(f
"{prompt} Check keys..")
441 for sec
in range(0, 16+1): # 16 normal, 1 dark
451 cmd
= f
"hf mf rdbl -c {ab} --key {key[sec][ab].hex()} --blk {bn}"
452 lprint(f
"{prompt} `{cmd}`", end
='', flush
=True)
454 res
= p
.console(f
"{cmd}", capture
=False)
455 lprint(" " * (3-len(str(bn
))), end
="")
457 lprint(" ... PASS", end
="")
459 lprint(" ... FAIL", end
="")
463 # check for Mifare Application Directory
464 if (sec
== 0) and (ab
== 0) \
465 and (key
[0][0] == b
'\xa0\xa1\xa2\xa3\xa4\xa5'):
472 lprint(f
"{prompt} ! {badk} bad key", end
='')
473 lprint("s exist" if badk
!= 1 else " exists")
477 lprint(f
"{prompt} All keys verified OK")
481 lprint(f
"{prompt} MAD key detected")
486 # +=============================================================================
487 # Read all block data - INCLUDING Dark blocks
490 # [=] # | sector 00 / 0x00 | ascii
491 # [=] ----+-------------------------------------------------+-----------------
492 # [=] 0 | 5C B4 9C A6 D2 08 04 00 04 59 92 25 BF 5F 70 90 | \........Y.%._p.
493 # ==============================================================================
499 blkn
= list(range(0, 63+1)) + list(range(128, 135+1))
501 # The user uses keyhole #1 (-a)
502 # The vendor uses keyhole #2 (-b)
503 # The thief uses keyhole #4 (backdoor)
505 rdbl
= f
"hf mf rdbl -c 4 --key {dkey} --blk"
508 lprint(prompt
+ " Load blocks {0..63, 128..135}[64+8=72] from the card")
513 print(f
"\r{prompt} `{cmd}`", end
='', flush
=True)
515 for retry
in range(5):
519 for line
in p
.grabbed_output
.split('\n'):
520 if " | " in line
and "# | s" not in line
:
528 data
.append(f
"{n:3d} | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- | ----------------")
534 # +=============================================================================
535 # Patch keys in to data
539 # 3 | 00 00 00 00 00 00 87 87 87 69 00 00 00 00 00 00 | .........i......
540 # ==============================================================================
541 def patchKeys(keyok
):
546 lprint(f
"{prompt} Patch keys in to data")
548 for sec
in range(0, 16+1):
549 blk
= (sec
* 4) + 3 # find "trailer" for this sector
551 if key
[sec
][0] == b
'':
552 keyA
= "-- -- -- -- -- -- "
554 kstr
= key
[sec
][0].hex()
555 keyA
= "".join([kstr
[i
:i
+2] + " " for i
in range(0, len(kstr
), 2)])
557 if key
[sec
][1] == b
'':
558 keyB
= "-- -- -- -- -- -- "
560 kstr
= key
[sec
][1].hex()
561 keyB
= "".join([kstr
[i
:i
+2] + " " for i
in range(0, len(kstr
), 2)])
563 data
[blk
] = data
[blk
][:6] + keyA
+ data
[blk
][24:36] + keyB
566 data
[blk
] = data
[blk
][:6] + "-- -- -- -- -- -- " + data
[blk
][24:36] + "-- -- -- -- -- --"
569 # +=============================================================================
573 # ==============================================================================
579 lprint(f
"{prompt} ===========")
580 lprint(f
"{prompt} Card Data")
581 lprint(f
"{prompt} ===========")
591 lprint(f
"{prompt} {sec:2d}:{data[cnt]}")
593 lprint(f
"{prompt} :{data[cnt]}")
596 if (cnt
% 4 == 0) and (n
!= blkn
[-1]): # Space between sectors
600 # +=============================================================================
601 # Let's try to detect a Bambu card by the date strings...
602 # ==============================================================================
605 dl
= bytes
.fromhex(data
[12][6:53]).decode('ascii').rstrip('\x00')
607 ds
= bytes
.fromhex(data
[13][6:41]).decode('ascii').rstrip('\x00')
612 # dl 2024_03_22_16_29
613 # yy y y m m d d h h m m
614 exp
= r
"20[2-3][0-9]_[0-1][0-9]_[0-3][0-9]_[0-2][0-9]_[0-5][0-9]"
617 if re
.search(exp
, dl
) and (ds
== dls
):
618 lprint(f
"{prompt} Bambu date strings detected.")
621 lprint(f
"{prompt} Bambu date strings not detected.")
625 # +=============================================================================
627 # https://github.com/Bambu-Research-Group/RFID-Tag-Guide/blob/main/README.md
631 # 3 | 00 00 00 00 00 00 87 87 87 69 00 00 00 00 00 00 | .........i......
632 # +=============================================================================
638 lprint(f
"{prompt} ===========")
639 lprint(f
"{prompt} Bambu Tag")
640 lprint(f
"{prompt} ===========")
642 lprint(f
"{prompt} Decompose as Bambu tag .. ", end
='')
644 MaterialVariantIdentifier_s
= bytes
.fromhex(data
[1][6:29]).decode('ascii').rstrip('\x00')
645 UniqueMaterialIdentifier_s
= bytes
.fromhex(data
[1][30:53]).decode('ascii').rstrip('\x00') # [**] 8not16
647 FilamentType_s
= bytes
.fromhex(data
[2][6:53]).decode('ascii').rstrip('\x00')
649 DetailedFilamentType_s
= bytes
.fromhex(data
[4][6:53]).decode('ascii').rstrip('\x00')
651 Colour_rgba
= int(data
[5][6:17].replace(' ', ''), 16)
652 SpoolWeight_g
= int(data
[5][21:23] + data
[5][18:20], 16)
653 Block5_7to8
= data
[5][24:29]
654 FilamentDiameter_mm
= struct
.unpack('f', bytes
.fromhex(data
[5][30:41].replace(' ', '')))[0]
655 Block5_12to15
= data
[5][42:50]
657 DryingTemperature_c
= int(data
[6][9:11] + data
[6][6: 8], 16)
658 DryingTime_h
= int(data
[6][15:17] + data
[6][12:14], 16)
659 BedTemperatureType_q
= int(data
[6][21:23] + data
[6][18:20], 16)
660 BedTemperature_c
= int(data
[6][27:29] + data
[6][24:26], 16)
661 MaxTemperatureForHotend_c
= int(data
[6][33:35] + data
[6][30:32], 16)
662 MinTemperatureForHotend_c
= int(data
[6][39:41] + data
[6][36:38], 16)
663 Block6_12to15
= data
[6][42:50]
665 # XCamInfo_x = bytes.fromhex(data[8][6:41].replace(' ', ''))
666 XCamInfo_x
= data
[8][6:41]
667 NozzleDiameter_q
= struct
.unpack('f', bytes
.fromhex(data
[8][42:53].replace(' ', '')))[0]
669 # TrayUID_s = bytes.fromhex(data[9][6:53]).decode('ascii').rstrip('\x00') #[**] !ascii
670 TrayUID_s
= data
[9][6:53]
672 Block10_0to3
= data
[10][6:17]
673 SppolWidth_um
= int(data
[10][21:23] + data
[14][18:20], 16)
674 Block10_6to15
= data
[10][24:50]
676 ProductionDateTime_s
= bytes
.fromhex(data
[12][6:53]).decode('ascii').rstrip('\x00')
678 ShortProductionDateTime_s
= bytes
.fromhex(data
[13][6:53]).decode('ascii').rstrip('\x00')
680 # Block14_0to3 = data[14][6:17]
681 FilamentLength_m
= int(data
[14][21:23] + data
[14][18:20], 16)
682 # Block14_6to15 = data[14][24:51]
684 # (16blocks * 16bytes = 256) * 8bits = 2048 bits
693 Hash
.append(data
[b
][6:53])
695 lprint("[offset:length]")
696 lprint(f
"{prompt} Block 1:")
697 lprint(f
"{prompt} [ 0: 8] MaterialVariantIdentifier_s = \"{MaterialVariantIdentifier_s}\"")
698 lprint(f
"{prompt} [ 8: 8] UniqueMaterialIdentifier_s = \"{UniqueMaterialIdentifier_s}\"")
699 lprint(f
"{prompt} Block 2:")
700 lprint(f
"{prompt} [ 0:16] FilamentType_s = \"{FilamentType_s}\"")
701 lprint(f
"{prompt} Block 4:")
702 lprint(f
"{prompt} [ 0:16] DetailedFilamentType_s = \"{DetailedFilamentType_s}\"")
703 lprint(f
"{prompt} Block 5:")
704 lprint(f
"{prompt} [ 0: 4] Colour_rgba = 0x{Colour_rgba:08X}")
705 lprint(f
"{prompt} [ 4: 2] SpoolWeight_g = {SpoolWeight_g}g")
706 lprint(f
"{prompt} [6: 2] Block5_7to8 = {{{Block5_7to8}}}")
707 lprint(f
"{prompt} [ 8: 4] FilamentDiameter_mm = {FilamentDiameter_mm}mm")
708 lprint(f
"{prompt} [12: 4] Block5_12to15 = {{{Block5_12to15}}}")
709 lprint(f
"{prompt} Block 6:")
710 lprint(f
"{prompt} [ 0: 2] DryingTemperature_c = {DryingTemperature_c}^C")
711 lprint(f
"{prompt} [ 2: 2] DryingTime_h = {DryingTime_h}hrs")
712 lprint(f
"{prompt} [ 4: 4] BedTemperatureType_q = {BedTemperatureType_q}")
713 lprint(f
"{prompt} [6: 2] BedTemperature_c = {BedTemperature_c}^C")
714 lprint(f
"{prompt} [ 8: 2] MaxTemperatureForHotend_c = {MaxTemperatureForHotend_c}^C")
715 lprint(f
"{prompt} [10: 2] MinTemperatureForHotend_c = {MinTemperatureForHotend_c}^C")
716 lprint(f
"{prompt} [12: 4] Block6_12to15 = {{{Block6_12to15}}}")
717 lprint(f
"{prompt} Block 8:")
718 lprint(f
"{prompt} [ 0:12] XCamInfo_x = {{{XCamInfo_x}}}")
719 lprint(f
"{prompt} [12: 4] NozzleDiameter_q = {NozzleDiameter_q:.6f}__")
720 lprint(f
"{prompt} Block 9:")
721 # lprint(f"{prompt} [ 0:16] TrayUID_s = \"{TrayUID_s}\"")
722 lprint(f
"{prompt} [ 0:16] TrayUID_s = {{{TrayUID_s}}} ; not ASCII")
723 lprint(f
"{prompt} Block 10:")
724 lprint(f
"{prompt} [ 0: 4] Block10_0to3 = {{{Block10_0to3}}}")
725 lprint(f
"{prompt} [ 4: 2] SppolWidth_um = {SppolWidth_um}um")
726 lprint(f
"{prompt} [6:10] Block10_6to15 = {{{Block10_6to15}}}")
727 lprint(f
"{prompt} Block 12:")
728 lprint(f
"{prompt} [ 0:16] ProductionDateTime_s = \"{ProductionDateTime_s}\"")
729 lprint(f
"{prompt} Block 13:")
730 lprint(f
"{prompt} [ 0:16] ShortProductionDateTime_s = \"{ShortProductionDateTime_s}\"")
731 lprint(f
"{prompt} Block 14:")
732 lprint(f
"{prompt} [ 0: 4] Block10_0to3 = {{{Block10_0to3}}}")
733 lprint(f
"{prompt} [ 4: 2] FilamentLength_m = {FilamentLength_m}m")
734 lprint(f
"{prompt} [6:10] Block10_6to15 = {{{Block10_6to15}}}")
736 lprint(f
"{prompt} Blocks {hblk}:")
737 for i
in range(0, len(hblk
)):
738 lprint(f
"{prompt} [ 0:16] HashBlock[{i:2d}] = {{{Hash[i]}}} // #{hblk[i]:2d}")
740 except Exception as e
:
741 lprint(f
"Failed: {e}")
744 # +=============================================================================
747 # 6 18 24 27 30 33 42 53
749 # 3 | 00 00 00 00 00 00 87 87 87 69 00 00 00 00 00 00 | .........i......
752 # ,-------------------.
753 # ( 2.2 : ACCESS BITS )
754 # `-------------------'
756 # The Access bits on both (used) Sectors is the same: 78 77 88
758 # Let's reorganise that according to the official spec Fig 9.
760 # ========== ===========
761 # 78 77 88 --> 78 87 87
762 # ab cd ef --> cb fa ed
764 # The second nybble of each byte is the inverse of the first nybble.
765 # It is there to trap tranmission errors, so we can just ignore it/them.
767 # So our Access Control value is : {c, f, e} == {7, 8, 8}
769 # Let's convert those nybbles to binary
773 # |||| ...and transpose them:
775 # |||`--- 100 - Block 0 Access bits
776 # ||`---- 100 - Block 1 Access bits
777 # |`----- 100 - Block 2 Access bits
778 # `------ 011 - Block 3 Access bits [Sector Trailer]
780 # Now we can use the lookup table [Table 3] to work out what we can do
781 # with the Sector Trailer (Block(S,3)):
783 # | Key A | | Access Bits | | Key B |
784 # | read ¦ write | | read ¦ write | | read ¦ write |
785 # +------¦-------+ +------¦-------+ +------¦-------+
786 # 000 : | -- ¦ KeyA | | KeyA ¦ -- | | KeyA ¦ KeyA |
787 # 001 : | -- ¦ KeyA | | KeyA ¦ KeyA | | KeyA ¦ KeyA | Transport Mode
788 # 010 : | -- ¦ -- | | KeyA ¦ -- | | KeyA ¦ -- |
790 # 011 : | -- ¦ KeyB | | A+B ¦ KeyB | | -- ¦ KeyB | <-- Our Card!
792 # 100 : | -- ¦ KeyB | | A+B ¦ -- | | -- ¦ KeyB |
793 # 101 : | -- ¦ -- | | A+B ¦ KeyB | | -- ¦ -- |
794 # 110 : | -- ¦ -- | | A+B ¦ -- | | -- ¦ -- | }__
795 # 111 : | -- ¦ -- | | A+B ¦ -- | | -- ¦ -- | } The Same!?
797 # Our card uses 011, for (both of) the (used) Sector Trailer(s). So:
798 # Both Key A and Key B can READ the Access Bits
799 # Key B can (additionally) WRITE to Key A, Key B (itself), and the Access Bits
801 # Then we can do a similar lookup for the 3 data Blocks (in this Sector)
802 # This time using [Table 4]
805 # | read ¦ write | Inc ¦ Dec |
806 # +------¦-------+------¦------+
807 # 000 : | A+B ¦ A+B | A+B ¦ A+B | Transport Mode
808 # 001 : | A+B ¦ -- | -- ¦ A+B |
809 # 010 : | A+B ¦ -- | -- ¦ -- |
810 # 011 : | KeyB ¦ KeyB | -- ¦ -- |
812 # 100 : | A+B ¦ KeyB | -- ¦ -- | <-- Our Card!
814 # 101 : | KeyB ¦ -- | -- ¦ -- |
815 # 110 : | A+B ¦ KeyB | KeyB ¦ A+B |
816 # 111 : | -- ¦ -- | -- ¦ -- |
818 # Our card uses 100, for all of the (used) Sectors. So:
819 # Both Key A and Key B can READ the Block
820 # Only Key B can WRITE to the Block
821 # The block cannot be used as a "counter" because:
822 # Neither key can perform increment nor decrement commands
825 # IF YOU PLAN TO CHANGE ACCESS BITS, RTFM, THERE IS MUCH TO CONSIDER !
826 # ==============================================================================
830 aclkh
= [] # key header
831 aclk
= [""] * 8 # key lookup
832 aclkx
= [] # key output
835 lprint(f
"{prompt} =====================")
836 lprint(f
"{prompt} Access Control List")
837 lprint(f
"{prompt} =====================")
839 aclkh
.append(" _______________________________________________________ ")
840 aclkh
.append("| | Sector Trailers |")
841 aclkh
.append("| |----------------------------------------------|")
842 aclkh
.append("| Sector |____Key_A_____||_Access_Bits__||____Key_B_____|")
843 aclkh
.append("| | read ¦ write || read ¦ write || read ¦ write |")
844 aclkh
.append("|--------+------¦-------++------¦-------++------¦-------|")
845 # "| xx | -- ¦ KeyA || KeyA ¦ -- || KeyA ¦ KeyA |"
846 aclk
[0] = "| -- ¦ KeyA || KeyA ¦ -- || KeyA ¦ KeyA | [000]" # noqa: E222
847 aclk
[1] = "| -- ¦ KeyA || KeyA ¦ KeyA || KeyA ¦ KeyA | [001]" # noqa: E222
848 aclk
[2] = "| -- ¦ -- || KeyA ¦ -- || KeyA ¦ -- | [010]" # noqa: E222
849 aclk
[3] = "| -- ¦ KeyB || A+B ¦ KeyB || -- ¦ KeyB | [011]" # noqa: E222
850 aclk
[4] = "| -- ¦ KeyB || A+B ¦ -- || -- ¦ KeyB | [100]" # noqa: E222
851 aclk
[5] = "| -- ¦ -- || A+B ¦ KeyB || -- ¦ -- | [101]" # noqa: E222
852 aclk
[6] = "| -- ¦ -- || A+B ¦ -- || -- ¦ -- | [110]" # noqa: E222 # yes, the same!?
853 aclk
[7] = "| -- ¦ -- || A+B ¦ -- || -- ¦ -- | [111]" # noqa: E222 # ...
855 acldh
= [] # data header
856 acld
= [""] * 8 # data lookup
857 acldx
= [] # data output
859 acldh
.append(" _____________________________________ ")
860 acldh
.append("| | Data Blocks |")
861 acldh
.append("| |-----------------------------|")
862 acldh
.append("| Block | Data || Counter |")
863 acldh
.append("| | read ¦ write || Inc ¦ Dec |")
864 acldh
.append("|-------+------¦-------++------¦------+")
865 # "| xxx | A+B ¦ A+B || A+B ¦ A+B | "
866 acld
[0] = "| A+B ¦ A+B || A+B ¦ A+B | [000]" # noqa: E222
867 acld
[1] = "| A+B ¦ -- || -- ¦ A+B | [001]" # noqa: E222
868 acld
[2] = "| A+B ¦ -- || -- ¦ -- | [010]" # noqa: E222
869 acld
[3] = "| KeyB ¦ KeyB || -- ¦ -- | [011]" # noqa: E222
870 acld
[4] = "| A+B ¦ KeyB || -- ¦ -- | [100]" # noqa: E222
871 acld
[5] = "| KeyB ¦ -- || -- ¦ -- | [101]" # noqa: E222
872 acld
[6] = "| A+B ¦ KeyB || KeyB ¦ A+B | [110]" # noqa: E222
873 acld
[7] = "| -- ¦ -- || -- ¦ -- | [111]" # noqa: E222
877 # --- calculate the ACL indices for each sector:block ---
883 sec
= sn
if sn
< 16 else sn
- 16
888 r0
= ((c
& (2**0)) << 2) |
((f
& (2**0)) << 1) |
((e
& (2**0)) ) # noqa: E202
889 r1
= ((c
& (2**1)) << 1) |
((f
& (2**1)) ) |
((e
& (2**1)) >> 1) # noqa: E202
890 r2
= ((c
& (2**2)) ) |
((f
& (2**2)) >> 1) |
((e
& (2**2)) >> 2) # noqa: E202
891 r3
= ((c
& (2**3)) >> 1) |
((f
& (2**3)) >> 2) |
((e
& (2**3)) >> 3) # noqa: E221
892 idx
[sec
] = [r0
, r1
, r2
, r3
]
894 # --- build the ACL conversion table ---
898 sec
= sn
if sn
< 16 else sn
- 16
901 aclkx
.append(f
"| {sn:2d} " + aclk
[idx
[sec
][bn
% 4]]
902 + f
" {{{d[24:32]}}} -> {{{d[27]}{d[31]}{d[30]}}}")
904 acldx
.append(f
"| {bn:3d} " + acld
[idx
[sec
][bn
% 4]])
906 # --- print it all out ---
908 lprint(f
"{prompt} {line}")
911 lprint(f
"{prompt} {line}")
913 lprint(f
"{prompt} | | ¦ || ¦ || ¦ |")
919 lprint(f
"{prompt} {line}")
922 lprint(f
"{prompt} {line}")
924 lprint(f
"{prompt} | | ¦ || ¦ |")
928 # +=============================================================================
932 # ==============================================================================
937 dump18
= f
"{dpath}hf-mf-{uid:08X}-dump18.bin"
940 lprint(f
"{prompt} Dump Card Data to file: {dump18}")
943 with
open(dump18
, 'wb') as f
:
947 b
= bytes
.fromhex(d
[6:53].replace(" ", "").replace("--", "FF"))
950 lprint(f
"{prompt} Bad data exists, and has been saved as 0xFF")
953 # +=============================================================================
956 # ==============================================================================
961 lprint(f
"{prompt} ====================================")
962 lprint(f
"{prompt} MiFare Application Directory (MAD)")
963 lprint(f
"{prompt} ====================================")
966 cmd
= f
"hf mf mad --verbose --file {dump18}"
967 print(f
"{prompt} `{cmd}`")
970 lprint(f
'{prompt} `-._,-\'"`-._,-"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,')
975 for line
in p
.grabbed_output
.split('\n'):
978 lprint(f
'{prompt} `-._,-\'"`-._,-"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,')
981 # ++============================================================================
982 if __name__
== "__main__":