Files
ChameleonUltra/software/script/chameleon_cli_unit.py
2026-03-03 16:24:26 +01:00

6737 lines
245 KiB
Python

import binascii
import glob
import math
import os
import tempfile
import re
import subprocess
import argparse
import timeit
import sys
import time
import serial.tools.list_ports
import threading
import random
import struct
import queue
from enum import Enum
from multiprocessing import Pool, cpu_count
from typing import Union
from pathlib import Path
from platform import uname
from datetime import datetime
import hardnested_utils
import chameleon_com
import chameleon_cmd
from chameleon_utils import (
ArgumentParserNoExit,
ArgsParserError,
UnexpectedResponseError,
execute_tool,
tqdm_if_exists,
print_key_table,
)
from chameleon_utils import CLITree
from chameleon_utils import CR, CG, CB, CC, CY, C0, color_string
from chameleon_utils import print_mem_dump
from chameleon_enum import Command, Status, SlotNumber, TagSenseType, TagSpecificType
from chameleon_enum import (
MifareClassicWriteMode,
MifareClassicPrngType,
MifareClassicDarksideStatus,
MfcKeyType,
)
from chameleon_enum import MifareUltralightWriteMode
from chameleon_enum import (
AnimationMode,
ButtonPressFunction,
ButtonType,
MfcValueBlockOperator,
)
from chameleon_enum import HIDFormat
from crypto1 import Crypto1
# NXP IDs based on https://www.nxp.com/docs/en/application-note/AN10833.pdf
type_id_SAK_dict = {
0x00: "MIFARE Ultralight Classic/C/EV1/Nano | NTAG 2xx",
0x08: "MIFARE Classic 1K | Plus SE 1K | Plug S 2K | Plus X 2K",
0x09: "MIFARE Mini 0.3k",
0x10: "MIFARE Plus 2K",
0x11: "MIFARE Plus 4K",
0x18: "MIFARE Classic 4K | Plus S 4K | Plus X 4K",
0x19: "MIFARE Classic 2K",
0x20: "MIFARE Plus EV1/EV2 | DESFire EV1/EV2/EV3 | DESFire Light | NTAG 4xx | "
"MIFARE Plus S 2/4K | MIFARE Plus X 2/4K | MIFARE Plus SE 1K",
0x28: "SmartMX with MIFARE Classic 1K",
0x38: "SmartMX with MIFARE Classic 4K",
}
default_cwd = Path.cwd() / Path(__file__).with_name("bin")
def load_key_file(import_key, keys):
"""
Load key file and append its content to the provided set of keys.
Each key is expected to be on a new line in the file.
"""
with open(import_key.name, "rb") as file:
keys.update(
line.encode("utf-8") for line in file.read().decode("utf-8").splitlines()
)
return keys
def load_dic_file(import_dic, keys):
return keys
def check_tools():
missing_tools = []
for tool in (
"staticnested",
"nested",
"darkside",
"mfkey32v2",
"staticnested_1nt",
"staticnested_2x1nt_rf08s",
"staticnested_2x1nt_rf08s_1key",
):
if any(default_cwd.glob(f"{tool}*")):
continue
else:
missing_tools.append(tool)
if missing_tools:
missing_tool_str = ", ".join(missing_tools)
warn_str = f"Warning, {missing_tool_str} not found. Corresponding commands will not work as intended."
print(color_string((CR, warn_str)))
class BaseCLIUnit:
def __init__(self):
# new a device command transfer and receiver instance(Send cmd and receive response)
self._device_com: Union[chameleon_com.ChameleonCom, None] = None
self._device_cmd: Union[chameleon_cmd.ChameleonCMD, None] = None
@property
def device_com(self) -> chameleon_com.ChameleonCom:
assert self._device_com is not None
return self._device_com
@device_com.setter
def device_com(self, com):
self._device_com = com
self._device_cmd = chameleon_cmd.ChameleonCMD(self._device_com)
@property
def cmd(self) -> chameleon_cmd.ChameleonCMD:
assert self._device_cmd is not None
return self._device_cmd
def args_parser(self) -> ArgumentParserNoExit:
"""
CMD unit args.
:return:
"""
raise NotImplementedError("Please implement this")
def before_exec(self, args: argparse.Namespace):
"""
Call a function before exec cmd.
:return: function references
"""
return True
def on_exec(self, args: argparse.Namespace):
"""
Call a function on cmd match.
:return: function references
"""
raise NotImplementedError("Please implement this")
def after_exec(self, args: argparse.Namespace):
"""
Call a function after exec cmd.
:return: function references
"""
return True
@staticmethod
def sub_process(cmd, cwd=default_cwd):
class ShadowProcess:
def __init__(self):
self.output = ""
self.time_start = timeit.default_timer()
self._process = subprocess.Popen(
cmd,
cwd=cwd,
shell=True,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
)
threading.Thread(target=self.thread_read_output).start()
def thread_read_output(self):
while self._process.poll() is None:
assert self._process.stdout is not None
data = self._process.stdout.read(1024)
if len(data) > 0:
self.output += data.decode(encoding="utf-8")
def get_time_distance(self, ms=True):
if ms:
return round((timeit.default_timer() - self.time_start) * 1000, 2)
else:
return round(timeit.default_timer() - self.time_start, 2)
def is_running(self):
return self._process.poll() is None
def is_timeout(self, timeout_ms):
time_distance = self.get_time_distance()
if time_distance > timeout_ms:
return True
return False
def get_output_sync(self):
return self.output
def get_ret_code(self):
return self._process.poll()
def stop_process(self):
# noinspection PyBroadException
try:
self._process.kill()
except Exception:
pass
def get_process(self):
return self._process
def wait_process(self):
return self._process.wait()
return ShadowProcess()
class DeviceRequiredUnit(BaseCLIUnit):
"""
Make sure of device online
"""
def before_exec(self, args: argparse.Namespace):
ret = self.device_com.isOpen()
if ret:
return True
else:
print("Please connect to chameleon device first (use 'hw connect').")
return False
class ReaderRequiredUnit(DeviceRequiredUnit):
"""
Make sure of device enter to reader mode.
"""
def before_exec(self, args: argparse.Namespace):
if not super().before_exec(args):
return False
if self.cmd.is_device_reader_mode():
return True
self.cmd.set_device_reader_mode(True)
print("Switch to { Tag Reader } mode successfully.")
return True
class SlotIndexArgsUnit(DeviceRequiredUnit):
@staticmethod
def add_slot_args(parser: ArgumentParserNoExit, mandatory=False):
slot_choices = [x.value for x in SlotNumber]
help_str = f"Slot Index: {slot_choices} Default: active slot"
parser.add_argument(
"-s",
"--slot",
type=int,
required=mandatory,
help=help_str,
metavar="<1-8>",
choices=slot_choices,
)
return parser
class SlotIndexArgsAndGoUnit(SlotIndexArgsUnit):
def before_exec(self, args: argparse.Namespace):
if super().before_exec(args):
self.prev_slot_num = SlotNumber.from_fw(self.cmd.get_active_slot())
if args.slot is not None:
self.slot_num = args.slot
if self.slot_num != self.prev_slot_num:
self.cmd.set_active_slot(self.slot_num)
else:
self.slot_num = self.prev_slot_num
return True
return False
def after_exec(self, args: argparse.Namespace):
if self.prev_slot_num != self.slot_num:
self.cmd.set_active_slot(self.prev_slot_num)
class SenseTypeArgsUnit(DeviceRequiredUnit):
@staticmethod
def add_sense_type_args(parser: ArgumentParserNoExit):
sense_group = parser.add_mutually_exclusive_group(required=True)
sense_group.add_argument("--hf", action="store_true", help="HF type")
sense_group.add_argument("--lf", action="store_true", help="LF type")
return parser
class MF1AuthArgsUnit(ReaderRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.add_argument(
"--blk",
"--block",
type=int,
required=True,
metavar="<dec>",
help="The block where the key of the card is known",
)
type_group = parser.add_mutually_exclusive_group()
type_group.add_argument(
"-a", "-A", action="store_true", help="Known key is A key (default)"
)
type_group.add_argument(
"-b", "-B", action="store_true", help="Known key is B key"
)
parser.add_argument(
"-k",
"--key",
type=str,
required=True,
metavar="<hex>",
help="tag sector key",
)
return parser
def get_param(self, args):
class Param:
def __init__(self):
self.block = args.blk
self.type = MfcKeyType.B if args.b else MfcKeyType.A
key: str = args.key
if not re.match(r"^[a-fA-F0-9]{12}$", key):
raise ArgsParserError("key must include 12 HEX symbols")
self.key: bytearray = bytearray.fromhex(key)
return Param()
class HF14AAntiCollArgsUnit(DeviceRequiredUnit):
@staticmethod
def add_hf14a_anticoll_args(parser: ArgumentParserNoExit):
parser.add_argument("--uid", type=str, metavar="<hex>", help="Unique ID")
parser.add_argument(
"--atqa", type=str, metavar="<hex>", help="Answer To Request"
)
parser.add_argument(
"--sak", type=str, metavar="<hex>", help="Select AcKnowledge"
)
ats_group = parser.add_mutually_exclusive_group()
ats_group.add_argument(
"--ats", type=str, metavar="<hex>", help="Answer To Select"
)
ats_group.add_argument(
"--delete-ats", action="store_true", help="Delete Answer To Select"
)
return parser
def update_hf14a_anticoll(self, args, uid, atqa, sak, ats):
anti_coll_data_changed = False
change_requested = False
if args.uid is not None:
change_requested = True
uid_str: str = args.uid.strip()
if re.match(r"[a-fA-F0-9]+", uid_str) is not None:
new_uid = bytes.fromhex(uid_str)
if len(new_uid) not in [4, 7, 10]:
raise Exception("UID length error")
else:
raise Exception("UID must be hex")
if new_uid != uid:
uid = new_uid
anti_coll_data_changed = True
else:
print(color_string((CY, "Requested UID already set")))
if args.atqa is not None:
change_requested = True
atqa_str: str = args.atqa.strip()
if re.match(r"[a-fA-F0-9]{4}", atqa_str) is not None:
new_atqa = bytes.fromhex(atqa_str)
else:
raise Exception("ATQA must be 4-byte hex")
if new_atqa != atqa:
atqa = new_atqa
anti_coll_data_changed = True
else:
print(color_string((CY, "Requested ATQA already set")))
if args.sak is not None:
change_requested = True
sak_str: str = args.sak.strip()
if re.match(r"[a-fA-F0-9]{2}", sak_str) is not None:
new_sak = bytes.fromhex(sak_str)
else:
raise Exception("SAK must be 2-byte hex")
if new_sak != sak:
sak = new_sak
anti_coll_data_changed = True
else:
print(color_string((CY, "Requested SAK already set")))
if (args.ats is not None) or args.delete_ats:
change_requested = True
if args.delete_ats:
new_ats = b""
else:
ats_str: str = args.ats.strip()
if re.match(r"[a-fA-F0-9]+", ats_str) is not None:
new_ats = bytes.fromhex(ats_str)
else:
raise Exception("ATS must be hex")
if new_ats != ats:
ats = new_ats
anti_coll_data_changed = True
else:
print(color_string((CY, "Requested ATS already set")))
if anti_coll_data_changed:
self.cmd.hf14a_set_anti_coll_data(uid, atqa, sak, ats)
return change_requested, anti_coll_data_changed, uid, atqa, sak, ats
class MFUAuthArgsUnit(ReaderRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
def key_parser(key: str) -> bytes:
try:
key = bytes.fromhex(key)
except:
raise ValueError("Key should be a hex string")
if len(key) not in [4, 16]:
raise ValueError("Key should either be 4 or 16 bytes long")
elif len(key) == 16:
raise ValueError("Ultralight-C authentication isn't supported yet")
return key
parser.add_argument(
"-k",
"--key",
type=key_parser,
metavar="<hex>",
help="Authentication key (EV1/NTAG 4 bytes).",
)
parser.add_argument(
"-l",
action="store_true",
dest="swap_endian",
help="Swap endianness of the key.",
)
return parser
def get_param(self, args):
key = args.key
if key is not None and args.swap_endian:
key = bytearray(key)
for i in range(len(key)):
key[i] = key[len(key) - 1 - i]
key = bytes(key)
class Param:
def __init__(self, key):
self.key = key
return Param(key)
def on_exec(self, args: argparse.Namespace):
raise NotImplementedError("Please implement this")
class LFEMIdArgsUnit(DeviceRequiredUnit):
@staticmethod
def add_card_arg(parser: ArgumentParserNoExit, required=False):
parser.add_argument(
"--id", type=str, required=required, help="EM410x tag id", metavar="<hex>"
)
return parser
def before_exec(self, args: argparse.Namespace):
if not super().before_exec(args):
return False
if args.id is None or not re.match(
r"^([a-fA-F0-9]{10}|[a-fA-F0-9]{26})$", args.id
):
raise ArgsParserError("ID must include 10 or 26 HEX symbols")
return True
def args_parser(self) -> ArgumentParserNoExit:
raise NotImplementedError("Please implement this")
def on_exec(self, args: argparse.Namespace):
raise NotImplementedError("Please implement this")
class LFHIDIdArgsUnit(DeviceRequiredUnit):
@staticmethod
def add_card_arg(parser: ArgumentParserNoExit, required=False):
formats = [x.name for x in HIDFormat]
parser.add_argument(
"-f",
"--format",
type=str,
required=required,
help="HIDProx card format",
metavar="",
choices=formats,
)
parser.add_argument(
"--fc",
type=int,
required=False,
help="HIDProx tag facility code",
metavar="<int>",
)
parser.add_argument(
"--cn",
type=int,
required=required,
help="HIDProx tag card number",
metavar="<int>",
)
parser.add_argument(
"--il",
type=int,
required=False,
help="HIDProx tag issue level",
metavar="<int>",
)
parser.add_argument(
"--oem", type=int, required=False, help="HIDProx tag OEM", metavar="<int>"
)
return parser
@staticmethod
def check_limits(
format: int,
fc: Union[int, None],
cn: Union[int, None],
il: Union[int, None],
oem: Union[int, None],
):
limits = {
HIDFormat.H10301: [0xFF, 0xFFFF, 0, 0],
HIDFormat.IND26: [0xFFF, 0xFFF, 0, 0],
HIDFormat.IND27: [0x1FFF, 0x3FFF, 0, 0],
HIDFormat.INDASC27: [0x1FFF, 0x3FFF, 0, 0],
HIDFormat.TECOM27: [0x7FF, 0xFFFF, 0, 0],
HIDFormat.W2804: [0xFF, 0x7FFF, 0, 0],
HIDFormat.IND29: [0x1FFF, 0xFFFF, 0, 0],
HIDFormat.ATSW30: [0xFFF, 0xFFFF, 0, 0],
HIDFormat.ADT31: [0xF, 0x7FFFFF, 0, 0],
HIDFormat.HCP32: [0, 0x3FFF, 0, 0],
HIDFormat.HPP32: [0xFFF, 0x7FFFF, 0, 0],
HIDFormat.KASTLE: [0xFF, 0xFFFF, 0x1F, 0],
HIDFormat.KANTECH: [0xFF, 0xFFFF, 0, 0],
HIDFormat.WIE32: [0xFFF, 0xFFFF, 0, 0],
HIDFormat.D10202: [0x7F, 0xFFFFFF, 0, 0],
HIDFormat.H10306: [0xFFFF, 0xFFFF, 0, 0],
HIDFormat.N10002: [0xFFFF, 0xFFFF, 0, 0],
HIDFormat.OPTUS34: [0x3FF, 0xFFFF, 0, 0],
HIDFormat.SMP34: [0x3FF, 0xFFFF, 0x7, 0],
HIDFormat.BQT34: [0xFF, 0xFFFFFF, 0, 0],
HIDFormat.C1K35S: [0xFFF, 0xFFFFF, 0, 0],
HIDFormat.C15001: [0xFF, 0xFFFF, 0, 0x3FF],
HIDFormat.S12906: [0xFF, 0xFFFFFF, 0x3, 0],
HIDFormat.ACTPHID: [0xFF, 0xFFFFFF, 0, 0x3FF],
HIDFormat.SIE36: [0x3FFFF, 0xFFFF, 0, 0],
HIDFormat.H10320: [0, 99999999, 0, 0],
HIDFormat.H10302: [0, 0x7FFFFFFFF, 0, 0],
HIDFormat.H10304: [0xFFFF, 0x7FFFF, 0, 0],
HIDFormat.P10004: [0x1FFF, 0x3FFFF, 0, 0],
HIDFormat.HGEN37: [0, 0xFFFFFFFF, 0, 0],
HIDFormat.MDI37: [0xF, 0x1FFFFFFF, 0, 0],
}
limit = limits.get(HIDFormat(format))
if limit is None:
return True
if fc is not None and fc > limit[0]:
raise ArgsParserError(
f"{HIDFormat(format)}: Facility Code must between 0 to {limit[0]}"
)
if cn is not None and cn > limit[1]:
raise ArgsParserError(
f"{HIDFormat(format)}: Card Number must between 0 to {limit[1]}"
)
if il is not None and il > limit[2]:
raise ArgsParserError(
f"{HIDFormat(format)}: Issue Level must between 0 to {limit[2]}"
)
if oem is not None and oem > limit[3]:
raise ArgsParserError(
f"{HIDFormat(format)}: OEM must between 0 to {limit[3]}"
)
def before_exec(self, args: argparse.Namespace):
if super().before_exec(args):
format = HIDFormat.H10301.value
if args.format is not None:
format = HIDFormat[args.format].value
LFHIDIdArgsUnit.check_limits(format, args.fc, args.cn, args.il, args.oem)
return True
return False
def args_parser(self) -> ArgumentParserNoExit:
raise NotImplementedError()
def on_exec(self, args: argparse.Namespace):
raise NotImplementedError()
class LFHIDIdReadArgsUnit(DeviceRequiredUnit):
@staticmethod
def add_card_arg(parser: ArgumentParserNoExit, required=False):
formats = [x.name for x in HIDFormat]
parser.add_argument(
"-f",
"--format",
type=str,
required=False,
help="HIDProx card format hint",
metavar="",
choices=formats,
)
return parser
def args_parser(self) -> ArgumentParserNoExit:
raise NotImplementedError()
def on_exec(self, args: argparse.Namespace):
raise NotImplementedError()
class LFIOProxIdArgsUnit(DeviceRequiredUnit):
"""
IOProx identity arguments:
--ver <int> version (0-255)
--fc <int> facility (0-255)
--cn <int> card number (0-65535)
--raw8 <hex8> raw 8 bytes hex, e.g. 007854E03A5D65AB
"""
@staticmethod
def add_card_arg(parser: ArgumentParserNoExit, required=False):
parser.add_argument("--ver", type=int, required=False, help="ioProx version", metavar="<int>")
parser.add_argument("--fc", type=str, required=False, help="ioProx facility code, e.g., 83 or 0x53", metavar="<str>")
parser.add_argument("--cn", type=int, required=required, help="ioProx card number", metavar="<int>")
parser.add_argument("--raw8", type=str, required=False, help="ioProx raw 8 bytes hex (e.g. 00AABBCCDDEEFF55)", metavar="<hex8>")
return parser
@staticmethod
def _check_u8(name: str, v: int):
if v < 0 or v > 0xFF:
raise ArgsParserError(f"{name} must be 0..255")
@staticmethod
def _check_u16(name: str, v: int):
if v < 0 or v > 0xFFFF:
raise ArgsParserError(f"{name} must be 0..65535")
@staticmethod
def parse_raw8(raw8: str) -> bytes:
s = raw8.replace(" ", "").replace("0x", "").strip()
b = bytes.fromhex(s)
if len(b) != 8:
raise ArgsParserError("ioProx --raw must be exactly 8 bytes (16 hex chars), e.g. 007854E03A5D65AB")
return b
@staticmethod
def checksum5(b1, b2, b3, b4, b5) -> int:
return (0xFF - ((b1 + b2 + b3 + b4 + b5) & 0xFF)) & 0xFF
def before_exec(self, args: argparse.Namespace):
if not super().before_exec(args):
return False
# validate if provided
if args.ver is not None:
self._check_u8("version", args.ver)
if args.fc is not None:
val = int(args.fc, 0)
self._check_u8("facility", val)
args.fc = val
if args.cn is not None:
self._check_u16("card number", args.cn)
# if raw is present, validate it
if args.raw8 is not None:
self.parse_raw8(args.raw8)
return True
class LFIOProxReadArgsUnit(DeviceRequiredUnit):
@staticmethod
def add_card_arg(parser: ArgumentParserNoExit, required=False):
parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
return parser
class LFVikingIdArgsUnit(DeviceRequiredUnit):
@staticmethod
def add_card_arg(parser: ArgumentParserNoExit, required=False):
parser.add_argument(
"--id", type=str, required=required, help="Viking tag id", metavar="<hex>"
)
return parser
def before_exec(self, args: argparse.Namespace):
if not super().before_exec(args):
return False
if args.id is None or not re.match(r"^[a-fA-F0-9]{8}$", args.id):
raise ArgsParserError("ID must include 8 HEX symbols")
return True
def args_parser(self) -> ArgumentParserNoExit:
raise NotImplementedError("Please implement this")
def on_exec(self, args: argparse.Namespace):
raise NotImplementedError("Please implement this")
class TagTypeArgsUnit(DeviceRequiredUnit):
@staticmethod
def add_type_args(parser: ArgumentParserNoExit):
type_names = [t.name for t in TagSpecificType.list()]
help_str = "Tag Type: " + ", ".join(type_names)
parser.add_argument(
"-t",
"--type",
type=str,
required=True,
metavar="TAG_TYPE",
help=help_str,
choices=type_names,
)
return parser
def args_parser(self) -> ArgumentParserNoExit:
raise NotImplementedError()
def on_exec(self, args: argparse.Namespace):
raise NotImplementedError()
root = CLITree(root=True)
hw = root.subgroup("hw", "Hardware-related commands")
hw_slot = hw.subgroup("slot", "Emulation slots commands")
hw_settings = hw.subgroup("settings", "Chameleon settings commands")
hf = root.subgroup("hf", "High Frequency commands")
hf_14a = hf.subgroup("14a", "ISO14443-a commands")
hf_mf = hf.subgroup("mf", "MIFARE Classic commands")
hf_mfu = hf.subgroup("mfu", "MIFARE Ultralight / NTAG commands")
lf = root.subgroup("lf", "Low Frequency commands")
lf_em = lf.subgroup("em", "EM commands")
lf_em_410x = lf_em.subgroup("410x", "EM410x commands")
lf_hid = lf.subgroup("hid", "HID commands")
lf_hid_prox = lf_hid.subgroup("prox", "HID Prox commands")
lf_ioprox = lf.subgroup("ioprox", "ioProx commands")
lf_viking = lf.subgroup("viking", "Viking commands")
lf_generic = lf.subgroup("generic", "Generic commands")
@root.command("clear")
class RootClear(BaseCLIUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Clear screen"
return parser
def on_exec(self, args: argparse.Namespace):
os.system("clear" if os.name == "posix" else "cls")
@root.command("rem")
class RootRem(BaseCLIUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Timestamped comment"
parser.add_argument("comment", nargs="*", help="Your comment")
return parser
def on_exec(self, args: argparse.Namespace):
# precision: second
# iso_timestamp = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
# precision: nanosecond (note that the comment will take some time too, ~75ns, check your system)
iso_timestamp = datetime.utcnow().isoformat() + "Z"
comment = " ".join(args.comment)
print(f"{iso_timestamp} remark: {comment}")
@root.command("exit")
class RootExit(BaseCLIUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Exit client"
return parser
def on_exec(self, args: argparse.Namespace):
print("Bye, thank you. ^.^ ")
self.device_com.close()
sys.exit(996)
@root.command("dump_help")
class RootDumpHelp(BaseCLIUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Dump available commands"
parser.add_argument(
"-d",
"--show-desc",
action="store_true",
help="Dump full command description",
)
parser.add_argument(
"-g",
"--show-groups",
action="store_true",
help="Dump command groups as well",
)
return parser
@staticmethod
def dump_help(cmd_node, depth=0, dump_cmd_groups=False, dump_description=False):
visual_col1_width = 28
col1_width = visual_col1_width + len(f"{CG}{C0}")
if cmd_node.cls:
p = cmd_node.cls().args_parser()
assert p is not None
if dump_description:
p.print_help()
else:
cmd_title = color_string((CG, cmd_node.fullname))
print(f"{cmd_title}".ljust(col1_width), end="")
p.prog = " " * (visual_col1_width - len("usage: ") - 1)
usage = p.format_usage().removeprefix("usage: ").strip()
print(color_string((CY, usage)))
else:
if dump_cmd_groups and not cmd_node.root:
if dump_description:
print("=" * 80)
print(color_string((CR, cmd_node.fullname)))
print(color_string((CC, cmd_node.help_text)))
else:
print(color_string((CB, f"== {cmd_node.fullname} ==")))
for child in cmd_node.children:
RootDumpHelp.dump_help(
child, depth + 1, dump_cmd_groups, dump_description
)
def on_exec(self, args: argparse.Namespace):
self.dump_help(
root, dump_cmd_groups=args.show_groups, dump_description=args.show_desc
)
@hw.command("connect")
class HWConnect(BaseCLIUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Connect to chameleon by serial port"
parser.add_argument("-p", "--port", type=str, required=False)
return parser
def on_exec(self, args: argparse.Namespace):
try:
if args.port is None: # Chameleon auto-detect if no port is supplied
platform_name = uname().release
if "Microsoft" in platform_name:
path = os.environ["PATH"].split(os.pathsep)
path.append("/mnt/c/Windows/System32/WindowsPowerShell/v1.0/")
powershell_path = None
for prefix in path:
fn = os.path.join(prefix, "powershell.exe")
if not os.path.isdir(fn) and os.access(fn, os.X_OK):
powershell_path = fn
break
if powershell_path:
process = subprocess.Popen(
[
powershell_path,
"Get-PnPDevice -Class Ports -PresentOnly |"
" where {$_.DeviceID -like '*VID_6868&PID_8686*'} |"
" Select-Object -First 1 FriendlyName |"
" % FriendlyName |"
" select-string COM\\d+ |"
"% { $_.matches.value }",
],
stdout=subprocess.PIPE,
)
res = process.communicate()[0]
_comport = res.decode("utf-8").strip()
if _comport:
args.port = _comport.replace("COM", "/dev/ttyS")
else:
# loop through all ports and find chameleon
for port in serial.tools.list_ports.comports():
if port.vid == 0x6868:
args.port = port.device
break
if args.port is None: # If no chameleon was found, exit
print(
"Chameleon not found, please connect the device or try connecting manually with the -p flag."
)
return
self.device_com.open(args.port)
self.device_com.commands = self.cmd.get_device_capabilities()
major, minor = self.cmd.get_app_version()
model = ["Ultra", "Lite"][self.cmd.get_device_model()]
print(f" {{ Chameleon {model} connected: v{major}.{minor} }}")
except Exception as e:
print(color_string((CR, f"Chameleon Connect fail: {str(e)}")))
self.device_com.close()
@hw.command("disconnect")
class HWDisconnect(BaseCLIUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Disconnect chameleon"
return parser
def on_exec(self, args: argparse.Namespace):
self.device_com.close()
@hw.command("mode")
class HWMode(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Get or change device mode: tag reader or tag emulator"
mode_group = parser.add_mutually_exclusive_group()
mode_group.add_argument(
"-r", "--reader", action="store_true", help="Set reader mode"
)
mode_group.add_argument(
"-e", "--emulator", action="store_true", help="Set emulator mode"
)
return parser
def on_exec(self, args: argparse.Namespace):
if args.reader:
self.cmd.set_device_reader_mode(True)
print("Switch to { Tag Reader } mode successfully.")
elif args.emulator:
self.cmd.set_device_reader_mode(False)
print("Switch to { Tag Emulator } mode successfully.")
else:
print(
f"- Device Mode ( Tag {'Reader' if self.cmd.is_device_reader_mode() else 'Emulator'} )"
)
@hw.command("chipid")
class HWChipId(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Get device chipset ID"
return parser
def on_exec(self, args: argparse.Namespace):
print(" - Device chip ID: " + self.cmd.get_device_chip_id())
@hw.command("address")
class HWAddress(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Get device address (used with Bluetooth)"
return parser
def on_exec(self, args: argparse.Namespace):
print(" - Device address: " + self.cmd.get_device_address())
@hw.command("version")
class HWVersion(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Get current device firmware version"
return parser
def on_exec(self, args: argparse.Namespace):
fw_version_tuple = self.cmd.get_app_version()
fw_version = f"v{fw_version_tuple[0]}.{fw_version_tuple[1]}"
git_version = self.cmd.get_git_version()
model = ["Ultra", "Lite"][self.cmd.get_device_model()]
print(f" - Chameleon {model}, Version: {fw_version} ({git_version})")
@hf_14a.command("config")
class HF14AConfig(DeviceRequiredUnit):
class Config(Enum):
def __new__(cls, value, desc):
obj = object.__new__(cls)
obj._value_ = value
obj.desc = desc
return obj
@classmethod
def choices(cls):
return [elem.name for elem in cls]
@classmethod
def format(cls, index):
item = cls(index)
color = CG if index == 0 else CR
return f" - {cls.__name__.upper()} override: {color_string((color, item.name))} ( {item.desc} )"
@classmethod
def help(cls):
return " / ".join([f"{elem.desc}" for elem in cls])
class Bcc(Config):
std = (0, "follow standard")
fix = (1, "fix bad BCC")
ignore = (2, "ignore bad BCC, always use card BCC")
class Cl2(Config):
std = (0, "follow standard")
force = (1, "always do CL2")
skip = (2, "always skip CL2")
class Cl3(Config):
std = (0, "follow standard")
force = (1, "always do CL3")
skip = (2, "always skip CL3")
class Rats(Config):
std = (0, "follow standard")
force = (1, "always do RATS")
skip = (2, "always skip RATS")
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Configure 14a settings (use with caution)"
parser.add_argument(
"--std",
action="store_true",
help="Reset default configuration (follow standard)",
)
parser.add_argument(
"--bcc", type=str, choices=self.Bcc.choices(), help=self.Bcc.help()
)
parser.add_argument(
"--cl2", type=str, choices=self.Cl2.choices(), help=self.Cl2.help()
)
parser.add_argument(
"--cl3", type=str, choices=self.Cl3.choices(), help=self.Cl3.help()
)
parser.add_argument(
"--rats", type=str, choices=self.Rats.choices(), help=self.Rats.help()
)
return parser
def on_exec(self, args: argparse.Namespace):
change_requested = False
if args.std:
config = {"bcc": 0, "cl2": 0, "cl3": 0, "rats": 0}
change_requested = True
else:
config = self.cmd.hf14a_get_config()
if args.bcc:
config["bcc"] = self.Bcc[args.bcc].value
change_requested = True
if args.cl2:
config["cl2"] = self.Cl2[args.cl2].value
change_requested = True
if args.cl3:
config["cl3"] = self.Cl3[args.cl3].value
change_requested = True
if args.rats:
config["rats"] = self.Rats[args.rats].value
change_requested = True
if change_requested:
self.cmd.hf14a_set_config(config)
config = self.cmd.hf14a_get_config()
print("HF 14a config")
print(self.Bcc.format(config["bcc"]))
print(self.Cl2.format(config["cl2"]))
print(self.Cl3.format(config["cl3"]))
print(self.Rats.format(config["rats"]))
@hf_14a.command("scan")
class HF14AScan(ReaderRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Scan 14a tag, and print basic information"
return parser
def check_mf1_nt(self):
# detect mf1 support
if self.cmd.mf1_detect_support():
# detect prng
print("- Mifare Classic technology")
prng_type = self.cmd.mf1_detect_prng()
print(f" # Prng: {MifareClassicPrngType(prng_type)}")
def sak_info(self, data_tag):
# detect the technology in use based on SAK
int_sak = data_tag["sak"][0]
if int_sak in type_id_SAK_dict:
print(f"- Guessed type(s) from SAK: {type_id_SAK_dict[int_sak]}")
def scan(self, deep=False):
resp = self.cmd.hf14a_scan()
if resp is not None:
for data_tag in resp:
print(f"- UID : {data_tag['uid'].hex().upper()}")
print(
f"- ATQA : {data_tag['atqa'].hex().upper()} "
f"(0x{int.from_bytes(data_tag['atqa'], byteorder='little'):04x})"
)
print(f"- SAK : {data_tag['sak'].hex().upper()}")
if len(data_tag["ats"]) > 0:
print(f"- ATS : {data_tag['ats'].hex().upper()}")
if deep:
self.sak_info(data_tag)
# TODO: following checks cannot be done yet if multiple cards are present
if len(resp) == 1:
self.check_mf1_nt()
# TODO: check for ATS support on 14A3 tags
else:
print("Multiple tags detected, skipping deep tests...")
else:
print("ISO14443-A Tag no found")
def on_exec(self, args: argparse.Namespace):
self.scan()
@hf_14a.command("info")
class HF14AInfo(ReaderRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Scan 14a tag, and print detail information"
return parser
def on_exec(self, args: argparse.Namespace):
scan = HF14AScan()
scan.device_com = self.device_com
scan.scan(deep=True)
@hf_mf.command("nested")
class HFMFNested(ReaderRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Mifare Classic nested recover key"
parser.add_argument(
"--blk",
"--known-block",
type=int,
required=True,
metavar="<dec>",
help="Known key block number",
)
srctype_group = parser.add_mutually_exclusive_group()
srctype_group.add_argument(
"-a", "-A", action="store_true", help="Known key is A key (default)"
)
srctype_group.add_argument(
"-b", "-B", action="store_true", help="Known key is B key"
)
parser.add_argument(
"-k", "--key", type=str, required=True, metavar="<hex>", help="Known key"
)
# tblk required because only single block mode is supported for now
parser.add_argument(
"--tblk",
"--target-block",
type=int,
required=True,
metavar="<dec>",
help="Target key block number",
)
dsttype_group = parser.add_mutually_exclusive_group()
dsttype_group.add_argument(
"--ta", "--tA", action="store_true", help="Target A key (default)"
)
dsttype_group.add_argument(
"--tb", "--tB", action="store_true", help="Target B key"
)
return parser
def from_nt_level_code_to_str(self, nt_level):
if nt_level == 0:
return "StaticNested"
if nt_level == 1:
return "Nested"
if nt_level == 2:
return "HardNested"
def recover_a_key(
self, block_known, type_known, key_known, block_target, type_target
) -> Union[str, None]:
"""
recover a key from key known.
:param block_known:
:param type_known:
:param key_known:
:param block_target:
:param type_target:
:return:
"""
# check nt level, we can run static or nested auto...
nt_level = self.cmd.mf1_detect_prng()
print(
f" - NT vulnerable: {color_string((CY, self.from_nt_level_code_to_str(nt_level)))}"
)
if nt_level == 2:
print(" [!] Use hf mf hardnested")
return None
# acquire
if nt_level == 0: # It's a staticnested tag?
nt_uid_obj = self.cmd.mf1_static_nested_acquire(
block_known, type_known, key_known, block_target, type_target
)
cmd_param = f"{nt_uid_obj['uid']} {int(type_target)}"
for nt_item in nt_uid_obj["nts"]:
cmd_param += f" {nt_item['nt']} {nt_item['nt_enc']}"
tool_name = "staticnested"
else:
dist_obj = self.cmd.mf1_detect_nt_dist(block_known, type_known, key_known)
nt_obj = self.cmd.mf1_nested_acquire(
block_known, type_known, key_known, block_target, type_target
)
# create cmd
cmd_param = f"{dist_obj['uid']} {dist_obj['dist']}"
for nt_item in nt_obj:
cmd_param += f" {nt_item['nt']} {nt_item['nt_enc']} {nt_item['par']}"
tool_name = "nested"
# Cross-platform compatibility
if sys.platform == "win32":
cmd_recover = f"{tool_name}.exe {cmd_param}"
else:
cmd_recover = f"./{tool_name} {cmd_param}"
print(f" Executing {cmd_recover}")
# start a decrypt process
process = self.sub_process(cmd_recover)
# wait end
while process.is_running():
msg = f" [ Time elapsed {process.get_time_distance()/1000:#.1f}s ]\r"
print(msg, end="")
time.sleep(0.1)
# clear \r
print()
if process.get_ret_code() == 0:
output_str = process.get_output_sync()
key_list = []
for line in output_str.split("\n"):
sea_obj = re.search(r"([a-fA-F0-9]{12})", line)
if sea_obj is not None:
key_list.append(sea_obj[1])
# Here you have to verify the password first, and then get the one that is successfully verified
# If there is no verified password, it means that the recovery failed, you can try again
print(f" - [{len(key_list)} candidate key(s) found ]")
for key in key_list:
key_bytes = bytearray.fromhex(key)
if self.cmd.mf1_auth_one_key_block(
block_target, type_target, key_bytes
):
return key
else:
# No keys recover, and no errors.
return None
def on_exec(self, args: argparse.Namespace):
block_known = args.blk
# default to A
type_known = MfcKeyType.B if args.b else MfcKeyType.A
key_known: str = args.key
if not re.match(r"^[a-fA-F0-9]{12}$", key_known):
print("key must include 12 HEX symbols")
return
key_known_bytes = bytes.fromhex(key_known)
block_target = args.tblk
# default to A
type_target = MfcKeyType.B if args.tb else MfcKeyType.A
if block_known == block_target and type_known == type_target:
print(color_string((CR, "Target key already known")))
return
print(" - Nested recover one key running...")
key = self.recover_a_key(
block_known, type_known, key_known_bytes, block_target, type_target
)
if key is None:
print(color_string((CY, "No key found, you can retry.")))
else:
print(
f" - Block {block_target} Type {type_target.name} Key Found: {color_string((CG, key))}"
)
return
@hf_mf.command("darkside")
class HFMFDarkside(ReaderRequiredUnit):
def __init__(self):
super().__init__()
self.darkside_list = []
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Mifare Classic darkside recover key"
return parser
def recover_key(self, block_target, type_target):
"""
Execute darkside acquisition and decryption.
:param block_target:
:param type_target:
:return:
"""
first_recover = True
retry_count = 0
while retry_count < 0xFF:
darkside_resp = self.cmd.mf1_darkside_acquire(
block_target, type_target, first_recover, 30
)
first_recover = False # not first run.
if darkside_resp[0] != MifareClassicDarksideStatus.OK:
print(
f"Darkside error: {MifareClassicDarksideStatus(darkside_resp[0])}"
)
break
darkside_obj = darkside_resp[1]
if darkside_obj["par"] != 0: # NXP tag workaround.
self.darkside_list.clear()
self.darkside_list.append(darkside_obj)
recover_params = f"{darkside_obj['uid']}"
for darkside_item in self.darkside_list:
recover_params += f" {darkside_item['nt1']} {darkside_item['ks1']} {darkside_item['par']}"
recover_params += f" {darkside_item['nr']} {darkside_item['ar']}"
if sys.platform == "win32":
cmd_recover = f"darkside.exe {recover_params}"
else:
cmd_recover = f"./darkside {recover_params}"
# subprocess.run(cmd_recover, cwd=os.path.abspath("../bin/"), shell=True)
# print(f" Executing {cmd_recover}")
# start a decrypt process
process = self.sub_process(cmd_recover)
# wait end
process.wait_process()
# get output
output_str = process.get_output_sync()
if "key not found" in output_str:
print(f" - No key found, retrying({retry_count})...")
retry_count += 1
continue # retry
else:
key_list = []
for line in output_str.split("\n"):
sea_obj = re.search(r"([a-fA-F0-9]{12})", line)
if sea_obj is not None:
key_list.append(sea_obj[1])
# auth key
for key in key_list:
key_bytes = bytearray.fromhex(key)
if self.cmd.mf1_auth_one_key_block(
block_target, type_target, key_bytes
):
return key
return None
def on_exec(self, args: argparse.Namespace):
key = self.recover_key(0x03, MfcKeyType.A)
if key is not None:
print(f" - Key Found: {key}")
else:
print(" - Key recover fail.")
return
@hf_mf.command("hardnested")
class HFMFHardNested(ReaderRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Mifare Classic hardnested recover key "
parser.add_argument(
"--blk",
"--known-block",
type=int,
required=True,
metavar="<dec>",
help="Known key block number",
)
srctype_group = parser.add_mutually_exclusive_group()
srctype_group.add_argument(
"-a", "-A", action="store_true", help="Known key is A key (default)"
)
srctype_group.add_argument(
"-b", "-B", action="store_true", help="Known key is B key"
)
parser.add_argument(
"-k", "--key", type=str, required=True, metavar="<hex>", help="Known key"
)
parser.add_argument(
"--tblk",
"--target-block",
type=int,
required=True,
metavar="<dec>",
help="Target key block number",
)
dsttype_group = parser.add_mutually_exclusive_group()
dsttype_group.add_argument(
"--ta", "--tA", action="store_true", help="Target A key (default)"
)
dsttype_group.add_argument(
"--tb", "--tB", action="store_true", help="Target B key"
)
parser.add_argument(
"--slow",
action="store_true",
help="Use slower acquisition mode (more nonces)",
)
parser.add_argument(
"--keep-nonce-file",
action="store_true",
help="Keep the generated nonce file (nonces.bin)",
)
parser.add_argument(
"--max-runs",
type=int,
default=200,
metavar="<dec>",
help="Maximum acquisition runs per attempt before giving up (default: 200)",
)
# Add max acquisition attempts
parser.add_argument(
"--max-attempts",
type=int,
default=3,
metavar="<dec>",
help="Maximum acquisition attempts if MSB sum is invalid (default: 3)",
)
return parser
def recover_key(
self,
slow_mode,
block_known,
type_known,
key_known,
block_target,
type_target,
keep_nonce_file,
max_runs,
max_attempts,
):
"""
Recover a key using the HardNested attack via a nonce file, with dynamic MSB-based acquisition and restart on invalid sum.
:param slow_mode: Boolean indicating if slow mode should be used.
:param block_known: Known key block number.
:param type_known: Known key type (A or B).
:param key_known: Known key bytes.
:param block_target: Target key block number.
:param type_target: Target key type (A or B).
:param keep_nonce_file: Boolean indicating whether to keep the nonce file.
:param max_runs: Maximum number of acquisition runs per attempt.
:param max_attempts: Maximum number of full acquisition attempts.
:return: Recovered key as a hex string, or None if not found.
"""
print(" - Starting HardNested attack...")
nonces_buffer = bytearray() # This will hold the final data for the file
uid_bytes = b"" # To store UID from the successful attempt
# --- Outer loop for acquisition attempts ---
acquisition_success = False # Flag to indicate if any attempt was successful
for attempt in range(max_attempts):
print(
f"\n--- Starting Acquisition Attempt {attempt + 1}/{max_attempts} ---"
)
total_raw_nonces_bytes = (
bytearray()
) # Accumulator for raw nonces for THIS attempt
nonces_buffer.clear() # Clear buffer for each new attempt
# --- MSB Tracking Initialization (Reset for each attempt) ---
seen_msbs = [False] * 256
unique_msb_count = 0
msb_parity_sum = 0
# --- End MSB Tracking Initialization ---
run_count = 0
acquisition_goal_met = False
# 1. Scan for the tag to get UID and prepare file header (Done ONCE per attempt)
print(" Scanning for tag...")
try:
scan_resp = self.cmd.hf14a_scan()
except Exception as e:
print(color_string((CR, f" Error scanning tag: {e}")))
# Decide if we should retry or fail completely. Let's fail for now.
print(
color_string((CR, " Attack failed due to error during scanning."))
)
return None
if scan_resp is None or len(scan_resp) == 0:
print(color_string((CR, "Error: No tag found.")))
if attempt + 1 < max_attempts:
print(color_string((CY, " Retrying scan in 1 second...")))
time.sleep(1)
continue # Retry the outer loop (next attempt)
else:
print(
color_string(
(
CR,
" Maximum attempts reached without finding tag. Attack failed.",
)
)
)
return None
if len(scan_resp) > 1:
print(
color_string(
(
CR,
" Error: Multiple tags found. Please present only one tag.",
)
)
)
# Fail immediately if multiple tags are present
return None
tag_info = scan_resp[0]
uid_bytes = tag_info["uid"] # Store UID for later verification
uid_len = len(uid_bytes)
uid_for_file = b""
if uid_len == 4:
uid_for_file = uid_bytes[0:4]
elif uid_len == 7:
uid_for_file = uid_bytes[3:7]
elif uid_len == 10:
uid_for_file = uid_bytes[6:10]
else:
print(
color_string(
(
CR,
f" Error: Unexpected UID length ({uid_len} bytes). Cannot create nonce file header.",
)
)
)
return None # Fail if UID length is unexpected
print(f" Tag found with UID: {uid_bytes.hex().upper()}")
# Prepare header in the main buffer for this attempt
nonces_buffer.extend(uid_for_file)
nonces_buffer.extend(
struct.pack("!BB", block_target, type_target.value & 0x01)
)
print(f" Nonce file header prepared: {nonces_buffer.hex().upper()}")
# 2. Acquire nonces dynamically based on MSB criteria (Inner loop for runs)
print(
f" Acquiring nonces (slow mode: {slow_mode}, max runs: {max_runs}). This may take a while..."
)
while run_count < max_runs:
run_count += 1
print(f" Starting acquisition run {run_count}/{max_runs}...")
try:
# Check if tag is still present before each run
current_scan = self.cmd.hf14a_scan()
if (
current_scan is None
or len(current_scan) == 0
or current_scan[0]["uid"] != uid_bytes
):
print(
color_string(
(
CY,
f" Error: Tag lost or changed before run {run_count}. Stopping acquisition attempt.",
)
)
)
acquisition_goal_met = False # Mark as failed
break # Exit inner run loop for this attempt
# Acquire nonces for this run
raw_nonces_bytes_this_run = self.cmd.mf1_hard_nested_acquire(
slow_mode,
block_known,
type_known,
key_known,
block_target,
type_target,
)
if not raw_nonces_bytes_this_run:
print(
color_string(
(
CY,
f" Run {run_count}: No nonces acquired in this run. Continuing...",
)
)
)
time.sleep(0.1) # Small delay before retrying
continue
# Append successfully acquired nonces to the total buffer for this attempt
total_raw_nonces_bytes.extend(raw_nonces_bytes_this_run)
# --- Process acquired nonces for MSB tracking ---
num_pairs_this_run = len(raw_nonces_bytes_this_run) // 9
print(
f" Run {run_count}: Acquired {num_pairs_this_run * 2} nonces ({len(raw_nonces_bytes_this_run)} bytes raw). Processing MSBs..."
)
new_msbs_found_this_run = 0
for i in range(num_pairs_this_run):
offset = i * 9
try:
nt, nt_enc, par = struct.unpack_from(
"!IIB", raw_nonces_bytes_this_run, offset
)
except struct.error as unpack_err:
print(
color_string(
(
CR,
f" Error unpacking nonce data at offset {offset}: {unpack_err}. Skipping pair.",
)
)
)
continue
msb = (nt_enc >> 24) & 0xFF
if not seen_msbs[msb]:
seen_msbs[msb] = True
unique_msb_count += 1
new_msbs_found_this_run += 1
parity_bit = hardnested_utils.evenparity32(
(nt_enc & 0xFF000000) | (par & 0x08)
)
msb_parity_sum += parity_bit
print(
f"\r Unique MSBs: {unique_msb_count}/256 | Current Sum: {msb_parity_sum} ",
end="",
)
if new_msbs_found_this_run > 0:
print() # Print a newline after progress update
# --- Check termination condition ---
if unique_msb_count == 256:
print()
print(
f"{color_string((CG, ' All 256 unique MSBs found.'))} Final parity sum: {msb_parity_sum}"
)
if msb_parity_sum in hardnested_utils.hardnested_sums:
print(
color_string(
(
CG,
f" Parity sum {msb_parity_sum} is VALID. Stopping acquisition runs.",
)
)
)
acquisition_goal_met = True
acquisition_success = True # Mark attempt as successful
break # Exit the inner run loop successfully
else:
print(
color_string(
(
CR,
f" Parity sum {msb_parity_sum} is INVALID (Expected one of {hardnested_utils.hardnested_sums}).",
)
)
)
acquisition_goal_met = False # Mark as failed
acquisition_success = False
break # Exit the inner run loop to restart the attempt
except chameleon_com.CMDInvalidException:
print(
color_string(
(
CR,
" Error: Hardnested command not supported by this firmware version.",
)
)
)
return None # Cannot proceed at all
except UnexpectedResponseError as e:
print(
color_string(
(
CR,
f" Error acquiring nonces during run {run_count}: {e}",
)
)
)
print(
color_string(
(CY, " Stopping acquisition runs for this attempt...")
)
)
acquisition_goal_met = False
break # Exit inner run loop
except TimeoutError:
print(
color_string(
(
CR,
f" Error: Timeout during nonce acquisition run {run_count}.",
)
)
)
print(
color_string(
(CY, " Stopping acquisition runs for this attempt...")
)
)
acquisition_goal_met = False
break # Exit inner run loop
except Exception as e:
print(
color_string(
(
CR,
f" Unexpected error during acquisition run {run_count}: {e}",
)
)
)
print(
color_string(
(CY, " Stopping acquisition runs for this attempt...")
)
)
acquisition_goal_met = False
break # Exit inner run loop
# --- End of inner run loop (while run_count < max_runs) ---
# --- Post-Acquisition Summary for this attempt ---
print(f"\n Finished acquisition phase for attempt {attempt + 1}.")
if acquisition_success:
print(
color_string(
(
CG,
f" Successfully acquired nonces meeting the MSB sum criteria in {run_count} runs.",
)
)
)
# Append collected raw nonces to the main buffer for the file
nonces_buffer.extend(total_raw_nonces_bytes)
break # Exit the outer attempt loop successfully
elif unique_msb_count == 256 and not acquisition_goal_met:
print(
color_string(
(CR, " Found all 256 MSBs, but the parity sum was invalid.")
)
)
if attempt + 1 < max_attempts:
print(color_string((CY, " Restarting acquisition process...")))
time.sleep(1) # Small delay before restarting
continue # Continue to the next iteration of the outer attempt loop
else:
print(
color_string(
(
CR,
f" Maximum attempts ({max_attempts}) reached with invalid sum. Attack failed.",
)
)
)
return None # Failed after max attempts
elif run_count >= max_runs:
print(
color_string(
(
CY,
f" Warning: Reached max runs ({max_runs}) for attempt {attempt + 1}. Found {unique_msb_count}/256 unique MSBs.",
)
)
)
if attempt + 1 < max_attempts:
print(color_string((CY, " Restarting acquisition process...")))
time.sleep(1)
continue # Continue to the next iteration of the outer attempt loop
else:
print(
color_string(
(
CR,
f" Maximum attempts ({max_attempts}) reached without meeting criteria. Attack failed.",
)
)
)
return None # Failed after max attempts
else: # Acquisition stopped due to error or tag loss
print(
color_string(
(
CR,
f"Acquisition attempt {attempt + 1} stopped prematurely due to an error after {run_count} runs.",
)
)
)
# Decide if we should retry or fail completely. Let's fail for now.
print(
color_string((CR, "Attack failed due to error during acquisition."))
)
return None # Failed due to error
# --- End of outer attempt loop ---
# If we exited the loop successfully (acquisition_success is True)
if not acquisition_success:
# This case should ideally be caught within the loop, but as a safeguard:
print(
color_string(
(CR, f" Error: Acquisition failed after {max_attempts} attempts.")
)
)
return None
# --- Proceed with the rest of the attack using the successfully collected nonces ---
total_nonce_pairs = (
len(total_raw_nonces_bytes) // 9
) # Use data from the successful attempt
print(
f"\n Proceeding with attack using {total_nonce_pairs * 2} nonces ({len(total_raw_nonces_bytes)} bytes raw)."
)
print(f" Total nonce file size will be {len(nonces_buffer)} bytes.")
if total_nonce_pairs == 0:
print(
color_string(
(
CR,
" Error: No nonces were successfully acquired in the final attempt.",
)
)
)
return None
# 3. Save nonces to a temporary file
nonce_file_path = None
temp_nonce_file = None
output_str = "" # To store the output read from the file
try:
# --- Nonce File Handling ---
delete_nonce_on_close = not keep_nonce_file
# Use delete_on_close=False to manage deletion manually in finally block
temp_nonce_file = tempfile.NamedTemporaryFile(
suffix=".bin",
prefix="hardnested_nonces_",
delete=False,
mode="wb",
dir=".",
)
temp_nonce_file.write(
nonces_buffer
) # Write the buffer from the successful attempt
temp_nonce_file.flush()
nonce_file_path = temp_nonce_file.name
temp_nonce_file.close() # Close it so hardnested can access it
temp_nonce_file = None # Clear variable after closing
print(
f" Nonces saved to {'temporary ' if delete_nonce_on_close else ''}file: {os.path.abspath(nonce_file_path)}"
)
# 4. Prepare and run the external hardnested tool, redirecting output
print(
color_string(
(CC, "--- Running Hardnested Tool (Output redirected) ---")
)
)
output_str = execute_tool("hardnested", [os.path.abspath(nonce_file_path)])
print(color_string((CC, "--- Hardnested Tool Finished ---")))
# 5. Read the output from the temporary log file
# 6. Process the result (using output_str read from the file)
key_list = []
key_prefix = "Key found: " # Define the specific prefix to look for
for line in output_str.splitlines():
line_stripped = line.strip() # Remove leading/trailing whitespace
if line_stripped.startswith(key_prefix):
# Found the target line, now extract the key using regex
# Regex now looks for 12 hex chars specifically after the prefix
sea_obj = re.search(
r"([a-fA-F0-9]{12})", line_stripped[len(key_prefix) :]
)
if sea_obj:
key_list.append(sea_obj.group(1))
# Optional: Break if you only expect one "Key found:" line
# break
if not key_list:
print(
color_string(
(
CY,
f" No line starting with '{key_prefix}' found in the output file.",
)
)
)
return None
# 7. Verify Keys (Same as before)
print(
f" [{len(key_list)} candidate key(s) found in output. Verifying...]"
)
# Use the UID from the successful acquisition attempt
uid_bytes_for_verify = (
uid_bytes # From the last successful scan in the outer loop
)
for key_hex in key_list:
key_bytes = bytes.fromhex(key_hex)
print(f" Trying key: {key_hex.upper()}...", end="")
try:
# Check tag presence before auth attempt
scan_check = self.cmd.hf14a_scan()
if (
scan_check is None
or len(scan_check) == 0
or scan_check[0]["uid"] != uid_bytes_for_verify
):
print(
color_string(
(
CR,
" Tag lost or changed during verification. Cannot verify.",
)
)
)
return None # Stop verification if tag is gone
if self.cmd.mf1_auth_one_key_block(
block_target, type_target, key_bytes
):
print(color_string((CG, " Success!")))
return key_hex # Return the verified key
else:
print(color_string((CR, "Auth failed.")))
except UnexpectedResponseError as e:
print(color_string((CR, f" Verification error: {e}")))
# Consider if we should continue trying other keys or stop
except Exception as e:
print(
color_string(
(CR, f" Unexpected error during verification: {e}")
)
)
# Consider stopping here
print(color_string((CY, " Verification failed for all candidate keys.")))
return None
finally:
# 8. Clean up nonce file
if nonce_file_path and os.path.exists(nonce_file_path):
if keep_nonce_file:
final_nonce_filename = "nonces.bin"
try:
if os.path.exists(final_nonce_filename):
os.remove(final_nonce_filename)
# Use replace for atomicity if possible
os.replace(nonce_file_path, final_nonce_filename)
print(
f" Nonce file kept as: {os.path.abspath(final_nonce_filename)}"
)
except OSError as e:
print(
color_string(
(
CR,
f" Error renaming/replacing temporary nonce file to {final_nonce_filename}: {e}",
)
)
)
print(f" Temporary file might remain: {nonce_file_path}")
else:
try:
os.remove(nonce_file_path)
# print(f" Temporary nonce file deleted: {nonce_file_path}") # Optional confirmation
except OSError as e:
print(
color_string(
(
CR,
f" Error deleting temporary nonce file {nonce_file_path}: {e}",
)
)
)
def on_exec(self, args: argparse.Namespace):
block_known = args.blk
type_known = MfcKeyType.B if args.b else MfcKeyType.A
key_known_str: str = args.key
if not re.match(r"^[a-fA-F0-9]{12}$", key_known_str):
raise ArgsParserError("Known key must include 12 HEX symbols")
key_known_bytes = bytes.fromhex(key_known_str)
block_target = args.tblk
type_target = MfcKeyType.B if args.tb else MfcKeyType.A
if block_known == block_target and type_known == type_target:
print(color_string((CR, "Target key is the same as the known key.")))
return
# Pass the max_runs and max_attempts arguments
recovered_key = self.recover_key(
args.slow,
block_known,
type_known,
key_known_bytes,
block_target,
type_target,
args.keep_nonce_file,
args.max_runs,
args.max_attempts,
)
if recovered_key:
print(
f" - Key Found: Block {block_target} Type {type_target.name} Key = {color_string((CG, recovered_key.upper()))}"
)
else:
print(color_string((CR, " - HardNested attack failed to recover the key.")))
@hf_mf.command("senested")
class HFMFStaticEncryptedNested(ReaderRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Mifare Classic static encrypted recover key via backdoor"
parser.add_argument(
"--key",
"-k",
help="Backdoor key (as hex[12] format), currently known: A396EFA4E24F (default), A31667A8CEC1, 518B3354E760. See https://eprint.iacr.org/2024/1275",
metavar="<hex>",
type=str,
)
parser.add_argument(
"--sectors", "-s", type=int, metavar="<dec>", help="Sector count"
)
parser.add_argument(
"--starting-sector",
type=int,
metavar="<dec>",
help="Start recovery from this sector",
)
parser.set_defaults(sectors=16)
parser.set_defaults(starting_sector=0)
parser.set_defaults(key="A396EFA4E24F")
return parser
def on_exec(self, args: argparse.Namespace):
key_map = self.senested(
args.key, args.starting_sector, args.sectors, args.sectors
)
print_key_table(key_map)
def senested(self, key, starting_sector, stopping_sector, sectors):
acquire_datas = self.cmd.mf1_static_encrypted_nested_acquire(
bytes.fromhex(key), sectors, starting_sector
)
if not acquire_datas:
print("Failed to collect nonces, is card present and has backdoor?")
uid = format(acquire_datas["uid"], "x")
key_map = {"A": {}, "B": {}}
check_speed = 1.95 # sec per 64 keys
for sector in range(starting_sector, stopping_sector):
sector_name = str(sector).zfill(2)
print("Recovering", sector, "sector...")
execute_tool(
"staticnested_1nt",
[
uid,
sector_name,
format(acquire_datas["nts"]["a"][sector]["nt"], "x").zfill(8),
format(acquire_datas["nts"]["a"][sector]["nt_enc"], "x").zfill(8),
str(acquire_datas["nts"]["a"][sector]["parity"]).zfill(4),
],
)
execute_tool(
"staticnested_1nt",
[
uid,
sector_name,
format(acquire_datas["nts"]["b"][sector]["nt"], "x").zfill(8),
format(acquire_datas["nts"]["b"][sector]["nt_enc"], "x").zfill(8),
str(acquire_datas["nts"]["b"][sector]["parity"]).zfill(4),
],
)
a_key_dic = f"keys_{uid}_{sector_name}_{format(acquire_datas['nts']['a'][sector]['nt'], 'x').zfill(8)}.dic"
b_key_dic = f"keys_{uid}_{sector_name}_{format(acquire_datas['nts']['b'][sector]['nt'], 'x').zfill(8)}.dic"
execute_tool("staticnested_2x1nt_rf08s", [a_key_dic, b_key_dic])
keys = open(
os.path.join(
tempfile.gettempdir(), b_key_dic.replace(".dic", "_filtered.dic")
)
).readlines()
keys_bytes = []
for key in keys:
keys_bytes.append(bytes.fromhex(key.strip()))
key = None
print(
"Start checking possible B keys, will take up to",
math.floor(len(keys_bytes) / 64 * check_speed),
"seconds for",
len(keys_bytes),
"keys",
)
for i in tqdm_if_exists(range(0, len(keys_bytes), 64)):
data = self.cmd.mf1_check_keys_on_block(
sector * 4 + 3, 0x61, keys_bytes[i : i + 64]
)
if data:
key = data.hex().zfill(12)
key_map["B"][sector] = key
print("Found B key", key)
break
if key:
a_key = execute_tool(
"staticnested_2x1nt_rf08s_1key",
[
format(acquire_datas["nts"]["b"][sector]["nt"], "x").zfill(8),
key,
a_key_dic,
],
)
keys_bytes = []
for key in a_key.split("\n"):
keys_bytes.append(bytes.fromhex(key.strip()))
data = self.cmd.mf1_check_keys_on_block(
sector * 4 + 3, 0x60, keys_bytes
)
if data:
key = data.hex().zfill(12)
print("Found A key", key)
key_map["A"][sector] = key
continue
else:
print(
"Failed to find A key by fast method, trying all possible keys"
)
keys = open(
os.path.join(
tempfile.gettempdir(),
a_key_dic.replace(".dic", "_filtered.dic"),
)
).readlines()
keys_bytes = []
for key in keys:
keys_bytes.append(bytes.fromhex(key.strip()))
print(
"Start checking possible A keys, will take up to",
math.floor(len(keys_bytes) / 64 * check_speed),
"seconds for",
len(keys_bytes),
"keys",
)
for i in tqdm_if_exists(range(0, len(keys_bytes), 64)):
data = self.cmd.mf1_check_keys_on_block(
sector * 4 + 3, 0x60, keys_bytes[i : i + 64]
)
if data:
key = data.hex().zfill(12)
print("Found A key", key)
key_map["A"][sector] = key
break
else:
print("Failed to find key")
for file in glob.glob(tempfile.gettempdir() + "/keys_*.dic"):
os.remove(file)
return key_map
@hf_mf.command("autopwn")
class HFMFAutopwn(ReaderRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Mifare Classic auto recovery tool"
parser.add_argument(
"-k", "--key", type=str, required=False, metavar="<hex>", help="Known key"
)
return parser
def get_mf_size(self, sak):
sizes = {
b"\x18": ("4k", 40),
b"\x08": ("1k", 16),
b"\x09": ("mini", 5),
b"\x10": ("2k", 32),
b"\x01": ("1k", 16),
}
if sak not in sizes:
print("Unknown SAK, defaulting to 16 sectors")
return sizes.get(sak, ("1k", 16))
def getsak(self, deep=False):
return self.scan(deep, "sak")
def getuid(self, deep=False):
return self.scan(deep, "uid")
def from_nt_level_code_to_str(self, nt_level):
return {0: "StaticNested", 1: "Nested", 2: "HardNested"}.get(
nt_level, "Unknown"
)
def scan(self, deep=False, scanitem="uid"):
resp = self.cmd.hf14a_scan()
if resp is None:
print("ISO14443-A tag not found")
return None
for data_tag in resp:
if deep:
self.sak_info(data_tag)
if len(resp) == 1:
self.check_mf1_nt()
else:
print("Multiple tags detected, skipping deep tests...")
return data_tag[scanitem].hex().upper()
def bits_to_10byte_mask(self, bits=0):
return bytes.fromhex(f"{(1 << (80 - bits)) - 1:020X}")
def try_key(self, key: bytes, mask: bytes):
return self.cmd.mf1_check_keys_of_sectors(mask, [key])
def neg_bytes(self, data: bytes) -> bytes:
return (~int.from_bytes(data, "big") & ((1 << (len(data) * 8)) - 1)).to_bytes(
len(data), "big"
)
def merge_found_sector_keys(self, existing, response, overwrite=False):
for idx, key in response.get("sectorKeys", {}).items():
if overwrite or idx not in existing:
existing[idx] = key
return existing
def print_key_table(self, keymap, max_sectors):
def fmt(k):
text = (
k.hex().upper().ljust(12)
if isinstance(k, (bytes, bytearray))
else "------------"
)
color = CG if isinstance(k, (bytes, bytearray)) else CR
return f"{color}{text}{C0}"
sep = "╠══════╬══════════════╬══════════════╣"
print("╔══════╦══════════════╦══════════════╗")
print("║ Sec ║ key A ║ key B ║")
for sector in range(max_sectors):
print(sep)
print(
f"{sector:03d}{fmt(keymap.get(sector * 2))}{fmt(keymap.get(sector * 2 + 1))}"
)
print("╚══════╩══════════════╩══════════════╝")
def find_missing_keys(self, existing_keys: dict, max_num: int):
return {
i: (MfcKeyType.A if i % 2 == 0 else MfcKeyType.B)
for i in range(max_num)
if i not in existing_keys
}
def choose_random_known_key(self, keys_dict):
index = random.choice(list(keys_dict.keys()))
return (
(index // 2) * 4,
keys_dict[index],
MfcKeyType.B if index % 2 else MfcKeyType.A,
)
def mask_from_keys(
self, pos_container, total_bits=80, one_indexed=False, msb_left=True
):
positions = (
pos_container.keys() if isinstance(pos_container, dict) else pos_container
)
field = ["0"] * total_bits
for p in positions:
try:
p = int(p)
except Exception:
continue
idx = p - 1 if one_indexed else p
if 0 <= idx < total_bits:
field[idx if msb_left else total_bits - 1 - idx] = "1"
bstr = "".join(field)
return bstr, bytes(int(bstr[i : i + 8], 2) for i in range(0, total_bits, 8))
def run_senested(self, current_keys_found, max_sectors_num):
print(
f" {CY}[+]{C0} This card could be cracked using static nested attack (May take a few minutes)"
)
if input(f" {C0}[+]{C0} Would you like to proceed? [y/n]: ").lower() != "y":
return current_keys_found
backdoor_key = "A396EFA4E24F"
if (
input(
f" {C0}[+]{C0} Would you like to use default backdoor key? [y/n]: "
).lower()
== "n"
):
while True:
backdoor_key = input(
f" {C0}[+]{C0} Backdoor key (known: A396EFA4E24F, A31667A8CEC1, 518B3354E760): "
).upper()
if re.fullmatch(r"[A-F0-9]{12}", backdoor_key):
print(f" {CG}[+]{C0} Valid key")
break
print(f" {CR}[!]{C0} Invalid format for key")
else:
print(f" {CY}[+]{C0} Using default key A396EFA4E24F")
print(f" {CG}[+]{C0} Running static nested..")
snested = HFMFStaticEncryptedNested.__new__(HFMFStaticEncryptedNested)
snested._device_cmd = self.cmd
missing_keys = self.find_missing_keys(current_keys_found, max_sectors_num * 2)
keys = list(missing_keys.items())
for i, (key_num, key_type) in enumerate(keys):
if current_keys_found.get(key_num) is not None:
print(f" {CG}[+]{C0} Key {key_num} found by reuse")
continue
sector_num = key_num // 2
found_keymap = snested.senested(
backdoor_key, sector_num, sector_num + 1, max_sectors_num
)
next_key_num = keys[i + 1][0] if i + 1 < len(keys) else -1
if next_key_num == key_num + 1 and key_type == MfcKeyType.A:
current_keys_found[key_num] = bytes.fromhex(
found_keymap["A"][sector_num]
)
current_keys_found[key_num + 1] = bytes.fromhex(
found_keymap["B"][sector_num]
)
elif key_type == MfcKeyType.A:
current_keys_found[key_num] = bytes.fromhex(
found_keymap["A"][sector_num]
)
else:
current_keys_found[key_num] = bytes.fromhex(
found_keymap["B"][sector_num]
)
current_keys_found = dict(sorted(current_keys_found.items()))
_, mask_bytes = self.mask_from_keys(missing_keys)
neg = self.neg_bytes(mask_bytes)
current_keys_found = self.merge_found_sector_keys(
current_keys_found,
self.try_key(bytes.fromhex(found_keymap["A"][sector_num]), neg),
)
current_keys_found = self.merge_found_sector_keys(
current_keys_found,
self.try_key(bytes.fromhex(found_keymap["B"][sector_num]), neg),
)
return current_keys_found
def autopwn(self, key_known):
uid = self.getuid()
sak = self.getsak()
mf_size, max_sectors_num = self.get_mf_size(bytes.fromhex(sak))
print(f" {CG}[+]{C0} Type: MIFARE Classic {CY}{mf_size}{C0}")
print(f" {CG}[+]{C0} UID: {uid}")
print(f" {CG}[+]{C0} SAK: {sak}")
nt_level = self.cmd.mf1_detect_prng()
print(
f" {CG}[+]{C0} NT vulnerable: {CY}{self.from_nt_level_code_to_str(nt_level)}{C0}"
)
current_keys_found = {}
full_mask = self.bits_to_10byte_mask(max_sectors_num * 2)
if key_known is not None:
current_keys_found = self.merge_found_sector_keys(
current_keys_found, self.try_key(bytes.fromhex(key_known), full_mask)
)
current_keys_found = self.merge_found_sector_keys(
current_keys_found, self.try_key(bytes.fromhex("FFFFFFFFFFFF"), bytes(10))
)
if not current_keys_found:
print(f" {CR}[!]{C0} No keys found yet, trying darkside..")
darkside = HFMFDarkside.__new__(HFMFDarkside)
HFMFDarkside.__init__(darkside)
darkside._device_cmd = self.cmd
darkside_key = darkside.recover_key(0x03, MfcKeyType.A)
if darkside_key is not None:
print(f" {CG}[+]{C0} Darkside key found: {darkside_key}")
current_keys_found[0] = bytes.fromhex(darkside_key)
current_keys_found = dict(sorted(current_keys_found.items()))
print(f" {CG}[+]{C0} Reuse key check..")
current_keys_found = self.merge_found_sector_keys(
current_keys_found,
self.try_key(bytes.fromhex(darkside_key), full_mask),
)
else:
print(f" {CR}[!]{C0} Darkside failed!")
total = max_sectors_num * 2
if len(current_keys_found) == total:
print(f" {CG}[+]{C0} All keys found")
return current_keys_found, max_sectors_num
if not current_keys_found:
return (
self.run_senested(current_keys_found, max_sectors_num),
max_sectors_num,
)
print(f" {CG}[+]{C0} Some keys found, recovering remaining..")
if nt_level == 2:
print(
f" {CY}[+]{C0} Hardened card — use 'hf mf hardnested' to recover remaining keys"
)
elif nt_level == 0:
current_keys_found = self.run_senested(current_keys_found, max_sectors_num)
else:
block_known, key_known_bytes, type_known = self.choose_random_known_key(
current_keys_found
)
missing_keys = self.find_missing_keys(current_keys_found, total)
nested = HFMFNested.__new__(HFMFNested)
BaseCLIUnit.__init__(nested)
nested._device_cmd = self.cmd
for missing_key_num, key_type_target in missing_keys.items():
if current_keys_found.get(missing_key_num) is not None:
print(f" {CG}[+]{C0} Key {missing_key_num} found by reuse")
continue
nested_key = nested.recover_a_key(
block_known,
type_known,
key_known_bytes,
(missing_key_num // 2) * 4,
key_type_target,
)
if nested_key is None:
continue
print(
f" {CG}[+]{C0} Found key {missing_key_num}: {nested_key.upper()}"
)
current_keys_found[missing_key_num] = bytes.fromhex(nested_key)
current_keys_found = dict(sorted(current_keys_found.items()))
_, mask_bytes = self.mask_from_keys(missing_keys)
current_keys_found = self.merge_found_sector_keys(
current_keys_found,
self.try_key(bytes.fromhex(nested_key), self.neg_bytes(mask_bytes)),
)
if len(current_keys_found) < total:
current_keys_found = self.run_senested(
current_keys_found, max_sectors_num
)
if len(current_keys_found) == total:
print(f" {CG}[+]{C0} All keys found")
return current_keys_found, max_sectors_num
def save_keys_to_file(self, extracted_keys, max_sectors_num):
if input(f" {CY}[?]{C0} Save keys to file? [y/n]: ").lower() != "y":
return
filename = input(
f" {C0}[+]{C0} Enter base filename (without extension): "
).strip()
if not filename:
print(f" {CR}[!]{C0} No filename provided, skipping.")
return
dic_path = filename + ".dic"
uniq_keys = set(
v for v in extracted_keys.values() if isinstance(v, (bytes, bytearray))
)
with open(dic_path, "w") as f:
for key in uniq_keys:
f.write(key.hex().upper() + "\n")
print(f" {CG}[+]{C0} Keys saved to {dic_path} (as .dic format)")
key_path = filename + ".key"
unknownkey = bytes(6)
with open(key_path, "wb") as f:
for sector_no in range(max_sectors_num):
f.write(extracted_keys.get(sector_no * 2, unknownkey))
f.write(extracted_keys.get(sector_no * 2 + 1, unknownkey))
print(f" {CG}[+]{C0} Keys saved to {key_path} (as .key format)")
def dump_card_to_file(self, extracted_keys, max_sectors_num):
if input(f" {CY}[?]{C0} Dump card to file? [y/n]: ").lower() != "y":
return
filename = input(
f" {C0}[+]{C0} Enter dump filename (without extension): "
).strip()
if not filename:
print(f" {CR}[!]{C0} No filename provided, skipping.")
return
dump_path = filename + ".bin"
buffer = bytearray()
for s in range(max_sectors_num):
key_a = extracted_keys.get(s * 2)
key_b = extracted_keys.get(s * 2 + 1)
num_blocks, first_block = (
(4, s * 4) if s < 32 else (16, 128 + (s - 32) * 16)
)
for b in range(num_blocks):
block_num = first_block + b
block_data = None
if key_b is not None:
try:
block_data = self.cmd.mf1_read_one_block(
block_num, MfcKeyType.B, key_b
)
except Exception:
pass
if block_data is None and key_a is not None:
try:
block_data = self.cmd.mf1_read_one_block(
block_num, MfcKeyType.A, key_a
)
except Exception:
pass
if block_data is None:
print(
f" {CR}[!]{C0} Block {block_num} unreadable, filling with zeros"
)
block_data = bytes(16)
buffer.extend(block_data)
with open(dump_path, "wb") as f:
f.write(buffer)
print(f" {CG}[+]{C0} Card dumped to {dump_path}")
def on_exec(self, args: argparse.Namespace):
key_known: str = args.key
if key_known is not None and not re.match(r"^[a-fA-F0-9]{12}$", key_known):
print("key must include 12 HEX symbols")
return
extracted_keys, max_sectors_num = self.autopwn(key_known)
self.print_key_table(extracted_keys, max_sectors_num)
self.save_keys_to_file(extracted_keys, max_sectors_num)
self.dump_card_to_file(extracted_keys, max_sectors_num)
@hf_mf.command("fchk")
class HFMFFCHK(ReaderRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Mifare Classic fast key check on sectors"
mifare_type_group = parser.add_mutually_exclusive_group()
mifare_type_group.add_argument(
"--mini",
help="MIFARE Classic Mini / S20",
action="store_const",
dest="maxSectors",
const=5,
)
mifare_type_group.add_argument(
"--1k",
help="MIFARE Classic 1k / S50 (default)",
action="store_const",
dest="maxSectors",
const=16,
)
mifare_type_group.add_argument(
"--2k",
help="MIFARE Classic/Plus 2k",
action="store_const",
dest="maxSectors",
const=32,
)
mifare_type_group.add_argument(
"--4k",
help="MIFARE Classic 4k / S70",
action="store_const",
dest="maxSectors",
const=40,
)
parser.add_argument(
dest="keys",
help="Key (as hex[12] format)",
metavar="<hex>",
type=str,
nargs="*",
)
parser.add_argument(
"--key",
dest="import_key",
type=argparse.FileType("rb"),
help="Read keys from .key format file",
)
parser.add_argument(
"--dic",
dest="import_dic",
type=argparse.FileType("r", encoding="utf8"),
help="Read keys from .dic format file",
)
parser.add_argument(
"--export-key",
type=argparse.FileType("wb"),
help=f'Export result as .key format, file will be {color_string((CR, "OVERWRITTEN"))} if exists',
)
parser.add_argument(
"--export-dic",
type=argparse.FileType("w", encoding="utf8"),
help=f'Export result as .dic format, file will be {color_string((CR, "OVERWRITTEN"))} if exists',
)
parser.add_argument(
"-m",
"--mask",
help="Which sectorKey to be skip, 1 bit per sectorKey. `0b1` represent to skip to check. (in hex[20] format)",
type=str,
default="00000000000000000000",
metavar="<hex>",
)
parser.set_defaults(maxSectors=16)
return parser
def check_keys(self, mask: bytearray, keys: list[bytes], chunkSize=20):
sectorKeys = dict()
for i in range(0, len(keys), chunkSize):
# print("mask = {}".format(mask.hex(sep=' ', bytes_per_sep=1)))
chunkKeys = keys[i : i + chunkSize]
print(
f' - progress of checking keys... {color_string((CY, i))} / {len(keys)} ({color_string((CY, f"{100 * i / len(keys):.1f}"))} %)'
)
resp = self.cmd.mf1_check_keys_of_sectors(mask, chunkKeys)
# print(resp)
if resp["status"] != Status.HF_TAG_OK:
print(
f' - check interrupted, reason: {color_string((CR, Status(resp["status"])))}'
)
break
elif "sectorKeys" not in resp:
print(
f' - check interrupted, reason: {color_string((CG, "All sectorKey is found or masked"))}'
)
break
for j in range(10):
mask[j] |= resp["found"][j]
sectorKeys.update(resp["sectorKeys"])
return sectorKeys
def on_exec(self, args: argparse.Namespace):
# print(args)
keys = set()
# keys from args
for key in args.keys:
if not re.match(r"^[a-fA-F0-9]{12}$", key):
print(
f' - {color_string((CR, "Key should in hex[12] format, invalid key is ignored"))}, key = "{key}"'
)
continue
keys.add(bytes.fromhex(key))
# read keys from key format file
if args.import_key is not None:
if not load_key_file(args.import_key, keys):
return
if args.import_dic is not None:
if not load_dic_file(args.import_dic, keys):
return
if len(keys) == 0:
print(f' - {color_string((CR, "No keys"))}')
return
print(f" - loaded {color_string((CG, len(keys)))} keys")
# mask
if not re.match(r"^[a-fA-F0-9]{1,20}$", args.mask):
print(
f' - {color_string((CR, "mask should in hex[20] format"))}, mask = "{args.mask}"'
)
return
mask = bytearray.fromhex(f"{args.mask:0<20}")
for i in range(args.maxSectors, 40):
mask[i // 4] |= 3 << (6 - i % 4 * 2)
# check keys
startedAt = datetime.now()
sectorKeys = self.check_keys(mask, list(keys))
endedAt = datetime.now()
duration = endedAt - startedAt
print(
f" - elapsed time: {color_string((CY, f'{duration.total_seconds():.3f}s'))}"
)
if args.export_key is not None:
unknownkey = bytes(6)
for sectorNo in range(args.maxSectors):
args.export_key.write(sectorKeys.get(2 * sectorNo, unknownkey))
args.export_key.write(sectorKeys.get(2 * sectorNo + 1, unknownkey))
print(
f" - result exported to: {color_string((CG, args.export_key.name))} (as .key format)"
)
if args.export_dic is not None:
uniq_result = set(sectorKeys.values())
for key in uniq_result:
args.export_dic.write(key.hex().upper() + "\n")
print(
f" - result exported to: {color_string((CG, args.export_dic.name))} (as .dic format)"
)
# print sectorKeys
print(f"\n - {color_string((CG, 'result of key checking:'))}\n")
print("-----+-----+--------------+---+--------------+----")
print(" Sec | Blk | key A |res| key B |res ")
print("-----+-----+--------------+---+--------------+----")
for sectorNo in range(args.maxSectors):
blk = (sectorNo * 4 + 3) if sectorNo < 32 else (sectorNo * 16 - 369)
keyA = sectorKeys.get(2 * sectorNo, None)
if keyA:
keyA = f"{color_string((CG, keyA.hex().upper()))} | {color_string((CG, '1'))}"
else:
keyA = (
f"{color_string((CR, '------------'))} | {color_string((CR, '0'))}"
)
keyB = sectorKeys.get(2 * sectorNo + 1, None)
if keyB:
keyB = f"{color_string((CG, keyB.hex().upper()))} | {color_string((CG, '1'))}"
else:
keyB = (
f"{color_string((CR, '------------'))} | {color_string((CR, '0'))}"
)
print(
f" {color_string((CY, f'{sectorNo:03d}'))} | {blk:03d} | {keyA} | {keyB} "
)
print("-----+-----+--------------+---+--------------+----")
print(
f"( {color_string((CR, '0'))}: Failed, {color_string((CG, '1'))}: Success )\n\n"
)
@hf_mf.command("rdbl")
class HFMFRDBL(MF1AuthArgsUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = super().args_parser()
parser.description = "Mifare Classic read one block"
return parser
def on_exec(self, args: argparse.Namespace):
param = self.get_param(args)
resp = self.cmd.mf1_read_one_block(param.block, param.type, param.key)
print(f" - Data: {resp.hex()}")
@hf_mf.command("wrbl")
class HFMFWRBL(MF1AuthArgsUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = super().args_parser()
parser.description = "Mifare Classic write one block"
parser.add_argument(
"-d",
"--data",
type=str,
required=True,
metavar="<hex>",
help="Your block data, as hex string.",
)
return parser
def on_exec(self, args: argparse.Namespace):
param = self.get_param(args)
if not re.match(r"^[a-fA-F0-9]{32}$", args.data):
raise ArgsParserError("Data must include 32 HEX symbols")
data = bytearray.fromhex(args.data)
resp = self.cmd.mf1_write_one_block(param.block, param.type, param.key, data)
if resp:
print(f" - {color_string((CG, 'Write done.'))}")
else:
print(f" - {color_string((CR, 'Write fail.'))}")
@hf_mf.command("view")
class HFMFView(MF1AuthArgsUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Display content from tag memory or dump file"
mifare_type_group = parser.add_mutually_exclusive_group()
mifare_type_group.add_argument(
"--mini",
help="MIFARE Classic Mini / S20",
action="store_const",
dest="maxSectors",
const=5,
)
mifare_type_group.add_argument(
"--1k",
help="MIFARE Classic 1k / S50 (default)",
action="store_const",
dest="maxSectors",
const=16,
)
mifare_type_group.add_argument(
"--2k",
help="MIFARE Classic/Plus 2k",
action="store_const",
dest="maxSectors",
const=32,
)
mifare_type_group.add_argument(
"--4k",
help="MIFARE Classic 4k / S70",
action="store_const",
dest="maxSectors",
const=40,
)
parser.add_argument(
"-d",
"--dump-file",
required=False,
type=argparse.FileType("rb"),
help="Dump file to read",
)
parser.add_argument(
"-k",
"--key-file",
required=False,
type=argparse.FileType("r"),
help="File containing keys of tag to write (exported with fchk --export)",
)
parser.set_defaults(maxSectors=16)
return parser
def on_exec(self, args: argparse.Namespace):
data = bytearray(0)
if args.dump_file is not None:
print("Reading dump file")
data = args.dump_file.read()
elif args.key_file is not None:
print("Reading tag memory")
# read keys from file
keys = list()
for line in args.key_file.readlines():
a, b = [bytes.fromhex(h) for h in line[:-1].split(":")]
keys.append((a, b))
if len(keys) != args.maxSectors:
raise ArgsParserError(
f"Invalid key file. Found {len(keys)}, expected {args.maxSectors}"
)
# iterate over blocks
for blk in range(0, args.maxSectors * 4):
resp = None
try:
# first try with key B
resp = self.cmd.mf1_read_one_block(
blk, MfcKeyType.B, keys[blk // 4][1]
)
except UnexpectedResponseError:
# ignore read errors at this stage as we want to try key A
pass
if not resp:
# try with key A if B was unsuccessful
# this will raise an exception if key A fails too
resp = self.cmd.mf1_read_one_block(
blk, MfcKeyType.A, keys[blk // 4][0]
)
data.extend(resp)
else:
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):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "MIFARE Classic value block commands"
operator_group = parser.add_mutually_exclusive_group()
operator_group.add_argument(
"--get", action="store_true", help="get value from src block"
)
operator_group.add_argument(
"--set",
type=int,
required=False,
metavar="<dec>",
help="set value X (-2147483647 ~ 2147483647) to src block",
)
operator_group.add_argument(
"--inc",
type=int,
required=False,
metavar="<dec>",
help="increment value by X (0 ~ 2147483647) from src to dst",
)
operator_group.add_argument(
"--dec",
type=int,
required=False,
metavar="<dec>",
help="decrement value by X (0 ~ 2147483647) from src to dst",
)
operator_group.add_argument(
"--res",
"--cp",
action="store_true",
help="copy value from src to dst (Restore and Transfer)",
)
parser.add_argument(
"--blk",
"--src-block",
type=int,
required=True,
metavar="<dec>",
help="block number of src",
)
srctype_group = parser.add_mutually_exclusive_group()
srctype_group.add_argument(
"-a", "-A", action="store_true", help="key of src is A key (default)"
)
srctype_group.add_argument(
"-b", "-B", action="store_true", help="key of src is B key"
)
parser.add_argument(
"-k",
"--src-key",
type=str,
required=True,
metavar="<hex>",
help="key of src",
)
parser.add_argument(
"--tblk",
"--dst-block",
type=int,
metavar="<dec>",
help="block number of dst (default to src)",
)
dsttype_group = parser.add_mutually_exclusive_group()
dsttype_group.add_argument(
"--ta",
"--tA",
action="store_true",
help="key of dst is A key (default to src)",
)
dsttype_group.add_argument(
"--tb",
"--tB",
action="store_true",
help="key of dst is B key (default to src)",
)
parser.add_argument(
"--tkey",
"--dst-key",
type=str,
metavar="<hex>",
help="key of dst (default to src)",
)
return parser
def on_exec(self, args: argparse.Namespace):
# print(args)
# src
src_blk = args.blk
src_type = MfcKeyType.B if args.b is not False else MfcKeyType.A
src_key = args.src_key
if not re.match(r"^[a-fA-F0-9]{12}$", src_key):
print("src_key must include 12 HEX symbols")
return
src_key = bytearray.fromhex(src_key)
# print(src_blk, src_type, src_key)
if args.get is not False:
self.get_value(src_blk, src_type, src_key)
return
elif args.set is not None:
self.set_value(src_blk, src_type, src_key, args.set)
return
# dst
dst_blk = args.tblk if args.tblk is not None else src_blk
dst_type = (
MfcKeyType.A
if args.ta is not False
else (MfcKeyType.B if args.tb is not False else src_type)
)
dst_key = args.tkey if args.tkey is not None else args.src_key
if not re.match(r"^[a-fA-F0-9]{12}$", dst_key):
print("dst_key must include 12 HEX symbols")
return
dst_key = bytearray.fromhex(dst_key)
# print(dst_blk, dst_type, dst_key)
if args.inc is not None:
self.inc_value(
src_blk, src_type, src_key, args.inc, dst_blk, dst_type, dst_key
)
return
elif args.dec is not None:
self.dec_value(
src_blk, src_type, src_key, args.dec, dst_blk, dst_type, dst_key
)
return
elif args.res is not False:
self.res_value(src_blk, src_type, src_key, dst_blk, dst_type, dst_key)
return
else:
raise ArgsParserError("Please specify a value command")
def get_value(self, block, type, key):
resp = self.cmd.mf1_read_one_block(block, type, key)
val1, val2, val3, adr1, adr2, adr3, adr4 = struct.unpack("<iiiBBBB", resp)
# print(f"{val1}, {val2}, {val3}, {adr1}, {adr2}, {adr3}, {adr4}")
if (val1 != val3) or (val1 + val2 != -1):
print(
f" - {color_string((CR, f'Invalid value of value block: {resp.hex()}'))}"
)
return
if (adr1 != adr3) or (adr2 != adr4) or (adr1 + adr2 != 0xFF):
print(
f" - {color_string((CR, f'Invalid address of value block: {resp.hex()}'))}"
)
return
print(
f" - block[{block}] = {color_string((CG, f'{{ value: {val1}, adr: {adr1} }}'))}"
)
def set_value(self, block, type, key, value):
if value < -2147483647 or value > 2147483647:
raise ArgsParserError(
f"Set value must be between -2147483647 and 2147483647. Got {value}"
)
adr_inverted = 0xFF - block
data = struct.pack(
"<iiiBBBB",
value,
-value - 1,
value,
block,
adr_inverted,
block,
adr_inverted,
)
resp = self.cmd.mf1_write_one_block(block, type, key, data)
if resp:
print(f" - {color_string((CG, 'Set done.'))}")
self.get_value(block, type, key)
else:
print(f" - {color_string((CR, 'Set fail.'))}")
def inc_value(self, src_blk, src_type, src_key, value, dst_blk, dst_type, dst_key):
if value < 0 or value > 2147483647:
raise ArgsParserError(
f"Increment value must be between 0 and 2147483647. Got {value}"
)
resp = self.cmd.mf1_manipulate_value_block(
src_blk,
src_type,
src_key,
MfcValueBlockOperator.INCREMENT,
value,
dst_blk,
dst_type,
dst_key,
)
if resp:
print(f" - {color_string((CG, 'Increment done.'))}")
self.get_value(dst_blk, dst_type, dst_key)
else:
print(f" - {color_string((CR, 'Increment fail.'))}")
def dec_value(self, src_blk, src_type, src_key, value, dst_blk, dst_type, dst_key):
if value < 0 or value > 2147483647:
raise ArgsParserError(
f"Decrement value must be between 0 and 2147483647. Got {value}"
)
resp = self.cmd.mf1_manipulate_value_block(
src_blk,
src_type,
src_key,
MfcValueBlockOperator.DECREMENT,
value,
dst_blk,
dst_type,
dst_key,
)
if resp:
print(f" - {color_string((CG, 'Decrement done.'))}")
self.get_value(dst_blk, dst_type, dst_key)
else:
print(f" - {color_string((CR, 'Decrement fail.'))}")
def res_value(self, src_blk, src_type, src_key, dst_blk, dst_type, dst_key):
resp = self.cmd.mf1_manipulate_value_block(
src_blk,
src_type,
src_key,
MfcValueBlockOperator.RESTORE,
0,
dst_blk,
dst_type,
dst_key,
)
if resp:
print(f" - {color_string((CG, 'Restore done.'))}")
self.get_value(dst_blk, dst_type, dst_key)
else:
print(f" - {color_string((CR, 'Restore fail.'))}")
_KEY = re.compile("[a-fA-F0-9]{12}", flags=re.MULTILINE)
def _run_mfkey32v2(items):
output_str = subprocess.run(
[
default_cwd / ("mfkey32v2.exe" if sys.platform == "win32" else "mfkey32v2"),
items[0]["uid"],
items[0]["nt"],
items[0]["nr"],
items[0]["ar"],
items[1]["nt"],
items[1]["nr"],
items[1]["ar"],
],
capture_output=True,
check=True,
encoding="ascii",
).stdout
sea_obj = _KEY.search(output_str)
if sea_obj is not None:
return sea_obj[0], items
return None
class ItemGenerator:
def __init__(self, rs, uid_found_keys=set()):
self.rs: list = rs
self.progress = 0
self.i = 0
self.j = 1
self.found = set()
self.keys = set()
for known_key in uid_found_keys:
self.test_key(known_key)
def __iter__(self):
return self
def __next__(self):
size = len(self.rs)
if self.j >= size:
self.i += 1
if self.i >= size - 1:
raise StopIteration
self.j = self.i + 1
item_i, item_j = self.rs[self.i], self.rs[self.j]
self.progress += 1
self.j += 1
if self.key_from_item(item_i) in self.found:
self.progress += max(0, size - self.j)
self.i += 1
self.j = self.i + 1
return next(self)
if self.key_from_item(item_j) in self.found:
return next(self)
return item_i, item_j
@staticmethod
def key_from_item(item):
return "{uid}-{nt}-{nr}-{ar}".format(**item)
def test_key(self, key, items=list()):
for item in self.rs:
item_key = self.key_from_item(item)
if item_key in self.found:
continue
if (item in items) or (
Crypto1.mfkey32_is_reader_has_key(
int(item["uid"], 16),
int(item["nt"], 16),
int(item["nr"], 16),
int(item["ar"], 16),
key,
)
):
self.keys.add(key)
self.found.add(item_key)
@hf_mf.command("elog")
class HFMFELog(DeviceRequiredUnit):
detection_log_size = 18
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "MF1 Detection log count/decrypt"
parser.add_argument(
"--decrypt", action="store_true", help="Decrypt key from MF1 log list"
)
return parser
def decrypt_by_list(self, rs: list, uid_found_keys: set = set()):
"""
Decrypt key from reconnaissance log list
:param rs:
:return:
"""
msg1 = f" > {len(rs)} records => "
msg2 = f"/{(len(rs)*(len(rs)-1))//2} combinations. "
msg3 = " key(s) found"
gen = ItemGenerator(rs, uid_found_keys)
print(f"{msg1}{gen.progress}{msg2}{len(gen.keys)}{msg3}\r", end="")
with Pool(cpu_count()) as pool:
for result in pool.imap(_run_mfkey32v2, gen):
if result is not None:
gen.test_key(*result)
print(f"{msg1}{gen.progress}{msg2}{len(gen.keys)}{msg3}\r", end="")
print(f"{msg1}{gen.progress}{msg2}{len(gen.keys)}{msg3}")
return gen.keys
def on_exec(self, args: argparse.Namespace):
if not args.decrypt:
count = self.cmd.mf1_get_detection_count()
print(f" - MF1 detection log count = {count}")
return
index = 0
count = self.cmd.mf1_get_detection_count()
if count == 0:
print(" - No detection log to download")
return
print(f" - MF1 detection log count = {count}, start download", end="")
result_list = []
while index < count:
tmp = self.cmd.mf1_get_detection_log(index)
recv_count = len(tmp)
index += recv_count
result_list.extend(tmp)
print("." * recv_count, end="")
print()
print(f" - Download done ({len(result_list)} records), start parse and decrypt")
# classify
result_maps = {}
for item in result_list:
uid = item["uid"]
if uid not in result_maps:
result_maps[uid] = {}
block = item["block"]
if block not in result_maps[uid]:
result_maps[uid][block] = {}
type = item["type"]
if type not in result_maps[uid][block]:
result_maps[uid][block][type] = []
result_maps[uid][block][type].append(item)
for uid in result_maps.keys():
print(f" - Detection log for uid [{uid.upper()}]")
result_maps_for_uid = result_maps[uid]
uid_found_keys = set()
for block in result_maps_for_uid:
for keyType in "AB":
records = (
result_maps_for_uid[block][keyType]
if keyType in result_maps_for_uid[block]
else []
)
if len(records) < 1:
continue
print(f" > Decrypting block {block} key {keyType} detect log...")
result_maps[uid][block][keyType] = self.decrypt_by_list(
records, uid_found_keys
)
uid_found_keys.update(result_maps[uid][block][keyType])
print(" > Result ---------------------------")
for block in result_maps_for_uid.keys():
if "A" in result_maps_for_uid[block]:
print(
f" > Block {block}, A key result: {result_maps_for_uid[block]['A']}"
)
if "B" in result_maps_for_uid[block]:
print(
f" > Block {block}, B key result: {result_maps_for_uid[block]['B']}"
)
return
@hf_mf.command("eload")
class HFMFELoad(SlotIndexArgsAndGoUnit, DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Load data to emulator memory"
self.add_slot_args(parser)
parser.add_argument("-f", "--file", type=str, required=True, help="file path")
parser.add_argument(
"-t",
"--type",
type=str,
required=False,
help="content type",
choices=["bin", "hex"],
)
return parser
def on_exec(self, args: argparse.Namespace):
file = args.file
if args.type is None:
if file.endswith(".bin"):
content_type = "bin"
elif file.endswith(".eml"):
content_type = "hex"
else:
raise Exception(
"Unknown file format, Specify content type with -t option"
)
else:
content_type = args.type
buffer = bytearray()
with open(file, mode="rb") as fd:
if content_type == "bin":
buffer.extend(fd.read())
if content_type == "hex":
buffer.extend(bytearray.fromhex(fd.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")
index = 0
block = 0
max_blocks = (self.device_com.data_max_length - 1) // 16
while index + 16 < len(buffer):
# split a block from buffer
block_data = buffer[index : index + 16 * max_blocks]
n_blocks = len(block_data) // 16
index += 16 * n_blocks
# load to device
self.cmd.mf1_write_emu_block_data(block, block_data)
print("." * n_blocks, end="")
block += n_blocks
print("\n - Load success")
@hf_mf.command("esave")
class HFMFESave(SlotIndexArgsAndGoUnit, DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Read data from emulator memory"
self.add_slot_args(parser)
parser.add_argument("-f", "--file", type=str, required=True, help="file path")
parser.add_argument(
"-t",
"--type",
type=str,
required=False,
help="content type",
choices=["bin", "hex"],
)
return parser
def on_exec(self, args: argparse.Namespace):
file = args.file
if args.type is None:
if file.endswith(".bin"):
content_type = "bin"
elif file.endswith(".eml"):
content_type = "hex"
else:
raise Exception(
"Unknown file format, Specify content type with -t option"
)
else:
content_type = args.type
selected_slot = self.cmd.get_active_slot()
slot_info = self.cmd.get_slot_info()
tag_type = TagSpecificType(slot_info[selected_slot]["hf"])
if tag_type == TagSpecificType.MIFARE_Mini:
block_count = 20
elif tag_type == TagSpecificType.MIFARE_1024:
block_count = 64
elif tag_type == TagSpecificType.MIFARE_2048:
block_count = 128
elif tag_type == TagSpecificType.MIFARE_4096:
block_count = 256
else:
raise Exception(
"Card in current slot is not Mifare Classic/Plus in SL1 mode"
)
index = 0
data = bytearray(0)
max_blocks = self.device_com.data_max_length // 16
while block_count > 0:
chunk_count = min(block_count, max_blocks)
data.extend(self.cmd.mf1_read_emu_block_data(index, chunk_count))
index += chunk_count
block_count -= chunk_count
print("." * chunk_count, end="")
with open(file, "wb") as fd:
if content_type == "hex":
for i in range(len(data) // 16):
fd.write(binascii.hexlify(data[i * 16 : (i + 1) * 16]) + b"\n")
else:
fd.write(data)
print("\n - Read success")
@hf_mf.command("eview")
class HFMFEView(SlotIndexArgsAndGoUnit, DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "View data from emulator memory"
self.add_slot_args(parser)
return parser
def on_exec(self, args: argparse.Namespace):
selected_slot = self.cmd.get_active_slot()
slot_info = self.cmd.get_slot_info()
tag_type = TagSpecificType(slot_info[selected_slot]["hf"])
if tag_type == TagSpecificType.MIFARE_Mini:
block_count = 20
elif tag_type == TagSpecificType.MIFARE_1024:
block_count = 64
elif tag_type == TagSpecificType.MIFARE_2048:
block_count = 128
elif tag_type == TagSpecificType.MIFARE_4096:
block_count = 256
else:
raise Exception(
"Card in current slot is not Mifare Classic/Plus in SL1 mode"
)
index = 0
data = bytearray(0)
max_blocks = self.device_com.data_max_length // 16
while block_count > 0:
# read all the blocks
chunk_count = min(block_count, max_blocks)
data.extend(self.cmd.mf1_read_emu_block_data(index, chunk_count))
index += chunk_count
block_count -= chunk_count
print_mem_dump(data, 16)
@hf_mf.command("econfig")
class HFMFEConfig(SlotIndexArgsAndGoUnit, HF14AAntiCollArgsUnit, DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Settings of Mifare Classic emulator"
self.add_slot_args(parser)
self.add_hf14a_anticoll_args(parser)
gen1a_group = parser.add_mutually_exclusive_group()
gen1a_group.add_argument(
"--enable-gen1a", action="store_true", help="Enable Gen1a magic mode"
)
gen1a_group.add_argument(
"--disable-gen1a", action="store_true", help="Disable Gen1a magic mode"
)
gen2_group = parser.add_mutually_exclusive_group()
gen2_group.add_argument(
"--enable-gen2", action="store_true", help="Enable Gen2 magic mode"
)
gen2_group.add_argument(
"--disable-gen2", action="store_true", help="Disable Gen2 magic mode"
)
block0_group = parser.add_mutually_exclusive_group()
block0_group.add_argument(
"--enable-block0",
action="store_true",
help="Use anti-collision data from block 0 for 4 byte UID tags",
)
block0_group.add_argument(
"--disable-block0",
action="store_true",
help="Use anti-collision data from settings",
)
write_names = [w.name for w in MifareClassicWriteMode.list()]
help_str = "Write Mode: " + ", ".join(write_names)
parser.add_argument(
"--write", type=str, help=help_str, metavar="MODE", choices=write_names
)
log_group = parser.add_mutually_exclusive_group()
log_group.add_argument(
"--enable-log",
action="store_true",
help="Enable logging of MFC authentication data",
)
log_group.add_argument(
"--disable-log",
action="store_true",
help="Disable logging of MFC authentication data",
)
field_off_reset_group = parser.add_mutually_exclusive_group()
field_off_reset_group.add_argument(
"--enable_field_off_do_reset",
action="store_true",
help="Enable FIELD_OFF_DO_RESET",
)
field_off_reset_group.add_argument(
"--disable_field_off_do_reset",
action="store_true",
help="Disable FIELD_OFF_DO_RESET",
)
return parser
def on_exec(self, args: argparse.Namespace):
# collect current settings
anti_coll_data = self.cmd.hf14a_get_anti_coll_data()
if anti_coll_data is None or len(anti_coll_data) == 0:
print(
f"{color_string((CR, f'Slot {self.slot_num} does not contain any HF 14A config'))}"
)
return
uid = anti_coll_data["uid"]
atqa = anti_coll_data["atqa"]
sak = anti_coll_data["sak"]
ats = anti_coll_data["ats"]
slotinfo = self.cmd.get_slot_info()
fwslot = SlotNumber.to_fw(self.slot_num)
hf_tag_type = TagSpecificType(slotinfo[fwslot]["hf"])
if hf_tag_type not in [
TagSpecificType.MIFARE_Mini,
TagSpecificType.MIFARE_1024,
TagSpecificType.MIFARE_2048,
TagSpecificType.MIFARE_4096,
]:
print(
f"{color_string((CR, f'Slot {self.slot_num} not configured as MIFARE Classic'))}"
)
return
mfc_config = self.cmd.mf1_get_emulator_config()
gen1a_mode = mfc_config["gen1a_mode"]
gen2_mode = mfc_config["gen2_mode"]
block_anti_coll_mode = mfc_config["block_anti_coll_mode"]
write_mode = MifareClassicWriteMode(mfc_config["write_mode"])
detection = mfc_config["detection"]
change_requested, change_done, uid, atqa, sak, ats = self.update_hf14a_anticoll(
args, uid, atqa, sak, ats
)
field_off_do_reset = self.cmd.mf1_get_field_off_do_reset()
if args.enable_gen1a:
change_requested = True
if not gen1a_mode:
gen1a_mode = True
self.cmd.mf1_set_gen1a_mode(gen1a_mode)
change_done = True
else:
print(f'{color_string((CY, "Requested gen1a already enabled"))}')
elif args.disable_gen1a:
change_requested = True
if gen1a_mode:
gen1a_mode = False
self.cmd.mf1_set_gen1a_mode(gen1a_mode)
change_done = True
else:
print(f'{color_string((CY, "Requested gen1a already disabled"))}')
if args.enable_gen2:
change_requested = True
if not gen2_mode:
gen2_mode = True
self.cmd.mf1_set_gen2_mode(gen2_mode)
change_done = True
else:
print(f'{color_string((CY, "Requested gen2 already enabled"))}')
elif args.disable_gen2:
change_requested = True
if gen2_mode:
gen2_mode = False
self.cmd.mf1_set_gen2_mode(gen2_mode)
change_done = True
else:
print(f'{color_string((CY, "Requested gen2 already disabled"))}')
if args.enable_block0:
change_requested = True
if not block_anti_coll_mode:
block_anti_coll_mode = True
self.cmd.mf1_set_block_anti_coll_mode(block_anti_coll_mode)
change_done = True
else:
print(
f'{color_string((CY, "Requested block0 anti-coll mode already enabled"))}'
)
elif args.disable_block0:
change_requested = True
if block_anti_coll_mode:
block_anti_coll_mode = False
self.cmd.mf1_set_block_anti_coll_mode(block_anti_coll_mode)
change_done = True
else:
print(
f'{color_string((CY, "Requested block0 anti-coll mode already disabled"))}'
)
if args.write is not None:
change_requested = True
new_write_mode = MifareClassicWriteMode[args.write]
if new_write_mode != write_mode:
write_mode = new_write_mode
self.cmd.mf1_set_write_mode(write_mode)
change_done = True
else:
print(f'{color_string((CY, "Requested write mode already set"))}')
if args.enable_log:
change_requested = True
if not detection:
detection = True
self.cmd.mf1_set_detection_enable(detection)
change_done = True
else:
print(
f'{color_string((CY, "Requested logging of MFC authentication data already enabled"))}'
)
elif args.disable_log:
change_requested = True
if detection:
detection = False
self.cmd.mf1_set_detection_enable(detection)
change_done = True
else:
print(
f'{color_string((CY, "Requested logging of MFC authentication data already disabled"))}'
)
if args.enable_field_off_do_reset:
change_requested = True
if not field_off_do_reset:
field_off_do_reset = True
self.cmd.mf1_set_field_off_do_reset(field_off_do_reset)
change_done = True
else:
print(
f'{color_string((CY, "Requested FIELD_OFF_DO_RESET already enabled"))}'
)
elif args.disable_field_off_do_reset:
change_requested = True
if field_off_do_reset:
field_off_do_reset = False
self.cmd.mf1_set_field_off_do_reset(field_off_do_reset)
change_done = True
else:
print(
f'{color_string((CY, "Requested FIELD_OFF_DO_RESET already disabled"))}'
)
if change_done:
print(" - MF1 Emulator settings updated")
if not change_requested:
enabled_str = color_string((CG, "enabled"))
disabled_str = color_string((CR, "disabled"))
atqa_string = f"{atqa.hex().upper()} (0x{int.from_bytes(atqa, byteorder='little'):04x})"
print(f'- {"Type:":40}{color_string((CY, hf_tag_type))}')
print(f'- {"UID:":40}{color_string((CY, uid.hex().upper()))}')
print(f'- {"ATQA:":40}{color_string((CY, atqa_string))}')
print(f'- {"SAK:":40}{color_string((CY, sak.hex().upper()))}')
if len(ats) > 0:
print(f'- {"ATS:":40}{color_string((CY, ats.hex().upper()))}')
print(
f'- {"Gen1A magic mode:":40}{f"{enabled_str}" if gen1a_mode else f"{disabled_str}"}'
)
print(
f'- {"Gen2 magic mode:":40}{f"{enabled_str}" if gen2_mode else f"{disabled_str}"}'
)
print(
f'- {"Use anti-collision data from block 0:":40}'
f'{f"{enabled_str}" if block_anti_coll_mode else f"{disabled_str}"}'
)
try:
print(
f'- {"Write mode:":40}{color_string((CY, MifareClassicWriteMode(write_mode)))}'
)
except ValueError:
print(f'- {"Write mode:":40}{color_string((CR, "invalid value!"))}')
print(
f'- {"Log (mfkey32) mode:":40}{f"{enabled_str}" if detection else f"{disabled_str}"}'
)
print(
f'- {"FIELD_OFF_DO_RESET:":40}{f"{enabled_str}" if field_off_do_reset else f"{disabled_str}"}'
)
@hf_mfu.command("ercnt")
class HFMFUERCNT(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Read MIFARE Ultralight / NTAG counter value."
parser.add_argument(
"-c", "--counter", type=int, required=True, help="Counter index."
)
return parser
def on_exec(self, args: argparse.Namespace):
value, no_tearing = self.cmd.mfu_read_emu_counter_data(args.counter)
print(f" - Value: {value:06x} ({value})")
if no_tearing:
print(f" - Tearing: {color_string((CG, 'not set'))}")
else:
print(f" - Tearing: {color_string((CR, 'set'))}")
@hf_mfu.command("ewcnt")
class HFMFUEWCNT(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Write MIFARE Ultralight / NTAG counter value."
parser.add_argument(
"-c", "--counter", type=int, required=True, help="Counter index."
)
parser.add_argument(
"-v", "--value", type=int, required=True, help="Counter value (24-bit)."
)
parser.add_argument(
"-t",
"--reset-tearing",
action="store_true",
help="Reset tearing event flag.",
)
return parser
def on_exec(self, args: argparse.Namespace):
if args.value > 0xFFFFFF:
print(color_string((CR, f"Counter value {args.value:#x} is too large.")))
return
self.cmd.mfu_write_emu_counter_data(
args.counter, args.value, args.reset_tearing
)
print("- Ok")
@hf_mfu.command("rdpg")
class HFMFURDPG(MFUAuthArgsUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = super().args_parser()
parser.description = "MIFARE Ultralight / NTAG read one page"
parser.add_argument(
"-p",
"--page",
type=int,
required=True,
metavar="<dec>",
help="The page where the key will be used against",
)
return parser
def on_exec(self, args: argparse.Namespace):
param = self.get_param(args)
options = {
"activate_rf_field": 0,
"wait_response": 1,
"append_crc": 1,
"auto_select": 1,
"keep_rf_field": 0,
"check_response_crc": 1,
}
if param.key is not None:
options["keep_rf_field"] = 1
try:
resp = self.cmd.hf14a_raw(
options=options,
resp_timeout_ms=200,
data=struct.pack("!B", 0x1B) + param.key,
)
failed_auth = len(resp) < 2
if not failed_auth:
print(f" - PACK: {resp[:2].hex()}")
except Exception:
# failed auth may cause tags to be lost
failed_auth = True
options["keep_rf_field"] = 0
options["auto_select"] = 0
else:
failed_auth = False
if not failed_auth:
resp = self.cmd.hf14a_raw(
options=options,
resp_timeout_ms=200,
data=struct.pack("!BB", 0x30, args.page),
)
print(f" - Data: {resp[:4].hex()}")
else:
try:
self.cmd.hf14a_raw(
options=options,
resp_timeout_ms=200,
data=struct.pack("!BB", 0x30, args.page),
)
except (ValueError, chameleon_com.CMDInvalidException, TimeoutError):
# we may lose the tag again here
pass
print(color_string((CR, " - Auth failed")))
@hf_mfu.command("wrpg")
class HFMFUWRPG(MFUAuthArgsUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = super().args_parser()
parser.description = "MIFARE Ultralight / NTAG write one page"
parser.add_argument(
"-p",
"--page",
type=int,
required=True,
metavar="<dec>",
help="The index of the page to write to.",
)
parser.add_argument(
"-d",
"--data",
type=bytes.fromhex,
required=True,
metavar="<hex>",
help="Your page data, as a 4 byte (8 character) hex string.",
)
return parser
def on_exec(self, args: argparse.Namespace):
param = self.get_param(args)
data = args.data
if len(data) != 4:
print(
color_string(
(CR, "Page data should be a 4 byte (8 character) hex string")
)
)
return
options = {
"activate_rf_field": 0,
"wait_response": 1,
"append_crc": 1,
"auto_select": 1,
"keep_rf_field": 0,
"check_response_crc": 0,
}
if param.key is not None:
options["keep_rf_field"] = 1
options["check_response_crc"] = 1
try:
resp = self.cmd.hf14a_raw(
options=options,
resp_timeout_ms=200,
data=struct.pack("!B", 0x1B) + param.key,
)
failed_auth = len(resp) < 2
if not failed_auth:
print(f" - PACK: {resp[:2].hex()}")
except Exception:
# failed auth may cause tags to be lost
failed_auth = True
options["keep_rf_field"] = 0
options["auto_select"] = 0
options["check_response_crc"] = 0
else:
failed_auth = False
if not failed_auth:
resp = self.cmd.hf14a_raw(
options=options,
resp_timeout_ms=200,
data=struct.pack("!BB", 0xA2, args.page) + data,
)
if resp[0] == 0x0A:
print(" - Ok")
else:
print(color_string((CR, f"Write failed ({resp[0]:#04x}).")))
else:
# send a command just to disable the field. use read to avoid corrupting the data
try:
self.cmd.hf14a_raw(
options=options,
resp_timeout_ms=200,
data=struct.pack("!BB", 0x30, args.page),
)
except (ValueError, chameleon_com.CMDInvalidException, TimeoutError):
# we may lose the tag again here
pass
print(color_string((CR, " - Auth failed")))
@hf_mfu.command("eview")
class HFMFUEVIEW(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "MIFARE Ultralight / NTAG view emulator data"
return parser
def on_exec(self, args: argparse.Namespace):
nr_pages = self.cmd.mfu_get_emu_pages_count()
page = 0
while page < nr_pages:
count = min(nr_pages - page, 16)
data = self.cmd.mfu_read_emu_page_data(page, count)
for i in range(0, len(data), 4):
print(f"#{page+(i >> 2):02x}: {data[i:i+4].hex()}")
page += count
@hf_mfu.command("eload")
class HFMFUELOAD(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "MIFARE Ultralight / NTAG load emulator data"
parser.add_argument(
"-f", "--file", required=True, type=str, help="File to load data from."
)
parser.add_argument(
"-t",
"--type",
type=str,
required=False,
help="Force writing as either raw binary or hex.",
choices=["bin", "hex"],
)
return parser
def get_param(self, args):
class Param:
def __init__(self):
pass
return Param()
def on_exec(self, args: argparse.Namespace):
file_type = args.type
if file_type is None:
if args.file.endswith(".eml") or args.file.endswith(".txt"):
file_type = "hex"
else:
file_type = "bin"
if file_type == "hex":
with open(args.file, "r") as f:
data = f.read()
data = re.sub("#.*$", "", data, flags=re.MULTILINE)
data = bytes.fromhex(data)
else:
with open(args.file, "rb") as f:
data = f.read()
# this will throw an exception on incorrect slot type
nr_pages = self.cmd.mfu_get_emu_pages_count()
size = nr_pages * 4
if len(data) > size:
print(
color_string(
(
CR,
f"Dump file is too large for the current slot (expected {size} bytes).",
)
)
)
return
elif (len(data) % 4) > 0:
print(
color_string((CR, "Dump file's length is not a multiple of 4 bytes."))
)
return
elif len(data) < size:
print(
color_string(
(
CY,
f"Dump file is smaller than the current slot's memory ({len(data)} < {size}).",
)
)
)
nr_pages = len(data) >> 2
page = 0
while page < nr_pages:
offset = page * 4
cur_count = min(16, nr_pages - page)
if offset >= len(data):
page_data = bytes.fromhex("00000000") * cur_count
else:
page_data = data[offset : offset + 4 * cur_count]
self.cmd.mfu_write_emu_page_data(page, page_data)
page += cur_count
print(" - Ok")
@hf_mfu.command("esave")
class HFMFUESAVE(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "MIFARE Ultralight / NTAG save emulator data"
parser.add_argument(
"-f", "--file", required=True, type=str, help="File to save data to."
)
parser.add_argument(
"-t",
"--type",
type=str,
required=False,
help="Force writing as either raw binary or hex.",
choices=["bin", "hex"],
)
return parser
def get_param(self, args):
class Param:
def __init__(self):
pass
return Param()
def on_exec(self, args: argparse.Namespace):
file_type = args.type
fd = None
save_as_eml = False
if file_type is None:
if args.file.endswith(".eml") or args.file.endswith(".txt"):
file_type = "hex"
else:
file_type = "bin"
if file_type == "hex":
fd = open(args.file, "w+")
save_as_eml = True
else:
fd = open(args.file, "wb+")
with fd:
# this will throw an exception on incorrect slot type
nr_pages = self.cmd.mfu_get_emu_pages_count()
fd.truncate(0)
# write version and signature as comments if saving as .eml
if save_as_eml:
try:
version = self.cmd.mf0_ntag_get_version_data()
fd.write(f"# Version: {version.hex()}\n")
except (ValueError, chameleon_com.CMDInvalidException, TimeoutError):
pass # slot does not have version data
try:
signature = self.cmd.mf0_ntag_get_signature_data()
if signature != b"\x00" * 32:
fd.write(f"# Signature: {signature.hex()}\n")
except (ValueError, chameleon_com.CMDInvalidException, TimeoutError):
pass # slot does not have signature data
page = 0
while page < nr_pages:
cur_count = min(32, nr_pages - page)
data = self.cmd.mfu_read_emu_page_data(page, cur_count)
if save_as_eml:
for i in range(0, len(data), 4):
fd.write(data[i : i + 4].hex() + "\n")
else:
fd.write(data)
page += cur_count
print(" - Ok")
@hf_mfu.command("rcnt")
class HFMFURCNT(MFUAuthArgsUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = super().args_parser()
parser.description = "MIFARE Ultralight / NTAG read counter"
parser.add_argument(
"-c",
"--counter",
type=int,
required=True,
metavar="<dec>",
help="Index of the counter to read (always 0 for NTAG, 0-2 for Ultralight EV1).",
)
return parser
def on_exec(self, args: argparse.Namespace):
param = self.get_param(args)
options = {
"activate_rf_field": 0,
"wait_response": 1,
"append_crc": 1,
"auto_select": 1,
"keep_rf_field": 0,
"check_response_crc": 1,
}
if param.key is not None:
options["keep_rf_field"] = 1
try:
resp = self.cmd.hf14a_raw(
options=options,
resp_timeout_ms=200,
data=struct.pack("!B", 0x1B) + param.key,
)
failed_auth = len(resp) < 2
if not failed_auth:
print(f" - PACK: {resp[:2].hex()}")
except Exception:
# failed auth may cause tags to be lost
failed_auth = True
options["keep_rf_field"] = 0
options["auto_select"] = 0
else:
failed_auth = False
if not failed_auth:
resp = self.cmd.hf14a_raw(
options=options,
resp_timeout_ms=200,
data=struct.pack("!BB", 0x39, args.counter),
)
print(f" - Data: {resp[:3].hex()}")
else:
try:
self.cmd.hf14a_raw(
options=options,
resp_timeout_ms=200,
data=struct.pack("!BB", 0x39, args.counter),
)
except (ValueError, chameleon_com.CMDInvalidException, TimeoutError):
# we may lose the tag again here
pass
print(color_string((CR, " - Auth failed")))
@hf_mfu.command("dump")
class HFMFUDUMP(MFUAuthArgsUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = super().args_parser()
parser.description = "MIFARE Ultralight dump pages"
parser.add_argument(
"-p",
"--page",
type=int,
required=False,
metavar="<dec>",
default=0,
help="Manually set number of pages to dump",
)
parser.add_argument(
"-q",
"--qty",
type=int,
required=False,
metavar="<dec>",
help="Manually set number of pages to dump",
)
parser.add_argument(
"-f",
"--file",
type=str,
required=False,
default="",
help="Specify a filename for dump file",
)
parser.add_argument(
"-t",
"--type",
type=str,
required=False,
choices=["bin", "hex"],
help="Force writing as either raw binary or hex.",
)
return parser
def do_dump(self, args: argparse.Namespace, param, fd, save_as_eml):
if args.qty is not None:
stop_page = min(args.page + args.qty, 256)
else:
stop_page = None
tags = self.cmd.hf14a_scan()
if len(tags) > 1:
print(f"- {color_string((CR, 'Collision detected, leave only one tag.'))}")
return
elif len(tags) == 0:
print(f"- {color_string((CR, 'No tag detected.'))}")
return
elif tags[0]["atqa"] != b"\x44\x00" or tags[0]["sak"] != b"\x00":
err = color_string(
(
CR,
f"Tag is not Mifare Ultralight compatible (ATQA {tags[0]['atqa'].hex()} SAK {tags[0]['sak'].hex()}).",
)
)
print(f"- {err}")
return
options = {
"activate_rf_field": 0,
"wait_response": 1,
"append_crc": 1,
"auto_select": 1,
"keep_rf_field": 1,
"check_response_crc": 1,
}
# if stop page isn't set manually, try autodetection
if stop_page is None:
tag_name = None
# first try sending the GET_VERSION command
try:
version = self.cmd.hf14a_raw(
options=options, resp_timeout_ms=100, data=struct.pack("!B", 0x60)
)
if len(version) == 0:
version = None
except (ValueError, chameleon_com.CMDInvalidException, TimeoutError):
version = None
# try sending AUTHENTICATE command and observe the result
try:
supports_auth = (
len(
self.cmd.hf14a_raw(
options=options,
resp_timeout_ms=100,
data=struct.pack("!B", 0x1A),
)
)
!= 0
)
except (ValueError, chameleon_com.CMDInvalidException, TimeoutError):
supports_auth = False
if version is not None and not supports_auth:
# either ULEV1 or NTAG
assert len(version) == 8
is_mikron_ulev1 = version[1] == 0x34 and version[2] == 0x21
if (
(version[2] == 3 or is_mikron_ulev1)
and version[4] == 1
and version[5] == 0
):
# Ultralight EV1 V0
size_map = {
0x0B: ("Mifare Ultralight EV1 48b", 20),
0x0E: ("Mifare Ultralight EV1 128b", 41),
}
elif version[2] == 4 and version[4] == 1 and version[5] == 0:
# NTAG 210/212/213/215/216 V0
size_map = {
0x0B: ("NTAG 210", 20),
0x0E: ("NTAG 212", 41),
0x0F: ("NTAG 213", 45),
0x11: ("NTAG 215", 135),
0x13: ("NTAG 216", 231),
}
else:
size_map = {}
if version[6] in size_map:
tag_name, stop_page = size_map[version[6]]
elif version is None and supports_auth:
# Ultralight C
tag_name = "Mifare Ultralight C"
stop_page = 48
elif version is None and not supports_auth:
try:
# Invalid command returning a NAK means that's some old type of NTAG.
self.cmd.hf14a_raw(
options=options,
resp_timeout_ms=100,
data=struct.pack("!B", 0xFF),
)
print(
color_string(
(CY, "Tag is likely NTAG 20x, reading until first error.")
)
)
stop_page = 256
except (ValueError, chameleon_com.CMDInvalidException, TimeoutError):
# Regular Ultralight
tag_name = "Mifare Ultralight"
stop_page = 16
else:
# This is probably Ultralight AES, but we don't support this one yet.
pass
if tag_name is not None:
print(f" - Detected tag type as {tag_name}.")
if stop_page is None:
err_str = "Couldn't autodetect the expected card size, reading until first error."
print(f"- {color_string((CY, err_str))}")
stop_page = 256
needs_stop = False
if param.key is not None:
try:
resp = self.cmd.hf14a_raw(
options=options,
resp_timeout_ms=200,
data=struct.pack("!B", 0x1B) + param.key,
)
needs_stop = len(resp) < 2
if not needs_stop:
print(f" - PACK: {resp[:2].hex()}")
except Exception:
# failed auth may cause tags to be lost
needs_stop = True
options["auto_select"] = 0
# this handles auth failure
if needs_stop:
print(color_string((CR, " - Auth failed")))
if fd is not None:
fd.close()
fd = None
for i in range(args.page, stop_page):
# this could be done once in theory but the command would need to be optimized properly
if param.key is not None and not needs_stop:
resp = self.cmd.hf14a_raw(
options=options,
resp_timeout_ms=200,
data=struct.pack("!B", 0x1B) + param.key,
)
options["auto_select"] = 0 # prevent resets
# disable the rf field after the last command
if i == (stop_page - 1) or needs_stop:
options["keep_rf_field"] = 0
try:
resp = self.cmd.hf14a_raw(
options=options,
resp_timeout_ms=200,
data=struct.pack("!BB", 0x30, i),
)
except (ValueError, chameleon_com.CMDInvalidException, TimeoutError):
# probably lost tag, but we still need to disable rf field
resp = None
if needs_stop:
# break if this command was sent just to disable RF field
break
elif resp is None or len(resp) == 0:
# we need to disable RF field if we reached the last valid page so send one more read command
needs_stop = True
continue
# after the read we are sure we no longer need to select again
options["auto_select"] = 0
# TODO: can be optimized as we get 4 pages at once but beware of wrapping
# in case of end of memory or LOCK on ULC and no key provided
data = resp[:4]
print(f" - Page {i:2}: {data.hex()}")
if fd is not None:
if save_as_eml:
fd.write(data.hex() + "\n")
else:
fd.write(data)
if needs_stop and stop_page != 256:
print(f"- {color_string((CY, 'Dump is shorter than expected.'))}")
if args.file != "":
print(f"- {color_string((CG, f'Dump written in {args.file}.'))}")
def on_exec(self, args: argparse.Namespace):
param = self.get_param(args)
file_type = args.type
fd = None
save_as_eml = False
if args.file != "":
if file_type is None:
if args.file.endswith(".eml") or args.file.endswith(".txt"):
file_type = "hex"
else:
file_type = "bin"
if file_type == "hex":
fd = open(args.file, "w+")
save_as_eml = True
else:
fd = open(args.file, "wb+")
if fd is not None:
with fd:
fd.truncate(0)
self.do_dump(args, param, fd, save_as_eml)
else:
self.do_dump(args, param, fd, save_as_eml)
@hf_mfu.command("version")
class HFMFUVERSION(ReaderRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Request MIFARE Ultralight / NTAG version data."
return parser
def on_exec(self, args: argparse.Namespace):
options = {
"activate_rf_field": 0,
"wait_response": 1,
"append_crc": 1,
"auto_select": 1,
"keep_rf_field": 0,
"check_response_crc": 1,
}
resp = self.cmd.hf14a_raw(
options=options, resp_timeout_ms=200, data=struct.pack("!B", 0x60)
)
print(f" - Data: {resp[:8].hex()}")
@hf_mfu.command("signature")
class HFMFUSIGNATURE(ReaderRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Request MIFARE Ultralight / NTAG ECC signature data."
return parser
def on_exec(self, args: argparse.Namespace):
options = {
"activate_rf_field": 0,
"wait_response": 1,
"append_crc": 1,
"auto_select": 1,
"keep_rf_field": 0,
"check_response_crc": 1,
}
resp = self.cmd.hf14a_raw(
options=options, resp_timeout_ms=200, data=struct.pack("!BB", 0x3C, 0x00)
)
print(f" - Data: {resp[:32].hex()}")
@hf_mfu.command("authnonce")
class HFMFUAUTHNONCE(ReaderRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Get authentication nonce from MIFARE Ultralight C tag."
return parser
def on_exec(self, args: argparse.Namespace):
options = {
"activate_rf_field": 0,
"wait_response": 1,
"append_crc": 1,
"auto_select": 1,
"keep_rf_field": 0,
"check_response_crc": 1,
}
resp = self.cmd.hf14a_raw(
options=options, resp_timeout_ms=200, data=struct.pack("!BB", 0x1A, 0x00)
)
# Response is 0xAF + 8 bytes nonce + 2 bytes CRC = 11 bytes
# We want to display just the 8-byte nonce (skip 0xAF prefix)
if len(resp) >= 9 and resp[0] == 0xAF:
print(f" - Nonce: {resp[1:9].hex()}")
else:
print(f" - Error: Unexpected response: {resp.hex()}")
class CrackEffect:
"""
A class to create a visual effect of cracking blocks of data.
"""
def __init__(
self, num_blocks: int = 4, block_size: int = 8, scramble_delay: float = 0.01
):
"""
Initialize the CrackEffect class with the given parameters.
Args:
num_blocks (int): Number of blocks to display. Default is 4.
block_size (int): Size of each block in characters. Default is 8.
scramble_delay (float): Delay between each scramble update in seconds. Default is 0.01.
"""
self.num_blocks = num_blocks
self.block_size = block_size
self.scramble_delay = scramble_delay
self.message_queue = queue.Queue()
self.revealed = [""] * num_blocks
self.stop_event = threading.Event()
self.cracked_blocks = set()
self.display_lock = threading.Lock()
self.output_enabled = True
def generate_random_hex(self) -> str:
"""Generate a random hex string of block_size length."""
import random
hex_chars = "0123456789ABCDEF"
return "".join(random.choice(hex_chars) for _ in range(self.block_size))
def format_block(self, block: str, is_cracked: bool) -> str:
"""Format a block with appropriate color based on its state."""
if is_cracked:
return f"\033[1;34m{block}\033[0m" # Bold blue
return f"\033[96m{block}\033[0m" # Bright cyan
def draw_static_box(self):
"""Draw the initial static box."""
if not self.output_enabled:
return
width = (self.block_size + 1) * self.num_blocks + 4
print("") # Add some padding above
print("" + "" * width + "")
print("" + " " * width + "")
print("" + " " * width + "")
print("" + " " * width + "")
print("" + "" * width + "")
# Move cursor to the middle line
sys.stdout.write("\033[3A") # Move up 3 lines to middle row
sys.stdout.flush()
def print_above(self, data):
"""Print the given data above the box and redraws the box."""
if not self.output_enabled:
print(data)
return
with self.display_lock:
# Move cursor above the box and clean the line
sys.stdout.write("\033[2A\033[1G\033[K" + data)
self.draw_static_box()
def display_current_state(self):
"""Display the current state of all blocks."""
if not self.output_enabled:
return
with self.display_lock:
formatted_blocks = [
self.format_block(block, i in self.cracked_blocks)
for i, block in enumerate(self.revealed)
]
display_text = " ".join(formatted_blocks)
# Update only the middle line
sys.stdout.write(f"\r{display_text}")
sys.stdout.flush()
def scramble_effect(self):
"""Run the main loop for the scrambling effect."""
if not self.output_enabled:
return
while not self.stop_event.is_set():
# Update all non-cracked blocks with random values
for block in range(self.num_blocks):
if block not in self.cracked_blocks:
self.revealed[block] = self.generate_random_hex()
self.display_current_state()
time.sleep(self.scramble_delay)
def erase_key(self):
"""Erase random parts of the key."""
if not self.output_enabled:
return
for block in range(self.num_blocks):
if block not in self.cracked_blocks:
self.revealed[block] = "." * self.block_size
self.display_current_state()
def process_message_queue(self):
"""Process incoming cracked blocks from the queue."""
if not self.output_enabled:
return
while not self.stop_event.is_set():
try:
block_idx, cracked_text = self.message_queue.get(timeout=0.1)
self.revealed[block_idx] = cracked_text
self.cracked_blocks.add(block_idx)
self.display_current_state()
# Check if all blocks are cracked
if len(self.cracked_blocks) == self.num_blocks:
self.stop_event.set()
print("\n" * 3) # Add newlines after completion
break
except queue.Empty:
continue
except Exception as e:
print(f"\nError processing message: {e}")
break
def add_cracked_block(self, block_idx: int, text: str):
"""Add a cracked block to the message queue."""
if not 0 <= block_idx < self.num_blocks:
raise ValueError(f"Block index {block_idx} out of range")
if len(text) != self.block_size:
raise ValueError(f"Block text must be {self.block_size} characters")
self.message_queue.put((block_idx, text))
def start(self):
"""Start the cracking effect."""
self.draw_static_box()
# Create and start the worker threads
scramble_thread = threading.Thread(target=self.scramble_effect)
process_thread = threading.Thread(target=self.process_message_queue)
scramble_thread.daemon = True
process_thread.daemon = True
scramble_thread.start()
process_thread.start()
# Wait for both threads to complete
process_thread.join()
self.stop_event.set()
scramble_thread.join()
@hf_mfu.command("ulcg")
class HFMFUULCG(ReaderRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Key recovery for Giantec ULCG and USCUID-UL cards (won't work on NXP cards!)"
parser.add_argument(
"-c",
"--challenges",
type=int,
default=1000,
help="Number of challenges to collect (default: 1000)",
)
parser.add_argument(
"-t",
"--threads",
type=int,
default=1,
help="Number of threads for key recovery (default: 1)",
)
parser.add_argument(
"-j",
"--json",
type=str,
help="Path to JSON file to load or save challenges",
)
parser.add_argument(
"-o",
"--offline",
action="store_true",
help="Use offline mode with pre-collected challenges",
)
return parser
def on_exec(self, args: argparse.Namespace):
import json
if not args.offline:
challenges = self.collect_challenges(args.challenges)
if challenges is None:
return
if args.json:
with open(args.json, "w") as f:
json.dump(challenges, f)
print(f"[+] Challenges saved to {args.json}.")
print("[!] Beware that the card key is now erased!")
return
else:
if not args.json:
print("[-] Error: --json required for offline mode")
return
with open(args.json, "r") as f:
challenges = json.load(f)
self.crack_key(challenges, args.threads, args.offline)
def collect_challenges(self, num_challenges):
"""Collect challenges from the card and check if it is vulnerable."""
# Sanity check: make sure an Ultralight C is detected
resp = self.cmd.hf14a_scan()
if resp is None or len(resp) == 0:
print("[-] Error: No tag detected")
return None
# Check SAK for Ultralight C (SAK should be 0x00)
print("[+] Checking for Ultralight C...")
# Check AUTH0 configuration
options = {
"activate_rf_field": 0,
"wait_response": 1,
"append_crc": 1,
"auto_select": 1,
"keep_rf_field": 0,
"check_response_crc": 1,
}
# Read page 40-43 (config pages)
resp = self.cmd.hf14a_raw(
options=options, resp_timeout_ms=200, data=struct.pack("!BB", 0x30, 0x28)
) # READ page 40
if len(resp) < 16:
print(
"[-] Error: Card not unlocked. Run relay attack in UNLOCK mode first."
)
return None
# Check AUTH0 (should be >= 0x30)
minimum_auth_page = resp[8]
if minimum_auth_page < 48:
print(
"[-] Error: Card not unlocked. Run relay attack in UNLOCK mode first."
)
return None
# Check lock bit
is_locked_key = ((resp[1] & 0x80) >> 7) == 1
if is_locked_key:
print("[-] Error: Card is not vulnerable (key is locked)")
return None
print(
"[+] All sanity checks \033[1;32mpassed\033[0m. Checking if card is vulnerable.\033[?25l"
)
# Collect 100 challenges to check for collision
challenges_collected = 0
challenges_100 = set()
challenges = {}
collision = False
while challenges_collected < num_challenges:
resp = self.cmd.hf14a_raw(
options=options,
resp_timeout_ms=200,
data=struct.pack("!BB", 0x1A, 0x00),
)
if len(resp) >= 9 and resp[0] == 0xAF:
hex_challenge = resp[1:9].hex().upper()
if hex_challenge in challenges_100:
collision = True
challenges["challenge_100"] = hex_challenge
break
else:
challenges_100.add(hex_challenge)
challenges_collected += 1
print(f"\r[+] Challenges collected: \033[96m{challenges_collected}\033[0m")
if collision:
print("[+] Status: \033[1;31mVulnerable\033[0m\033[?25h")
else:
print("[+] Status: \033[1;32mNot vulnerable\033[0m\033[?25h")
return None
# Card is vulnerable, proceed with attack
print("[+] Collecting key-specific challenges...")
# Overwrite block 47 and collect challenge_75
self.write_block(47, b"\x00\x00\x00\x00")
resp = self.cmd.hf14a_raw(
options=options, resp_timeout_ms=200, data=struct.pack("!BB", 0x1A, 0x00)
)
if len(resp) >= 9 and resp[0] == 0xAF:
challenges["challenge_75"] = resp[1:9].hex().upper()
print("[+] 75 collection complete")
# Overwrite block 46 and collect challenge_50
self.write_block(46, b"\x00\x00\x00\x00")
resp = self.cmd.hf14a_raw(
options=options, resp_timeout_ms=200, data=struct.pack("!BB", 0x1A, 0x00)
)
if len(resp) >= 9 and resp[0] == 0xAF:
challenges["challenge_50"] = resp[1:9].hex().upper()
print("[+] 50 collection complete")
# Overwrite block 45 and collect challenge_25
self.write_block(45, b"\x00\x00\x00\x00")
resp = self.cmd.hf14a_raw(
options=options, resp_timeout_ms=200, data=struct.pack("!BB", 0x1A, 0x00)
)
if len(resp) >= 9 and resp[0] == 0xAF:
challenges["challenge_25"] = resp[1:9].hex().upper()
print("[+] 25 collection complete")
# Overwrite block 44 and collect challenge_0
self.write_block(44, b"\x00\x00\x00\x00")
resp = self.cmd.hf14a_raw(
options=options, resp_timeout_ms=200, data=struct.pack("!BB", 0x1A, 0x00)
)
if len(resp) >= 9 and resp[0] == 0xAF:
challenges["challenge_0"] = resp[1:9].hex().upper()
print("[+] 0 collection complete")
return challenges
def write_block(self, block, data):
"""Write a block using hf14a_raw"""
options = {
"activate_rf_field": 0,
"wait_response": 1,
"append_crc": 1,
"auto_select": 1,
"keep_rf_field": 0,
"check_response_crc": 1,
}
# WRITE command (0xA2) + block number + 4 bytes of data
cmd_data = struct.pack("!BB4s", 0xA2, block, data)
self.cmd.hf14a_raw(options=options, resp_timeout_ms=200, data=cmd_data)
def crack_key(self, challenges, num_threads, offline):
"""Crack the key using collected challenges"""
import signal
import traceback
key_segment_values = {0: "00" * 4, 1: "00" * 4, 2: "00" * 4, 3: "00" * 4}
key_found = False
print("[+] Cracking in progress...\033[?25l")
# Create and start the cracking effect
crack_effect = CrackEffect()
effect_thread = threading.Thread(target=crack_effect.start)
effect_thread.start()
def signal_handler(sig, frame):
print("\n\n\n[!] Interrupt received, stopping...\033[?25h")
crack_effect.stop_event.set()
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
ciphertexts = {
1: challenges["challenge_25"],
0: challenges["challenge_50"],
3: challenges["challenge_75"],
2: challenges["challenge_100"],
}
try:
for key_segment_idx in [1, 0, 3, 2]:
ciphertext = ciphertexts[key_segment_idx]
cmd = [
str(default_cwd / "mfulc_des_brute"),
"-c",
challenges["challenge_0"],
ciphertext,
"".join(key_segment_values.values()),
str(key_segment_idx + 1),
str(num_threads),
]
try:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=3600
)
if "Could not detect LFSR" in result.stderr:
key_found = False
crack_effect.stop_event.set()
crack_effect.erase_key()
print(f"\n\n\n[-] Error: {result.stderr}\033[?25h")
break
if "No matching key was found" in result.stdout:
key_found = False
crack_effect.stop_event.set()
crack_effect.erase_key()
print(
f"\n\n\n[-] Error: No matching key found for segment {key_segment_idx + 1}\033[?25h"
)
break
if "Full key (hex): " not in result.stdout:
key_found = False
crack_effect.stop_event.set()
crack_effect.erase_key()
print(
"\n\n\n[-] Error: Unexpected output from mfulc_des_brute\033[?25h"
)
break
# Extract the key segment from output
full_key_line = [
line
for line in result.stdout.split("\n")
if "Full key (hex):" in line
][0]
full_key = full_key_line.split("Full key (hex): ")[1].strip()
key_segment_values[key_segment_idx] = full_key[
(8 * key_segment_idx) :
][:8]
key_found = True
crack_effect.add_cracked_block(
key_segment_idx, key_segment_values[key_segment_idx]
)
except subprocess.TimeoutExpired:
key_found = False
crack_effect.stop_event.set()
crack_effect.erase_key()
print(
f"\n\n\n[-] Error: Timeout cracking segment {key_segment_idx + 1}\033[?25h"
)
break
except Exception as e:
key_found = False
crack_effect.stop_event.set()
crack_effect.erase_key()
print(f"\n\n\n[-] Error: {e}\033[?25h")
break
except Exception as e:
crack_effect.stop_event.set()
print(f"\n\n\nAn error occurred: {e}\033[?25h")
traceback.print_exc()
finally:
effect_thread.join()
if key_found:
result_key = "".join(key_segment_values.values())
formatted_key = f"\033[1;34m{result_key}\033[0m"
print(f"[+] Found key: {formatted_key}\033[?25h")
if offline:
print(
"You can restore found key on the card with appropriate write commands"
)
else:
# Restore the key on the card
print("[+] Restoring key to card...")
key_bytes = bytes.fromhex(result_key)
# Need to swap endianness in 8-byte chunks before writing
# UL-C stores key with swapped endianness
key_swapped = bytearray(16)
# Swap first 8 bytes
for i in range(8):
key_swapped[i] = key_bytes[7 - i]
# Swap second 8 bytes
for i in range(8):
key_swapped[8 + i] = key_bytes[15 - i]
# Write 4 blocks of 4 bytes each
for i in range(4):
block = 44 + i
data = bytes(key_swapped[i * 4 : (i + 1) * 4])
self.write_block(block, data)
print("[+] Key restored on the card")
@hf_mfu.command("econfig")
class HFMFUEConfig(SlotIndexArgsAndGoUnit, HF14AAntiCollArgsUnit, DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Settings of Mifare Ultralight / NTAG emulator"
self.add_slot_args(parser)
self.add_hf14a_anticoll_args(parser)
uid_magic_group = parser.add_mutually_exclusive_group()
uid_magic_group.add_argument(
"--enable-uid-magic", action="store_true", help="Enable UID magic mode"
)
uid_magic_group.add_argument(
"--disable-uid-magic", action="store_true", help="Disable UID magic mode"
)
# Add this new write mode parameter
write_names = [w.name for w in MifareUltralightWriteMode.list()]
help_str = "Write Mode: " + ", ".join(write_names)
parser.add_argument(
"--write", type=str, help=help_str, metavar="MODE", choices=write_names
)
parser.add_argument(
"--set-version",
type=bytes.fromhex,
help="Set data to be returned by the GET_VERSION command.",
)
parser.add_argument(
"--set-signature",
type=bytes.fromhex,
help="Set data to be returned by the READ_SIG command.",
)
parser.add_argument(
"--reset-auth-cnt",
action="store_true",
help="Resets the counter of unsuccessful authentication attempts.",
)
detection_group = parser.add_mutually_exclusive_group()
detection_group.add_argument(
"--enable-log",
action="store_true",
help="Enable password authentication logging",
)
detection_group.add_argument(
"--disable-log",
action="store_true",
help="Disable password authentication logging",
)
return parser
def on_exec(self, args: argparse.Namespace):
aux_data_changed = False
aux_data_change_requested = False
if args.set_version is not None:
aux_data_change_requested = True
aux_data_changed = True
if len(args.set_version) != 8:
print(color_string((CR, "Version data should be 8 bytes long.")))
return
try:
self.cmd.mf0_ntag_set_version_data(args.set_version)
except (ValueError, chameleon_com.CMDInvalidException, TimeoutError):
print(
color_string((CR, "Tag type does not support GET_VERSION command."))
)
return
if args.set_signature is not None:
aux_data_change_requested = True
aux_data_changed = True
if len(args.set_signature) != 32:
print(color_string((CR, "Signature data should be 32 bytes long.")))
return
try:
self.cmd.mf0_ntag_set_signature_data(args.set_signature)
except (ValueError, chameleon_com.CMDInvalidException, TimeoutError):
print(color_string((CR, "Tag type does not support READ_SIG command.")))
return
if args.reset_auth_cnt:
aux_data_change_requested = True
old_value = self.cmd.mfu_reset_auth_cnt()
if old_value != 0:
aux_data_changed = True
print(
f"- Unsuccessful auth counter has been reset from {old_value} to 0."
)
# collect current settings
anti_coll_data = self.cmd.hf14a_get_anti_coll_data()
if len(anti_coll_data) == 0:
print(
color_string(
(CR, f"Slot {self.slot_num} does not contain any HF 14A config")
)
)
return
uid = anti_coll_data["uid"]
atqa = anti_coll_data["atqa"]
sak = anti_coll_data["sak"]
ats = anti_coll_data["ats"]
slotinfo = self.cmd.get_slot_info()
fwslot = SlotNumber.to_fw(self.slot_num)
hf_tag_type = TagSpecificType(slotinfo[fwslot]["hf"])
if hf_tag_type not in [
TagSpecificType.MF0ICU1,
TagSpecificType.MF0ICU2,
TagSpecificType.MF0UL11,
TagSpecificType.MF0UL21,
TagSpecificType.NTAG_210,
TagSpecificType.NTAG_212,
TagSpecificType.NTAG_213,
TagSpecificType.NTAG_215,
TagSpecificType.NTAG_216,
]:
print(
color_string(
(
CR,
f"Slot {self.slot_num} not configured as MIFARE Ultralight / NTAG",
)
)
)
return
change_requested, change_done, uid, atqa, sak, ats = self.update_hf14a_anticoll(
args, uid, atqa, sak, ats
)
if args.enable_uid_magic:
change_requested = True
self.cmd.mf0_ntag_set_uid_magic_mode(True)
magic_mode = True
elif args.disable_uid_magic:
change_requested = True
self.cmd.mf0_ntag_set_uid_magic_mode(False)
magic_mode = False
else:
magic_mode = self.cmd.mf0_ntag_get_uid_magic_mode()
# Add this new write mode handling
write_mode = None
if args.write is not None:
change_requested = True
new_write_mode = MifareUltralightWriteMode[args.write]
try:
current_write_mode = self.cmd.mf0_ntag_get_write_mode()
if new_write_mode != current_write_mode:
self.cmd.mf0_ntag_set_write_mode(new_write_mode)
change_done = True
write_mode = new_write_mode
else:
print(color_string((CY, "Requested write mode already set")))
except (ValueError, chameleon_com.CMDInvalidException, TimeoutError):
print(
color_string(
(
CR,
"Failed to set write mode. Check if device firmware supports this feature.",
)
)
)
detection = self.cmd.mf0_ntag_get_detection_enable()
if args.enable_log:
change_requested = True
if detection is not None:
if not detection:
detection = True
self.cmd.mf0_ntag_set_detection_enable(detection)
change_done = True
else:
print(
color_string(
(
CY,
"Requested logging of MFU authentication data already enabled",
)
)
)
else:
print(
color_string(
(CR, "Detection functionality not available in this firmware")
)
)
elif args.disable_log:
change_requested = True
if detection is not None:
if detection:
detection = False
self.cmd.mf0_ntag_set_detection_enable(detection)
change_done = True
else:
print(
color_string(
(
CY,
"Requested logging of MFU authentication data already disabled",
)
)
)
else:
print(
color_string(
(CR, "Detection functionality not available in this firmware")
)
)
if change_done or aux_data_changed:
print(" - MFU/NTAG Emulator settings updated")
if not (change_requested or aux_data_change_requested):
atqa_string = f"{atqa.hex().upper()} (0x{int.from_bytes(atqa, byteorder='little'):04x})"
print(f'- {"Type:":40}{color_string((CY, hf_tag_type))}')
print(f'- {"UID:":40}{color_string((CY, uid.hex().upper()))}')
print(f'- {"ATQA:":40}{color_string((CY, atqa_string))}')
print(f'- {"SAK:":40}{color_string((CY, sak.hex().upper()))}')
if len(ats) > 0:
print(f'- {"ATS:":40}{color_string((CY, ats.hex().upper()))}')
# Display UID Magic status
magic_status = "enabled" if magic_mode else "disabled"
print(f'- {"UID Magic:":40}{color_string((CY, magic_status))}')
# Add this to display write mode if available
try:
write_mode = MifareUltralightWriteMode(
self.cmd.mf0_ntag_get_write_mode()
)
print(f'- {"Write mode:":40}{color_string((CY, write_mode))}')
except (ValueError, chameleon_com.CMDInvalidException, TimeoutError):
# Write mode not supported in current firmware
pass
# Existing version/signature display code
try:
version = self.cmd.mf0_ntag_get_version_data().hex().upper()
print(f'- {"Version:":40}{color_string((CY, version))}')
except (ValueError, chameleon_com.CMDInvalidException, TimeoutError):
pass
try:
signature = self.cmd.mf0_ntag_get_signature_data().hex().upper()
print(f'- {"Signature:":40}{color_string((CY, signature))}')
except (ValueError, chameleon_com.CMDInvalidException, TimeoutError):
pass
try:
detection = (
color_string((CG, "enabled"))
if self.cmd.mf0_ntag_get_detection_enable()
else color_string((CR, "disabled"))
)
print(f'- {"Log (password) mode:":40}{f"{detection}"}')
except (ValueError, chameleon_com.CMDInvalidException, TimeoutError):
pass
@hf_mfu.command("edetect")
class HFMFUEDetect(SlotIndexArgsAndGoUnit, DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Get Mifare Ultralight / NTAG emulator detection logs"
self.add_slot_args(parser)
parser.add_argument(
"--count",
type=int,
help="Number of log entries to retrieve",
metavar="COUNT",
)
parser.add_argument(
"--index",
type=int,
default=0,
help="Starting index (default: 0)",
metavar="INDEX",
)
return parser
def on_exec(self, args: argparse.Namespace):
detection_enabled = self.cmd.mf0_ntag_get_detection_enable()
if not detection_enabled:
print(color_string((CY, "Detection logging is disabled for this slot")))
return
total_count = self.cmd.mf0_ntag_get_detection_count()
print(f"Total detection log entries: {total_count}")
if total_count == 0:
print(color_string((CY, "No detection logs available")))
return
if args.count is not None:
entries_to_get = min(args.count, total_count - args.index)
else:
entries_to_get = total_count - args.index
if entries_to_get <= 0:
print(color_string((CY, f"No entries available from index {args.index}")))
return
logs = self.cmd.mf0_ntag_get_detection_log(args.index)
print(
f"\nPassword detection logs (showing {len(logs)} entries from index {args.index}):"
)
print("-" * 50)
for i, log_entry in enumerate(logs):
actual_index = args.index + i
password = log_entry["password"]
print(f"{actual_index:3d}: {color_string((CY, password.upper()))}")
@lf_em_410x.command("read")
class LFEMRead(ReaderRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Scan em410x tag and print id"
return parser
def on_exec(self, args: argparse.Namespace):
data = self.cmd.em410x_scan()
print(f"{TagSpecificType(data[0])}: {color_string((CG, data[1].hex()))}")
@lf_em_410x.command("write")
class LFEM410xWriteT55xx(LFEMIdArgsUnit, ReaderRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Write em410x id to t55xx"
return self.add_card_arg(parser, required=True)
def on_exec(self, args: argparse.Namespace):
id_hex = args.id
if len(id_hex) not in (10, 26):
raise ArgsParserError(
"Writing to T55xx supports 5-byte EM410X (10 hex) or 13-byte Electra (26 hex) IDs."
)
id_bytes = bytes.fromhex(id_hex)
self.cmd.em410x_write_to_t55xx(id_bytes)
print(f" - EM410x ID write done: {id_hex}")
@lf_hid_prox.command("read")
class LFHIDProxRead(LFHIDIdReadArgsUnit, ReaderRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Scan hid prox tag and print card format, facility code, card number, issue level and OEM code"
return self.add_card_arg(parser, required=True)
def on_exec(self, args: argparse.Namespace):
format = 0
if args.format is not None:
format = HIDFormat[args.format].value
(format, fc, cn1, cn2, il, oem) = self.cmd.hidprox_scan(format)
cn = (cn1 << 32) + cn2
print(f"HIDProx/{HIDFormat(format)}")
if fc > 0:
print(f" FC: {color_string((CG, fc))}")
if il > 0:
print(f" IL: {color_string((CG, il))}")
if oem > 0:
print(f" OEM: {color_string((CG, oem))}")
print(f" CN: {color_string((CG, cn))}")
@lf_hid_prox.command("write")
class LFHIDProxWriteT55xx(LFHIDIdArgsUnit, ReaderRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Write hidprox card data to t55xx"
return self.add_card_arg(parser, required=True)
def on_exec(self, args: argparse.Namespace):
if args.fc is None:
args.fc = 0
if args.il is None:
args.il = 0
if args.oem is None:
args.oem = 0
format = HIDFormat[args.format]
id = struct.pack(
">BIBIBH",
format.value,
args.fc,
(args.cn >> 32),
args.cn & 0xFFFFFFFF,
args.il,
args.oem,
)
self.cmd.hidprox_write_to_t55xx(id)
print(f"HIDProx/{format}")
if args.fc > 0:
print(f" FC: {args.fc}")
if args.il > 0:
print(f" IL: {args.il}")
if args.oem > 0:
print(f" OEM: {args.oem}")
print(f" CN: {args.cn}")
print("write done.")
@lf_hid_prox.command("econfig")
class LFHIDProxEconfig(SlotIndexArgsAndGoUnit, LFHIDIdArgsUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Set emulated hidprox card id"
self.add_slot_args(parser)
self.add_card_arg(parser)
return parser
def on_exec(self, args: argparse.Namespace):
if args.cn is not None:
slotinfo = self.cmd.get_slot_info()
selected = SlotNumber.from_fw(self.cmd.get_active_slot())
lf_tag_type = TagSpecificType(slotinfo[selected - 1]["lf"])
if lf_tag_type != TagSpecificType.HIDProx:
print(f"{color_string((CR, 'WARNING'))}: Slot type not set to HIDProx.")
if args.fc is None:
args.fc = 0
if args.il is None:
args.il = 0
if args.oem is None:
args.oem = 0
format = HIDFormat.H10301
if args.format is not None:
format = HIDFormat[args.format]
id = struct.pack(
">BIBIBH",
format.value,
args.fc,
(args.cn >> 32),
args.cn & 0xFFFFFFFF,
args.il,
args.oem,
)
self.cmd.hidprox_set_emu_id(id)
print(" - SET hidprox tag id success.")
else:
(format, fc, cn1, cn2, il, oem) = self.cmd.hidprox_get_emu_id()
cn = (cn1 << 32) + cn2
print(" - GET hidprox tag id success.")
print(f" - HIDProx/{HIDFormat(format)}")
if fc > 0:
print(f" FC: {color_string((CG, fc))}")
if il > 0:
print(f" IL: {color_string((CG, il))}")
if oem > 0:
print(f" OEM: {color_string((CG, oem))}")
print(f" CN: {color_string((CG, cn))}")
@lf_ioprox.command("read")
class LFIOProxRead(LFIOProxReadArgsUnit, ReaderRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Scan ioProx tag and print version, facility, card number and raw"
return self.add_card_arg(parser, required=False)
def on_exec(self, args: argparse.Namespace):
ver, fc, cn, raw8, *futureuse = self.cmd.ioprox_scan()
print(f"ioProx XSF format")
print(f" Version: {color_string((CG, ver))}")
print(f" Facility: {color_string((CG, f'{fc} [0x{fc:02X}]'))}")
print(f" ID: {color_string((CY, cn))}")
print(f" Raw: {color_string((CY, raw8.hex().upper()))}")
@lf_ioprox.command("write")
class LFIOProxWriteT55xx(LFIOProxIdArgsUnit, ReaderRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Write ioProx card data to t55xx"
return self.add_card_arg(parser, required=False)
def on_exec(self, args: argparse.Namespace):
# defaults
ver = args.ver if args.ver is not None else 1
fc = args.fc if args.fc is not None else 0
cn = args.cn if args.cn is not None else 0
# raw8 priority
if args.raw8 is not None:
raw8 = self.parse_raw8(args.raw8)
ver, fc, cn, raw8, *futureuse = self.cmd.ioprox_decode_raw(raw8)
else:
res = self.cmd.ioprox_compose_id(args.ver, args.fc, args.cn)
raw8 = res[3]
payload16 = struct.pack(
">BBH8s4x",
ver & 0xFF,
fc & 0xFF,
cn & 0xFFFF,
raw8
)
result = self.cmd.ioprox_write_to_t55xx(payload16)
print(f"ioProx XSF format")
print(f" Version: {color_string((CG, ver))}")
print(f" Facility: {color_string((CG, f'{fc} [0x{fc:02X}]'))}")
print(f" ID: {color_string((CY, cn))}")
print(f" Raw: {color_string((CY, raw8.hex().upper()))}")
print("Write done.")
@lf_ioprox.command("econfig")
class LFIOProxEconfig(SlotIndexArgsAndGoUnit, LFIOProxIdArgsUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Set/Get emulated ioProx card id (stored in slot)"
self.add_slot_args(parser)
self.add_card_arg(parser, required=False) # SET when --cn or --raw present; GET otherwise
return parser
def on_exec(self, args: argparse.Namespace):
do_set = (args.cn is not None) or (args.raw8 is not None) or (args.fc is not None) or (args.ver is not None)
if do_set:
# warn if slot isn't ioProx
slotinfo = self.cmd.get_slot_info()
selected = SlotNumber.from_fw(self.cmd.get_active_slot())
lf_tag_type = TagSpecificType(slotinfo[selected - 1]["lf"])
if lf_tag_type != TagSpecificType.ioProx:
print(f"{color_string((CR, 'WARNING'))}: Slot type not set to IOProx.")
# defaults
ver = args.ver if args.ver is not None else 1
fc = args.fc if args.fc is not None else 0
cn = args.cn if args.cn is not None else 0
# raw8 priority
if args.raw8 is not None:
raw8 = self.parse_raw8(args.raw8)
ver, fc, cn, raw8, *futureuse = self.cmd.ioprox_decode_raw(raw8)
else:
res = self.cmd.ioprox_compose_id(args.ver, args.fc, args.cn)
raw8 = res[3]
payload16 = struct.pack(
">BBH8s4x",
ver & 0xFF,
fc & 0xFF,
cn & 0xFFFF,
raw8
)
result = self.cmd.ioprox_set_emu_id(payload16)
print(f"ioProx XSF format")
print(f" Version: {color_string((CG, ver))}")
print(f" Facility: {color_string((CG, f'{fc} [0x{fc:02X}]'))}")
print(f" ID: {color_string((CY, cn))}")
print(f" Raw: {color_string((CY, raw8.hex().upper()))}")
else:
# GET
ver, fc, cn, raw8, *futureuse = self.cmd.ioprox_get_emu_id()
print(f"ioProx XSF format")
print(f" Version: {color_string((CG, ver))}")
print(f" Facility: {color_string((CG, f'{fc} [0x{fc:02X}]'))}")
print(f" ID: {color_string((CY, cn))}")
print(f" Raw: {color_string((CY, raw8.hex().upper()))}")
@lf_viking.command("read")
class LFVikingRead(ReaderRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Scan Viking tag and print id"
return parser
def on_exec(self, args: argparse.Namespace):
id = self.cmd.viking_scan()
print(f" Viking: {color_string((CG, id.hex()))}")
@lf_viking.command("write")
class LFVikingWriteT55xx(LFVikingIdArgsUnit, ReaderRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Write Viking id to t55xx"
return self.add_card_arg(parser, required=True)
def on_exec(self, args: argparse.Namespace):
id_hex = args.id
id_bytes = bytes.fromhex(id_hex)
self.cmd.viking_write_to_t55xx(id_bytes)
print(f" - Viking ID(8H): {id_hex} write done.")
@lf_generic.command("adcread")
class LFADCGenericRead(ReaderRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Read ADC and return the array"
return parser
def on_exec(self, args: argparse.Namespace):
resp = self.cmd.adc_generic_read()
if resp is not None:
print(f"generic read data[{len(resp)}]:")
width = 50
for i in range(0, len(resp), width):
chunk = resp[i : i + width]
hexpart = " ".join(f"{b:02x}" for b in chunk)
binpart = "".join("1" if b >= 0xBF else "0" for b in chunk)
print(f"{i:04x} {hexpart:<{width * 3}} {binpart}")
avg = 0
for val in resp:
avg += val
print(f"avg: {hex(round(avg / len(resp)))}")
else:
print(f"generic read error")
@hw_slot.command("list")
class HWSlotList(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Get information about slots"
parser.add_argument(
"--short",
action="store_true",
help="Hide slot nicknames and Mifare Classic emulator settings",
)
return parser
def get_slot_name(self, slot, sense):
try:
name = self.cmd.get_slot_tag_nick(slot, sense)
return {
"baselen": len(name),
"metalen": len(CC + C0),
"name": color_string((CC, name)),
}
except UnexpectedResponseError:
return {"baselen": 0, "metalen": 0, "name": ""}
except UnicodeDecodeError:
name = "UTF8 Err"
return {
"baselen": len(name),
"metalen": len(CC + C0),
"name": color_string((CC, name)),
}
def on_exec(self, args: argparse.Namespace):
slotinfo = self.cmd.get_slot_info()
selected = SlotNumber.from_fw(self.cmd.get_active_slot())
current = selected
enabled = self.cmd.get_enabled_slots()
maxnamelength = 0
slotnames = []
all_nicks = self.cmd.get_all_slot_nicks()
for slot_data in all_nicks:
hfn = {
"baselen": len(slot_data["hf"]),
"metalen": len(CC + C0),
"name": color_string((CC, slot_data["hf"])),
}
lfn = {
"baselen": len(slot_data["lf"]),
"metalen": len(CC + C0),
"name": color_string((CC, slot_data["lf"])),
}
m = max(hfn["baselen"], lfn["baselen"])
maxnamelength = m if m > maxnamelength else maxnamelength
slotnames.append({"hf": hfn, "lf": lfn})
for slot in SlotNumber:
fwslot = SlotNumber.to_fw(slot)
status = f"({color_string((CG, 'active'))})" if slot == selected else ""
hf_tag_type = TagSpecificType(slotinfo[fwslot]["hf"])
lf_tag_type = TagSpecificType(slotinfo[fwslot]["lf"])
print(f' - {f"Slot {slot}:":{4+maxnamelength+1}} {status}')
# HF
field_length = maxnamelength + slotnames[fwslot]["hf"]["metalen"] + 1
status = (
f"({color_string((CR, 'disabled'))})"
if not enabled[fwslot]["hf"]
else ""
)
print(
f" HF: " f'{slotnames[fwslot]["hf"]["name"]:{field_length}}', end=""
)
print(status, end="")
if hf_tag_type != TagSpecificType.UNDEFINED:
color = CY if enabled[fwslot]["hf"] else C0
print(color_string((color, hf_tag_type)))
else:
print("undef")
if (
(not args.short)
and enabled[fwslot]["hf"]
and hf_tag_type != TagSpecificType.UNDEFINED
):
if current != slot:
self.cmd.set_active_slot(slot)
current = slot
anti_coll_data = self.cmd.hf14a_get_anti_coll_data()
uid = anti_coll_data["uid"]
atqa = anti_coll_data["atqa"]
sak = anti_coll_data["sak"]
ats = anti_coll_data["ats"]
# print(' - ISO14443A emulator settings:')
atqa_hex_le = f"(0x{int.from_bytes(atqa, byteorder='little'):04x})"
print(f' {"UID:":40}{color_string((CY, uid.hex().upper()))}')
print(
f' {"ATQA:":40}{color_string((CY, f"{atqa.hex().upper()} {atqa_hex_le}"))}'
)
print(f' {"SAK:":40}{color_string((CY, sak.hex().upper()))}')
if len(ats) > 0:
print(f' {"ATS:":40}{color_string((CY, ats.hex().upper()))}')
if hf_tag_type in [
TagSpecificType.MIFARE_Mini,
TagSpecificType.MIFARE_1024,
TagSpecificType.MIFARE_2048,
TagSpecificType.MIFARE_4096,
]:
config = self.cmd.mf1_get_emulator_config()
# print(' - Mifare Classic emulator settings:')
enabled_str = color_string((CG, "enabled"))
disabled_str = color_string((CR, "disabled"))
print(
f' {"Gen1A magic mode:":40}'
f'{enabled_str if config["gen1a_mode"] else disabled_str}'
)
print(
f' {"Gen2 magic mode:":40}'
f'{enabled_str if config["gen2_mode"] else disabled_str}'
)
print(
f' {"Use anti-collision data from block 0:":40}'
f'{enabled_str if config["block_anti_coll_mode"] else disabled_str}'
)
try:
print(
f' {"Write mode:":40}'
f'{color_string((CY, MifareClassicWriteMode(config["write_mode"])))}'
)
except ValueError:
print(
f' {"Write mode:":40}{color_string((CR, "invalid value!"))}'
)
print(
f' {"Log (mfkey32) mode:":40}'
f'{enabled_str if config["detection"] else disabled_str}'
)
# LF
field_length = maxnamelength + slotnames[fwslot]["lf"]["metalen"] + 1
status = (
f"({color_string((CR, 'disabled'))})"
if not enabled[fwslot]["lf"]
else ""
)
print(
f" LF: " f'{slotnames[fwslot]["lf"]["name"]:{field_length}}', end=""
)
print(status, end="")
if lf_tag_type != TagSpecificType.UNDEFINED:
color = CY if enabled[fwslot]["lf"] else C0
print(color_string((color, lf_tag_type)))
else:
print("undef")
if (
(not args.short)
and enabled[fwslot]["lf"]
and lf_tag_type != TagSpecificType.UNDEFINED
):
if current != slot:
self.cmd.set_active_slot(slot)
current = slot
if lf_tag_type == TagSpecificType.EM410X:
id = self.cmd.em410x_get_emu_id()
print(f' {"ID:":40}{color_string((CY, id.hex().upper()))}')
if lf_tag_type == TagSpecificType.HIDProx:
(format, fc, cn1, cn2, il, oem) = self.cmd.hidprox_get_emu_id()
cn = (cn1 << 32) + cn2
print(
f" {'Format:':40}{color_string((CY, HIDFormat(format)))}"
)
if fc > 0:
print(f" {'FC:':40}{color_string((CG, fc))}")
if il > 0:
print(f" {'IL:':40}{color_string((CG, il))}")
if oem > 0:
print(f" {'OEM:':40}{color_string((CG, oem))}")
print(f" {'CN:':40}{color_string((CG, cn))}")
if lf_tag_type == TagSpecificType.ioProx:
ver, fc, cn, raw8, *futureuse = self.cmd.ioprox_get_emu_id()
print(f" {'Version:':40}{color_string((CG, ver))}")
print(f" {'Facility:':40}{color_string((CG, f'{fc} [0x{fc:02X}]'))}")
print(f" {'ID:':40}{color_string((CY, cn))}")
print(f" {'Raw:':40}{color_string((CY, raw8.hex().upper()))}")
if lf_tag_type == TagSpecificType.Viking:
id = self.cmd.viking_get_emu_id()
print(f" {'ID:':40}{color_string((CY, id.hex().upper()))}")
if current != selected:
self.cmd.set_active_slot(selected)
@hw_slot.command("change")
class HWSlotSet(SlotIndexArgsUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Set emulation tag slot activated"
return self.add_slot_args(parser, mandatory=True)
def on_exec(self, args: argparse.Namespace):
slot_index = args.slot
self.cmd.set_active_slot(slot_index)
print(f" - Set slot {slot_index} activated success.")
@hw_slot.command("type")
class HWSlotType(TagTypeArgsUnit, SlotIndexArgsUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Set emulation tag type"
self.add_slot_args(parser)
self.add_type_args(parser)
return parser
def on_exec(self, args: argparse.Namespace):
tag_type = TagSpecificType[args.type]
if args.slot is not None:
slot_num = args.slot
else:
slot_num = SlotNumber.from_fw(self.cmd.get_active_slot())
self.cmd.set_slot_tag_type(slot_num, tag_type)
print(f" - Set slot {slot_num} tag type success.")
@hw_slot.command("delete")
class HWDeleteSlotSense(SlotIndexArgsUnit, SenseTypeArgsUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Delete sense type data for a specific slot"
self.add_slot_args(parser)
self.add_sense_type_args(parser)
return parser
def on_exec(self, args: argparse.Namespace):
if args.slot is not None:
slot_num = args.slot
else:
slot_num = SlotNumber.from_fw(self.cmd.get_active_slot())
if args.lf:
sense_type = TagSenseType.LF
else:
sense_type = TagSenseType.HF
self.cmd.delete_slot_sense_type(slot_num, sense_type)
print(f" - Delete slot {slot_num} {sense_type.name} tag type success.")
@hw_slot.command("init")
class HWSlotInit(TagTypeArgsUnit, SlotIndexArgsUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Set emulation tag data to default"
self.add_slot_args(parser)
self.add_type_args(parser)
return parser
def on_exec(self, args: argparse.Namespace):
tag_type = TagSpecificType[args.type]
if args.slot is not None:
slot_num = args.slot
else:
slot_num = SlotNumber.from_fw(self.cmd.get_active_slot())
self.cmd.set_slot_data_default(slot_num, tag_type)
print(" - Set slot tag data init success.")
@hw_slot.command("enable")
class HWSlotEnable(SlotIndexArgsUnit, SenseTypeArgsUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Enable tag slot"
self.add_slot_args(parser)
self.add_sense_type_args(parser)
return parser
def on_exec(self, args: argparse.Namespace):
if args.slot is not None:
slot_num = args.slot
else:
slot_num = SlotNumber.from_fw(self.cmd.get_active_slot())
if args.lf:
sense_type = TagSenseType.LF
else:
sense_type = TagSenseType.HF
self.cmd.set_slot_enable(slot_num, sense_type, True)
print(f" - Enable slot {slot_num} {sense_type.name} success.")
@hw_slot.command("disable")
class HWSlotDisable(SlotIndexArgsUnit, SenseTypeArgsUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Disable tag slot"
self.add_slot_args(parser)
self.add_sense_type_args(parser)
return parser
def on_exec(self, args: argparse.Namespace):
slot_num = args.slot
if args.lf:
sense_type = TagSenseType.LF
else:
sense_type = TagSenseType.HF
self.cmd.set_slot_enable(slot_num, sense_type, False)
print(f" - Disable slot {slot_num} {sense_type.name} success.")
@lf_em_410x.command("econfig")
class LFEM410xEconfig(SlotIndexArgsAndGoUnit, LFEMIdArgsUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Set emulated em410x card id"
self.add_slot_args(parser)
self.add_card_arg(parser)
return parser
def on_exec(self, args: argparse.Namespace):
if args.id is not None:
self.cmd.em410x_set_emu_id(bytes.fromhex(args.id))
print(" - Set em410x tag id success.")
else:
response = self.cmd.em410x_get_emu_id()
print(" - Get em410x tag id success.")
print(f"ID: {response.hex()}")
@lf_viking.command("econfig")
class LFVikingEconfig(SlotIndexArgsAndGoUnit, LFVikingIdArgsUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Set emulated Viking card id"
self.add_slot_args(parser)
self.add_card_arg(parser)
return parser
def on_exec(self, args: argparse.Namespace):
if args.id is not None:
slotinfo = self.cmd.get_slot_info()
selected = SlotNumber.from_fw(self.cmd.get_active_slot())
lf_tag_type = TagSpecificType(slotinfo[selected - 1]["lf"])
if lf_tag_type != TagSpecificType.Viking:
print(f"{color_string((CR, 'WARNING'))}: Slot type not set to Viking.")
self.cmd.viking_set_emu_id(bytes.fromhex(args.id))
print(" - Set Viking tag id success.")
else:
response = self.cmd.viking_get_emu_id()
print(" - Get Viking tag id success.")
print(f"ID: {response.hex().upper()}")
@hw_slot.command("nick")
class HWSlotNick(SlotIndexArgsUnit, SenseTypeArgsUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Get/Set/Delete tag nick name for slot"
self.add_slot_args(parser)
self.add_sense_type_args(parser)
action_group = parser.add_mutually_exclusive_group()
action_group.add_argument(
"-n", "--name", type=str, required=False, help="Set tag nick name for slot"
)
action_group.add_argument(
"-d", "--delete", action="store_true", help="Delete tag nick name for slot"
)
return parser
def on_exec(self, args: argparse.Namespace):
if args.slot is not None:
slot_num = args.slot
else:
slot_num = SlotNumber.from_fw(self.cmd.get_active_slot())
if args.lf:
sense_type = TagSenseType.LF
else:
sense_type = TagSenseType.HF
if args.name is not None:
name: str = args.name
self.cmd.set_slot_tag_nick(slot_num, sense_type, name)
print(f" - Set tag nick name for slot {slot_num} {sense_type.name}: {name}")
elif args.delete:
self.cmd.delete_slot_tag_nick(slot_num, sense_type)
print(f" - Delete tag nick name for slot {slot_num} {sense_type.name}")
else:
res = self.cmd.get_slot_tag_nick(slot_num, sense_type)
print(
f" - Get tag nick name for slot {slot_num} {sense_type.name}" f": {res}"
)
@hw_slot.command("store")
class HWSlotUpdate(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Store slots config & data to device flash"
return parser
def on_exec(self, args: argparse.Namespace):
self.cmd.slot_data_config_save()
print(" - Store slots config and data from device memory to flash success.")
@hw_slot.command("openall")
class HWSlotOpenAll(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Open all slot and set to default data"
return parser
def on_exec(self, args: argparse.Namespace):
# what type you need set to default?
hf_type = TagSpecificType.MIFARE_1024
lf_type = TagSpecificType.EM410X
# set all slot
for slot in SlotNumber:
print(f" Slot {slot} setting...")
# first to set tag type
self.cmd.set_slot_tag_type(slot, hf_type)
self.cmd.set_slot_tag_type(slot, lf_type)
# to init default data
self.cmd.set_slot_data_default(slot, hf_type)
self.cmd.set_slot_data_default(slot, lf_type)
# finally, we can enable this slot.
self.cmd.set_slot_enable(slot, TagSenseType.HF, True)
self.cmd.set_slot_enable(slot, TagSenseType.LF, True)
print(f" Slot {slot} setting done.")
# update config and save to flash
self.cmd.slot_data_config_save()
print(" - Succeeded opening all slots and setting data to default.")
@hw.command("dfu")
class HWDFU(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Restart application to bootloader/DFU mode"
return parser
def on_exec(self, args: argparse.Namespace):
print("Application restarting...")
self.cmd.enter_bootloader()
# In theory, after the above command is executed, the dfu mode will enter, and then the USB will restart,
# To judge whether to enter the USB successfully, we only need to judge whether the USB becomes the VID and PID
# of the DFU device.
# At the same time, we remember to confirm the information of the device,
# it is the same device when it is consistent.
print(" - Enter success @.@~")
# let time for comm thread to send dfu cmd and close port
time.sleep(0.1)
@hw_settings.command("animation")
class HWSettingsAnimation(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Get or change current animation mode value"
mode_names = [m.name for m in list(AnimationMode)]
help_str = "Mode: " + ", ".join(mode_names)
parser.add_argument(
"-m",
"--mode",
type=str,
required=False,
help=help_str,
metavar="MODE",
choices=mode_names,
)
return parser
def on_exec(self, args: argparse.Namespace):
if args.mode is not None:
mode = AnimationMode[args.mode]
self.cmd.set_animation_mode(mode)
print("Animation mode change success.")
print(color_string((CY, "Do not forget to store your settings in flash!")))
else:
print(AnimationMode(self.cmd.get_animation_mode()))
@hw_settings.command("bleclearbonds")
class HWSettingsBleClearBonds(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Clear all BLE bindings. Warning: effect is immediate!"
parser.add_argument(
"--force", default=False, action="store_true", help="Just to be sure"
)
return parser
def on_exec(self, args: argparse.Namespace):
if not args.force:
print(
"If you are you really sure, read the command documentation to see how to proceed."
)
return
self.cmd.delete_all_ble_bonds()
print(" - Successfully clear all bonds")
@hw_settings.command("store")
class HWSettingsStore(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Store current settings to flash"
return parser
def on_exec(self, args: argparse.Namespace):
print("Storing settings...")
if self.cmd.save_settings():
print(" - Store success @.@~")
else:
print(" - Store failed")
@hw_settings.command("reset")
class HWSettingsReset(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Reset settings to default values"
parser.add_argument(
"--force", default=False, action="store_true", help="Just to be sure"
)
return parser
def on_exec(self, args: argparse.Namespace):
if not args.force:
print(
"If you are you really sure, read the command documentation to see how to proceed."
)
return
print("Initializing settings...")
if self.cmd.reset_settings():
print(" - Reset success @.@~")
else:
print(" - Reset failed")
@hw.command("factory_reset")
class HWFactoryReset(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = (
"Wipe all slot data and custom settings and return to factory settings"
)
parser.add_argument(
"--force", default=False, action="store_true", help="Just to be sure"
)
return parser
def on_exec(self, args: argparse.Namespace):
if not args.force:
print(
"If you are you really sure, read the command documentation to see how to proceed."
)
return
if self.cmd.wipe_fds():
print(" - Reset successful! Please reconnect.")
# let time for comm thread to close port
time.sleep(0.1)
else:
print(" - Reset failed!")
@hw.command("battery")
class HWBatteryInfo(DeviceRequiredUnit):
# How much remaining battery is considered low?
BATTERY_LOW_LEVEL = 30
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Get battery information, voltage and level"
return parser
def on_exec(self, args: argparse.Namespace):
voltage, percentage = self.cmd.get_battery_info()
print(" - Battery information:")
print(f" voltage -> {voltage} mV")
print(f" percentage -> {percentage}%")
if percentage < HWBatteryInfo.BATTERY_LOW_LEVEL:
print(color_string((CR, "[!] Low battery, please charge.")))
@hw_settings.command("btnpress")
class HWButtonSettingsGet(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Get or set button press function of Button A and Button B"
button_group = parser.add_mutually_exclusive_group()
button_group.add_argument("-a", "-A", action="store_true", help="Button A")
button_group.add_argument("-b", "-B", action="store_true", help="Button B")
duration_group = parser.add_mutually_exclusive_group()
duration_group.add_argument(
"-s", "--short", action="store_true", help="Short-press (default)"
)
duration_group.add_argument(
"-l", "--long", action="store_true", help="Long-press"
)
function_names = [f.name for f in list(ButtonPressFunction)]
function_descs = [f"{f.name} ({f})" for f in list(ButtonPressFunction)]
help_str = "Function: " + ", ".join(function_descs)
parser.add_argument(
"-f",
"--function",
type=str,
required=False,
help=help_str,
metavar="FUNCTION",
choices=function_names,
)
return parser
def on_exec(self, args: argparse.Namespace):
if args.function is not None:
function = ButtonPressFunction[args.function]
if not args.a and not args.b:
print(
color_string(
(CR, "You must specify which button you want to change")
)
)
return
if args.a:
button = ButtonType.A
else:
button = ButtonType.B
if args.long:
self.cmd.set_long_button_press_config(button, function)
else:
self.cmd.set_button_press_config(button, function)
print(
f" - Successfully set function '{function}'"
f" to Button {button.name} {'long-press' if args.long else 'short-press'}"
)
print(color_string((CY, "Do not forget to store your settings in flash!")))
else:
if args.a:
button_list = [ButtonType.A]
elif args.b:
button_list = [ButtonType.B]
else:
button_list = list(ButtonType)
for button in button_list:
if not args.long:
resp = self.cmd.get_button_press_config(button)
button_fn = ButtonPressFunction(resp)
print(f"{color_string((CG, f'{button.name} short'))}: {button_fn}")
if not args.short:
resp_long = self.cmd.get_long_button_press_config(button)
button_long_fn = ButtonPressFunction(resp_long)
print(
f"{color_string((CG, f'{button.name} long'))}: {button_long_fn}"
)
print("")
@hw_settings.command("blekey")
class HWSettingsBLEKey(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Get or set the ble connect key"
parser.add_argument(
"-k", "--key", required=False, help="Ble connect key for your device"
)
return parser
def on_exec(self, args: argparse.Namespace):
key = self.cmd.get_ble_pairing_key()
print(f" - The current key of the device(ascii): {color_string((CG, key))}")
if args.key is not None:
if len(args.key) != 6:
print(
f" - {color_string((CR, 'The ble connect key length must be 6'))}"
)
return
if re.match(r"[0-9]{6}", args.key):
self.cmd.set_ble_connect_key(args.key)
print(
f" - Successfully set ble connect key to : {color_string((CG, args.key))}"
)
print(
color_string((CY, "Do not forget to store your settings in flash!"))
)
else:
print(
f" - {color_string((CR, 'Only 6 ASCII characters from 0 to 9 are supported.'))}"
)
@hw_settings.command("blepair")
class HWBlePair(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Show or configure BLE pairing"
set_group = parser.add_mutually_exclusive_group()
set_group.add_argument(
"-e", "--enable", action="store_true", help="Enable BLE pairing"
)
set_group.add_argument(
"-d", "--disable", action="store_true", help="Disable BLE pairing"
)
return parser
def on_exec(self, args: argparse.Namespace):
is_pairing_enable = self.cmd.get_ble_pairing_enable()
enabled_str = color_string((CG, "Enabled"))
disabled_str = color_string((CR, "Disabled"))
if not args.enable and not args.disable:
if is_pairing_enable:
print(f" - BLE pairing: {enabled_str}")
else:
print(f" - BLE pairing: {disabled_str}")
elif args.enable:
if is_pairing_enable:
print(color_string((CY, "BLE pairing is already enabled.")))
return
self.cmd.set_ble_pairing_enable(True)
print(f" - Successfully change ble pairing to {enabled_str}.")
print(color_string((CY, "Do not forget to store your settings in flash!")))
elif args.disable:
if not is_pairing_enable:
print(color_string((CY, "BLE pairing is already disabled.")))
return
self.cmd.set_ble_pairing_enable(False)
print(f" - Successfully change ble pairing to {disabled_str}.")
print(color_string((CY, "Do not forget to store your settings in flash!")))
@hw.command("raw")
class HWRaw(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Send raw command"
cmd_names = sorted([c.name for c in list(Command)])
help_str = "Command: " + ", ".join(cmd_names)
command_group = parser.add_mutually_exclusive_group(required=True)
command_group.add_argument(
"-c",
"--command",
type=str,
metavar="COMMAND",
help=help_str,
choices=cmd_names,
)
command_group.add_argument(
"-n",
"--num_command",
type=int,
metavar="<dec>",
help="Numeric command ID: <dec>",
)
parser.add_argument(
"-d", "--data", type=str, help="Data to send", default="", metavar="<hex>"
)
parser.add_argument(
"-t",
"--timeout",
type=int,
help="Timeout in seconds",
default=3,
metavar="<dec>",
)
return parser
def on_exec(self, args: argparse.Namespace):
if args.command is not None:
command = Command[args.command]
else:
# We accept not-yet-known command ids as "hw raw" is meant for debugging
command = args.num_command
response = self.cmd.device.send_cmd_sync(
command, data=bytes.fromhex(args.data), status=0x0, timeout=args.timeout
)
print(" - Received:")
try:
command = Command(response.cmd)
print(f" Command: {response.cmd} {command.name}")
except ValueError:
print(f" Command: {response.cmd} (unknown)")
status_string = f" Status: {response.status:#02x}"
try:
status = Status(response.status)
status_string += f" {status.name}"
status_string += f": {str(status)}"
except ValueError:
pass
print(status_string)
print(f" Data (HEX): {response.data.hex()}")
@hf_14a.command("raw")
class HF14ARaw(ReaderRequiredUnit):
def bool_to_bit(self, value):
return 1 if value else 0
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.formatter_class = argparse.RawDescriptionHelpFormatter
parser.description = "Send raw command"
parser.add_argument(
"-a",
"--activate-rf",
help="Active signal field ON without select",
action="store_true",
default=False,
)
parser.add_argument(
"-s",
"--select-tag",
help="Active signal field ON with select",
action="store_true",
default=False,
)
# TODO: parser.add_argument('-3', '--type3-select-tag',
# help="Active signal field ON with ISO14443-3 select (no RATS)", action='store_true', default=False,)
parser.add_argument(
"-d", "--data", type=str, metavar="<hex>", help="Data to be sent"
)
parser.add_argument(
"-b",
"--bits",
type=int,
metavar="<dec>",
help="Number of bits to send. Useful for send partial byte",
)
parser.add_argument(
"-c",
"--crc",
help="Calculate and append CRC",
action="store_true",
default=False,
)
parser.add_argument(
"-r",
"--no-response",
help="Do not read response",
action="store_true",
default=False,
)
parser.add_argument(
"-cc",
"--crc-clear",
help="Verify and clear CRC of received data",
action="store_true",
default=False,
)
parser.add_argument(
"-k",
"--keep-rf",
help="Keep signal field ON after receive",
action="store_true",
default=False,
)
parser.add_argument(
"-t",
"--timeout",
type=int,
metavar="<dec>",
help="Timeout in ms",
default=100,
)
parser.epilog = """
examples/notes:
hf 14a raw -b 7 -d 40 -k
hf 14a raw -d 43 -k
hf 14a raw -d 3000 -c
hf 14a raw -sc -d 6000
"""
return parser
def on_exec(self, args: argparse.Namespace):
options = {
"activate_rf_field": self.bool_to_bit(args.activate_rf),
"wait_response": self.bool_to_bit(not args.no_response),
"append_crc": self.bool_to_bit(args.crc),
"auto_select": self.bool_to_bit(args.select_tag),
"keep_rf_field": self.bool_to_bit(args.keep_rf),
"check_response_crc": self.bool_to_bit(args.crc_clear),
# 'auto_type3_select': self.bool_to_bit(args.type3-select-tag),
}
data: str = args.data
if data is not None:
data = data.replace(" ", "")
if re.match(r"^[0-9a-fA-F]+$", data):
if len(data) % 2 != 0:
print(
f" [!] {color_string((CR, 'The length of the data must be an integer multiple of 2.'))}"
)
return
else:
data_bytes = bytes.fromhex(data)
else:
print(f" [!] {color_string((CR, 'The data must be a HEX string'))}")
return
else:
data_bytes = []
if args.bits is not None and args.crc:
print(
f" [!] {color_string((CR, '--bits and --crc are mutually exclusive'))}"
)
return
# Exec 14a raw cmd.
resp = self.cmd.hf14a_raw(options, args.timeout, data_bytes, args.bits)
if len(resp) > 0:
print(
# print head
" - "
+
# print data
" ".join([hex(byte).replace("0x", "").rjust(2, "0") for byte in resp])
)
else:
print(f" [*] {color_string((CY, 'No response'))}")