diff --git a/CHANGELOG.md b/CHANGELOG.md index 6584340..71eeaa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ All notable changes to this project will be documented in this file. This project uses the changelog in accordance with [keepchangelog](http://keepachangelog.com/). Please use this to write notable changes, which is not the same as git commit log... ## [unreleased][unreleased] + - Added commands to dump and clone Mifare tags - Fix bad missing tools warning (@suut) - Fix for FAST_READ command for nfc - mf0 tags - Rewrite of the dynamic and static locks logic for NTAG213, NTAG215 and NTAG216; we shouldn't take into account the block lock bits diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index 31855b0..9d64d87 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -1772,6 +1772,148 @@ class HFMFView(MF1AuthArgsUnit): raise ArgsParserError("Missing args. Specify --dump-file (-d) or --key-file (-k)") print_mem_dump(data, 16) +@hf_mf.command('dump') +class HFMFDump(MF1AuthArgsUnit): + def args_parser(self) -> ArgumentParserNoExit: + parser = ArgumentParserNoExit() + parser.description = 'Mifare Classic dump tag' + parser.add_argument('-t', '--dump-file-type', type=str, required=False, help="Dump file content type", choices=['bin', 'hex']) + parser.add_argument('-f', '--dump-file', type=argparse.FileType("wb"), required=True, + help="Dump file to write data from tag") + parser.add_argument('-d', '--dic', type=argparse.FileType("r"), required=True, + help="Read keys (to communicate with tag to dump) from .dic format file") + return parser + + def on_exec(self, args: argparse.Namespace): + # check dump type + if args.dump_file_type is None: + if args.dump_file.name.endswith('.bin'): + content_type = 'bin' + elif args.dump_file.name.endswith('.eml'): + content_type = 'hex' + else: + raise Exception("Unknown file format, Specify content type with -t option") + else: + content_type = args.dump_file_type + + # read keys from file + keys = [bytes.fromhex(line[:-1]) for line in args.dic.readlines()] + + # data to write from dump file + buffer = bytearray() + + # iterate over sectors + for s in range(16): + # try all keys for this sector + typ = None + for key in keys: + # first try key B + try: + self.cmd.mf1_read_one_block(4*s, MfcKeyType.B, key) + typ = MfcKeyType.B + break + except UnexpectedResponseError: + # ignore read errors at this stage as we want to try key A + pass + # try with key A if B was unsuccessful + try: + self.cmd.mf1_read_one_block(4*s, MfcKeyType.A, key) + typ = MfcKeyType.A + break + except UnexpectedResponseError: + pass + else: + raise Exception(f"No key found for sector {s}") + # iterate over blocks + for b in range(4): + block_data = self.cmd.mf1_read_one_block(4*s + b, typ, key) + # add data to buffer + if content_type == 'bin': + buffer.extend(block_data) + elif content_type == 'hex': + buffer.extend(block_data.hex().encode("utf-8")) + # write buffer to file + args.dump_file.write(buffer) + +@hf_mf.command('clone') +class HFMFClone(MF1AuthArgsUnit): + def args_parser(self) -> ArgumentParserNoExit: + parser = ArgumentParserNoExit() + parser.description = 'Mifare Classic clone tag from dump' + parser.add_argument('-t', '--dump-file-type', type=str, required=False, help="Dump file content type", choices=['bin', 'hex']) + parser.add_argument('-a', '--clone-access', type=bool, default=False, help="Write ACL from original dump too (/!\ could brick your tag)") + parser.add_argument('-f', '--dump-file', type=argparse.FileType("rb"), required=True, + help="Dump file containing data to write on new tag") + parser.add_argument('-d', '--dic', type=argparse.FileType("r"), required=True, + help="Read keys (to communicate with tag to write) from .dic format file") + return parser + + def on_exec(self, args: argparse.Namespace): + if args.dump_file_type is None: + if args.dump_file.name.endswith('.bin'): + content_type = 'bin' + elif args.dump_file.name.endswith('.eml'): + content_type = 'hex' + else: + raise Exception("Unknown file format, Specify content type with -t option") + else: + content_type = args.dump_file_type + + # data to write from dump file + buffer = bytearray() + if content_type == 'bin': + buffer.extend(args.dump_file.read()) + if content_type == 'hex': + buffer.extend(bytearray.fromhex(args.dump_file.read().decode())) + if len(buffer) % 16 != 0: + raise Exception("Data block not align for 16 bytes") + if len(buffer) / 16 > 256: + raise Exception("Data block memory overflow") + + # keys to use from file + keys = [bytes.fromhex(line[:-1]) for line in args.dic.readlines()] + + # iterate over sectors + for s in range(16): + # try all keys for this sector + keyA, keyB = None, None + for key in keys: + # first try key B + try: + self.cmd.mf1_read_one_block(4*s, MfcKeyType.B, key) + keyB = key + except UnexpectedResponseError: + # ignore read errors at this stage as we want to try key A + pass + # try with key A if B was unsuccessful + try: + self.cmd.mf1_read_one_block(4*s, MfcKeyType.A, key) + keyA = key + except UnexpectedResponseError: + pass + # both keys were found, no need to continue iterating + if keyA and keyB: + break + # neither A or B key was found + if not keyA and not keyB: + raise Exception(f"No key found for sector {s}") + # iterate over blocks + for b in range(4): + block_data = buffer[(4*s+b)*16:(4*s+b+1)*16] + # special case for last block of each sector + if b == 3: + # check ACL option + if not args.clone_access: + # if option is not specified, use generic ACL to be able to write again + block_data = block_data[:6] + bytes.fromhex("ff0780") + block_data[9:] + try: + # try B key first + self.cmd.mf1_write_one_block(4*s + b, MfcKeyType.B, keyB, block_data) + continue + except UnexpectedResponseError: + pass + self.cmd.mf1_write_one_block(4*s + b, MfcKeyType.A, keyA, block_data) + @hf_mf.command('value') class HFMFVALUE(ReaderRequiredUnit):