Reversing a coffee machine key
Posted on Fri 07 May 2021 in blog
At $DAYJOB, a long time ago, we had big a coffee machine allowing us to store money in NFC keys. NFC keys were Mifare 1K ones, so they had a security hole (Search mfoc), so I tried reverse engineering them, you know, free coffee…
Before starting you can download key dumps to follow along with me.
I will not paste all dumps in this page, (1k dumps are big in hexadecimal on a blog post) but I dumped a few keys with a few different values, and I'll post the diffs between dumps.
I got two keys, and two dumps per key. First key from 9.2€ to 8.35€. Second key from 0€ to 0.10€
Diff of the first key (Between a dump of 9.2€ and a dump of 8.35€):
< 2 060 b491 7e19 0000 0000 0000 0000 1801 003f 100 R:AB W:-B I:-- DTR:-- r/w block
---
> 2 060 82bb 261a 0000 0000 0000 0000 1c01 0020 100 R:AB W:-B I:-- DTR:-- r/w block
26,27c26,27
< 0 080 9803 0000 67fc ffff 9803 0000 09f6 09f6 110 R:AB W:-B I:-B DTR:AB r/w block
< 1 090 c503 0000 3afc ffff c503 0000 09f6 09f6 110 R:AB W:-B I:-B DTR:AB r/w block
---
> 0 080 4303 0000 bcfc ffff 4303 0000 09f6 09f6 110 R:AB W:-B I:-B DTR:AB r/w block
> 1 090 7003 0000 8ffc ffff 7003 0000 09f6 09f6 110 R:AB W:-B I:-B DTR:AB r/w block
Diff of the second key (Between a dump of 0€ and a dump of 0.10€):
< 2 060 daa0 9019 0000 0000 0000 0000 2200 0098 100 R:AB W:-B I:-- DTR:-- r/w block
---
> 2 060 2eaf 261a 0000 0000 0000 0000 2300 00d2 100 R:AB W:-B I:-- DTR:-- r/w block
26,27c26,27
< 0 080 0000 0000 ffff ffff 0000 0000 09f6 09f6 110 R:AB W:-B I:-B DTR:AB r/w block
< 1 090 2d00 0000 d2ff ffff 2d00 0000 09f6 09f6 110 R:AB W:-B I:-B DTR:AB r/w block
---
> 0 080 0a00 0000 f5ff ffff 0a00 0000 09f6 09f6 110 R:AB W:-B I:-B DTR:AB r/w block
> 1 090 0000 0000 ffff ffff 0000 0000 09f6 09f6 110 R:AB W:-B I:-B DTR:AB r/w block
To start let's focus on the two-lines diff, at adresses 0x080
and 0x090
.
When I reverse engineer I like to loop between "presentation" (put an
effort to make the data readable) and "understanding" (get an
information from the data), so my first step, is to render this in a
clean way. I had an intuition for a one's complement (as I spotted
ffff
/ 0000
, what an intuition...), so I wanted to see binary
data. I also dropped columns of data that were identical between two
dumps:
9.2 : 9803 0000 67fc c503 0000 3afc | 10011000.00000011 ... 01100111.11111100 11000101.00000011 ... 00111010.11111100
8.3 : 4303 0000 bcfc 7003 0000 8ffc | 01000011.00000011 ... 10111100.11111100 01110000.00000011 ... 10001111.11111100
0.1 : 0a00 0000 f5ff 0000 0000 ffff | 00001010.00000000 ... 11110101.11111111 00000000.00000000 ... 11111111.11111111
0.0 : 0000 0000 ffff 2d00 0000 d2ff | 00000000.00000000 ... 11111111.11111111 00101101.00000000 ... 11010010.11111111
So, my first intuition was true: the data is stored twice, the 2nd one is the one's complement of the first. So half of the data is useless for me, I can drop it from my representation.
Follow a simplified presentation witout duplicate (complemented) data:
9.2 : 9803 c503 10011000.00000011 11000101.00000011 0398 -> 920 | 03c5 -> 965
8.3 : 4303 7003 01000011.00000011 01110000.00000011 0343 -> 835 | 0370 -> 880
0.1 : 0a00 0000 00001010.00000000 00000000.00000000 000A -> 10 | 0000 -> 0
0.0 : 0000 2d00 00000000.00000000 00101101.00000000 0000 -> 0 | 002d -> 45
At this point I see 0a00
on the 0.10€
key, as 0a(16)
is
10(10)
, 0a00
is 10
in big endian... money may be stored here…
in big endian in 1/100 of euros. Let's test with 9803(16be)
, gives
920(10)
that give 9.20€
, yes!! Free coffee not far away!
This is the big part of the dump, the remaining part (top one) seems to store metadata but is not reversed yet.
Follow two tables, for the two keys, showing old_value -> new_value, with, for each value, its binay representation and its base 10 representation as if value is stored in big endian.
In the following table, 16be mean "From base 16 big endian to decimal", 16le for little endian.
9.2 -> 8.35
Value: As binary 16be 16le Value: As binary 16be 16le
DAA0 : 11011010.10100000 41178 55968 -> 2EAF : 00101110.10101111 44846 11951 date ?
9019 : 10010000.00011001 6544 36889 -> 261A : 00100110.00011010 6694 9754
2200 : 00100010.00000000 34 8704 -> 2300 : 00100011.00000000 35 8960 count ?
0098 : 00000000.10011000 38912 152 -> 00D2 : 00000000.11010010 53760 210
0.0 -> 0.1
Value: As binary 16be 16le Value: As binary 16be 16le
B491 : 10110100.10010001 37300 46225 -> 82BB : 10000010.10111011 48002 d33467 date ?
7E19 : 01111110.00011001 6526 32281 -> 261A : 00100110.00011010 6694 9754
1801 : 00011000.00000001 280 6145 -> 1C01 : 00011100.00000001 284 7169 count ?
003F : 00000000.00111111 16128 63 -> 0020 : 00000000.00100000 8192 32
Non reversed data:
mandark@blanc$ grep 00000060 *.dmp.hex | column -t
step1-0.dmp.hex:00000060 da a0 90 19 00 00 00 00 00 00 00 00 22 00 00 98 |............"...|
step2-0.1.dmp.hex:00000060 2e af 26 1a 00 00 00 00 00 00 00 00 23 00 00 d2 |..&.........#...|
step3-0.2.dmp.hex:00000060 53 1a 51 1a 00 00 00 00 00 00 00 00 24 00 00 98 |S.Q.........$...|
mandark@blanc$ grep 00000060 *.dmp.hex | column -t
step1-9.2.dmp.hex:00000060 b4 91 7e 19 00 00 00 00 00 00 00 00 18 01 00 3f |..~............?|
step2-8.35.dmp.hex:00000060 82 bb 26 1a 00 00 00 00 00 00 00 00 1c 01 00 20 |..&............ |
step3-3.9.dmp.hex:00000060 c7 9a 59 1a 00 00 00 00 00 00 00 00 2a 01 00 91 |..Y.........*...|
Clearly 0022 0023 0024
, and 0118 011C 012A
are juste counters. I only
add 10 cents on the key1 between each dumps, but I drink some coffee
between each dumps on key2, so it's normal values. I now know I drank
18 coffees between first and last dump!
-----------------------------------------------------------------------------------
|money | counter | Last byte ? | First long, kind of timestamp |
|---------------------------------------------------------------------------------|
|euro | hex dec | hex dec bin | hex little endian dec |
|---------------------------------------------------------------------------------|
|0 | 22 00 34 | 98 152 10011000 | da a0 90 19 19 90 a0 da 428908762 |
|0.1 | 23 00 35 | D2 210 11010010 | 2e af 26 1a 1a 26 af 2e 438742830 |
|0.2 | 24 00 36 | 98 152 10011000 | 53 1a 51 1a 1a 51 1a 53 441522771 |
|---------------------------------------------------------------------------------|
|9.2 | 18 01 280 | 3F 63 00111111 | b4 91 7e 19 19 7e 91 b4 427725236 |
|8.35 | 1C 01 284 | 20 32 00100000 | 82 bb 26 1a 1a 26 bb 82 438745986 |
|3.9 | 2A 01 298 | 91 145 10010001 | c7 9a 59 1a 1a 59 9a c7 442079943 |
-----------------------------------------------------------------------------------
First long seems to be a kind of timestamp, but it's not a unix timestamp. It seems to count seconds, but I don't know the start point. Start point may be random ^-^
About last byte, I tried some crc's (namely crc-8, crc-8-darc, crc-8-i-code, crc-8-itu, crc-8-maxim, crc-8-rohc, crc-8-wcdma, crc-16, crc-16-buypass, crc-16-dds-110, crc-16-dect, crc-16-dnp, crc-16-en-13757, crc-16-genibus, crc-16-maxim, crc-16-mcrf4xx, crc-16-riello, crc-16-t10-dif, crc-16-teledisk, crc-16-usb, x-25, xmodem, modbus, kermit, crc-ccitt-false, crc-aug-ccitt, crc-24, crc-24-flexray-a, crc-24-flexray-b, crc-32, crc-32-bzip2, crc-32c, crc-32d, crc-32-mpeg, posix, crc-32q, jamcrc, xfer, crc-64, crc-64-we, crc-64-jones)
I tried with last byte set to any possible value and I '% 255'ed results, also tried without last byte, so I got a lot of false positive matches, for example, for step1-0.dmp.hex:00000060, I have 39 possibilities yielding to 98, but I found NO possibility working with the same params for two different dumps.
We may try to compute more value in CRC's, for example whole block, I just tried to CRC a single line (16 bytes), but I stopped my research here and get back to work.