Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CLI commands to dump and clone tag #201

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
- Added command to check keys of multiple sectors at once (@taichunmin)
- Fixed unused target key type parameter for nested (@petepriority)
- Skip already used items `hf mf elog --decrypt` (@p-l-)
Expand Down
142 changes: 142 additions & 0 deletions software/script/chameleon_cli_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -1011,6 +1011,148 @@ def on_exec(self, args: argparse.Namespace):
else:
print(f" - {CR}Write fail.{C0}")

@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):
Expand Down
Loading