Categories
RFID Security

From a Drawer of Old Chipcards to a Working Proxmark3: A Mifare Classic Bring-Up Story

While cleaning up I found a stack of old chipcards and got curious. Here’s the full, honest log of getting a Proxmark3 built, flashed, and reading/writing Mifare Classic cards — segfaults, all-zero dumps, and every gotcha in between.

While cleaning up I found a stack of old chipcards and got curious. What is actually on these things? Could I read them, back them up, maybe even copy one? That curiosity turned into a weekend with a brand-new Proxmark3 — the Swiss-army knife of RFID/NFC research — and a crash course in Mifare Classic. This post is the honest log: not a glossy tutorial, but the real sequence of errors, head-scratching, and fixes. If you are starting the same journey, this should save you a few hours.

Scope & ethics: everything here was done on cards I own, for learning and backup. Cloning cards you do not own — or anything tied to access control or payments you are not authorized for — is off the table. Note: the data-block contents in the dumps below have been replaced with placeholder bytes; the commands, errors, keys and tool output are otherwise as they happened.

The hardware and the plan

The Proxmark3 reads and writes 13.56 MHz (HF) and 125 kHz (LF) cards. My goal was simple, in four steps:

  1. Read / identify a card (UID, type, sectors).
  2. Dump & back up its contents to a file.
  3. Clone / write to prove the writer works.
  4. Verify that reads and writes are reliable.

I went with the community-standard Iceman / RRG firmware, which supports far more Mifare attacks than stock.

Building the client (and two missing dependencies)

The build needs two compilers: your system gcc for the PC client, and the ARM cross-compiler for the device firmware. My first build died with:

/bin/sh: 1: arm-none-eabi-gcc: not found
make[2]: *** [obj/flash-reset-at91.o] Error 127

Two packages tripped me up. The ARM C library ships under a confusing name (the docs call it libnewlib-dev, but on some distros it is libnewlib-arm-none-eabi), and the client also needs the compression libs liblz4-dev and libbz2-dev. The full install line that worked on Debian/Ubuntu:

sudo apt install --no-install-recommends git ca-certificates build-essential \
  pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev \
  libbz2-dev liblz4-dev zlib1g-dev libssl-dev libpython3-dev libpcsclite-dev

make clean && make -j$(nproc)

Lesson: if the firmware half of the build fails on missing headers like stdint.h, it is the ARM newlib package, not your compiler.

Flashing: a version mismatch, then a segfault

Freshly built client, device plugged in, and… it refused to talk:

[=] Using UART port /dev/ttyACM0
[!!] 🚨 Capabilities structure version sent by Proxmark3 is not the one expected by this client! (v6 != v13)
[!!] 🚨 Please flash the Proxmark3 with a version matching the client.
[!!] 🚨 ERROR: cannot communicate with the Proxmark3

The device shipped with old factory firmware (capabilities v6) while my client expected v13. The client flatly refuses a mismatch — the fix is to reflash. So I ran ./pm3-flash-all… and it crashed with a segmentation fault mid-flash. Not what you want to see while writing firmware.

The recovery turned out to be simple: force bootloader mode by hand. Power off, then hold the device button down while plugging in USB (about 5 seconds, until the LEDs settle). Holding the button makes the chip boot its ROM bootloader instead of the half-written image, so the flasher can take over cleanly. This time it ran to completion, and the banner finally appeared with matching Client / Bootrom / OS versions:

    MCU....... AT91SAM7S512 Rev A
    Memory.... 512 KB ( 93% used )

    Client.... Iceman/master/v4.23346-170-gd5f701709 2026-10-15 20:44:55
    Bootrom... Iceman/master/v4.23346-170-gd5f701709-suspect 2026-10-15 20:43:20
    OS........ Iceman/master/v4.23346-170-gd5f701709-suspect 2026-10-15 20:43:25

Lessons: always confirm with hw version that client and firmware match — it is the #1 cause of “won’t connect.” A segfault during flashing is rarely fatal to the hardware; the button-hold bootloader trick gets you back. And flash over a direct USB port, not a hub. (The -suspect tag on the firmware just means it was built from a non-pristine git checkout — cosmetic.)

Reading the first card

With the device alive, identifying a card is one command inside the pm3> prompt:

[usb] pm3 --> hf mf info

[=] --- ISO14443-a Information -----------------------------
[+]  UID: 8A 00 06 CE
[+] ATQA: 00 04
[+]  SAK: 08 [2]
[=] --- Magic Tag Information
[=] <n/a>
[=] --- PRNG Information
[+] Prng....... weak

Two important readings here. The weak PRNG means the card is vulnerable to the classic “nested” key-recovery attack (good news for me). And Magic Tag Information: <n/a> means this is a genuine, non-magic card — its UID / block 0 is permanently locked. That matters later for cloning. SAK 08 confirms it is a Mifare Classic 1K.

How do you even get the keys? A word on Mifare Classic’s crypto

Every sector of a Mifare Classic is locked by two 48-bit keys (Key A and Key B), and you must authenticate with a valid key before you can read or write that sector. So the whole exercise hinges on one question: where do the keys come from? You will notice I use a key (67930635BDD7) in the read/write test further down — here is how you actually obtain one.

The good news (for research; bad news for the card’s security) is that Mifare Classic’s CRYPTO1 cipher was broken years ago. The Proxmark ships a whole ladder of attacks, from cheapest to most expensive:

  • Dictionary — just try a list of known/common keys (the default FFFFFFFFFFFF, vendor defaults, leaked keys). Instant if the card uses a known key. (res code D)
  • Darkside — recovers a first key from a card with a weak PRNG, needing no prior key at all. This is how you get your foot in the door from scratch. (res code S)
  • Nested — once you know one key on the card, exploit the weak PRNG to derive all the others very quickly. (res code N)
  • Hardnested — for later cards with a hardened PRNG where plain nested fails; slower, but still needs no prior key. (res code H)
  • Static / statiCnested — variants for cards that emit static or predictable nonces. (res code C)

You almost never run these by hand. hf mf autopwn is the orchestrator: it tries the dictionary first, falls back to darkside / nested / hardnested as needed, reuses every key it finds across the other sectors, and finishes by dumping the card and writing a key file — all in one command. On my weak-PRNG card it works even if you know nothing:

# simplest: let autopwn try everything, from zero knowledge
[usb] pm3 --> hf mf autopwn

# or, if you already recovered/know one key, seed it to go faster
[usb] pm3 --> hf mf autopwn -k 67930635BDD7

Here is what a successful pwn actually looks like — autopwn runs through its strategies, recovers Key A on every sector from the seed, then cracks Key B with a nested attack (after one retry) and reuses it across the card:

[usb] pm3 --> hf mf autopwn -k 67930635BDD7
[+] loaded 9 user keys
[+] loaded 64 hardcoded keys
[=] Running strategy 1
[=] Running strategy 2
[+] Target sector   0 key type A -- found valid key [ 67930635BDD7 ]
[+] Target sector   1 key type A -- found valid key [ 67930635BDD7 ]
[+]  ... (Key A recovered on all 16 sectors)
[+] Target sector  15 key type A -- found valid key [ 67930635BDD7 ]

[+] Target block    0 key type B
[-] ⛔ Nested attack failed, trying again (1/6)
[+] Found 1 key candidate
[+] Target block    0 key type B -- found valid key [ F4A9EF2AFC6D ]
[+] Target sector   0 key type B -- found valid key [ F4A9EF2AFC6D ]
[+]  ... (Key B then reused across all sectors)
[+] Target sector  15 key type B -- found valid key [ F4A9EF2AFC6D ]

[+] Found keys have been dumped to `hf-mf-8A0006CE-key-002.bin`
[=] Autopwn execution time: 16 seconds

So to answer “where did 67930635BDD7 come from?” — a from-scratch autopwn (darkside to crack the first key, then nested for the rest) hands it to you. Seeding it with -k is just an optional shortcut once you know one. The res column in autopwn’s output is the receipt telling you which method cracked each key — we will read it in the next section.

Is it writable? Magic vs. normal cards

A quick primer that took me a while to internalize:

  • On a normal Mifare Classic, the data blocks are writable if you hold the right key — but block 0 (the UID) is read-only forever.
  • To clone a UID you need a “magic” card (gen1a or gen2/CUID) with a backdoor that unlocks block 0.
  • And never confuse a sector trailer (the last block of each sector, holding the keys + access bits) with a data block — writing garbage there can lock a sector permanently. On a 1K card the trailers are blocks 3, 7, 11, …, 63 (i.e. block mod 4 == 3).

To prove the writer without a magic card, I did a non-destructive round-trip on a data block: save the original, write a test value, read it back, restore the original. (The “original” bytes shown here are placeholders — the real card held actual data.)

# 1. note the current contents
[usb] pm3 --> hf mf rdbl --blk 1 -k 67930635BDD7
[=]   1 | 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 | ABCDEFGHIJKLMNOP

# 2. write a known test value
[usb] pm3 --> hf mf wrbl --blk 1 -k 67930635BDD7 -d 00112233445566778899AABBCCDDEEFF
[=] data: 00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF
[+] Write ( ok )

# 3. read back — should match what you wrote
[usb] pm3 --> hf mf rdbl --blk 1 -k 67930635BDD7
[=]   1 | 00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF | .."3DUfw........

# 4. restore the original 16 bytes
[usb] pm3 --> hf mf wrbl --blk 1 -k 67930635BDD7 -d 4142434445464748494A4B4C4D4E4F50
[+] Write ( ok )
[usb] pm3 --> hf mf rdbl --blk 1 -k 67930635BDD7
[=]   1 | 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 | ABCDEFGHIJKLMNOP

Write confirmed, original restored. Reader and writer both work — and these old cards were clearly not empty.

The mystery of the all-zero dump

Here is the bit that cost me the most time. A full hf mf dump came back as nothing but zeros — yet I had just read real data from a single block. How can both be true?

The answer: hf mf dump pulls keys from a key file, and when it cannot authenticate a sector it does not throw an error — it silently fills that block with 00. My key file was full of the default key FFFFFFFFFFFF, because the dictionary attack never found this card’s custom keys:

$ xxd ~/hf-mf-8A0006CE-key.bin
00000000: ffff ffff ffff ffff ffff ffff ffff ffff  ................
00000010: ffff ffff ffff ffff ffff ffff ffff ffff  ................
...   (every entry FFFFFFFFFFFF)

My single-block reads only worked because I had typed the real key (67930635BDD7) by hand. The dump had no idea it existed. The fix is to actually recover the real keys. Because the PRNG is weak and I already knew one key, I could seed autopwn and let a nested attack derive the rest:

[usb] pm3 --> hf mf autopwn -k 67930635BDD7

[+]  Sec | Blk | key A        |res| key B        |res
[+] -----+-----+--------------+---+--------------+----
[+]  000 | 003 | 67930635BDD7 | D | F4A9EF2AFC6D | N
[+]  001 | 007 | 67930635BDD7 | D | F4A9EF2AFC6D | R
[+]  ...  (all 16 sectors recovered)
[+]  015 | 063 | 67930635BDD7 | D | F4A9EF2AFC6D | R
[=] ( D:Dictionary / S:darkSide / U:User / R:Reused / N:Nested / H:Hardnested )

[+] Found keys have been dumped to `hf-mf-8A0006CE-key-002.bin`
[=] --[ FFFFFFFFFFFF ]-- has been inserted for unknown keys where res is 0
[+] Saved 1024 bytes to binary file `hf-mf-8A0006CE-dump-003.bin`
[=] Autopwn execution time: 16 seconds

Sixteen seconds, all keys found (Key A seeded from the one I knew, Key B by nested attack and then reused across sectors), and the dump was finally full of real data instead of zeros.

Lesson: an all-zero dump almost never means an empty card — it means failed authentication. Check your keys first.

One last gotcha: the key file that “didn’t update”

After autopwn succeeded, I checked my key file again — still all FFFFFFFFFFFF. Did it not save? It did, just not where I looked. Iceman never overwrites an existing output file; it appends a counter. My real keys landed in hf-mf-8A0006CE-key-002.bin (see the output above), while the original (failed) hf-mf-8A0006CE-key.bin sat there untouched.

This matters: autopwn‘s own dump worked because it passes freshly-cracked keys directly in memory. But a later plain hf mf dump reloads the base filename — the stale all-FF one — and you are back to zeros. The fix is to promote the good file:

# cp ~/hf-mf-8A0006CE-key-002.bin ~/hf-mf-8A0006CE-key.bin

One more subtlety from that output: FFFFFFFFFFFF has been inserted for unknown keys — so a full-size 192-byte key file does not guarantee every key is real. Always check the res column (or xxd) to be sure.

Where your files go (and how to change it)

By default, pm3 writes dumps, key files and traces into the directory you launched the client from — which is exactly why mine kept landing in /root (I started it there, as root). There are three ways to control that.

1. Per command — pass an explicit path with -f:

[usb] pm3 --> hf mf dump -f /home/marcel/cards/office-card

2. Just start the client where you want the files — relative filenames follow the working directory:

$ mkdir -p ~/cards && cd ~/cards
$ pm3

3. Set a permanent default via the prefs system (stored in ~/.proxmark3/preferences.json):

[usb] pm3 --> prefs show                      # see current dump / trace paths
[usb] pm3 --> prefs set savepaths --dump  ~/cards
[usb] pm3 --> prefs set savepaths --trace ~/cards

(The flag names vary slightly between builds, so run prefs set savepaths -h to confirm the exact options on yours.) Note that logs and preferences.json live in ~/.proxmark3/, separate from your dump files.

And the real fix for the /root mess: run the client as your own user, not root. Add yourself to the dialout group so you get serial access without sudo, and the home dir, logs and save paths all follow your account:

$ sudo usermod -aG dialout $USER     # then log out / back in

Where it ended up

  • ✅ Device built, flashed, and talking (matching v4.23346 versions).
  • ✅ Cards read and identified.
  • ✅ Full dumps working, after recovering the custom keys.
  • ✅ Writer verified with a non-destructive round-trip.
  • ⬜ Full UID clone — still pending, because it needs a magic card and my test card is a genuine, non-magic one.

So the Proxmark3 itself is confirmed fully functional against Mifare Classic. The only unticked box is the end-to-end clone, which is a shopping problem (a couple of gen1a/gen2 magic cards) rather than a device problem. That — and whatever else is hiding on the rest of that drawer full of old cards — is the next post.

Got a Proxmark horror story or a cleaner workflow? Drop it in the comments.

Leave a Reply

Your email address will not be published. Required fields are marked *

Are you human? Please solve:Captcha