diff --git a/pm3py/core/client.py b/pm3py/core/client.py index e57cdb5..d7be34a 100644 --- a/pm3py/core/client.py +++ b/pm3py/core/client.py @@ -14,6 +14,7 @@ from .hf import HFCommands from .hf_iso14a import HF14ACommands from .hf_iso15 import HF15Commands from .hf_mfc import HFMFCommands +from .hf_mfu import HFMFUCommands log = logging.getLogger(__name__) @@ -71,7 +72,8 @@ class Proxmark3: self.hf = HFCommands(self._transport) self.hf.iso14a = HF14ACommands(self._transport) self.hf.iso15 = HF15Commands(self._transport) - self.hf.mfc = HFMFCommands(self._transport) + self.hf.mf = HFMFCommands(self._transport) + self.hf.mfu = HFMFUCommands(self._transport) self.firmware = FirmwareInfo() async def connect(self) -> None: @@ -156,7 +158,8 @@ class _SyncProxy: def __getattr__(self, name): attr = getattr(self._target, name) if isinstance(attr, (HWCommands, LFCommands, HFCommands, - HF14ACommands, HF15Commands, HFMFCommands)): + HF14ACommands, HF15Commands, HFMFCommands, + HFMFUCommands)): return _SyncProxy(attr, self._loop) # Sub-modules attached dynamically (e.g. hf.iso14a, hf.mfc) if hasattr(attr, '_t'): diff --git a/pm3py/core/hf.py b/pm3py/core/hf.py index 2035897..0163bf9 100644 --- a/pm3py/core/hf.py +++ b/pm3py/core/hf.py @@ -13,7 +13,8 @@ class HFCommands: # Sub-modules attached by client.py self.iso14a = None # type: ignore self.iso15 = None # type: ignore - self.mfc = None # type: ignore + self.mf = None # MIFARE Classic # type: ignore + self.mfu = None # MIFARE Ultralight / NTAG # type: ignore async def tune(self, on_progress: ProgressCallback = None) -> dict: """Measure HF antenna voltage. diff --git a/pm3py/core/hf_mfu.py b/pm3py/core/hf_mfu.py new file mode 100644 index 0000000..61c255f --- /dev/null +++ b/pm3py/core/hf_mfu.py @@ -0,0 +1,64 @@ +"""MIFARE Ultralight / NTAG commands: hf.mfu.* + +ISO14443-3A tags with 4-byte pages. A READ (0x30) returns 4 consecutive +pages (16 bytes). keytype selects the auth scheme: + 0 = none, 1 = UL-C (16-byte 3DES key), 2 = UL-EV1/NTAG (4-byte pwd), + 3 = UL-AES (16-byte key). +""" +import struct +from .protocol import Cmd +from .transport import PM3Transport + + +def _key_bytes(key) -> bytes: + if key is None: + return b"" + if isinstance(key, str): + return bytes.fromhex(key) + return bytes(key) + + +class HFMFUCommands: + """MIFARE Ultralight / NTAG commands.""" + + def __init__(self, transport: PM3Transport): + self._t = transport + + async def rdbl(self, block: int, key=None, keytype: int = 0) -> dict: + """Read starting at page ``block``; returns 16 bytes (4 pages).""" + kb = _key_bytes(key) + # mful_readblock_t { bool use_schann; u8 block_no; u8 num_of_blocks; + # u8 keytype; u8 keylen; u8 key[16] } + payload = struct.pack(" dict: + """Write 4 bytes to page ``block``.""" + if isinstance(data, str): + data = bytes.fromhex(data) + kb = _key_bytes(key) + # mful_writeblock_t { bool use_schann; u8 block_no; u8 keytype; + # u8 keylen; u8 key[16]; u8 data[16] } + payload = struct.pack(" dict: + """Read ``pages`` pages from ``start`` (each READ returns 4 pages).""" + out = bytearray() + page = start + while page < start + pages: + r = await self.rdbl(page) + if not r.get("success"): + break + out += r["raw"][:16] + page += 4 + return {"success": len(out) > 0, "pages": len(out) // 4, + "data": out.hex(), "raw": bytes(out)} diff --git a/tests/test_hf_mf.py b/tests/test_hf_mf.py index fc85477..ba2a949 100644 --- a/tests/test_hf_mf.py +++ b/tests/test_hf_mf.py @@ -108,3 +108,35 @@ def test_mf_sniff_routes_to_14a(): 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() + + +def test_mfu_rdbl(): + import struct as _struct + from pm3py.core.hf_mfu import HFMFUCommands + t = AsyncMock() + mfu = HFMFUCommands(t) + # firmware replies 16 bytes (4 pages) directly + t.send_ng.return_value = PM3Response(cmd=Cmd.HF_MIFAREU_READBL, status=0, + reason=0, ng=True, data=bytes(range(16))) + r = asyncio.get_event_loop().run_until_complete(mfu.rdbl(block=4)) + assert r["success"] is True + assert r["data"] == "000102030405060708090a0b0c0d0e0f" + # request packs mful_readblock_t {use_schann,block_no,num,keytype,keylen,key[16]} = 21 bytes + payload = t.send_ng.call_args.args[1] + assert len(payload) == 21 + assert payload[1] == 4 # block_no + assert t.send_ng.call_args.args[0] == Cmd.HF_MIFAREU_READBL + + +def test_mfu_wrbl(): + from pm3py.core.hf_mfu import HFMFUCommands + t = AsyncMock() + mfu = HFMFUCommands(t) + t.send_ng.return_value = PM3Response(cmd=Cmd.HF_MIFAREU_WRITEBL, status=0, + reason=0, ng=True, data=b"") + r = asyncio.get_event_loop().run_until_complete(mfu.wrbl(block=5, data="aabbccdd")) + assert r["success"] is True + payload = t.send_ng.call_args.args[1] + assert len(payload) == 36 # {use_schann,block,keytype,keylen,key[16],data[16]} + assert payload[1] == 5 + assert payload[20:24] == bytes.fromhex("aabbccdd") diff --git a/tests/test_integration.py b/tests/test_integration.py index a784838..b65ce25 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -16,7 +16,7 @@ def test_full_api_surface(): assert hasattr(pm3.lf, "t55") assert hasattr(pm3.hf, "iso14a") assert hasattr(pm3.hf, "iso15") - assert hasattr(pm3.hf, "mfc") + assert hasattr(pm3.hf, "mf") assert hasattr(pm3, "send_ng") assert hasattr(pm3, "send_mix") assert hasattr(pm3, "firmware") @@ -45,9 +45,9 @@ def test_mf_rdbl_returns_hex(): block = bytes(range(16)) mock_transport.send_ng.return_value = PM3Response( cmd=Cmd.HF_MIFARE_READBL, status=0, reason=0, ng=True, data=block) - pm3.hf.mfc._t = mock_transport + pm3.hf.mf._t = mock_transport result = asyncio.get_event_loop().run_until_complete( - pm3.hf.mfc.rdbl(block=4, key="FFFFFFFFFFFF")) + pm3.hf.mf.rdbl(block=4, key="FFFFFFFFFFFF")) assert result["data"] == "000102030405060708090a0b0c0d0e0f" assert result["raw"] == block