1. Your address, and how you submit a flag
Do this before you start on the cards, from
~/ierg4851/contactless-ctf on the lab machine. You submit a flag
by redeeming it — there is no form, nothing goes to
Blackboard, and OK 250 is your receipt.
Make and register your address — once
This is where your points are paid, and the same command registers the
address with the scoreboard under your login name, so the board shows
u1155203181, not a hex string:
python3 score/keygen.py
wrote mykey.json address: 3c42822cbbcfb07cac13ad9afa7555cb10fc58f7 pubkey : 04a1f0… registered as u1155203181
Registration is signed with your key and tied to your account on the lab machine, so nobody can register a classmate's name. If it says ledger not reachable, the lab is not up yet — run the command again once it is; your address will not change.
How the address is verified. Every run
re-derives the address from the private key and prints
verified: address derives from this key; a
WARNING means mykey.json was edited or damaged.
The ledger checks the same rule on registration — an address is
sha256(public key)[:20], and REGISTER must carry a
signature from the matching private key. So an address cannot be claimed by
someone who does not hold its key, and you cannot register someone else's
name.
keygen.py
writes mykey.json, which holds the key to your address. Run it
once; running it again keeps the existing address rather than replacing it
(--force makes a new one, and points at the old address stay
there). To see the address later: python3 score/keygen.py --show.Submit a flag — each time you read one
The --flag is the text: string from your own
dump; the --to is the address printed above:
python3 score/redeem.py --flag IERG4851_NFC_DEFKEY_933e4aa8 --to 3c42822cbbcfb07cac13ad9afa7555cb10fc58f7
OK 250 your total is now 250 points
OK means the flag was accepted and paid. ERR …
says why it was refused — unknown flag, already redeemed, or a malformed
address. Each flag pays 250 points, once.
Check your score
myscore
your name u1155203181 your address 3c42822cbbcfb07cac13ad9afa7555cb10fc58f7 your points 500 (250 per flag) rank 3 of 21 top 5: 1. u1155203181 500 2. u1155203191 250
http://<lab-machine>:8080/ — every name, its points, and a
grid of which of the five cards each student has solved. It cannot be
written to; only the ledger can credit a flag. The page asks for a login:
user ierg4851, and the instructor gives the class the
password.ERR unknown flag). Each flag pays once, to the
address that redeems it: use your own.2. What you are attacking
The target is a software simulator, and its README says so. There is no reader and no chip: it models an ISO 14443A card — its sectors, trailers and access bits — over a socket. The mistakes are the ones real deployments make; the silicon is not real.
Every card here fails the way a great many real deployments have: the secret is computable from public data.
3. What you are given
There is no reader and no card in your hand. On the lab machine every
challenge is a simulated card: a small process listening on
a port of 127.0.0.1. Talking to that port is talking to the
card. You have five challenges, five ports.
Your ports — one command
Log in to the lab machine and run:
myports
It prints the five cards that belong to you, with their port numbers:
your cards, u1155203181: default 7105 weakuid 7106 valueblk 7107 chain 7108 canteen 7109
ctf-demo; everyone gets their own
five ports.You do not even need the numbers
nfctool.py takes the card by name and looks
your port up itself, so from here on you only ever type this:
cd ~/ierg4851/contactless-ctf python3 nfc-ctf/nfctool.py --card default scan
The five names are default, weakuid,
valueblk, chain and canteen — one per
challenge, in the order above. --port 7105 still works if you
prefer numbers; that form is mainly for the instructor.
The reader: nfctool.py
One program does everything a reader can do. It lives at
~/ierg4851/contactless-ctf/nfc-ctf/nfctool.py; run it from
~/ierg4851/contactless-ctf. The key you type stays on your side —
the tool computes the authentication proofs, and the key is never sent to
the card.
| command | what it does | example |
|---|---|---|
scan | REQA and anticollision: prints the ATQA and the card's UID. Needs no key. | nfctool.py --card default scan |
select | Anticollision and SELECT: prints the SAK (08 = MIFARE Classic 1K). | nfctool.py --card default select |
auth <sector> <key> | The three-pass authentication for one sector. Prints OK A / OK B or FAIL. | nfctool.py --card default auth 1 ffffffffffff |
dump <sector> [key] | Authenticates (if you give a key), then reads the four blocks: hex, ASCII, and a text: line for anything readable. | nfctool.py --card default dump 1 ffffffffffff |
read <sector> | Reads the four blocks of a sector you have already authenticated. | nfctool.py --card default read 1 |
write <block> <32 hex> | Writes one 16-byte block. Needs that block's sector authenticated first. | nfctool.py --card default write 8 e8030000… |
auth followed by read in two separate
invocations works. A dump with a key authenticates by itself.https://yajin.org/teaching/IERG4851/2026/materials/nfc-demo.html
— the same page shown in class, no login needed. It is the same protocol
with the card drawn on the screen: useful for remembering what a command is
doing, not a substitute for the real tool.4. The first three cards
Three cards, three mistakes. Each card holds a flag in one sector; read it out, then redeem it (section 1). The reader tells you when authentication fails, and the card keeps an authentication for the rest of the connection.
- Format:
IERG4851_NFC_<CARD>_<8 hex>, for exampleIERG4851_NFC_DEFKEY_933e4aa8. - Yours alone. Every flag is generated when the lab starts,
with a random suffix: no two people have the same one — not the students,
not the TA, not
ctf-demo. You cannot guess your neighbour's, and nobody can guess yours. - Stored as plain ASCII in a data block of the card, which
is why
dumpprints it on atext:line. It is not in the trailer block, and it is not in block 0. - Where: each card below opens with the sector and block that holds its flag, so you know where to look once the sector opens.
- You get it by reading the card. There is no list of flags anywhere you can consult — only the ledger knows which strings are valid, and it does not tell you them.
scanthe card. It always answers with its UID, before any key.- Try the factory key on sector 1:
dump 1 ffffffffffff. - If that works, you are done — read the blocks and find the flag.
- If it fails, this card changed its key. The clue to the new one is either something the card told you (its UID, a note in another sector) or the way the deployment manages keys. That is the actual puzzle.
- When a sector opens, the flag is plain text in one of its four blocks.
- Redeem it (section 1). Nothing else to hand in.
Card 1 — the factory key
Sector 1 still wears the key it left the factory with. If you have ever seen a MIFARE dump, you know which six bytes those are.
Where the flag is: sector 1, block 5 (it spills into block 6). It is the text: line at the bottom of the dump.
python3 nfc-ctf/nfctool.py --card default scan python3 nfc-ctf/nfctool.py --card default dump 1 <key>
Card 2 — a key made from the serial number
This vendor did change the key: every card has its own. But it derived that key from the UID — the four bytes the card announces to anyone in range before any key is involved. scan prints them. The scheme is the first six bytes of a hash of the UID. Six bytes is twelve hex characters — exactly one key.
scan and hash its four bytes:
python3 -c "import hashlib; u=bytes.fromhex('<the four bytes from scan>'); print(hashlib.md5(u).hexdigest()[:12])"
If the card answers OK, that string is the key — and it is the key for every sector of that card.Where the flag is: sector 1, block 5 — the same place as card 1, once you have the key.
Card 3 — the wrong access bit
Sector 2 holds a stored-value block. Its access bits say key A may write, which is exactly what they should not say. Those bits sit in the sector's fourth block, the trailer: a read returns the two keys as zeros but does return the four access bytes, so the last block is not hidden. The sector stays locked until the value block says the balance is at least 1000.
python3 nfc-ctf/nfctool.py --card valueblk dump 2 ffffffffffff # authenticates, then the read fails python3 nfc-ctf/nfctool.py --card valueblk write 8 <16 bytes, hex> python3 nfc-ctf/nfctool.py --card valueblk dump 2 ffffffffffff # now it opens
A MIFARE value block is value, its one's complement, the value again, then the block address and its complement twice. The arithmetic is checkable on paper.
Where the flag is: sector 2, block 9. It appears in the dump only after the sector unlocks — the read fails until the balance is at least 1000.
5. Two more cards
The last two are not a single trick; they are the shape of a real deployment.
Card 4 — the chained badge
Nobody leaves the key to everything on one page. This badge opens with the factory key, and what is inside tells you where the next key is. Follow it to the end.
python3 nfc-ctf/nfctool.py --card chain dump 1 ffffffffffff # the text in the blocks tells you the key for the NEXT sector python3 nfc-ctf/nfctool.py --card chain dump 2 <that key> # and that sector tells you the next one, until the flag
text: line is a
12-hex key; use that on sector 2, read the next text: line, and
repeat. Two hops, then the flag. If the tool answers auth failed,
the key is wrong — copy all twelve hex characters again, no spaces.Where the flag is: sector 3, block 12 — the last hop of the chain. Each earlier sector's text tells you the key for the next one.
Card 5 — the canteen balance
Ten credits on a stored-value card, and the till costs a thousand. The balance is a value block: the value, its bitwise complement, the value again, then the block's address and its complement, twice (16 bytes in all). The card trusts the block only if every copy agrees — write only the value, or the wrong address bytes, and it stays invalid and the sector stays locked.
python3 nfc-ctf/nfctool.py --card canteen dump 1 ffffffffffff # see the value block # then write 16 bytes: value, ~value, value, address python3 nfc-ctf/nfctool.py --card canteen write 4 <32 hex chars>
Where the flag is: sector 2, block 8. The sector stays locked until block 4 is a valid value block of at least 1000, so the flag appears only after a correct write.
04 fb 04 fb in
its last four bytes; block 8 carries 08 f7 08 f7.
A write copied from card 3 fails on those address bytes, not on the balance.
(1000 is e8 03 00 00 in little-endian; its complement is
17 fc ff ff.) dump 1 already authenticated sector 1,
so the write to block 4 can follow it.6. The explain round
Three or four students, picked by the board, spend three minutes each. You do not need slides. You need:
- the command you ran and the output that mattered,
- the value you recovered and how you checked it (the address must match),
- one sentence: what made the secret computable?
"I read it out of the source" is a true answer that scores nothing. The question is what someone outside the device could have computed.
7. Rules
- Attack only the cards assigned to you.
- Everything here is simulated and local. Nothing in this lab touches a real card or a network.
- Redeem to your own address; a flag pays once, to whoever redeems it first.
- Ask when stuck. The first question is usually "which port is mine".