Review of the SureSense Reader [to read Bio-Thermo chips - e.g. xBT] + EXPLOIT!

I just purchased a SureSense Reader to compare it with my Halo Scanner. Both are affordable 134kHz FDX-B readers that support the temperature field in Destron Fearing Bio-Thermo chips - aka the xBT in DangerousThings parlance. Just to be clear, there are other readers that read Bio-Thermo chips - including Destron Fearing’s own Global Pocket Reader Plus GPR+ scanner but these devices are insanely expensive - as in rip-off sort of expensive, considering what they do.

This is going to be a quick review, because… well, it’s a very simple device. First of all, here’s a picture of the thing:

So, how does it work? Simple: plop 2 AA batteries in it, press the button, put it close to a chip, and voila: it displays the chip’s UID, and the temperature obviously if it’s a Bio-Thermo chip. It’ll turn itself off after a few seconds. You can’t long-press the button to do so.

When you next power it up, it’ll displays the last chip it read, in case you failed to write down the UID before it went off. If you want to take another reading, press the button again. That’s it: it’s so simple a dog could scan itself.

What’s less simple is changing the temperature unit (F or C). To do that, you have to pull the batteries out and reinstall them while pressing the button. How cheesy is that? Never heard of long press buttons SureSense? But well, it does the trick.

How does it compare to the Halo Scanner?

  • It’s smaller and doesn’t have a stupid shape. But the Halo Scanner is easier to handle because it has… a handle.
  • The button is too soft and too easily pressed, and a single short press turns on the device. It’s fine if you leave it on a table. But it you plan on carrying it in your pocket - something the Halo Scanner and its stupid shape doesn’t easily land itself to - forget it: it’ll keep turning itself on and deplete its batteries in less time than it takes to say “damn, I wish that button worked like an electronic cigarette’s”.
  • The reading range is good. A bit better than than the Halo Scanner’s in fact.
  • It doesn’t beep. That’s annoying, I wish it did. SureSense sells the lack of beep as something that won’t stress out a pet being scanned. But I think it’s just an excuse some marketdroid came up with to sell the fact that they were too cheap to include a buzzer in the device. Having said that, the range is good enough that it doesn’t matter: provided you’re vaguely close to the chip, you can be sure it’ll read okay.
  • It uses AA batteries, meaning it’ll still work many years from now, when the Halo Scanner’s battery will be long dead.
  • It’s affordable, but it’s still 3 times the price of a Halo Scanner. But it’s worth it for the hackability value (see below).

Now the bad bit: the device’s readings are off compared to the Halo Scanner, and I suspect the Halo Scanner is the one that’s correct. With the code below (read on), I pulled several measurements at different temperatures, and obviously the device applies some kind of correction the Halo Scanner doesn’t apply. For instance, here are a list of raw values from the reading log and the temperatures displayed on the screen:

105 -> 35.1C [Halo: 35.0C]
106 -> 35.2C [Halo: 35.1C]
107 -> 35.3C [Halo: 35.2C]
108 -> 35.4C [Halo: 35.3C]
109 -> 35.5C
110 -> 35.6C
111 -> 35.7C
112 -> 35.8C
113 -> 36.0C
114 -> 36.1C
115 -> 36.2C
116 -> 36.3C
117 -> 36.4C
118 -> 36.5C
119 -> 36.6C
120 -> 36.7C

It looks like the temperature is VAL / 10 + 24.6 in Celsius, but then the series break at 35.8C - meaning the device applies a correction, and you’ll never see 35.9C displayed on the SureSense’s screen. Weird… Also, that 24.6C constant corresponds to nothing documented and is weird also.

But hey, we’re talking tenths of degrees here. Nothing serious :slight_smile:

Now then, for the exploit:

The SureSense reader comes with a USB cable. The manual says it’s only used for updating the firmware, but I plugged it to my computer anyway. When I did, the device showed a computer icon on the screen and didn’t respond to the button anymore. But the computer saw it as a USB mass storage device, containing a READINGS.CSV file, which is a log of the readings the device has taken since the last reset.

So, at first sight, no way to use it as a USB scanner.

But but… There’s a bug in the firmware: if you press the button to trigger a read, then connect the USB cable after the doggy icon appears and wait a bit, the device returns to the regular standalone screen display with the display full of XXXXXs, it lets you scan stuff, AND the computer sees it as a USB device at the same time!

Now, sadly you can’t just tail the CSV log file and expect to see new readings get concatenated at the end of the file as they’re being taken. But if you unmount the drive and remount it - without unplugging the device - you get the updated log file.

So, with that exploit, it’s quite easy - if inelegant - to turn your SureSense Reader into a USB chip scanner for your computer. In fact, I just threw together a quick Python script to do exactly that. It runs on Linux, it needs to run as root, and it doesn’t check errors or anything. But it’s just a proof of concept. Here it is:

#!/usr/bin/python3

import os
import re
import json
import time
from subprocess import Popen, PIPE, DEVNULL

mntpoint="/tmp/SureSense"

# Determine the SureSense's block device file
bdevs=json.loads(Popen(["lsblk", "-l",  "--json", "--output", "NAME,LABEL"],
	stdout=PIPE, stderr=DEVNULL).communicate()[0].
	decode("utf-8"))["blockdevices"]
dev=(([bdev["name"] for bdev in bdevs if bdev["label"]=="SureSense"])+[None])[0]
if not dev:
  print("Error: SureSense block device not found")
  exit(1)
dev="/dev/"+dev

# Determine if it's already mounted. Unmount it if it is.
mnts=(Popen(["mount"], stdout=PIPE, stderr=DEVNULL).communicate()[0].
	decode("utf-8").split("\n"))
mnt=(([l.split()[2] for l in mnts if re.match(r"^"+dev+" on [^\s].+$", l)]) \
	+[None])[0]
if(mnt):
  Popen(["umount", dev], stdout=DEVNULL, stderr=DEVNULL).communicate()

# Make a temporary mountpoint if it doesn't exist already
if not os.path.isdir(mntpoint):
  os.mkdir(mntpoint)

# Detect changes in the SureSense's log file and display new readings
prev_csv=None
while True:

  # Mount the device
  Popen(["mount", dev, mntpoint], stdout=DEVNULL, stderr=DEVNULL).communicate()

  # Read the CSV file
  with open(mntpoint+"/READINGS.CSV", "r") as f:
    csv=([l.strip().split(",") for l in f.readlines() \
	if re.match("^([^,]+,){3}[^,]+$", l)])

  # If the CSV file has changed, display the new reading
  if prev_csv and len(csv) > len(prev_csv):
    bogouid=csv[-1][1]			# Encoding unknown.
    bogotemp=float(csv[-1][2])/10+24.6	# Doesn't match the display above 35.8C.
					# The device seems to apply some sort
					# of correction. But close enough.
    bogotstamp=int(csv[-1][3])		# In seconds. Starting date unknown.
    print("New reading: T={}C".format(bogotemp))

  prev_csv=csv

  # Unmount the device
  Popen(["umount", dev], stdout=DEVNULL, stderr=DEVNULL).communicate()

  # Wait a bit until the next poll
  time.sleep(1)

Note that if you unplug the device without unmounting it, the firmware will be left all weirded out and it will report a hardware error to the USB mass storage driver when you plug it back in. If that happens, simply reset the device by pulling/reinstalling a battery. In fact, sometimes it does that even if you unmount the device. Oh well, after all it wasn’t meant to work like that. Small wonder that it works at all really :slight_smile:

8 Likes

@Rosco fuck yeah! Love that you hacked the shit out of a cost effective device like that! Deffinetly gonna order one this morning. Are you gonna toss this up on GitHub at all
This could be a cool project, I like playing with displays. My pwnagotchi display is one of a kind :slight_smile:.

Nice clean python script. And it’s written in python3, instead of 2, thank you :wink:

Mechanics of it look clean and efficient.

Wow, I had not heard of pwnagotchi before thats kinda wild.

As for that CSV file does it have the UIDs as well as the tempature readings? @Rosco

Python2 is getting very long in the tooth. Anybody who uses it for anything other than maintaining existing code should be shot through the head. And Python3 is so much better for so many things - i18n being one.

I won’t. Believe it or not, computers bore me to shit. I’ve had my fun exploiting the thing for an hour, now I’ll move on to something more interesting that doesn’t involve computers.

It does, but I can’t work out the encoding. It’s probably mixed with other bits from the FDX datagram, and possibly with stuff from the reader itself. Anyway, the encoding is non-trivial - as in, it takes more than 5 minutes converting a few hex value to dec - so I got bored and I left it at that :slight_smile: I was only interested in the temp reading.

@Rosco Great share thanks very much.

@MouSkxy pwnagotchi is indeed
It’s cute as f— .

Who’s gonna open it and let us see what’s inside, if it can be hacked… like empty soldering points for extra connectors like USB, or to add a power button… :slight_smile:

I suppose I could have a look.

Actually I should also have a look at the Halo Scanner: I seem to remember reading about people interfacing it with Arduinos or RaspPis, but I can’t find the pages I read that from for the life of me. I assume the Halo Scanner must then be reasonably easy to hack if that’s the case.

1 Like

Could probably find those databases you were looking for on there, too.

Link to where you purchased from? Wasn’t coming across one right off…

I got it from Kivuton, a Finnish web store for pet supplies:

It won’t help you much if you don’t live in Europe I guess…

Damn…well I just started a job at a vet hospital so maybe someone there can help me out! Thanks

Worst case you could possibly just decode the signal to the display.

I revisited my script in light of my recent Bio-Thermo findings, and reverse-engineered the complete format of the SureSense reader’s log entries. Now it correctly reports the FDX national code, country code and temperature in case of Destron Fearing Bio-Thermo / xBT chips, and also correctly detect no-read and invalid-read events. And since I was at it, I commented the code a bit more.

Here it is:

#!/usr/bin/python3

# This script reads FDX-B animal identification transponders in real-time, and
# also reports the temperature from Destron Fearing LifeChip with Bio-Thermo
# technology transponders, using a SureFlap SureSense reader connected to USB.
#
# It does so by exploiting a bug in the SureSense reader's firmware that lets
# the device scan transponders while connected to a USB port at the same time,
# which is not normally permitted.
#
# To trigger the exploit, press the SureSense reader's scan button. When the
# dog-and-magnifier logo appears, connect the USB cable. The device switches to
# USB connection mode and displays an animated computer logo, but the scanning
# operation previously started isn't cancelled. When it completes after the
# computer logo has appeared (successfully or not), the device returns to
# normal scanning mode but the USB connection isn't closed, thereby letting
# the attached computer monitor what the device writes in its log file in
# real-time.
#
# It is of limited value however, because the device only logs 50 consecutive
# scans. After that, it stops logging. To wipe the log file, the SureSense
# reader must be reset by disconnecting and reconnecting one of the batteries,
# after which 50 more reads can be captured by this script. Therefore, this
# exploit is more proof of concept than anything else.
#
# This script must be run as root.

import os
import re
import json
import time
from subprocess import Popen, PIPE, DEVNULL

mntpoint="/tmp/SureSense"

# Determine the SureSense's block device file
bdevs=json.loads(Popen(["lsblk", "-l",  "--json", "--output", "NAME,LABEL"],
	stdout=PIPE, stderr=DEVNULL).communicate()[0].
	decode("utf-8"))["blockdevices"]
dev=(([bdev["name"] for bdev in bdevs if bdev["label"]=="SureSense"])+[None])[0]
if not dev:
  print("Error: SureSense block device not found")
  exit(1)
dev="/dev/"+dev

# Determine if it's already mounted. Unmount it if it is.
mnts=(Popen(["mount"], stdout=PIPE, stderr=DEVNULL).communicate()[0].
	decode("utf-8").split("\n"))
mnt=(([l.split()[2] for l in mnts if re.match(r"^"+dev+" on [^\s].+$", l)]) \
	+[None])[0]
if(mnt):
  Popen(["umount", dev], stdout=DEVNULL, stderr=DEVNULL).communicate()

# Make a temporary mountpoint if it doesn't exist already
if not os.path.isdir(mntpoint):
  os.mkdir(mntpoint)

# Detect changes in the SureSense's log file and display new readings
prev_csv=None
while True:

  # Mount the device
  Popen(["mount", dev, mntpoint], stdout=DEVNULL, stderr=DEVNULL).communicate()

  # Read the CSV file
  with open(mntpoint+"/READINGS.CSV", "r") as f:
    csv=([l.strip().split(",") for l in f.readlines() \
				if re.match("^([^,]+,){3}[^,]+$", l)])

  # If the CSV file has changed, display the new reading
  if prev_csv and len(csv) > len(prev_csv):

    # The second log entry field encodes the national code (38 bits) followed
    # by the country code (10 bits) little-endian, as they arrive in the FDX
    # telegram
    ncode=int(csv[-1][1][0:2], 16) + \
		(int(csv[-1][1][2:4], 16) << 8) + \
		(int(csv[-1][1][4:6], 16) << 16) + \
		(int(csv[-1][1][6:8], 16) << 24) + \
		((int(csv[-1][1][8:10], 16) & 0x3f) << 32)

    ccode=((int(csv[-1][1][8:10], 16) & 0xc0) >> 6) + \
		(int(csv[-1][1][10:12], 16) << 2)

    # The third log entry field encodes the temperature
    tempf=float(csv[-1][2]) * 0.2 + 74	# Temp in F is value * 0.2 + 74
    tempc=(tempf - 32) / 1.8		# Temp in C

    # The fourth log entry field encodes the timestamp of the record in seconds
    # since bootup in the 31 least significant bits, and the most significant
    # bit is a flag indicating the presence of a Bio-Thermo temperature value
    hastemp=int(csv[-1][3]) & 0x80000000
    tstamp=int(csv[-1][3]) & 0x7fffffff

    # The device seems to indicate that no valid tag was read by adding a new
    # entry in the CSV file but not updating the timestamp
    tag_read=(int(csv[-2][3]) & 0x7fffffff) != tstamp

    # If the device did update the timestamp but the temperature parity bit in
    # the FDX telegram was invalid, it seems to return invalid country code 932
    invalid_tag_read=ccode == 932

    # Display the results
    if tag_read:
      if invalid_tag_read:
        print("Invalid tag read")
      else:
        print("Tag read:")
        print("  National code: {}".format(ncode))
        print("   Country code: {}".format(ccode))
        if hastemp:
          print("    Temperature: {:.1f} F / {:.1f} C".format(tempf, tempc))
    else:
      print("No tag read")

  prev_csv=csv

  # Unmount the device
  Popen(["umount", dev], stdout=DEVNULL, stderr=DEVNULL).communicate()

  # Wait a bit until the next poll
  time.sleep(1)
2 Likes