Is your feature request related to a problem? Please describe.
Now, the HF sniff frame annotation's only way to determine the frame's nature is by looking at this frame and only this frame. When encountering frames such as resp to READ cmd, it will most likely be gibberish since that frame contains only data. Also, some other annotations will benefit from implementing a context-relevant parser.
Describe the solution you'd like
Implement a context-relevant parser.
Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.
Additional context
There's my approach written in Python to implement a context-relevant parser. It now only focuses on Mifare Ultralight commands since this is what I am currently working with.
Click to expand
class HfSniffDirection(Enum):
reader_to_card = 0
card_to_reader = 1
@dataclass(frozen=True)
class HfSniffFrame:
raw_bit_length: int
bit_length: int
data: bytes
direction: HfSniffDirection
@property
def is_reader_to_card(self) -> bool:
return self.direction == HfSniffDirection.reader_to_card
@property
def is_card_to_reader(self) -> bool:
return self.direction == HfSniffDirection.card_to_reader
@property
def hex_string(self) -> str:
return _hex(self.data)
@dataclass(frozen=True)
class HfSniffAnnotatedFrame:
frame: HfSniffFrame
label: str
@dataclass
class Hf14aDecodeContext:
probable_ultralight: bool = False
probable_ultralight_ev1: bool = False
pending_ul_cmd: Optional[str] = None
pending_page: Optional[int] = None
pending_end_page: Optional[int] = None
pending_counter: Optional[int] = None
pending_anticoll_level: Optional[str] = None
def annotate_hf14a_sniff_frames(frames: List[HfSniffFrame]) -> List[HfSniffAnnotatedFrame]:
annotated = []
expect_nt = False
expect_nrar = False
last_auth_key_type = None
last_auth_block = None
context = Hf14aDecodeContext()
for frame in frames:
data = frame.data
label = ""
if (frame.is_reader_to_card and frame.bit_length == 32 and len(data) == 4
and data and data[0] in (0x60, 0x61)):
last_auth_key_type = 'A' if data[0] == 0x60 else 'B'
last_auth_block = data[1]
expect_nt = True
expect_nrar = False
label = f"MIFARE Classic AUTH Key{last_auth_key_type} block=0x{last_auth_block:02X} ({last_auth_block})"
elif (frame.is_card_to_reader and expect_nt and frame.bit_length == 32 and len(data) == 4):
label = f"AUTH: NT (card nonce) = {_hex(data, spaced=False)}"
expect_nt = False
expect_nrar = True
elif (frame.is_reader_to_card and expect_nrar and frame.bit_length == 64 and len(data) == 8):
label = (f"AUTH continuation: NR||AR (enc) NR={_hex(data[:4], spaced=False)} "
f"AR={_hex(data[4:8], spaced=False)}")
expect_nrar = False
else:
expect_nt = False
expect_nrar = False
label = _decode_hf14a_frame_with_context(frame, context)
annotated.append(HfSniffAnnotatedFrame(frame=frame, label=label))
return annotated
def _decode_hf14a_frame_with_context(frame: HfSniffFrame, context: Hf14aDecodeContext) -> str:
data = frame.data
if not data:
return _decode_hf14a_frame(frame)
if frame.is_reader_to_card and len(data) >= 2 and data[0] in (0x93, 0x95, 0x97):
level = {0x93: 'CL1', 0x95: 'CL2', 0x97: 'CL3'}[data[0]]
context.pending_anticoll_level = level
if frame.is_card_to_reader and frame.bit_length == 40 and len(data) == 5:
level = context.pending_anticoll_level or 'CL1'
calc = data[0] ^ data[1] ^ data[2] ^ data[3]
uid = _hex(data[:4], spaced=False)
if calc == data[4]:
return f"ANTICOLL {level} response: UID={uid} BCC=0x{data[4]:02X} (OK)"
return f"ANTICOLL-like {level}: UID={uid} BCC=0x{data[4]:02X}"
if frame.is_reader_to_card:
ul_label = _decode_ultralight_reader_command(frame, context)
if ul_label:
return ul_label
else:
ul_label = _decode_ultralight_card_response(frame, context)
if ul_label:
return ul_label
return _decode_hf14a_frame(frame)
def _decode_ultralight_reader_command(frame: HfSniffFrame, context: Hf14aDecodeContext) -> Optional[str]:
data = frame.data
if not data:
return None
b0 = data[0]
if b0 == 0x60 and len(data) in (1, 3):
context.probable_ultralight = True
context.probable_ultralight_ev1 = True
context.pending_ul_cmd = "get_version"
return "UL EV1 GET_VERSION"
if len(data) >= 2 and b0 == 0x30:
page = data[1]
context.probable_ultralight = True
context.pending_ul_cmd = "read"
context.pending_page = page
return f"UL READ page={page}"
if len(data) >= 3 and b0 == 0x3A:
start_page, end_page = data[1], data[2]
context.probable_ultralight = True
context.pending_ul_cmd = "fast_read"
context.pending_page = start_page
context.pending_end_page = end_page
return f"UL FAST_READ start={start_page} end={end_page}"
if len(data) >= 6 and b0 == 0xA2:
page = data[1]
payload = _hex(data[2:6]).upper()
context.probable_ultralight = True
context.pending_ul_cmd = "write"
context.pending_page = page
return f"UL WRITE page={page} data={payload}"
if len(data) >= 2 and b0 == 0xA0:
page = data[1]
context.probable_ultralight = True
context.pending_ul_cmd = "compat_write"
context.pending_page = page
return f"UL COMPAT_WRITE page={page}"
if len(data) == 16 and context.pending_ul_cmd == "compat_write":
page = context.pending_page if context.pending_page is not None else -1
context.pending_ul_cmd = "compat_write_data"
return f"UL COMPAT_WRITE DATA page={page} data={_hex(data).upper()}"
if len(data) >= 5 and b0 == 0x1B:
context.probable_ultralight = True
context.probable_ultralight_ev1 = True
context.pending_ul_cmd = "pwd_auth"
return f"UL EV1 PWD_AUTH pwd={_hex(data[1:5]).upper()}"
if len(data) >= 2 and b0 == 0x39:
context.probable_ultralight = True
context.probable_ultralight_ev1 = True
context.pending_ul_cmd = "read_cnt"
context.pending_counter = data[1]
return f"UL EV1 READ_CNT counter={data[1]}"
if len(data) >= 6 and b0 == 0xA5:
context.probable_ultralight = True
context.probable_ultralight_ev1 = True
context.pending_ul_cmd = "incr_cnt"
context.pending_counter = data[1]
value = _bytes_to_int(data[2:5])
return f"UL EV1 INCR_CNT counter={data[1]} inc={value}"
if len(data) >= 2 and b0 == 0x3E:
context.probable_ultralight = True
context.probable_ultralight_ev1 = True
context.pending_ul_cmd = "check_tearing"
context.pending_counter = data[1]
return f"UL EV1 CHECK_TEARING_EVENT counter={data[1]}"
if b0 == 0x3C:
context.probable_ultralight = True
context.probable_ultralight_ev1 = True
context.pending_ul_cmd = "read_sig"
return "UL EV1 READ_SIG"
return None
def _decode_ultralight_card_response(frame: HfSniffFrame, context: Hf14aDecodeContext) -> Optional[str]:
data = frame.data
if not data:
return None
if frame.bit_length == 4 and len(data) >= 1:
code = data[0] & 0x0F
if code == 0xA:
pending = context.pending_ul_cmd or "unknown"
context.pending_ul_cmd = None
return f"UL ACK (for {pending})"
return f"UL NAK/4bit response code=0x{code:X}"
pending = context.pending_ul_cmd
if pending == "read":
page = context.pending_page if context.pending_page is not None else -1
context.pending_ul_cmd = None
data_hex = _hex(data).upper()
if len(data) in (16, 18):
last_page = page + 3 if page >= 0 else -1
if last_page >= 0:
return f"UL READ RESP pages={page}-{last_page} len={len(data)} data={data_hex}"
return f"UL READ RESP len={len(data)} data={data_hex}"
return f"UL READ RESP? len={len(data)} data={data_hex}"
if pending == "fast_read":
start_page = context.pending_page if context.pending_page is not None else -1
end_page = context.pending_end_page if context.pending_end_page is not None else -1
context.pending_ul_cmd = None
data_hex = _hex(data).upper()
if start_page >= 0 and end_page >= start_page:
expected = (end_page - start_page + 1) * 4
return (f"UL FAST_READ RESP pages={start_page}-{end_page} "
f"len={len(data)} expected={expected} data={data_hex}")
return f"UL FAST_READ RESP len={len(data)} data={data_hex}"
if pending == "get_version":
context.pending_ul_cmd = None
if len(data) >= 8:
context.probable_ultralight_ev1 = True
return (f"UL EV1 VERSION vendor=0x{data[0]:02X} type=0x{data[1]:02X} "
f"sub=0x{data[2]:02X} ver={data[3]}.{data[4]} "
f"size=0x{data[6]:02X} proto=0x{data[7]:02X}")
return f"UL EV1 VERSION RESP? len={len(data)}"
if pending == "pwd_auth":
context.pending_ul_cmd = None
if len(data) >= 2:
return f"UL EV1 PWD_AUTH RESP PACK={_hex(data[:2]).upper()}"
return f"UL EV1 PWD_AUTH RESP? len={len(data)}"
if pending == "read_cnt":
counter = context.pending_counter if context.pending_counter is not None else -1
context.pending_ul_cmd = None
if len(data) >= 3:
value = data[0] | (data[1] << 8) | (data[2] << 16)
return f"UL EV1 READ_CNT RESP counter={counter} value={value}"
return f"UL EV1 READ_CNT RESP? len={len(data)}"
if pending == "check_tearing":
counter = context.pending_counter if context.pending_counter is not None else -1
context.pending_ul_cmd = None
if len(data) >= 1:
return f"UL EV1 CHECK_TEARING_EVENT RESP counter={counter} value=0x{data[0]:02X}"
return f"UL EV1 CHECK_TEARING_EVENT RESP? len={len(data)}"
if pending == "read_sig":
context.pending_ul_cmd = None
if len(data) >= 32:
return f"UL EV1 SIGNATURE RESP len={len(data)}"
return f"UL EV1 SIGNATURE RESP? len={len(data)}"
if pending in ("write", "incr_cnt", "compat_write_data"):
context.pending_ul_cmd = None
# Some traces contain full-byte ACK/NAK instead of 4-bit representation.
if len(data) == 1 and data[0] == 0x0A:
return "UL ACK"
return None
Is your feature request related to a problem? Please describe.
Now, the HF sniff frame annotation's only way to determine the frame's nature is by looking at this frame and only this frame. When encountering frames such as resp to READ cmd, it will most likely be gibberish since that frame contains only data. Also, some other annotations will benefit from implementing a context-relevant parser.
Describe the solution you'd like
Implement a context-relevant parser.
Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.
Additional context
There's my approach written in Python to implement a context-relevant parser. It now only focuses on Mifare Ultralight commands since this is what I am currently working with.
Click to expand