186 lines
5.8 KiB
Markdown
186 lines
5.8 KiB
Markdown
# pm3py
|
|
|
|
Pure-Python library for talking to a [Proxmark3](https://github.com/RfidResearchGroup/proxmark3) over its NG wire protocol. Returns structured Python dicts instead of parsing terminal output. Async-first with a sync wrapper for REPL use.
|
|
|
|
**No dependency on the C client.** Speaks the wire protocol directly over USB serial.
|
|
|
|
## Install
|
|
|
|
```bash
|
|
cd pm3py
|
|
pip install -e .
|
|
```
|
|
|
|
Requires Python 3.10+ and `pyserial-asyncio`.
|
|
|
|
## Quick Start
|
|
|
|
```python
|
|
from pm3py import Proxmark3
|
|
|
|
# Sync mode — auto-detects the device
|
|
pm3 = Proxmark3.sync()
|
|
print(pm3.firmware.version_string)
|
|
print(pm3.hw.ping())
|
|
print(pm3.hw.version())
|
|
pm3.close()
|
|
```
|
|
|
|
Or with async:
|
|
|
|
```python
|
|
import asyncio
|
|
from pm3py import Proxmark3
|
|
|
|
async def main():
|
|
async with Proxmark3() as pm3:
|
|
print(await pm3.hw.ping())
|
|
print(await pm3.hf.a14.scan())
|
|
|
|
asyncio.run(main())
|
|
```
|
|
|
|
## API
|
|
|
|
All commands return Python dicts. The API mirrors the C client's command tree:
|
|
|
|
### Hardware — `pm3.hw.*`
|
|
|
|
```python
|
|
pm3.hw.ping() # verify connectivity
|
|
pm3.hw.version() # firmware version, chip ID
|
|
pm3.hw.status() # device status
|
|
pm3.hw.capabilities() # compiled features, platform (Easy vs RDV4)
|
|
pm3.hw.tune() # full antenna tuning (LF + HF voltages)
|
|
pm3.hw.led("green", on=True) # LED control with color names
|
|
pm3.hw.led("red", pulse=True, speed=300)# PWM effects (validates hardware support)
|
|
pm3.hw.led("all", on=False) # turn off
|
|
pm3.hw.dbg(level=2) # set debug level
|
|
pm3.hw.break_loop() # stop long-running operations
|
|
pm3.hw.reset() # reset device
|
|
```
|
|
|
|
LED colors are platform-aware — `"green"` maps to the correct physical LED on both PM3 Easy and RDV4. PWM operations (brightness, pulse, fade) validate hardware capability and raise `PM3Error` if the LED doesn't support it.
|
|
|
|
### Low Frequency — `pm3.lf.*`
|
|
|
|
```python
|
|
pm3.lf.tune(divisor=95) # LF antenna voltage at frequency
|
|
pm3.lf.read(samples=30000) # capture ADC samples
|
|
pm3.lf.config() # get sampling config
|
|
pm3.lf.config(divisor=88) # set config (134 kHz)
|
|
pm3.lf.search() # capture + return LFSearchResult
|
|
pm3.lf.sniff() # alias for read
|
|
pm3.lf.sim(gap=0, data=b"...") # simulate from buffer
|
|
|
|
# T55xx tags
|
|
pm3.lf.t55.readbl(block=0)
|
|
pm3.lf.t55.writebl(block=1, data=0xDEADBEEF)
|
|
pm3.lf.t55.wakeup(password=0x12345678)
|
|
```
|
|
|
|
`lf.search()` returns an `LFSearchResult` with callable next steps:
|
|
|
|
```python
|
|
result = pm3.lf.search()
|
|
print(result) # shows available actions
|
|
raw = result.download() # download raw ADC samples
|
|
result.tune() # check antenna voltage
|
|
result.read_t55xx(block=0) # try reading as T55xx
|
|
```
|
|
|
|
Full tag demodulation (EM410x, HID, AWID, etc.) requires the C client's DSP stack.
|
|
|
|
### High Frequency — `pm3.hf.*`
|
|
|
|
```python
|
|
pm3.hf.tune() # HF antenna voltage
|
|
pm3.hf.search() # try 14a then 15693
|
|
pm3.hf.sniff() # sniff HF traffic
|
|
pm3.hf.dropfield() # turn off field
|
|
```
|
|
|
|
### ISO 14443-A — `pm3.hf.a14.*`
|
|
|
|
```python
|
|
pm3.hf.a14.scan() # → {uid, atqa, sak, ats}
|
|
pm3.hf.a14.raw(data=b"\x50\x00") # raw APDU
|
|
pm3.hf.a14.sniff()
|
|
pm3.hf.a14.sim(uid=b"\x01\x02\x03\x04")
|
|
```
|
|
|
|
### ISO 15693 — `pm3.hf.iso15.*`
|
|
|
|
```python
|
|
pm3.hf.iso15.scan() # → {uid, dsfid}
|
|
pm3.hf.iso15.rdbl(block=0) # read block
|
|
pm3.hf.iso15.wrbl(block=0, data="00112233")
|
|
pm3.hf.iso15.sniff()
|
|
```
|
|
|
|
### MIFARE Classic — `pm3.hf.mf.*`
|
|
|
|
```python
|
|
pm3.hf.mf.rdbl(block=0) # read block (default key FF..FF)
|
|
pm3.hf.mf.rdbl(block=4, key="A0A1A2A3A4A5", key_type=1) # key B
|
|
pm3.hf.mf.wrbl(block=4, key="FFFFFFFFFFFF", data="00" * 16)
|
|
pm3.hf.mf.rdsc(sector=0) # read entire sector
|
|
pm3.hf.mf.chk(block=0, keys=["FFFFFFFFFFFF", "A0A1A2A3A4A5"])
|
|
pm3.hf.mf.sniff()
|
|
pm3.hf.mf.sim(uid="01020304", size="1k")
|
|
pm3.hf.mf.nested(block=0, key="FFFFFFFFFFFF")
|
|
pm3.hf.mf.cident() # identify magic card type
|
|
```
|
|
|
|
### Raw Commands
|
|
|
|
For anything not wrapped:
|
|
|
|
```python
|
|
from pm3py import Cmd
|
|
|
|
resp = pm3.send_ng(Cmd.STATUS) # NG frame
|
|
resp = pm3.send_mix(Cmd.HF_ISO14443A_READER, arg0=0x0103) # MIX frame
|
|
```
|
|
|
|
## Firmware Compatibility
|
|
|
|
On connect, pm3py pings the device and fetches the firmware version. Results are in `pm3.firmware`:
|
|
|
|
```python
|
|
pm3 = Proxmark3.sync()
|
|
print(pm3.firmware.compatible) # True if NG protocol works
|
|
print(pm3.firmware.version_string) # firmware version
|
|
print(pm3.firmware.chip_id) # e.g. "0x270B0A40"
|
|
print(pm3.firmware.warnings) # any issues detected
|
|
```
|
|
|
|
## Architecture
|
|
|
|
```
|
|
pm3py/
|
|
├── protocol.py # Wire protocol constants, CRC-16/A, command IDs, error codes
|
|
├── transport.py # Frame encode/decode, async serial I/O, PM3Transport
|
|
├── client.py # Proxmark3 class, sync proxy, firmware probe
|
|
├── hw.py # hw.* commands
|
|
├── lf.py # lf.* commands + T55xx
|
|
├── hf.py # hf.* commands (tune, search, sniff)
|
|
├── hf_14a.py # hf.14a.* (ISO 14443-A)
|
|
├── hf_15.py # hf.15.* (ISO 15693)
|
|
└── hf_mf.py # hf.mf.* (MIFARE Classic)
|
|
```
|
|
|
|
- **Transport layer** handles serial I/O, CRC, NG/MIX frame encoding/decoding
|
|
- **Command modules** send structured commands and parse responses into dicts
|
|
- **USB connections** skip CRC (matching the C client behavior)
|
|
- **FPC UART connections** use CRC-16/A with byte-swapped wire format
|
|
|
|
## Tests
|
|
|
|
```bash
|
|
cd pm3py
|
|
python -m pytest tests/ -v
|
|
```
|
|
|
|
All tests use mock transports — no hardware required.
|