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.