Reverse Engineering the Kinsa Smart Thermometer

"It wasn't very difficult, but it was still fun."

Kinsa Bluetooth Thermometer

I received a Kinsa bluetooth thermometer as a gift from the company I work for. The exact reasons why are a little lost on me. I get that they want us to be safe and not come into work sick, but I'm still not sure why beaming my body temperature up to someone else's cloud is helpful, especially, since I should be able to have tight control over my personal data. I was mildly irritated to learn that their app doesn't put my data in HealthKit, where I keep a centralized repository of all my sensitive health information, like workouts, heart rate, body weight, etc. If my data goes in there, I have direct control over who can see it, write entries to it, etc. I can revoke access to my data quite easily, giving me total control over my personal information.

Since the thermometer is a Bluetooth LE device, I decided to take a shot at reverse engineering the data it transmits in an effort to take control of my personal information. The long and short of it is that it wasn't terribly difficult and I posted an open source repo to be able to snatch the temperature readings as they are transmitted to my phone.

Bluetooth devices have a concept of services: a grouping of one or more attributes. A service defines one more characteristics, or information that can be read from the device. In this case, the Kinsa thermometer exposes two services: one for updating the thermometer's firmware, and second temperature data. I was able to determine the first service by it's UUID, FE59 which is a common service for putting the device into Device Firmware Update (DFU) mode. The GATT already defines a specification for Bluetooth health thermometers. Kinsa decided not to implement this, for reasons that are not clear. Because of this, I had to do my own sleuthing to figure out how to use read data off of the device.

The first part of my reverse engineering involved setting up my own Swift test application to discover, connect, and then subscribe to events from the device. I'm not going to post all that code here, but once setup, I could see data that my phone was able to read off of the device, deducing what each of these messages meant as I was using the device. I received output like the following:

[KinsaTherm] Read value: 9100
[KinsaTherm] Read value: 304b696e73610000000000000000000000
[KinsaTherm] Read value: 061406070e0e2e
[KinsaTherm] Read value: 0200011b160713
[KinsaTherm] Read value: 4000
[KinsaTherm] Read value: 0830303032333034323031353136313736
[KinsaTherm] Read value: 0500
[KinsaTherm] Read value: 0a011a

After putting the thermometer in my mouth, I would then see this output:

[KinsaTherm] Read value: 4200016b
[KinsaTherm] Read value: 4201016d
[KinsaTherm] Read value: 4202016f
[KinsaTherm] Read value: 4203016f
[KinsaTherm] Read value: 4600016f00000b

When the thermometer would turn itself off, I would get this output:

[KinsaTherm] Read value: 0d04

After observing a few runs of using the thermometer, a few things became clear with the messages starting with 42: the second byte was consistently incrementing by one. When viewing the device, it was obvious that these were counting the number of readings it was taking before settling on a final reading with the message starting with 46.

42 == intermediate reading
46 == final reading

Got it.

I decided to take a couple of measurements and write down the displayed temperature and byte reading:

98.2    4600017000000b
98.8    4600017300000c
97.0    4600016900001a

The third and fourth bytes are changing, but so is the seventh. I converted the third and fourth bytes, 0170 to decimal: 368. Definitely not 98.2. B is 11. Not even close. I took even more measurements and added them to the few I had taken, but also tried to put them in different places in my mouth in order to get varying readings:

98.2    4600017000000b
98.8    4600017300000c
97.0    4600016900001a
98.8    4600017300000f
99.1    46000175000020
98.8    46000173000009
98.4    46000171000023
98.2    46000170000038
98.4    46000171000051
97.3    4600016b00006b
97.9    4600016e000082
96.8    4600016800009f

The bytes were changing with each measurement, but I noticed that the third and fourth bytes were remaining the same for the same temperature reading:

98.2    4600017000000b
98.2    46000170000038

That has to be the temperature. Then it dawned on me: you idiot, these are likely to be in metric, not imperial! I converted my reading over to metric and the answer instantly revealed itself:

36.8    4600017000000b
36.8    46000170000038

Hex 0170 is 368 in decimal. They don't waste memory with sending the decimal (which makes perfect sense), so 0170 is 36.8. Solved! The technical design of sending the intermediate and final reading was nice since the temperature was always sent in the third and fourth bytes. I also knew that I could differentiate those messages based on the header sent. Below are their byte maps:

Intermediate temperature reading: 42000170

  1. 42 - Intermediate temperature reading header.
  2. 00 - Sequence byte. Increments with each subsequent reading.
  3. 01 - First byte of the temperature.
  4. 70 - Second byte of the temperature.

Final temperature reading: 46000170000038

  1. 46 - Final temperature reading header.
  2. 00 - Always 0. Reserved for some future use.
  3. 01 - First byte of the temperature.
  4. 70 - Second byte of the temperature.
  5. 00 - Always 0. Reserved for some future use.
  6. 00 - Always 0. Reserved for some future use.
  7. 38 - Unknown. No discernible pattern. Some sort of confidence or error byte?

This thermometer has many other functions and other data that it can send, but this accomplished my primary design. Supposedly this thing stores readings, but this will require additional reverse engineering work to tease out.

UPDATE 07 JUN 2020 16:42

I just reverse engineered another message, this one corresponds to the date and time of the device. After connecting, it will send a series of bytes with an 06 header:

[KinsaTherm] Read value: 061406070e0e2e

I noticed that this series of bytes was incrementing, so I started noting the time at which I received the reading:

061406070e0e2e  -
061406070f1d09  -
06140607101725  -
0614060710180d  4:29
06140607101c32  4:31
06140607101f0f  4:33
06140607102106  4:35

Each byte then decodes to the following:

06 20 06 07 14 14 46  -
06 20 06 07 15 29 09  -
06 20 06 07 16 23 37  -
06 20 06 07 16 24 13  4:29
06 20 06 07 16 28 50  4:31
06 20 06 07 16 31 15  4:33
06 20 06 07 16 33 06  4:35

Example mapping: 06140607102106

  1. 06 - Date/Time header
  2. 14 - Year #Y2K
  3. 06 - Month
  4. 07 - Day
  5. 10 - Hour (expressed in 24 hour time)
  6. 21 - Minute
  7. 06 - Second

It would appear that the time is behind roughly 2 minutes of the clock of my phone. I did notice that if the device is off for some extended period of time that its system clock reports being quite behind. A subsequent power-cycle of the thermometer and its reported time revealed that it had somehow updated its time from the central from the previous connection. A cursory search helped me learn that there are some built in methods for clock synchronization in the BLE specification, but the time it reported again put it about 2 minutes behind my phone's clock. More investigation will have to be done to get a better understanding of the specification and what variables might be in play that would explain such a wide drift, even after synchronization. UPDATE: It isn't drift. This likely isn't the system clock, but rather just a timestamp of when it was last used. I just happen to be powering it up every couple of minutes when getting the message and doing some deciphering of it: pure coincidence.

UPDATE 07 JUN 2020 19:13

Found another one! The 30 header is a text message:

Example bytes: 304b696e73610000000000000000000000

  1. 30 - Text Header
  2. 4b - Ascii K
  3. 69 - Ascii i
  4. 6e - Ascii n
  5. 73 - Ascii s
  6. 61 - Ascii a

UPDATE 07 JUN 2020 19:42

Found that it is sending its MAC address:

0830303032333034323031353136313736

I found the repeating structure of it to be quite curious: repeating between values between 0x30 and 0x37. I tried multiple different groupings to see if I could discern a pattern of any kind:

08 30 30 30 32 33 30 34 32 30 31 35 31 36 31 37 36
08 3030 3032 3330 3432 3031 3531 3631 3736
08 303030 323330 343230 313531 363137 36

But nothing really stood out at me. I also found it curious that the hex values all where ASCII numerics, resulting in a value of 0002304201516176 I took that and plugged it into a BT MAC address lookup and got:

00:02:30 Intersoft Electronics

I then looked at the sticker and--lo and behold--it matched!

The thing that was puzzling to me is that why send the MAC? That seems like something that should be easily discerned from the central. But then a question immediately popped into my head: is that even in the CoreBluetooth iOS API? A quick search revealed that Apple intentionally obfuscate the MAC address behind a UUID that is unique per iPhone. It would appear that the Kinsa needs/wants the MAC address for some sort of validation or tracking they wish to have with their BLE devices.

The last remaining message that I am curious about, like the MAC address, is always sent the same: 0200011b160713.

More investigation will be needed.

Posted on Jun 7
Written by Wayne Hartman