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:
@@ -49,8 +49,9 @@ class HFMFCommands:
|
|||||||
raise ValueError(f"Block data must be {MFBLOCK_SIZE} bytes")
|
raise ValueError(f"Block data must be {MFBLOCK_SIZE} bytes")
|
||||||
|
|
||||||
# MIX format: arg0=blockno, arg1=keytype
|
# MIX format: arg0=blockno, arg1=keytype
|
||||||
# payload = key(6) + reserved(10) + data(16) = 32 bytes
|
# firmware reads key at asBytes[0], block_data at asBytes[10]
|
||||||
payload = key_bytes + b"\x00" * 10 + data
|
# payload = key(6) + reserved(4) + data(16) = 26 bytes
|
||||||
|
payload = key_bytes + b"\x00" * 4 + data
|
||||||
resp = await self._t.send_mix(
|
resp = await self._t.send_mix(
|
||||||
Cmd.HF_MIFARE_WRITEBL, arg0=block, arg1=key_type,
|
Cmd.HF_MIFARE_WRITEBL, arg0=block, arg1=key_type,
|
||||||
payload=payload, timeout=5.0,
|
payload=payload, timeout=5.0,
|
||||||
@@ -96,14 +97,24 @@ class HFMFCommands:
|
|||||||
continue
|
continue
|
||||||
return {"found": False}
|
return {"found": False}
|
||||||
|
|
||||||
async def sniff(self) -> dict:
|
async def sniff(self, param: int = 0) -> dict:
|
||||||
"""Sniff MIFARE traffic."""
|
"""Sniff MIFARE (ISO14443-A) traffic.
|
||||||
resp = await self._t.send_ng(Cmd.HF_MIFARE_SNIFF, timeout=30.0)
|
|
||||||
|
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}
|
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.
|
"""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):
|
if isinstance(uid, str):
|
||||||
uid = bytes.fromhex(uid)
|
uid = bytes.fromhex(uid)
|
||||||
@@ -116,23 +127,70 @@ class HFMFCommands:
|
|||||||
if uid_flag is None:
|
if uid_flag is None:
|
||||||
raise ValueError("UID must be 4, 7, or 10 bytes")
|
raise ValueError("UID must be 4, 7, or 10 bytes")
|
||||||
|
|
||||||
flags = 0x0001 | uid_flag | size_flags[size] # FLAG_INTERACTIVE
|
flags = uid_flag | size_flags[size]
|
||||||
resp = await self._t.send_mix(
|
if interactive:
|
||||||
Cmd.HF_MIFARE_SIMULATE, arg0=flags, payload=uid, timeout=60.0,
|
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}
|
return {"status": resp.status}
|
||||||
|
|
||||||
async def nested(self, block: int, key: str | bytes = "FFFFFFFFFFFF",
|
async def nested(self, block: int, key: str | bytes = "FFFFFFFFFFFF",
|
||||||
key_type: int = MF_KEY_A,
|
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."""
|
"""Run nested attack to recover keys."""
|
||||||
key_bytes = self._parse_key(key)
|
key_bytes = self._parse_key(key)
|
||||||
payload = struct.pack("<BB", block, key_type) + key_bytes
|
# struct { block, keytype, target_block, target_keytype, bool calibrate, key[6] }
|
||||||
payload += struct.pack("<BB", target_block, target_key_type)
|
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)
|
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:
|
# response struct { int16 isOK; u8 block; u8 keytype; u8 cuid[4];
|
||||||
"""Identify magic (gen1/gen2) card type."""
|
# u8 nt_a[4]; u8 ks_a[4]; u8 nt_b[4]; u8 ks_b[4] }
|
||||||
resp = await self._t.send_ng(Cmd.HF_MIFARE_CIDENT, timeout=5.0)
|
if len(resp.data) < 24:
|
||||||
return {"status": resp.status, "data": resp.data.hex() if resp.data else None}
|
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}
|
||||||
|
|||||||
@@ -22,5 +22,89 @@ def test_mf_wrbl():
|
|||||||
t.send_mix.return_value = PM3Response(cmd=Cmd.ACK, status=0, reason=0,
|
t.send_mix.return_value = PM3Response(cmd=Cmd.ACK, status=0, reason=0,
|
||||||
ng=False, data=b"", oldarg=[1, 0, 0])
|
ng=False, data=b"", oldarg=[1, 0, 0])
|
||||||
result = asyncio.get_event_loop().run_until_complete(
|
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
|
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()
|
||||||
|
|||||||
Reference in New Issue
Block a user