- TypeScript 95.7%
- JavaScript 4.3%
| .forgejo/workflows | ||
| .github | ||
| .vscode | ||
| docs | ||
| src | ||
| test | ||
| .gitignore | ||
| .prettierrc | ||
| bun.lock | ||
| eslint.config.js | ||
| jsr.json | ||
| LEGAL.md | ||
| LICENSE | ||
| package.json | ||
| README.md | ||
| tsconfig.build.json | ||
| tsconfig.json | ||
Zaragoza Avanza Bus Transit Pass pwn
Important
⚠️ Aviso legal: Este repositorio es un proyecto de investigación de seguridad independiente.
No está destinado a cometer fraude ni a facilitar el uso indebido del transporte público.
No se proporcionará asistencia para ningún uso ilícito.
Consulta LEGAL.md para el descargo de responsabilidad completo.
Below are some of my notes on the Zaragoza bus transit pass, which is a MIFARE Classic 1K card. The card has 16 sectors, each with 4 blocks of 16 bytes each. Each sector has two keys (Key A and Key B) that control access to the blocks within that sector.
Presented to you by zaragoza ⚡️ nerds 🤓
Click here to open the reverse engineering spreadsheet with all the collected data and highlights:
See also:
Keys
| Sectors | Key A | Key B |
|---|---|---|
| 0-8 | 04000C0F0903 |
0B02070A0409 |
| 9-15 | A0A1A2A3A4A5 |
B0B1B2B3B4B5 |
Sectors
| Sector | Description |
|---|---|
| 0 | Manufacturer block, IDs |
| 1 | Appears to be latest transaction |
| 2 | Balance (blocks 8 and 9) |
| 3 | Empty on top up cards |
| 4 | Empty on top up cards |
| 5 | Empty |
| 6 | Empty |
| 7 | Appears to be transaction logs |
| 8 | Appears to be transaction logs |
| 9 | Unused |
| 10 | Unused |
| 11 | Unused |
| 12 | Unused |
| 13 | Unused |
| 14 | Unused |
| 15 | Unused |
Blocks
| Sector | Block | Description | Template | Access Conditions |
|---|---|---|---|---|
| 0 | 0 | [00-03] RFID's UID [04] BCC (checksum byte) [05] SAK (always 88 for MIFARE Classic 1K)[15] last two digits of year the card was manufactured in (i.e. 20 for 2020, 25 for 2025) |
..,,..,,..880400C8,,0020000000.. |
Read-only |
| 1 | [00-14] Card type (02699F for balance cards, 0A9775 for personal expiring cards)[15] XOR of all previous bytes |
..,,..000000000000000000000000,, |
Only Key B can write | |
| 2 | [00-01] ASCII prefix (4245 for BE, 4250 for BP)[02-04] Zaragoza card ID number (together forming prefix + number) [05-14] empty, zeroed [15] XOR of all previous bytes |
42,,..,,..00000000000000000000.. |
Read-only | |
| 3 | 0th sector's trailer block | 04000C0F0903..,,..,,0B02070A0409 |
Trailer | |
| 1 | 4 | Empty on top-up cards [00-14] unknown [15] XOR of all previous bytes |
00000000000000000000000000000000 |
Only Key B can write |
| 5 | Empty if new card [00-02] 020002, 0A0100 or 0A0200[03-04] unknown card-specific constant [05-06] unknown [07] integer [08] either 01 or 02 09 unknown [10-11] date 12 BCD-encoded hour (0-23) 13 BCD-encoded minute 14 BCD-encoded second [15] sequence counter |
020002..,,..,,..,,..,,..,,..,,.. |
No restrictions | |
| 6 | Appears to always be empty | 00000000000000000000000000000000 |
No restrictions | |
| 7 | 1st sector's trailer block | 04000C0F0903..,,..,,0B02070A0409 |
Trailer | |
| 2 | 8 | Balance | ..,,0000..,,FFFF..,,000002FD02FD |
Value block1 |
| 9 | Always has the same value as block 8 | ..,,0000..,,FFFF..,,000002FD02FD |
Value block1 | |
| 10 | Empty if new card, correlates to block 5 (see Blocks 5 and 10) [00-01] unknown [02-05] same as [10-13] in block 5 [06-08] always 010200[09-10] same as [07-08] in block 5 [11-14] always 00006300[15] XOR of all previous bytes Always 000000000000000A000000000000000A on unlimited personal cards |
..,,..,,..,,010200..,,00006300.. |
No restrictions | |
| 11 | 2nd sector's trailer block | 04000C0F0903..,,..,,0B02070A0409 |
Trailer | |
| 3 | 12 | Subscription metadata | 00000000000000000000000000000000 |
Only Key B can write |
| 13 | Subscription on personal unlimited cards | 00000000000000000000000000000000 |
Only Key B can write | |
| 14 | See block 13 | 00000000000000000000000000000000 |
Only Key B can write | |
| 15 | 3rd sector's trailer block | 04000C0F0903..,,..,,0B02070A0409 |
Trailer | |
| 4 | 16 | See block 12 | 00000000000000000000000000000000 |
Only Key B can write |
| 17 | See block 13 | 00000000000000000000000000000000 |
Only Key B can write | |
| 18 | See block 13 | 00000000000000000000000000000000 |
Only Key B can write | |
| 19 | 4th sector's trailer block | 04000C0F0903..,,..,,0B02070A0409 |
Trailer | |
| 5 | 20 | Empty | 00000000000000000000000000000000 |
Only Key B can write |
| 21 | Empty | 00000000000000000000000000000000 |
Only Key B can write | |
| 22 | Empty | 00000000000000000000000000000000 |
Only Key B can write | |
| 23 | 5th sector's trailer blocks | 04000C0F0903..,,..,,0B02070A0409 |
Trailer | |
| 6 | 24 | Empty | 00000000000000000000000000000000 |
Only Key B can write |
| 25 | Empty | 00000000000000000000000000000000 |
Only Key B can write | |
| 26 | Empty | 00000000000000000000000000000000 |
Only Key B can write | |
| 27 | 6th sector's trailer block | 04000C0F0903..,,..,,0B02070A0409 |
Trailer | |
| 7 | 28 | Transaction logs; value of block 5 right before overwriting it; each subsequent write adds an entry to blocks 29, then 30, then 32, then 33 | 020002..,,..,,..,,..,,..,,..,,.. |
No restrictions |
| 29 | See block 28 | 020002..,,..,,..,,..,,..,,..,,.. |
No restrictions | |
| 30 | See block 28 | 020002..,,..,,..,,..,,..,,..,,.. |
No restrictions | |
| 31 | 7th sector's trailer block | 04000C0F0903..,,..,,0B02070A0409 |
Trailer | |
| 8 | 32 | See block 28 | 020002..,,..,,..,,..,,..,,..,,.. |
No restrictions |
| 33 | See block 28 | 020002..,,..,,..,,..,,..,,..,,.. |
No restrictions | |
| 34 | Expiration date, encoding unknown; always 00000000FFFFFFFF0000000000FF00FF on top up cards |
00000000FFFFFFFF0000000000FF00FF |
Value block1 | |
| 35 | 8th sector's trailer block | 04000C0F0903..,,..,,0B02070A0409 |
Trailer | |
Value blocks 8, 9 and 34 store a 32-bit integer three times for redundancy: the value, its bitwise complement, then the value again.
The balance is stored in blocks 8 and 9 for redundancy.
Balance
€1.00 = 1000 units. For example, a balance of €5.00 would be stored as 8813000077ecffff8813000002fd02fd in blocks 8 and 9:
- Convert
5.00to units:5000 - Convert
5000to hexadecimal:1388 - Convert to little-endian:
8813 - Calculate the complement:
77ec(flip every bit) - Write the value, complement, and value again, then append the static address
02fd02fdand write to blocks 8 and 9
- Bytes 00-03:
88130000-> little-endian ->5000 - Bytes 04-07:
77ecffff-> complement of5000 - Bytes 08-11:
88130000-> value repeated - Bytes 12-13:
02fd-> address bytes (block pointer for transfer operations) - Bytes 14-15:
02fd-> address bytes repeated
Balance on personal unlimited cards is always 00000000FFFFFFFF0000000002FD02FD (zero).
See src/balance.ts for encoder/decoder implementation in JavaScript/TypeScript.
Transaction logs
Transaction logs are stored in sectors 1 (block 5), 7 (blocks 28-30) and 8 (blocks 32-33). Each log entry is 16 bytes, so each block can store one log entry.
- [00-02]: Always
020002for top up cards and0A0100or0A0200for personal cards - [03-04]: Card-specific constant, same across BE322742 and BE322743 dumps, could be related to fare id or season
- [05-06]: Unknown, highest recorded value for first byte is 129, several appearances of (128, 1)
- [07]: Most likely bus line number, but unknown how to decode lines > 255 and trams
- [08]: Line direction (always either
01or02) - [10-11]: Transaction date
- [15]: Unknown, some sort of sequence counter but doesn't correlate to dates
See src/transaction.ts for decoder implementation in JavaScript/TypeScript.
Subscription
- [00-01] Subscription start date
- [02-03] Subscription end date
- [04-05] Unknown, appears to be
0000 - [06-09] Unknown
- [10-11] Unknown date, most likely the date of the last usage
- [12-14] Unknown
- [15] XOR of all previous bytes
Subscription metadata
- [00-01] Unknown
- [02-03] Some date
- [04-07] Unknown, appears to always be
00210000 - [08-09] Validity days (e.g.
016Dfor 365 days) - [10-14] Unknown, appears to always be
0021000000 - [15] XOR of all previous bytes
Dates
Mad respect to li0ard for figuring out the date encoding!
Dates are composed of two bytes (16 bits), which when converted to binary:
- the first 7 bits represent the year (based 0 at 2000)
- 4 bits for month (1-12)
- 5 bits for day
Examples:
34 4E -> 0011010001001110 -> 0011010 0010 01110
- Year:
0011010-> 26 + 2000 -> 2026 - Month:
0010-> 2 (February) - Day:
01110-> 14
See src/date.ts for encoder/decoder implementation in JavaScript/TypeScript.
Blocks 5 and 10
Block 10 stores complimentary data to block 5, but unknown how it correlates.
00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15
cType ?? ?? linDr ?? yr d? HH MM SS sq
5: 02 00 02 F8 01 06 40 D2 02 25 2A DC 09 1B 2F 01
10: 00 00 2A DC 09 1B 01 02 00 D2 02 00 00 63 00 54
?? ?? yr d? HH MM linDr
cType ?? ?? linDr ?? yr d? HH MM SS sq
5: 02 00 02 F8 01 03 20 D2 01 FE 2A D8 13 2D 12 01
10: 00 00 2A D8 13 2D 01 02 00 D2 01 00 00 63 00 7F
?? ?? yr d? HH MM linDr
cType ?? ?? linDr ?? yr d? HH MM SS sq
5: 02 00 02 F8 01 05 DC D2 02 6B 2A D9 0D 33 3B 02
10: D2 01 2A D9 0D 33 01 02 00 D2 02 00 00 63 00 AE
?? ?? yr d? HH MM linDr
cType ?? ?? linDr ?? yr d? HH MM SS sq
5: 02 00 02 F8 01 02 BC D2 01 99 2A D9 10 26 0D 03
10: D2 02 2A D9 10 26 01 02 00 D2 01 00 00 63 00 A6
?? ?? yr d? HH MM linDr
cType ?? ?? linDr ?? yr d? HH MM SS sq
5: 02 00 02 26 01 80 CE 16 01 0D 34 54 10 2D 24 03
10: 23 02 34 54 10 2D 01 02 00 16 01 00 00 63 00 0B
?? ?? yr d? HH MM linDr
cType = some constant, could be related to season or card type
linDr = line number and direction (either `01` or `02`)
yr = related to year, see ^2 in footnotes
d? = probably date in some form
HH, MM, SS = hour, minute, second
sq = sequence counter
Acknowledgements
Huge thanks to li0ard for help with decoding RFIDs!
License
Donate
-
Value blocks have the following restrictions: Key A can read, decrement, restore and transfer, Key B can also write and increment. ↩︎