| 1 | #!/usr/bin/env python |
| 2 | # -*- coding: utf-8 -*- |
| 3 | |
| 4 | # mfdread.py - Mifare dumps parser in human readable format |
| 5 | # Pavel Zhovner <pavel@zhovner.com> |
| 6 | # https://github.com/zhovner/mfdread |
| 7 | |
| 8 | |
| 9 | |
| 10 | import codecs |
| 11 | import sys |
| 12 | import copy |
| 13 | from struct import unpack |
| 14 | from datetime import datetime |
| 15 | from bitstring import BitArray |
| 16 | |
| 17 | class Options: |
| 18 | FORCE_1K = False |
| 19 | |
| 20 | if len(sys.argv) == 1: |
| 21 | print(''' |
| 22 | ------------------ |
| 23 | Usage: mfdread.py ./dump.mfd |
| 24 | Mifare dumps reader. |
| 25 | ''') |
| 26 | sys.exit(); |
| 27 | |
| 28 | def d(bytes): |
| 29 | decoded = codecs.decode(bytes, "hex") |
| 30 | try: |
| 31 | return str(decoded, "utf-8").rstrip('\0') |
| 32 | except: |
| 33 | return "" |
| 34 | |
| 35 | |
| 36 | class bashcolors: |
| 37 | BLUE = '\033[34m' |
| 38 | RED = '\033[91m' |
| 39 | GREEN = '\033[32m' |
| 40 | WARNING = '\033[93m' |
| 41 | ENDC = '\033[0m' |
| 42 | |
| 43 | |
| 44 | def accbits_for_blocknum(accbits_str, blocknum): |
| 45 | ''' |
| 46 | Decodes the access bit string for block "blocknum". |
| 47 | Returns the three access bits for the block or False if the |
| 48 | inverted bits do not match the access bits. |
| 49 | ''' |
| 50 | bits = BitArray([0]) |
| 51 | inverted = BitArray([0]) |
| 52 | # Block 0 access bits |
| 53 | if blocknum == 0: |
| 54 | bits = BitArray([accbits_str[11], accbits_str[23], accbits_str[19]]) |
| 55 | inverted = BitArray([accbits_str[7], accbits_str[3], accbits_str[15]]) |
| 56 | |
| 57 | # Block 0 access bits |
| 58 | elif blocknum == 1: |
| 59 | bits = BitArray([accbits_str[10], accbits_str[22], accbits_str[18]]) |
| 60 | inverted = BitArray([accbits_str[6], accbits_str[2], accbits_str[14]]) |
| 61 | # Block 0 access bits |
| 62 | elif blocknum == 2: |
| 63 | bits = BitArray([accbits_str[9], accbits_str[21], accbits_str[17]]) |
| 64 | inverted = BitArray([accbits_str[5], accbits_str[1], accbits_str[13]]) |
| 65 | # Sector trailer / Block 3 access bits |
| 66 | elif blocknum == 3: |
| 67 | bits = BitArray([accbits_str[8], accbits_str[20], accbits_str[16]]) |
| 68 | inverted = BitArray([accbits_str[4], accbits_str[0], accbits_str[12]]) |
| 69 | |
| 70 | # Check the decoded bits |
| 71 | inverted.invert() |
| 72 | if bits.bin == inverted.bin: |
| 73 | return bits |
| 74 | else: |
| 75 | return False |
| 76 | |
| 77 | |
| 78 | def accbits_to_permission_sector(accbits): |
| 79 | permissions = { |
| 80 | '000': "- A | A - | A A [read B]", |
| 81 | '010': "- - | A - | A - [read B]", |
| 82 | '100': "- B | A/B - | - B", |
| 83 | '110': "- - | A/B - | - -", |
| 84 | '001': "- A | A A | A A [transport]", |
| 85 | '011': "- B | A/B B | - B", |
| 86 | '101': "- - | A/B B | - -", |
| 87 | '111': "- - | A/B - | - -", |
| 88 | } |
| 89 | if isinstance(accbits, BitArray): |
| 90 | return permissions.get(accbits.bin, "unknown") |
| 91 | else: |
| 92 | return "" |
| 93 | |
| 94 | def accbits_to_permission_data(accbits): |
| 95 | permissions = { |
| 96 | '000': "A/B | A/B | A/B | A/B [transport]", |
| 97 | '010': "A/B | - | - | - [r/w]", |
| 98 | '100': "A/B | B | - | - [r/w]", |
| 99 | '110': "A/B | B | B | A/B [value]", |
| 100 | '001': "A/B | - | - | A/B [value]", |
| 101 | '011': " B | B | - | - [r/w]", |
| 102 | '101': " B | - | - | - [r/w]", |
| 103 | '111': " - | - | - | - [r/w]", |
| 104 | } |
| 105 | if isinstance(accbits, BitArray): |
| 106 | return permissions.get(accbits.bin, "unknown") |
| 107 | else: |
| 108 | return "" |
| 109 | |
| 110 | |
| 111 | def accbit_info(accbits): |
| 112 | ''' |
| 113 | Returns a dictionary of a access bits for all three blocks in a sector. |
| 114 | If the access bits for block could not be decoded properly, the value is set to False. |
| 115 | ''' |
| 116 | decAccbits = {} |
| 117 | # Decode access bits for all 4 blocks of the sector |
| 118 | for i in range(0, 4): |
| 119 | decAccbits[i] = accbits_for_blocknum(accbits, i) |
| 120 | return decAccbits |
| 121 | |
| 122 | |
| 123 | |
| 124 | |
| 125 | |
| 126 | def print_info(data): |
| 127 | |
| 128 | blocksmatrix = [] |
| 129 | blockrights = {} |
| 130 | |
| 131 | # determine what dump we get 1k or 4k |
| 132 | if len(data) == 4096: |
| 133 | cardsize = 64 |
| 134 | elif len(data) == 1024: |
| 135 | cardsize = 16 |
| 136 | else: |
| 137 | print("Wrong file size: %d bytes.\nOnly 1024 or 4096 allowed." % len(data)) |
| 138 | sys.exit(); |
| 139 | |
| 140 | if Options.FORCE_1K: |
| 141 | cardsize = 16 |
| 142 | |
| 143 | # read all sectors |
| 144 | for i in range(0, cardsize): |
| 145 | start = i * 64 |
| 146 | end = (i + 1) * 64 |
| 147 | sector = data[start:end] |
| 148 | sector = codecs.encode(sector, 'hex') |
| 149 | if not type(sector) is str: |
| 150 | sector = str(sector, 'ascii') |
| 151 | blocksmatrix.append([sector[x:x+32] for x in range(0, len(sector), 32)]) |
| 152 | |
| 153 | blocksmatrix_clear = copy.deepcopy(blocksmatrix) |
| 154 | # add colors for each keyA, access bits, KeyB |
| 155 | for c in range(0, len(blocksmatrix)): |
| 156 | # Fill in the access bits |
| 157 | blockrights[c] = accbit_info(BitArray('0x'+blocksmatrix[c][3][12:20])) |
| 158 | # Prepare colored output of the sector trailor |
| 159 | keyA = bashcolors.RED + blocksmatrix[c][3][0:12] + bashcolors.ENDC |
| 160 | accbits = bashcolors.GREEN + blocksmatrix[c][3][12:20] + bashcolors.ENDC |
| 161 | keyB = bashcolors.BLUE + blocksmatrix[c][3][20:32] + bashcolors.ENDC |
| 162 | blocksmatrix[c][3] = keyA + accbits + keyB |
| 163 | |
| 164 | |
| 165 | print("File size: %d bytes. Expected %d sectors" %(len(data),cardsize)) |
| 166 | print("\n\tUID: " + blocksmatrix[0][0][0:8]) |
| 167 | print("\tBCC: " + blocksmatrix[0][0][8:10]) |
| 168 | print("\tSAK: " + blocksmatrix[0][0][10:12]) |
| 169 | print("\tATQA: " + blocksmatrix[0][0][12:14]) |
| 170 | print(" %sKey A%s %sAccess Bits%s %sKey B%s" %(bashcolors.RED,bashcolors.ENDC,bashcolors.GREEN,bashcolors.ENDC,bashcolors.BLUE,bashcolors.ENDC)) |
| 171 | print("╔═════════╦═════╦══════════════════════════════════╦════════╦═════════════════════════════════════╗") |
| 172 | print("║ Sector ║Block║ Data ║ Access ║ A | Acc. | B ║") |
| 173 | print("║ ║ ║ ║ ║ r w | r w | r w [info] ║") |
| 174 | print("║ ║ ║ ║ ║ r | w | i | d/t/r ║") |
| 175 | for q in range(0, len(blocksmatrix)): |
| 176 | print("╠═════════╬═════╬══════════════════════════════════╬════════╬═════════════════════════════════════╣") |
| 177 | |
| 178 | # z is the block in each sector |
| 179 | for z in range(0, len(blocksmatrix[q])): |
| 180 | # Format the access bits. Print ERR in case of an error |
| 181 | accbits = "" |
| 182 | if isinstance(blockrights[q][z], BitArray): |
| 183 | accbits = bashcolors.GREEN + blockrights[q][z].bin + bashcolors.ENDC |
| 184 | else: |
| 185 | accbits = bashcolors.WARNING + "ERR" + bashcolors.ENDC |
| 186 | |
| 187 | if (q == 0 and z == 0): |
| 188 | permissions = "-" |
| 189 | elif (z == 3): |
| 190 | permissions = accbits_to_permission_sector(blockrights[q][z]) |
| 191 | else: |
| 192 | permissions = accbits_to_permission_data(blockrights[q][z]) |
| 193 | |
| 194 | # Add Padding after the sector number |
| 195 | padLen = max(1, 5 - len(str(q))) |
| 196 | padding = " " * padLen |
| 197 | # Only print the sector number in the second third row |
| 198 | if (z == 2): |
| 199 | print("║ %d%s║ %d ║ %s ║ %s ║ %-35s ║ %s" %(q,padding,z,blocksmatrix[q][z], accbits, permissions, d(blocksmatrix_clear[q][z]))) |
| 200 | else: |
| 201 | print("║ ║ %d ║ %s ║ %s ║ %-35s ║ %s" %(z,blocksmatrix[q][z], accbits, permissions, d(blocksmatrix_clear[q][z]))) |
| 202 | print("╚═════════╩═════╩══════════════════════════════════╩════════╩═════════════════════════════════════╝") |
| 203 | |
| 204 | |
| 205 | def main(args): |
| 206 | if args[0] == '-n': |
| 207 | args.pop(0) |
| 208 | bashcolors.BLUE = "" |
| 209 | bashcolors.RED = "" |
| 210 | bashcolors.GREEN = "" |
| 211 | bashcolors.WARNING = "" |
| 212 | bashcolors.ENDC = "" |
| 213 | |
| 214 | if args[0] == '-1': |
| 215 | args.pop(0) |
| 216 | Options.FORCE_1K = True |
| 217 | |
| 218 | filename = args[0] |
| 219 | with open(filename, "rb") as f: |
| 220 | data = f.read() |
| 221 | print_info(data) |
| 222 | |
| 223 | if __name__ == "__main__": |
| 224 | main(sys.argv[1:]) |