fix(mfc): correct WRITEBL offset, NESTED struct order, CIDENT/SIM payloads; route sniff to 14a

wrbl padded key to 16 (firmware reads block at asBytes+10) -> corrupt writes; pad to 10. nested sent {block,keytype,key,target...} but firmware struct is {block,keytype,target_block,target_keytype,calibrate,key[6]} -> attacked wrong target; reorder + add calibrate + decode reply. cident/sim sent empty/MIX payloads for NG handlers -> send the packed structs. mf.sniff (no firmware handler) reroutes to CMD_HF_ISO14443A_SNIFF. Pre-existing, independent of rebase.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
michael
2026-07-05 00:08:40 -07:00
parent 01357e4e78
commit 625104ea03
2 changed files with 161 additions and 19 deletions

View File

@@ -49,8 +49,9 @@ class HFMFCommands:
raise ValueError(f"Block data must be {MFBLOCK_SIZE} bytes")
# MIX format: arg0=blockno, arg1=keytype
# payload = key(6) + reserved(10) + data(16) = 32 bytes
payload = key_bytes + b"\x00" * 10 + data
# firmware reads key at asBytes[0], block_data at asBytes[10]
# payload = key(6) + reserved(4) + data(16) = 26 bytes
payload = key_bytes + b"\x00" * 4 + data
resp = await self._t.send_mix(
Cmd.HF_MIFARE_WRITEBL, arg0=block, arg1=key_type,
payload=payload, timeout=5.0,
@@ -96,14 +97,24 @@ class HFMFCommands:
continue
return {"found": False}
async def sniff(self) -> dict:
"""Sniff MIFARE traffic."""
resp = await self._t.send_ng(Cmd.HF_MIFARE_SNIFF, timeout=30.0)
async def sniff(self, param: int = 0) -> dict:
"""Sniff MIFARE (ISO14443-A) traffic.
Firmware has no dedicated MIFARE sniff handler; the C client routes
``hf mf sniff`` through the ISO14443-A sniffer.
"""
resp = await self._t.send_mix(
Cmd.HF_ISO14443A_SNIFF, arg0=param, timeout=30.0,
)
return {"status": resp.status}
async def sim(self, uid: str | bytes, size: str = "1k") -> dict:
async def sim(self, uid: str | bytes, size: str = "1k",
atqa: str | bytes | None = None, sak: int | None = None,
exit_after: int = 0, interactive: bool = False) -> dict:
"""Simulate MIFARE Classic card.
size: "mini", "1k", "2k", "4k"
size: "mini", "1k", "2k", "4k". Optional atqa (2 bytes) / sak (1 byte)
override the defaults; exit_after N stops after N auth attempts (0=never).
"""
if isinstance(uid, str):
uid = bytes.fromhex(uid)
@@ -116,23 +127,70 @@ class HFMFCommands:
if uid_flag is None:
raise ValueError("UID must be 4, 7, or 10 bytes")
flags = 0x0001 | uid_flag | size_flags[size] # FLAG_INTERACTIVE
resp = await self._t.send_mix(
Cmd.HF_MIFARE_SIMULATE, arg0=flags, payload=uid, timeout=60.0,
flags = uid_flag | size_flags[size]
if interactive:
flags |= 0x0001 # FLAG_INTERACTIVE
atqa_val = 0
if atqa is not None:
if isinstance(atqa, str):
atqa = bytes.fromhex(atqa)
if len(atqa) != 2:
raise ValueError("ATQA must be 2 bytes")
atqa_val = (atqa[1] << 8) | atqa[0]
flags |= 0x0002 # FLAG_ATQA_IN_DATA
sak_val = 0
if sak is not None:
sak_val = sak & 0xFF
flags |= 0x0004 # FLAG_SAK_IN_DATA
# struct { u16 flags; u8 exitAfter; u8 uid[10]; u16 atqa; u8 sak }
payload = struct.pack("<HB", flags, exit_after & 0xFF)
payload += uid + b"\x00" * (10 - len(uid))
payload += struct.pack("<HB", atqa_val, sak_val)
resp = await self._t.send_ng(
Cmd.HF_MIFARE_SIMULATE, payload, timeout=60.0,
)
return {"status": resp.status}
async def nested(self, block: int, key: str | bytes = "FFFFFFFFFFFF",
key_type: int = MF_KEY_A,
target_block: int = 0, target_key_type: int = MF_KEY_A) -> dict:
target_block: int = 0, target_key_type: int = MF_KEY_A,
calibrate: bool = True) -> dict:
"""Run nested attack to recover keys."""
key_bytes = self._parse_key(key)
payload = struct.pack("<BB", block, key_type) + key_bytes
payload += struct.pack("<BB", target_block, target_key_type)
# struct { block, keytype, target_block, target_keytype, bool calibrate, key[6] }
payload = struct.pack("<BBBBB", block, key_type, target_block,
target_key_type, 1 if calibrate else 0) + key_bytes
resp = await self._t.send_ng(Cmd.HF_MIFARE_NESTED, payload, timeout=30.0)
return {"status": resp.status, "data": resp.data.hex()}
async def cident(self) -> dict:
"""Identify magic (gen1/gen2) card type."""
resp = await self._t.send_ng(Cmd.HF_MIFARE_CIDENT, timeout=5.0)
return {"status": resp.status, "data": resp.data.hex() if resp.data else None}
# response struct { int16 isOK; u8 block; u8 keytype; u8 cuid[4];
# u8 nt_a[4]; u8 ks_a[4]; u8 nt_b[4]; u8 ks_b[4] }
if len(resp.data) < 24:
return {"success": False, "status": resp.status}
is_ok, r_block, r_keytype = struct.unpack_from("<hBB", resp.data, 0)
return {
"success": is_ok == 0,
"is_ok": is_ok,
"block": r_block,
"key_type": r_keytype,
"cuid": resp.data[4:8].hex(),
"nt_a": resp.data[8:12].hex(),
"ks_a": resp.data[12:16].hex(),
"nt_b": resp.data[16:20].hex(),
"ks_b": resp.data[20:24].hex(),
}
async def cident(self, is_mfc: bool = True, key_type: int = MF_KEY_A,
key: str | bytes = "FFFFFFFFFFFF") -> dict:
"""Identify magic (gen1/gen2/...) card type."""
key_bytes = self._parse_key(key)
# struct { uint8 is_mfc; uint8 keytype; uint8 key[6] }
payload = bytes([1 if is_mfc else 0, key_type & 0xFF]) + key_bytes
resp = await self._t.send_ng(Cmd.HF_MIFARE_CIDENT, payload, timeout=5.0)
magic = 0
if resp.status == 0 and len(resp.data) >= 2:
magic = struct.unpack_from("<H", resp.data, 0)[0]
return {"status": resp.status, "magic": magic, "is_magic": magic != 0}

View File

@@ -22,5 +22,89 @@ def test_mf_wrbl():
t.send_mix.return_value = PM3Response(cmd=Cmd.ACK, status=0, reason=0,
ng=False, data=b"", oldarg=[1, 0, 0])
result = asyncio.get_event_loop().run_until_complete(
mf.wrbl(block=4, key="FFFFFFFFFFFF", data="00" * 16))
mf.wrbl(block=4, key="FFFFFFFFFFFF", data="11" * 16))
assert result["success"] is True
# firmware: key at asBytes[0], block_data at asBytes[10]
kwargs = t.send_mix.call_args.kwargs
payload = kwargs["payload"]
assert len(payload) == 26
assert payload[:6] == bytes.fromhex("FFFFFFFFFFFF")
assert payload[6:10] == b"\x00" * 4
assert payload[10:26] == bytes.fromhex("11" * 16)
assert kwargs["arg0"] == 4
assert kwargs["arg1"] == 0
def test_mf_nested():
t = AsyncMock()
mf = HFMFCommands(t)
# reply: isOK=0, block=8, keytype=1, cuid, nt_a, ks_a, nt_b, ks_b
reply = struct.pack("<hBB", 0, 8, 1)
reply += bytes.fromhex("01020304") # cuid
reply += bytes.fromhex("11111111") # nt_a
reply += bytes.fromhex("22222222") # ks_a
reply += bytes.fromhex("33333333") # nt_b
reply += bytes.fromhex("44444444") # ks_b
t.send_ng.return_value = PM3Response(cmd=Cmd.HF_MIFARE_NESTED, status=0,
reason=0, ng=True, data=reply)
result = asyncio.get_event_loop().run_until_complete(
mf.nested(block=0, key="FFFFFFFFFFFF", key_type=0,
target_block=8, target_key_type=1, calibrate=True))
# firmware struct order: block, keytype, target_block, target_keytype, calibrate, key[6]
payload = t.send_ng.call_args.args[1]
assert payload == struct.pack("<BBBBB", 0, 0, 8, 1, 1) + bytes.fromhex("FFFFFFFFFFFF")
assert result["success"] is True
assert result["block"] == 8
assert result["key_type"] == 1
assert result["cuid"] == "01020304"
assert result["ks_a"] == "22222222"
assert result["nt_b"] == "33333333"
def test_mf_cident():
t = AsyncMock()
mf = HFMFCommands(t)
# MAGIC_FLAG value returned as little-endian uint16
t.send_ng.return_value = PM3Response(cmd=Cmd.HF_MIFARE_CIDENT, status=0,
reason=0, ng=True,
data=struct.pack("<H", 0x0001))
result = asyncio.get_event_loop().run_until_complete(mf.cident())
payload = t.send_ng.call_args.args[1]
assert payload == bytes([1, 0]) + bytes.fromhex("FFFFFFFFFFFF")
assert result["magic"] == 0x0001
assert result["is_magic"] is True
def test_mf_sim():
t = AsyncMock()
mf = HFMFCommands(t)
t.send_ng.return_value = PM3Response(cmd=Cmd.HF_MIFARE_SIMULATE, status=0,
reason=0, ng=True, data=b"")
result = asyncio.get_event_loop().run_until_complete(
mf.sim(uid="11223344", size="1k", atqa="0400", sak=0x08))
assert result["status"] == 0
payload = t.send_ng.call_args.args[1]
# struct { u16 flags; u8 exitAfter; u8 uid[10]; u16 atqa; u8 sak }
assert len(payload) == 16
flags, exit_after = struct.unpack_from("<HB", payload, 0)
# 4B_UID_IN_DATA | FLAG_MF_1K | ATQA_IN_DATA | SAK_IN_DATA (interactive off by default)
assert flags == (0x0010 | 0x0040 | 0x0002 | 0x0004)
assert exit_after == 0
assert payload[3:7] == bytes.fromhex("11223344")
assert payload[7:13] == b"\x00" * 6
atqa_val, sak_val = struct.unpack_from("<HB", payload, 13)
assert atqa_val == 0x0004 # (0x00<<8)|0x04
assert sak_val == 0x08
def test_mf_sniff_routes_to_14a():
t = AsyncMock()
mf = HFMFCommands(t)
t.send_mix.return_value = PM3Response(cmd=Cmd.HF_ISO14443A_SNIFF, status=0,
reason=0, ng=False, data=b"")
result = asyncio.get_event_loop().run_until_complete(mf.sniff())
assert result["status"] == 0
# must target the ISO14443-A sniffer, not a (nonexistent) MF sniff handler
assert t.send_mix.call_args.args[0] == Cmd.HF_ISO14443A_SNIFF
assert t.send_mix.call_args.kwargs["arg0"] == 0
t.send_ng.assert_not_called()