Merge pull request #201 from rickNmorty2/enh-clone

Add CLI commands to dump and clone tag
This commit is contained in:
GameTec-live
2026-02-07 20:47:38 +01:00
committed by GitHub
2 changed files with 143 additions and 0 deletions
+1
View File
@@ -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
+142
View File
@@ -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):