Research, development and trades concerning the powerful Proxmark3 device.
Remember; sharing is caring. Bring something back to the community.
"Learn the tools of the trade the hard way." +Fravia
You are not logged in.
Time changes and with it the technology
Proxmark3 @ discord
Users of this forum, please be aware that information stored on this site is not private.
In the city where I live, the local public transport company sells NFC carnets of 5 or 15 rides in the form of 14443-A NFC Forum Type 2 tags.
In the past they were Mifare Ultralight (MF0ICU1) but recently they use Infineon my-move lean (SLE66R01L).
I have collected something like 850 dumps of sequential obliterations and I have gathered most of the carnet structure/semantic [1].
The missing piece is how the last 2 bytes are computed [2]. Despite the relatively abundant data, I still have not found a correlation/mapping between the ticket data and these 16 bits –that I have improperly called "checksum".
Would you like to help me in my research?
P.S.: the corpus contains a Ruby script used to convert a structured YAML file into a sequence of ASCII hex dump files named conveniently[3].
[1]: carnet structure
0KXXYYZZ 0x88 ⊕ 0x0K ⊕ 0xXX ⊕ 0xYY = 0xZZ
GGHHIIJJ UID: 0x0KXXYYGGHHIIJJ (represented in decimal in the receipt)
LL 0xLL = 0xGG ⊕ 0xHH ⊕ 0xII ⊕ 0xJJ
KK internal: 0xKK related to chip vendor identifier
F2 Lock byte #1:
0xF: 1 1 1 1
meaning: Page7locked, Page6locked, Page5locked, Page4locked
0x2: 0 0 1 0
meaning: OTPlocked, Pages10to15locked, Pages4to9locked, OTPlocked
03 Lock byte #2:
0x0: 0 0 0 0
meaning: Page15locked, Page14locked, Page13locked, Page12locked
0x3: 0 0 1 1
meaning: Page11locked, Page10locked, Page9locked, Page8locked
MMNNOOPP OTP: remaining rides (zero bits population in OOPP)
01LL0000 Layout -> Carnet rides
QQ01RRRR Mask, Tariff (commercial info)
TTTTTT00 Purchase date in minutes since epoch (2005-01-01)
00######
##00%%%% Purchase serial number (found in the receipt, represented as ########-%%%% where %%%% in decimal)
CCCCCCCC Locked constant (maybe random)
TTTTTT00 First obliteration timestamp in minutes since epoch
04F80000 Zone
TTTTTT00 Last obliteration timestamp within 90' of validity period
PPPPPPPP Bus line / Metro stop
F8AEHHHH Zone, Obliterator Id part 1
HH12???? Obliterator Id part 2, constant, "checksum"
Lock bytes are F2:03 then only the last 6 pages are effectively changeable after purchase.
[2] Are they a function of UID, Locked constant, Last timestamp and/or who knows what?
I have also found some collisions: 2869, 4F11, D7EE, E4BB.
[3] dump file name structure:
+-- rides subtracted from total
|
v
c15_03_minus_12M_+44.txt
^ ^ ^ ^
| | | +-- next obliteration within 90' of validity period (44' in this case)
| | + -- M stands for a Metro entrance (it's a Bus line when not present)
| +-- sequential hex id for carnet
+-- total rides
Last edited by altamic (2018-01-14 02:27:44)
Offline
Someone has done his homework well. I'll take a look at it later.
Offline
Someone has done his homework well. I'll take a look at it later.
Wow!! iceman!! I am honoured that you stop and look at my puzzle
Offline
I noticed that the YAML source has annoying quotes for some values.
You can remove them with:
$ sed -i "s/'//g" carnets.yaml # Linux
or
$ sed -i '' -e "s/'//g" carnets.yaml # MacOS/BSD
Last edited by altamic (2018-01-13 16:31:48)
Offline
The YAML file contains the carnet structure that I have represented in the first post.
In particular, I have used the following names for various fields:
uid
cb0
cb1
internal
lock
otp
layout
mask
tariff
purchase_date
serial_number
unknown
first_validation
constant_1
zone_1
last_validation
line
zone_2
constant_2
obliterator
constant_3
checksum
Such structure can be manipulated easily with unix commands.
For example, to have a list of distinct sorted UIDs you just fire up:
$ grep uid carnets.yaml | cut -d ' ' -f 4 | sort -u
0416E1CA004980
041CBB32882881
04208E5A753381
042C5002323680
043DA2CA004981
0462015A753384
0468252A3C3C84
...
or to find collisions in "checksum", you can run the longer:
$ grep checksum carnets.yaml | cut -d ' ' -f 4 | sort | uniq -c | grep ' 2 ' | cut -d ' ' -f 5
2869
4F11
BACC
D7EE
E4BB
Suppose you would like to find a conventional date for a given date/timestamp found in a ticket, say 63A3B0.
You can find it as follows:
$ date -d @$((16#63A3B0 * 60 + 1104534000)) # Linux
Thu Jun 1 17:48:00 CEST 2017
or
$ date -r $((16#63A3B0 * 60 + 1104534000)) # MacOs/BSD
Thu Jun 1 17:48:00 CEST 2017
Last edited by altamic (2018-01-14 12:57:28)
Offline
The problem with my puzzle is that we do not know the algorithm nor the inputs of what produces what I called "checksum" as output.
If the algorithm depends on some non trivial secret salt shared by all obliterators the problem would be infeasible.
We can only hope that the algorithm is a function of some fields of the carnet stucture, exclusively.
For a given a carnet c_0 obliterated at the minute t resulting in the carnet c_1 we have that the possible inputs are:
fields(c_0) ∪ fields(c_1) / {checksum_c1} =
((fields(c_0) ∩ fields(c_1)) ∪ (fields(c_0) / (fields(c_0) ∩ fields(c_1)) ∪ (fields(c_1) / (fields(c_0) ∩ fields(c_1)) / {checksum_c1}
= { uid, cb0, cb1, internal, lock, layout, mask, tariff, purchase_date, serial_number, unknown, constant_3 } ∪ { otp_c0, first_validation_c0, constant_1_c0, zone_1_c0, line_c0, last_validation_c0, zone_2_c0, constant_2_c0, obliterator_c0, checksum_c0 } ∪ { otp_c1, first_validation_c1, constant_1_c1, zone_1_c1, line_c1, last_validation_c1, zone_2_c1, constant_2_c1, obliterator_c1 }
totaling in 31 possible inputs.
In order to maximize identical inputs we should minimize differences between carnets c0 and c1. Hence we will consider only those that have been obliterated in the same line and obliterator within 90' (a ticket ride maximum duration) so that for c0 and c1 we basically restrict inputs to the set:
{ last_validation_c0, last_validation_c1, checksum_c0 }
Such tickets are:
c15_01_minus_13.txt
c15_01_minus_13_+5.txt
c15_01_minus_13_+51.txt
048A828462753380A448F2031FFFFFFC01050000020102BD59C2200000AE10A6B80044F3705BE1355A02EA0004F80000 5A02EA 00003C0004F8AE10795C12 9EB3
048A828462753380A448F2031FFFFFFC01050000020102BD59C2200000AE10A6B80044F3705BE1355A02EA0004F80000 5A02EF 00003C0004F8AE10795C12 25F9
048A828462753380A448F2031FFFFFFC01050000020102BD59C2200000AE10A6B80044F3705BE1355A02EA0004F80000 5A031D 00003C0004F8AE10795C12 CEDC
c15_17_minus_01.txt
c15_17_minus_01_+8.txt
0570F60B515754E9BB15F2030001C00001050000020102BD5EF1A00000AE10FE2000F0C981CE10825F06B80004F80000 5F06B8 00003C0004F8AE1078C912 CB5F
0570F60B515754E9BB15F2030001C00001050000020102BD5EF1A00000AE10FE2000F0C981CE10825F06B80004F80000 5F06C0 00003C0004F8AE1078C912 624E
With CRC RevEng I explore a possible CRC solution.
For f(last_validation_c0, checksum_c0, last_validation_c1) = checksum_c1
$ reveng -s -w 16 5A02EA9EB35A02EF25F9 5F06B8CB5F5F06C0624E
reveng: no models found
trying with f(last_validation_c0, checksum_c0) = checksum_c1
$ reveng -s -w 16 5A02EA9EB325F9 5F06B8CB5F624E 5A02EF25F9CEDC
reveng: no models found
we try also with f(last_validation_c0, last_validation_c1) = checksum_c1
$ reveng -s -w 16 5A02EA5A02EF25F9 5A02EF5A031DCEDC 5F06B85F06C0624E
reveng: no models found
and finally with f(last_validation_c0) = checksum_c1
$ reveng -s -w 16 5A02EA25F9 5A02EFCEDC 5F06B8624E
reveng: no models found
I would conclude that the "checksum" is not a CRC. Any idea/observation?
Last edited by altamic (2018-01-21 12:38:25)
Offline
It seems that this thread is somewhat stale and it does not cause any interest.
I will continue the research on my own with some Machine Learning techniques. First with some features engineering and then modeling the problem, while collecting more data.
Thank you anyway.
Offline
Altamic,could you give me your email? I would like to share some information
Offline