Skip to content

kodarn/Sparsnas

Repository files navigation

IKEA Sparsnäs

Introduction

Ikea Sparsnäs is an energy monitor which aim is to monitor electricity usage. It consists of two parts; a sensor and a display:

Introduction to IKEA Sparsnas Introduction to IKEA Sparsnas - RF

It uses a Texas Instruments CC115L transmitter, and the display-enabled receiver uses a Texas Instruments CC113L.

(The CC-series is originally a ChipCon product, and came into the TI-line back in 2006 when Texas Instruments aquired ChipCon)

The sending sensor

The sensor consists of a led impulse sensor connected to a Texas Instruments MSP430G2433 micro-controller (packaged as a 20-TSSOP), where the sensor data is processed. Every 15'th second, the micro-controller sends the collected sensor data via SPI to the CL115 RF-transmitter, which broadcasts the data wireless to the receiving display. The sending sensor

Radio Signal Analysis

This section describes how to decode the radio transmission.

Recording the signal

First, you need to have some sort of Software Defined Radio (SDR) installed on your system. There are many out there; RTL-SDR, HackRF-One, AirSpy just to name a few.

Second, you need some software to record the signal. You will find alternatives ranging from simple commandline apps to more advanced guis. Here are a few alternatives you can use. The Ikea Sparsnäs sends a signal on the 868 MHz band:

rtl_sdr -f 868000000 -s 1024000 -g 40 - > outfile.cu8

hackrf_transfer -r outfile.cs8 -f 868000000 -s 2000000

osmocom_fft -a airspy -f 868000000 -v

This text will not go into the details on how to install the software. This text will continue assuming that you managed to record a signal to file on disk using one of the command lines above. Note: different applications stores the signal data in different formats such as *.cu8, *.cs8, *.cu16, *.cfile, etc. Common to all these formats is the sample form called "IQ". Here is another IQ resource.

If you have never worked with signal analysis before, you can check out Mike Ossmann's introduction tutorials on the Yard Stick One: Part 1 and Part 2.

Open the recorded signal file in a graphical interface for analysis

There are many different techniques and softwares for doing signal analysis. Here we will be using Inspectrum and DspectrumGUI.

 inspectrum -r SampleRateInHz Filename.Ext
 inspectrum -r 1024000        outfile.cu8

Now begins the process of locating the signal. Browsing the horizontal timeline we finally find the following colorful signal: Locating the signal

By modifying the sliders on the left in the GUI, we can zoom in into the signal. The Y-axis is the FFT size and the X-axis is the Zoom which corresponds to the timeline of your signal recording. By doing this, we end up with the following result:

Zooming into the signal The recording frequency of 868 MHz is represented at zero on the Y-axis. The short signal in the middle is an artifact called DC-Spike which is an anomaly generated by the SDR-device. But here we see two frequencies along the recording frequency. This is typical for signal modulation type "FSK". Let's zoom in even further:

Zooming into the signal ever further Now this certainly looks like a FSK-signal.

By right-clicking on the FSK-signal in Inspectrum we can do a "Frequency Plot":

Right-clicking and enable frequency plot Frequency Plot

The frequency plot (see the green-lined graph above) show how the two frequencies changes and creates binary 1's and 0's. Right-clicking on the frequency plot graph enables us to do a "Threshold Plot" which modifies the somewhat buzzy frequency plot into a nice binary form:

Right-clicking and enable threshold plot Threshold Plot

Next up in the analysis is to determine signal characteristics like data rate of the 1's and 0's. Inspectrum helps us with that by using "cursors" which are enabled by hitting the checkbox on the left in the gui. We can here graphically position the cursors such that they align as perfectly as possible with the edges of the 1's and 0's. We do this for as long as we have what seems as valid data, ending up with 208 symbols (i.e. 1's or 0's).

Position cursors to define the whole message

Now, if we look in the reference documentation of the transmitter CC115L, we find this packet description: Lookup packet format in the TI-CC113L documentation

So, our green binary bit-stream should fit into this packet.

Working with binary streams can be inefficient. A more preferable form is hexadecimal. We could start counting the binary string, 8-bits at a time, but instead we use the application DspectrumGui which automates that process for us. Right-click and send the data to stdout. (For this to work it is required that you have started Inspectrum from within DspectrumGUI). RightClick and send symbol data back to DspectrumGui Review the decoded binary stream in the DspectrumGUI

Here in DspectrumGUI we see the binary stream and the "Raw Binary To Hex" conversion. Now its easier to map the data into the packet format:

Mapping the values from DspectrumGUI to the Texas Instruments packet format

Verifing our work so far

We now want to verify that our analysis is correct. We do this by looking up the CRC-algorithm in the Texas Instruments documentation and test our values:

Look up the CRC algorithm used in the Texas Instruments documentation and test the values

We do a quick implementation of the algorithm in an online C++ compiler/debugger environment, and when executing it we end up with "crc checksum: 0x1204" which matches the expected crc value.

Use Yard Stick One with RfCat to capture packets

We can now go on to the next step in the analysis which is recording more data. Now since the sender and receiver are part of the Texas Instruments family CCxxxx, we use a usb hardware dongle called "Yard Stick One". It consists of a CC1111 chip which can be controlled using the Python-library RfCat.

To start doing this, we need to feed the things we have seen so far in the analysis into the CC1111-tranceiver. The screen below demonstrates how to retrieve all the necessary values.

Determine CC1111 tranceiver parameters in RfCat

By starting RfCat, defining the function init(d) and calling it we have configured the CC1111-chip. To start listening we call d.RFlisten() and as you can see we start to get some packets.

However, to be able to test the packet content better, we write a small Python script. Take a moment to read it, in order to get an understanding of what's going on:

#=============================================================================
# Ikea Sparsnas packet decoder using Yard Stick One along with RfCat
#=============================================================================
import sys
import readline
import rlcompleter
readline.parse_and_bind("tab: complete")
from rflib import *                           

#-----------------------------------------------------------------------------
#------------------------------ Global variables -----------------------------
#-----------------------------------------------------------------------------
d = RfCat()
Verbose = False


#-----------------------------------------------------------------------------
# Initialize radio
#-----------------------------------------------------------------------------
def init(d):
    d.setFreq(868000000)            # Main frequency
    d.setMdmModulation(MOD_2FSK)    # Modulation type
    d.setMdmChanSpc(40000)          # Channel spacing
    d.setMdmDeviatn(20000)          # Deviation
    d.setMdmNumPreamble(32)         # Number of preamble bits
    d.setMdmDRate(38391)            # Data rate
    d.setMdmSyncWord(0xD201)        # Sync Word
    d.setMdmSyncMode(1)             # 15 of 16 bits must match
    d.makePktFLEN(20)               # Packet length
    d.setMaxPower()

#-----------------------------------------------------------------------------
# Crc16 helper
#-----------------------------------------------------------------------------
def culCalcCRC(crcData, crcReg):
    CRC_POLY = 0x8005
    
    for i in xrange(0,8):
        if ((((crcReg & 0x8000) >> 8) ^ (crcData & 0x80)) & 0XFFFF) :
            crcReg = (((crcReg << 1) & 0XFFFF) ^ CRC_POLY ) & 0xFFFF
        else:
            crcReg = (crcReg << 1) & 0xFFFF
        crcData = (crcData << 1) & 0xFF
    return crcReg

#-----------------------------------------------------------------------------
# crc16
#-----------------------------------------------------------------------------
def crc16(txtBuffer, expectedChksum):
    CRC_INIT = 0xFFFF
    checksum = CRC_INIT

    hexarray = bytearray.fromhex(txtBuffer)
    for i in hexarray:
        checksum = culCalcCRC(i, checksum)

    if checksum == int(expectedChksum, 16):
        #print "(CRC OK)"
        return True
    else:
        #print "(CRC FAIL) Expected=" + expectedChksum + " Calculated=" + str(hex(checksum))
        return False

#-----------------------------------------------------------------------------
# "main"
#-----------------------------------------------------------------------------
print "Initialize modem..."
init(d)

print "Waiting for packet..."
#d.RFlisten()

#-----------------
# Read packet loop
#-----------------
while True:
    capture = ""
    
    #---------------------------------
    # Wait for a packet to be captured
    #---------------------------------
    try:
        y,z = d.RFrecv()
        capture = y.encode('hex')
        #print capture

    except ChipconUsbTimeoutException:
        pass

    #------------------------
    # When we get a packet...
    #------------------------
    if capture:

        # Extract packet content to the formal TexasInstruments packet layout
        pkt_length  = capture[0:0+2]
        pkt_address = capture[2:2+2]
        pkt_data    = ""
        for x in xrange(4, len(capture) - 4, 2):
            currElement = capture[x:x+2]
            pkt_data += currElement + " "
        pkt_crc     = capture[36:36+2] + " " + capture[38:38+2]

        # Verify crc16 accordingly to the TexasInstruments implementation
        crcBuf_str  = (pkt_length + pkt_address + pkt_data).replace(" ","")
        expectedCrc = capture[36:36+2] + capture[38:38+2]
        crcOk       = crc16(crcBuf_str, expectedCrc)

        if Verbose:
            print "pkt.length        = " + pkt_length
            print "pkt.address_field = " + pkt_address
            print "pkt.data_field    = " + pkt_data
            print "pkt.crc16         = " + pkt_crc + " (CRC verification: " + str(crcOk) + ")"
            print ""
        else:
            if crcOk:
                print "Pkt: " + pkt_length + " ",
                print  pkt_address + " ",
                print  pkt_data.replace(" ","") + " ",
                print  pkt_crc.replace(" ","")

When we run the script and start to get some data, we quickly identify that the packet content does not match what is shown on the receiving display. We can therefore conclude that the packet content is scrambled in some way. However, since the sensor is a small battery powered device with limited computational resources it is a fair assumption that we're dealing with some kind of simplistic XOR obfuscation of sorts, and not power-hungry encryption algorithms.

First attempt to look for patterns in packet content

Packet content analysis

At this point, we know nothing of the internal packet layout, but we can start to identify some patterns. This is a creative process which can be quite time consuming. First we need to list possible entities that may, or not may, be in the Data Field-part of the signal.

Constants

  • Variable identifiers (such as data for variable X is always prepended with the constant Y)
  • Length fields (packet length, length of individual fields in the packet, etc)
  • Sync words or other "magics"
  • Sender identifiers (addresses, serials, hardware or software versions/revisions)
  • etc

Variables

  • Timestamps
  • Things we (in this case) see on the display
    • Current power usage seen on the display
    • Accumulative power consumption seen on the display
    • Battery life properties
  • Signal strength/RSSI if we're dealing with a two-way communication protocol
  • Extra crc's or other hashes
  • etc

The list goes on an on, but lets start with those elements for now. When identifying element-patterns we need to control the signal being sent as much as possible. Therefore we build a simple led-blinker with an Arduino board. The led is flashing at a predetermined rate which we control. While forcing the stable flash rate we can observe what kWh the led blinks translate into on the receiving display. This is a good starting point for our analysis. Other things we may consider could be to hook up the sensor to a voltage cube and vary the transmitters battery voltage. A third option is to purchase several Sparsnäs devices, and decode signals from the different senders which may have different sender properties or identifiers.

Led blink helper tool

You can find the source code here.

We hook up the Sparsnäs sensor to the red led on the right in the image above. Using the yellow and green push buttons we can increase or decrease the delay between led blinks, allowing us to experiment while running our RfCat on the side.

Experiment 1: Finding counters

Experiment 1 In the first experiment, we isolate the sensor in total darkness (using some black electrical tape). Any changing fields would not be related to measured data, but rather to counters such as unique packet identifiers, timestamps etc. In this case, we use a sender with ID 400-565-321 printed on the plastic case, and by looking at the hexdump we can identify some patterns. This listing goes on for a very long time in order to detect static and dynamic values in the hexdump-mass. This enables us to separate values and insert spaces to form columns.

 len  ID  Cnt Fix  Fixed    Cnt2 Data Fixed      Crc16
 11   49   00 070f a276170e cfa2 8148 47cfa27ed3 f80d
 11   49   01 070f a276170e cfa3 8148 47cfa27ed3 6e0e
 11   49   02 070e a276170e cfa0 c6b7 47cfa27ed3 be8c
 11   49   04 070f a276170e cfa6 6db7 47cfa27ed3 6a3d
 11   49   05 070f a276170e cfa7 6877 47cfa27ed3 f9a2
 11   49   06 070f a276170e cfa4 6437 47cfa27ed3 4f25
 11   49   07 070f a276170e cfa5 60f7 47cfa27ed3 5da9
 11   49   08 070f a276170e cfaa 5cb7 47cfa27ed3 302e
 11   49   09 070f a276170e cfab 5b77 47cfa27ed3 2192
 11   49   0a 070f a276170e cfa8 5737 47cfa27ed3 9715
 11   49   0b 070f a276170e cfa9 53f7 47cfa27ed3 8599
 11   49   0c 070f a276170e cfae 4fb7 47cfa27ed3 7a1e
 11   49   0d 070f a276170e cfaf 4a77 47cfa27ed3 e981
 11   49   0e 070f a276170e cfac 4637 47cfa27ed3 5f06
 11   49   0f 070f a276170e cfad 42f7 47cfa27ed3 4d8a
 11   49   10 070f a276170e cfb2 3eb7 47cfa27ed3 8408
 11   49   11 070f a276170e cfb3 3d77 47cfa27ed3 11f7
 11   49   12 070f a276170e cfb0 3937 47cfa27ed3 2ff3
 11   49   13 070f a276170e cfb1 35f7 47cfa27ed3 b5fc
 11   49   14 070f a276170e cfb6 31b7 47cfa27ed3 53fb
 11   49   15 070f a276170e cfb7 2c77 47cfa27ed3 d9e4
 11   49   16 070f a276170e cfb4 2837 47cfa27ed3 e7e0
 11   49   17 070f a276170e cfb5 24f7 47cfa27ed3 7def
 ..   ..   .. .... ........ .... .... .......... ....
 ..   ..   .. .... ........ .... .... .......... ....
 ..   ..   .. .... ........ .... .... .......... ....
 11   49   40 070f a276170e cfe2 8ab7 47cfa27ed3 5b53
 11   49   41 070f a276170e cfe3 8977 47cfa27ed3 ceac
 11   49   42 070f a276170e cfe0 8537 47cfa27ed3 782b
 11   49   43 070f a276170e cfe1 81f7 47cfa27ed3 6aa7
 11   49   44 070f a276170e cfe6 8177 47cfa27ed3 082f # Column 'Data' becomes stable
 11   49   45 070f a276170e cfe7 8177 47cfa27ed3 9e2c
 11   49   46 070f a276170e cfe4 8177 47cfa27ed3 a42c
 11   49   47 070f a276170e cfe5 8177 47cfa27ed3 322f
 11   49   48 070f a276170e cfea 8177 47cfa27ed3 e02f
 11   49   49 070f a276170e cfeb 8177 47cfa27ed3 762c
 ..   ..   .. .... ........ .... .... .......... ....
 ..   ..   .. .... ........ .... .... .......... ....
 ..   ..   .. .... ........ .... .... .......... ....
 11   49   7a 070f a276170e cfd8 8177 47cfa27ed3 ec26
 11   49   7b 070f a276170e cfd9 8177 47cfa27ed3 7a25
 11   49   7c 070f a276170e cfde 8177 47cfa27ed3 9826
 11   49   7d 070f a276170e cfdf 8177 47cfa27ed3 0e25
       pkt 7e was lost during signal recording
 11   49   7f 070f a276170e cfdd 8177 47cfa27ed3 a226
 11   49   00 070f a276170e cf22 8177 47cfa27ed3 5302 # Column 'Cnt' wraps
 11   49   01 070f a276170e cf23 8177 47cfa27ed3 c501
 11   49   02 070f a276170e cf20 8177 47cfa27ed3 ff01
 11   49   03 070f a276170e cf21 8177 47cfa27ed3 6902
 11   49   04 070f a276170e cf26 8177 47cfa27ed3 8b01
 11   49   05 070f a276170e cf27 8177 47cfa27ed3 1d02
 11   49   06 070f a276170e cf24 8177 47cfa27ed3 2702

(We crop the listing here; hope you get the idea by now)

  • This experiment results in
    • Len = This column matches the number of payload bytes. In the Texas Instruments-case, the payload starts with the column after the length column (namely the 'ID' column) and ends where the CRC16 column begins.
    • ID = The signal analysis we performed in DspectrumGUI (previously) was performed using a different sensor. Here we see that the 2nd byte is changed when we're using the new sensor. We assume that this is some sort of sensor ID, and therefore name the column 'ID'.
    • We find what looks like two counters and name them 'Cnt'.
      • As for the first, it isn't scrambled and continues to increase until it reaches 0x7F. Then it restarts at 0x00 again.
      • The second 'Cnt2' is scrambled, and its easily mixed with the column next to it named 'Data'. However, when scrolling down until packet 0x45, we see that the 'Data' column stabillizes at '8177'. This makes it very likely we're dealing with two separate columns.
    • The remaining columns contain fixed values and we leave them as is (for now).
    • Another important finding:
      • Power-cycling the sensors' battery, will make the sequences repeat exactly as the previous testrun. -> This makes our analysis much more doable.

Review of how XOR works

  • We concluded earlier that it is likely that we're dealing with some sort of XOR-obfuscation. Therefore; it is a good time to review the characteristics of the XOR-operation (denoted with the ^ character). Some handy facts:

    • Fact 01: Any byte ^ 0x00 will result in the original value. That is, 0xAA ^ 0x00 = 0xAA. We can use this fact when identifying counters which starts from zero and then increases. Lets say we have a 32-bit counter (i.e. 4 bytes) which we find it reasonably that is starts from zero, but is XOR'ed with an unknown key:
    Packet ID Clear text before send XOR'ed data read in the air
    packet 01 00 00 00 00 11 22 33 44
    packet 02 00 00 00 01 11 22 33 45
    packet 03 00 00 00 02 11 22 33 46
    packet 04 00 00 00 03 11 22 33 47
    packet 05 00 00 00 04 11 22 33 40
    • Knowing that a number xor'ed with 0x00 results in the original value, we can conclude that the unknown XOR-key for the packets above is 11 22 33 44.

    • Fact 02: How XOR works when dealing with increasing value series in relation to each other. Consider the following set of values:

    | Packet ID  | In the air  | Packet 01 XOR with Packet 0* | Packet 01   ^ Current Val = Unscrambled Cnt
    | ---------- |:-----------:| ----------------------------:| -------------------------------------------
    | packet 01  | 11 22 33 44 | --+--+--+--+                 | 11 22 33 44 ^ 11 22 33 44 = 00 00 00 00
    | packet 02  | 11 22 33 45 | <-+  |  |  |                 | 11 22 33 44 ^ 11 22 33 45 = 00 00 00 01
    | packet 03  | 11 22 33 46 | <----+  |  |                 | 11 22 33 44 ^ 11 22 33 46 = 00 00 00 02
    | packet 04  | 11 22 33 47 | <-------+  |                 | 11 22 33 44 ^ 11 22 33 47 = 00 00 00 03
    | packet 05  | 11 22 33 40 | <----------+                 | 11 22 33 44 ^ 11 22 33 48 = 00 00 00 04
  • In counter series starting with zero, xor'ing the start value with the following elements, results in the counter sequence in clear text.

Applying what we now know about XOR to our own data

Lets assume that the Cnt2 counter starts at 0x000. To verify this assumption we do the following operation. Xor the starting value with each following value in the column and see what we get:

 len  ID  Cnt Fix  Fixed    Cnt2 Data Fixed      Crc16
 11   49   00 070f a276170e cfa2 8148 47cfa27ed3 f80d
 11   49   01 070f a276170e cfa3 8148 47cfa27ed3 6e0e
 11   49   02 070e a276170e cfa0 c6b7 47cfa27ed3 be8c
 11   49   04 070f a276170e cfa6 6db7 47cfa27ed3 6a3d
 ..   ..   .. .... ........ .... .... .......... ....
 
  cfa2 ^ cfa2 = 0000
  cfa2 ^ cfa3 = 0001
  cfa2 ^ cfa0 = 0002
  cfa2 ^ cfa6 = 0003
  .... ^ .... = ....

  Great, it looks like our assumption is valid. The XOR key for
  those two bytes in the 'Cnt2' colum is definitely 'cfa2'.

Look for repetitions in the XOR-data

We now know that given the value of zero, the xor-key at some positions in the dataset is cfa2. But if we're lucky, there can be other columns which also begins with the value of zero, but isn't counters. Lets consider the top row:

Prior to identifying Xor key length

Can we detect the cfa2 sequence anywhere else? Well, yes, we seem to have one hit in the last 'Fixed' column.

Simple Frequency Analysis

If we now make the following assumptions:

* That last column named "Fixed" also starts with 0x000 (at least at the cfa2 positions).
* Repeating sequences indicates a rolling XOR-key, where we have a 
  shorter key compared to the longer data to be scrambled.

Lets measure the byte-distance: Xor key length

We seem to have 5 bytes before the values repeat. In cryptanalysis, what we're doing here is known as Frequency analysis. If our assumptions are correct, it means we have a 5-byte long XOR-key.

Lets measure if a 5-byte XOR-key would go fit into the packet. We saw in the long packet dump above, that the three first columns (i.e. Len, ID, Cnt) was most likely in clear text. So, the scrambled data begins with the first 'Fix' column and ends where the Crc16 begins. Attempt to fit a 5-byte XOR-key based on that: Xor key length matching

It aligns perfectly, which strengthens our assumption. So, the assumption is now that we have the following XOR-key: ?? cf a2 ?? ??

Experiment 2: Controlling input data

Experiment 2 Next up is to use our Arduino-based "Led blink helper tool" we built earlier. We remove the black electrical tape and attach the sensor to the led on the breadboard. Looking at the packet dump above we can very easily conclude that the sensor sends one packet every 15'th second. If we configure our helper-tool to blink once every minute, we would have four packets per blink. (To reduce space I have removed duplicate 'NewCnt' packets. That's why the 'Cnt' & XOR-'PCnt' columns aren't sequential.)

Len ID Cnt Fix Fixed    PCnt Data NewCnt      Crc16
 11 49 00 070f a276170e cfa2 8148 47cfa27e d3 f80d
 11 49 02 070e a276170e cfa0 c6b7 47cfa27e d3 be8c
 11 49 04 070e a276170e cfa6 99eb 47cfa27f d3 b625
 11 49 08 070e a276170e cfaa 916f 47cfa27c d3 bc2b
 11 49 0d 070e a276170e cfaf 8ef3 47cfa27d d3 4a4d
 11 49 0f 070e a276170e cfad 9129 47cfa27a d3 5a6a
 11 49 19 070e a276170e cfbb 917d 47cfa278 d3 223c
 11 49 1b 070e a276170e cfb9 9179 47cfa279 d3 e83a
 11 49 22 070e a276170e cf80 9161 47cfa276 d3 0c2b
 11 49 24 070e a276170e cf86 9167 47cfa277 d3 6e2d
 11 49 27 070e a276170e cf85 9161 47cfa274 d3 ce28
 11 49 2c 070e a276170e cf8e 914d 47cfa275 d3 6206
 11 49 31 070e a276170e cf93 8ecf 47cfa272 d3 807b
 11 49 33 070e a276170e cf91 9117 47cfa273 d3 f45f
 11 49 37 070e a276170e cf95 8e83 47cfa270 d3 5830
 11 49 3b 070e a276170e cf99 9101 47cfa271 d3 5848

Initial observations

  • The first fixed column is now 070e instead of 070f. This could be a status-field which indicates that the sensor is receiving led-blinks. One could speculate that either 070e or 070f represents a TRUE/FALSE value.
  • The NewCnt and the following value d3 seems to be two separate columns, since d3 is constant and NewCnt behaves like a 32-bit counter, so we space them apart.

Power cycling

Now lets pull the battery out and put it back in again. The interesting thing is to determine whether any values are persistent over the power cycle, or if they are being reset back to default values. At the same time, we take our assumption of a 5-byte XOR-key length into account. Watch what happens:

First captured packet after power cycling:

Len ID Cnt Fix Fixed    PCnt Data NewCnt      Crc16
 11 49 00 070f a276170e cfa2 8148 47cfa27e d3 f80d
          |-5-bytes-||--5-bytes-| |-5-bytes-|

The first packet is identical to the one previous to the power cycling. Now, having determined that the NewCnt column is a counter, and its being reset on boot, we can make the assumption that NewCnt starts with the value zero. We can test if this assumption is reasonably. Remember what we stated in "Fact 01" earlier, that is, 0xAA ^ 0x00 = 0xAA. This would mean that we have the have the 4 first bytes XOR-key if our assumption is correct. The remaining value d3 remains to be figured out. Enough, lets test our assumption. (Again; To reduce space I have removed duplicate 'NewCnt' packets. That's why the 'Cnt' & XOR-'PCnt' columns aren't sequential.)

Len ID Cnt Fix Fixed    PCnt Data NewCnt      Crc16      Xor-Key      NewCnt      Result
 11 49 00 070f a276170e cfa2 8148 47cfa27e d3 f80d     # 47cfa27e  ^  47cfa27e  = 00000000 
 11 49 04 070e a276170e cfa6 99eb 47cfa27f d3 b625     # 47cfa27e  ^  47cfa27f  = 00000001
 11 49 08 070e a276170e cfaa 916f 47cfa27c d3 bc2b     # 47cfa27e  ^  47cfa27c  = 00000002
 11 49 0d 070e a276170e cfaf 8ef3 47cfa27d d3 4a4d     # 47cfa27e  ^  47cfa27d  = 00000003
 11 49 0f 070e a276170e cfad 9129 47cfa27a d3 5a6a     # 47cfa27e  ^  47cfa27a  = 00000004
 11 49 19 070e a276170e cfbb 917d 47cfa278 d3 223c     # 47cfa27e  ^  47cfa278  = 00000006
 11 49 1b 070e a276170e cfb9 9179 47cfa279 d3 e83a     # 47cfa27e  ^  47cfa279  = 00000007
 11 49 22 070e a276170e cf80 9161 47cfa276 d3 0c2b     # 47cfa27e  ^  47cfa276  = 00000008
 11 49 24 070e a276170e cf86 9167 47cfa277 d3 6e2d     # 47cfa27e  ^  47cfa277  = 00000009
 11 49 27 070e a276170e cf85 9161 47cfa274 d3 ce28     # 47cfa27e  ^  47cfa274  = 0000000A
 11 49 2c 070e a276170e cf8e 914d 47cfa275 d3 6206     # 47cfa27e  ^  47cfa275  = 0000000B
 11 49 31 070e a276170e cf93 8ecf 47cfa272 d3 807b     # 47cfa27e  ^  47cfa272  = 0000000C
 11 49 33 070e a276170e cf91 9117 47cfa273 d3 f45f     # 47cfa27e  ^  47cfa273  = 0000000D
 11 49 37 070e a276170e cf95 8e83 47cfa270 d3 5830     # 47cfa27e  ^  47cfa270  = 0000000E
 11 49 3b 070e a276170e cf99 9101 47cfa271 d3 5848     # 47cfa27e  ^  47cfa271  = 0000000F

Well, what a nice counter! Thus, we can (with some certainty) conclude the XOR-key being:

    47 cf a2 7e ??

Now we only need to figure out the last byte. The bytes affected by the missing XOR-byte are:

Xor key last byte

Testing an unscramble operation by XOR'ing the first packet with the assumed XOR-key:

Len ID Cnt Fix Fixed    PCnt Data NewCnt      Crc16
 11 49 00 070f a276170e cfa2 8148 47cfa27e d3 f80d
          47cf a27e??47 cfa2 7e?? 47cfa27e ??
 -------------------------------------------------
          40C0 0008??49 0000 FF?? 47cfa27e ??

The first and last values (17 & d3) have been static during our whole analysis so far. This makes them very dificult to work with. However, the byte in the middle column 'Data', containing the value 48, has been fluctuating since we started feeding the sensor with led blinks. If we could figure out what this column is used for, perhaps we can solve it. Let summarize what we assumed (and partially know) so far:

  • Len - Length of payload bytes, starting with column Fix (070f) and ending before the Crc16
  • ID - Seems to be a sender ID of some sort
  • Cnt - A 8-bit packet counter, wrapping at 0x7F (which makes it 7-bits actually)
  • Fix - Some sort of flag/status column with true/false like properies stating if the sensor is detecting any blinks.
  • Fixed - 5 bytes of static data. At present, it is hard to make something of it.
  • PCnt - A 16-bit packet counter.
  • Data - Only modified when we prove led blinks, so it should have something to do with the measurement process.
  • NewCnt - A 32-bit led blink counter. Increases by one for every blink.
  • d3 - At present, it is hard to make something of it.
  • Crc16 - The standard Texas Instruments Crc16

Figuring out the last byte in the XOR-key

Lets take a step back and reason a little bit. The only two columns which are changed relative to led blinks are Data and NewCnt. That means they are the only two columns which can affect the Watt-value printed on the receiving display. Now, the NewCnt column only measures the total amounts of led blinks. However, the receiving display also shows the current power usage in Watts. We should look into the theory of how that works. Infact, this is commonly described as the process of converting led impulses to Watts in modern domestic electricity consumption and microgeneration meters.

There are numerous projects out there describing the mathematics in detail, so we'll keep it short here.

Convert impulses to Watt

Example: If the LED flashes once every 5.2 seconds on a meter labeled 800 kWh, the power going through the meter at that time will be 3600 / (5.2 * 800) = 0.865 kW. If we want Watt instead of kW, we multiply by 1024 which yields (3600 * 1024) / (5.2 * 800) = 886 Watt.

Side note: We have set k=1024 instead of k=1000. As it turns out (which we will see later) the Sparsnas manufacturer have defined k as 1024 when transfering the packets. However, in a generic formula it might have been more correct to write it as k=1000, but that isn't applicable here in our scenario. One can also speculate whether its "cheaper" to shift 10 bits (i.e. k=1024) compared to the multiply/division by 1000, but we won't know that until we dump the flash memory.

With this in mind, we can make an assumption that the Data column should contain the timing information in the fraction-denominator in some form. But to get to this, we first need to figure out the last byte in the XOR-key.

Default value assumption

In our test-unscramble operation above we received the following result:

    Data column: 8148
    XOR-key:     7e??
                 ----
                 FF??

When we see FF as the highbyte, we can start to reason. We know the lowbyte must be somewhere between 00 & FF, right? FFFF would translate into -1 in decimal form, which would be a plausible initialization value. Other values, such as FF00, translates into -256 (or 65280 decimal) which may be valid but seems less likely. So we start with an assumption that the Data column starts with the unscrambled value of 0xFFFF.

How do we figure out the XOR-key value? Well, this is what we're asking: 48 ^ ?? = FF which in XOR-math translates into ?? = 48 ^ FF, which in turn equals B7.

How do we verify this XOR-key? Well, let us capture some data and investigate it. When we receive a packet, pay attention to the values on the receiving display and write them down. We speed up the blink-rate on the Arduino-connected led to one blink per second in order to get more dynamic values. Here's a few selected lines:

Len ID Cnt Fix Fixed    PCnt Data NewCnt     Crc16     Data ^ Key    (Hex) (Dec)   Watt on display
--- -- -- ---- -------- ---- ---- ---------- ----      -------------------------   ---------------
 11 49 1e 070e a276170e cfbc 7aa5 47cfa3aed3 9143    # 7aa5 ^ 7EB7 = 0412  (1042)  3537
 11 49 24 070e a276170e cf86 7aa3 47cfa054d3 a17f    # 7aa3 ^ 7EB7 = 0414  (1044)  3531
 11 49 45 070e a276170e cfe7 7aa7 47cfa667d3 bd13    # 7aa7 ^ 7EB7 = 0410  (1040)  3544

Now, we can verify our assumptions so far by putting the received (and unscrambled) data into the mathematical formula defined above:

Power (W)  = (1024) * (60 * 60) / (the seconds between flashes * number of Imp/kWh printed on meter)
           = (1024) * (60 * 60) / (unscrambled value sent in the 'Data' column)
           = 3686400            / (unscrambled value sent in the 'Data' column)

===>

Power (W) = 3686400 / 1042 = 3537
Power (W) = 3686400 / 1044 = 3531
Power (W) = 3686400 / 1040 = 3544

These values match what we have seen on the receiving display, and thus we can consider our assumptions verified.

Summary of the packet content analysis

Knowing the scrambling scheme, we only need to capture the first packet after power-cycling the sensor. This will enable us to determine the XOR-key as described below. We also attempt to rename the columns:

Packet Analysis Summary

The XOR-Key algorithm

  1. Capture the first packet after power cycling the sensor.
  2. Copy the content from the "PulseCnt" column.
  3. Take the last byte from the "AvgTime" column and XOR it with 0xFF. Then append the result as the last byte in our XOR-Key.

Applying that XOR-key then yields:

Len ID Cnt Status Fixed    PCnt AvgTime PulseCnt ?? Crc16
 11 49 00 070f    a276170e cfa2 8148    47cfa27e d3 f80d   <--- Packet data
          47cf    a27eb747 cfa2 7eb7    47cfa27e b7        <--- XOR key
 -------------------------------------------------
          40C0    0008A049 0000 FFFF    00000000 64        <--- Unscrambled result

Packet layout with data example

To summarize what we have found out of the packet content:

Column Description
Len Length of payload bytes, starting with column Fix (070f) and ending before the Crc16
ID Seems to be a sender ID of some sort
Cnt A 8-bit packet counter, wrapping at 0x7F (which makes it 7-bits actually)
Status Some sort of flag/status column with true/false like properties stating if the sensor is detecting any blinks.
Fixed 5 bytes of static data. At present, it is hard to make something of it.
PCnt A 16-bit packet counter.
AvgTime Average time between pulses which can be used to calculate current power usage using the formula: 3686400 / (unscrambled value sent in the 'Data' column). Note: This is a simplified form for energy meters using 1000 impulses per kWh. If your meter is using other values, you need to use the formula in its original form as discussed above.
PulseCnt A 32-bit led blink counter. Increases by one for every blink.
d3 At present, it is hard to make something of it.
Crc16 The standard Texas Instruments Crc16

Writing the packet decoder

This enables us to write a small and simple Python-script to decode the signal:

Decoder using RfCat

The source code can be found here.

Finding the XOR-Key for any device

I took the opportunity to shop when there was a sale at the local store.

Sale at the local store

Now comes a repetitive job; for each sensor, capture the first (scrambled) packet after battery insertion. To do this we use our RfCat-script we wrote earlier, which also highlights differences using colors. The source code is here. This is what the capture looked like:

| S/N          | Len | ID | Cnt | Status | Fixed    | PCnt | AvgTime | PulseCnt | d3 | Crc16 | XOR-Key (applying our algorithm) |
| -----------: | :-- | :- | :-- | :----- | :------- | :--- | :------ | :------- | :- | :---- | :------------------------------- |
| 400 565 321  | 11  | 49 | 00  | 070f   | a276170e | cfa2 | 8148    | 47cfa27e | d3 | f80d  | 47 cf a2 7e b7                   |
| 400 595 807  | 11  | 5f | 00  | 070f   | a29d3918 | d0a2 | 6bd1    | 47d0a294 | 4a | b472  | 47 d0 a2 94 2e                   |
| 400 628 220  | 11  | fc | 00  | 070f   | a23838bb | d0a2 | ce52    | 47d0a231 | c9 | 40d8  | 47 d0 a2 31 ad                   |
| 400 629 153  | 11  | a1 | 00  | 070f   | a2df29e6 | d0a2 | 294f    | 47d0a2d6 | d4 | a250  | 47 d0 a2 d6 b0                   |
| 400 630 087  | 11  | 47 | 00  | 070f   | a2752900 | d0a2 | 834b    | 47d0a27c | d0 | b906  | 47 d0 a2 7c b4                   |
| 400 631 291  | 11  | fb | 00  | 070f   | a23918bc | d0a2 | cf46    | 47d0a230 | dd | 7dd3  | 47 d0 a2 30 b9                   |
| 400 673 174  | 11  | 96 | 00  | 070f   | a2c119d1 | d1a2 | 34a3    | 47d1a2cb | 38 | ab5f  | 47 d1 a2 cb 5c                   |
| 400 710 424  | 11  | 18 | 00  | 070f   | a247395f | d1a2 | b211    | 47d1a24d | 8a | 3049  | 47 d1 a2 4d ee                   |

Pay attention to the serial number (S/N) and the XOR-Key.

| S/N          | S/N (in hex) | XOR-Key        |
| -----------: | :----------- | :------------- |
| 400 565 321  | 17 E0 24 49  | 47 cf a2 7e b7 |
| 400 595 807  | 17 E0 9B 5F  | 47 d0 a2 94 2e |
| 400 628 220  | 17 E1 19 FC  | 47 d0 a2 31 ad |
| 400 629 153  | 17 E1 1D A1  | 47 d0 a2 d6 b0 |
| 400 630 087  | 17 E1 21 47  | 47 d0 a2 7c b4 |
| 400 631 291  | 17 E1 25 FB  | 47 d0 a2 30 b9 |
| 400 673 174  | 17 E1 C9 96  | 47 d1 a2 cb 5c |
| 400 710 424  | 17 E2 5B 18  | 47 d1 a2 4d ee |
                  ^  ^  ^  ^     ^  ^  ^  ^  ^
                  |  |  |  |     |  |  |  |  | 
   Column names: S1 S2 S3 S4    X1 X2 X3 X4 X5

One thing we can spot quite easily is that the ID column matches the last byte in the serial number S4. But more importantly, imagine if there were some relationship between S/N and the XOR-Key. What if we could formulate it as:

S/N -----> secret operation ----> XOR-Key

Can you see any trends or patterns? 😉 Here we must try many different approaches, which requires scrapping many ideas along the way (e.g. this, this, and so on...). However, observe how X2 looks like an increasing counter, and at the same time is enclosed by X1 & X3. This is an indication that we might be dealing with some sort of column permutation. After changing column order several times we end up with swapping X2 & X3, and X4 & X5 which is illustrated below. In hope of finding some patterns, we attempt to subtract the column values...

| S/N          | S/N (in hex) | XOR-Key        | PemutatedXor - S/N      = Hopefully some pattern
| -----------: | :----------- | :------------- | ------------------------------------------------
| 400 565 321  | 17 E0 24 49  | 47 a2 cf b7 7e | 47a2cfb77e   - 17E02449 = 478AEF9335
| 400 595 807  | 17 E0 9B 5F  | 47 a2 d0 2e 94 | 47a2d02e94   - 17E09B5F = 478AEF9335
| 400 628 220  | 17 E1 19 FC  | 47 a2 d0 ad 31 | 47a2d0ad31   - 17E119FC = 478AEF9335
| 400 629 153  | 17 E1 1D A1  | 47 a2 d0 b0 d6 | 47a2d0b0d6   - 17E11DA1 = 478AEF9335
| 400 630 087  | 17 E1 21 47  | 47 a2 d0 b4 7c | 47a2d0b47c   - 17E12147 = 478AEF9335
| 400 631 291  | 17 E1 25 FB  | 47 a2 d0 b9 30 | 47a2d0b930   - 17E125FB = 478AEF9335
| 400 673 174  | 17 E1 C9 96  | 47 a2 d1 5c cb | 47a2d15ccb   - 17E1C996 = 478AEF9335
| 400 710 424  | 17 E2 5B 18  | 47 a2 d1 ee 4d | 47a2d1ee4d   - 17E25B18 = 478AEF9335
                  ^  ^  ^  ^     ^  ^  ^  ^  ^ 
                  |  |  |  |     |  |  |  |  | 
   Column names: S1 S2 S3 S4    X1 X3 X2 X5 X4 

... and look! Its a clear linear relation!

Side note: Remember the microcontroller on the sensor board? The Texas Instruments MSP430G2433 have 16-bit registers which operate in little endian byte order. So, what we're seeing here is probably just the MCU byte order, and not a cunning plan to obfuscate things by swapping the columns.

This finding enables us to write a function that given the S/N outputs the XOR-Key:

#include <stdio.h>
#include <stdint.h>

void GenerateXorKey(uint32_t SerialNumber)
{
    uint8_t *valueArray = NULL;

    // Calculate the permutated xor value
    uint64_t PermutatedXor = (uint64_t) SerialNumber +  (uint64_t) 0x478AEF9335;
    
    // View the PermutatedXor as an array of bytes
    valueArray = (uint8_t *) &PermutatedXor;

    // Print the XOR-Key and swap X2<->X3, X4<->X5 
    printf("%02x ", valueArray[4]);
    printf("%02x ", valueArray[2]); 
    printf("%02x ", valueArray[3]);
    printf("%02x ", valueArray[0]);
    printf("%02x ", valueArray[1]);

    return;
}

int main()
{
    // Generate XOR-Key for a specific device
    uint32_t SerialNumber = 0x17E02449; // Serial: 400 565 321
    GenerateXorKey(SerialNumber);

    // Generate XOR-Keys for a whole range of devices
    for (uint32_t i = 400000000; i < 400999999; i++)
    {
        printf("Serial: %u    XOR-Key: ", i);
        GenerateXorKey(i);
        printf("\n");
    }
    return 0;
}

Convert serial to XOR-Key

Applying the XOR-Keys to all our sensors

This is what the first captured packets looked like:

| S/N          | Len | ID | Cnt | Status | Fixed    | PCnt | AvgTime | PulseCnt | d3 | Crc16 | XOR-Key (applying our algorithm) |
| -----------: | :-- | :- | :-- | :----- | :------- | :--- | :------ | :------- | :- | :---- | :------------------------------- |
| 400 565 321  | 11  | 49 | 00  | 070f   | a276170e | cfa2 | 8148    | 47cfa27e | d3 | f80d  | 47 cf a2 7e b7                   |
| 400 595 807  | 11  | 5f | 00  | 070f   | a29d3918 | d0a2 | 6bd1    | 47d0a294 | 4a | b472  | 47 d0 a2 94 2e                   |
| 400 628 220  | 11  | fc | 00  | 070f   | a23838bb | d0a2 | ce52    | 47d0a231 | c9 | 40d8  | 47 d0 a2 31 ad                   |
| 400 629 153  | 11  | a1 | 00  | 070f   | a2df29e6 | d0a2 | 294f    | 47d0a2d6 | d4 | a250  | 47 d0 a2 d6 b0                   |
| 400 630 087  | 11  | 47 | 00  | 070f   | a2752900 | d0a2 | 834b    | 47d0a27c | d0 | b906  | 47 d0 a2 7c b4                   |
| 400 631 291  | 11  | fb | 00  | 070f   | a23918bc | d0a2 | cf46    | 47d0a230 | dd | 7dd3  | 47 d0 a2 30 b9                   |
| 400 673 174  | 11  | 96 | 00  | 070f   | a2c119d1 | d1a2 | 34a3    | 47d1a2cb | 38 | ab5f  | 47 d1 a2 cb 5c                   |
| 400 710 424  | 11  | 18 | 00  | 070f   | a247395f | d1a2 | b211    | 47d1a24d | 8a | 3049  | 47 d1 a2 4d ee                   |
                                  \-------------/\--------------/      \-----------/
                                       XOR Key        XOR Key             XOR-Key

When comparing the 'Status' and 'Fixed' columns we start to realize some things...

  • Applying the the XOR-keys will produce different results
    • If the information is some unique static sender id this might be correct
    • If the information is some common static version info etc this would not be correct
  • The 'Status' column should not have different values in the same state. This tells us that if we're to understand we need to review our assumption of the first XOR Key.
  • In order to properly investigate this, we should dump the flash memory of the MSP430G2433 microcontroller. More on that later.

SPI signal analysis

The MSP430G2433-microcontroller communicates with the CC115L-transmitter using SPI. On boot, the microcontroller will configure the transmitter radio settings by writing a set of registers in the transmitter. If we can read these settings we could verify our Inspectrum analysis above. Infact, we could have benefitted from having this knowledge before we did the Inspectrum analysis.

SPI wires

Soldering probes

In a SPI setup, one participant is appointed master, and the other acts as slave. In this Sparsnäs setup, the microcontroller is Master and the CC115L-transmitter is Slave.

SPI consists of four wires:

  • MOSI: Data sent Master -> Slave
  • MISO: Data sent Master <- Slave
  • SCLK: Clock
  • CSn: Slave Select, used to enable the specific slave.

First, we need to solder four probes on to the board. We begin by gently scrubbing off the board-coating on top of the SPI wires. We then apply some solder on the exposed coppar wires. Next step is to prepare four coupling wires. We cut them in appropriate lengths and apply some solder to their ends. Finally, we solder the coupling wires to the board.

It might not be the prettiest of soldering, but it works :-)

Recording the signals

Logic analyzers are great tools to debug and analyze electronics. Here we use a a logic analyzer called DSLogic. They're available on EBay or BangGood etc. Connect the coupling wires to the analyzer, and connect the analyzer using a USB cable to your computer. Now, start the DSView software and begin the recording process. Click the record-button in the gui and plug the batteries into the Sparsnäs-sensor. We record about 40 seconds of data, resulting with the following:

Connecting the logic analyzer Data recording

Here we can see the four recorded signals in separate channels (MOSI, MISO, CLK, CSn). The top channel, SPI, is generated by a 'decoder' in the DSView software.

We can see three "spikes" on the timeline at 5.4, 18.8 & 33.8 seconds. They are:

  • The transmitter setup
  • Sending of packet 01
  • Sending of packet 02

In order to make some sense, we must zoom into the image. However, before we can understand the details, we need to do some reading in the CC115L datasheet.

Studying the CC115L datasheet

Reading datasheets can sometimes take some getting used to. But if you take your time they often make sense eventually. CC115L datasheet SPI specification Reading the datasheet we learn the following:

  • All transfers on the SPI interface are done most significant bit first.
  • All transactions on the SPI interface start with a header byte containing:
    • Bit 7: R/W bit (0=Write, 1=Read)
    • Bit 6: Burst access bit (B=1)
    • Bit 0-5: 6-bit address (A5–A0)
  • Registers with consecutive addresses can be accessed in an efficient way by setting the burst bit (B) in the header byte. The address bits (A5 - A0) set the start address in an internal address counter. This counter is incremented by one each new byte (every 8 clock pulses).
  • Single byte instructions to CC115L are called "Command Strobes"
    • See Table 5-13 for a list of single-byte instructions
  • Writing to registers are two-byte instructions:
    • Byte 1: Register address
    • Byte 2: value
    • See Table 5-14 for the list of registers available
  • Writing packet data to the transmitter in Burst-mode is performed using the 0x7F command:
    • 0x7F: Burst access to TX FIFO
    • Byte 1
    • Byte 2
    • ...
    • Byte N

Equipped with this knowledge, we can understand the SPI-stream.

Decoding the SPI stream

The CC115L-transmitter is configured by setting values into specific registers. We zoom in to the first SPI-packet burst, which is the setup of the transmitter, and hope to see these registers being set. Setup of registers in the CC115L

Zooming in even further we see the beginning of the register setup. MOSI is data sent to the CC115L-chip, and its counter part MISO data sent back to the microcontroller. In the image you can observe bytes going out to the CC115L-chip, 00, 0B and 01, 2E in green text. What that means is Register00 = 0x0B, Register01=0x2E. What these registers and values do is well documented in the CC115L datasheet section 5.19.

Start of the registers setup in the CC115L We scroll to the right, to about 18,8 seconds into the recording. There we find the first data-packet being sent. Note the sequence 11 49 00 07 0F A2 76. This matches what we seen previously in the analysis. If you don't recall them, you may go up in the text to the XOR-analysis and check out the data sent in the first packet of the 400 565 321 sensor.

Sending the first packet

To the far right in the DSView-window the SPI-decoder dumps all the decoded MOSI/MISO values. By clicking 'Export' in the gui we can save the values to .csv/.txt-files (here & here). Now, using what we learned from reading the CC115L-datasheet, we annotate the exported textfile as:

#-----------------------------------------------------------------------------
# Setup registers
# Timeline position in DSView: 5,3 sec
# SPI command values and meaning is located in Texas Instruments Design Notes document "DN503"
# or the ordinary Datasheet.
#-----------------------------------------------------------------------------

SPI    MOSI  Comment
===    ====  ==================================================================
  0    30    0x30: SRES (Reset Chip) [0x30 -> 0011 0000 (WriteBit, StrobeBit, 0x30=SRES)]
  1    00    0x00: IOCFG2 - GDO2 Output Pin Configuration (Table 5-17)
  2    0B          -> 0x0B (Serial Clock)
  3    01    0x01: IOCFG1 - GDO1 Output Pin Configuration (Table 5-18)
  4    2E          -> 0x2E (High impedance (3-state))
  5    02    0x02: IOCFG0 - GDO0 Output Pin Configuration (Table 5-19)
  6    06          -> 0x06 (Assist interrupt-driven model in MCU by go high when sync word has been sent, and low at the end of the packet)
  7    03    0x03: FIFOTHR - TX FIFO Thresholds (Table 5-20)
  8    47           -> 33 bytes in TX FIFO
  9    04    0x04: SYNC1 - Sync Word, High Byte (Table 5-21)
 10    D2          -> 0xD2
 11    05    0x05: SYNC0 - Sync Word, Low Byte (Table 5-22)
 12    01          -> 0x01
 13    06    0x06: PKTLEN - Packet Length (Table 5-23)
 14    28           -> 0x28
 15    07    0x07: Not used
 16    8C 
 17    08    0x08: PKTCTRL0 - Packet Automation Control (Table 5-24)
 18    05          -> Normal mode, use TX FIFO
                   -> CRC calculation enabled
                   -> Variable packet length mode. Packet length configured
                      by the first byte written to the TX FIFO
 19    09    0x09: Not used
 20    00 
 21    0A    0x0A: CHANNR - Channel Number (Table 5-25)
 22    00          -> 0x00
 23    0B    0x0B: Not used
 24    06 
 25    0C    0x0C: FSCTRL0 - Frequency Synthesizer Control (Table 5-26)
 26    00          -> 0x00
 27    0D    0x0D: FREQ2 - Frequency Control Word, High Byte (Table 5-27)
 28    21          -> 0x21 
 29    0E    0x0E: FREQ1 - Frequency Control Word, Middle Byte (Table 5-28)
 30    62          -> 0x62
 31    0F    0x0F: FREQ0 - Frequency Control Word, Low Byte (Table 5-29)
 32    76          -> 0x76
 33    10    0x10: MDMCFG4 - Modem Configuration (Table 5-30)
 34    CA          -> 0xCA 
                   -> The exponent of the user specified symbol rate
 35    11    0x11: MDMCFG3 - Modem Configuration (Table 5-31)
 36    83          -> 0x83
                   -> Symbol rate defined by the specified mantissa
 37    12    0x12: MDMCFG2 - Modem Configuration (Table 5-32)
 38    11          -> 0x11
                   -> Bit 4 is set     => GFSK Modulation
                   -> Bit 3 is not set => Manchester encoding disabled
                   -> Bit 1 is set     => 16-bits sync word
 39    13    0x13: MDMCFG1 - Modem Configuration (Table 5-33)
 40    22          -> 0x22
                   -> Bit 5 is set     => Number of preamble bytes is 4
                   -> Bit 1 is set     => Channel spacing exponent
 41    14    0x14: MDMCFG0 - Modem Configuration (Table 5-34)
 42    F8          -> 0xF8
                   -> Channel spacing mantissa
 43    15    0x15: DEVIATN - Modem Deviation Setting (Table 5-35)
 44    35          -> 0x35
                   -> Mantissa & Exponent for deviation
 45    16    0x016: Not used
 46    07 
 47    17    0x17: MCSM1 - Main Radio Control State Machine Configuration (Table 5-36)
 48    30          -> 0x30 (When a packet has been sent, Stay in TX (start sending preamble))
 49    18    0x18: MCSM0 - Main Radio Control State Machine Configuration (Table 5-37)
 50    18          -> 0x18
 51    19    0x19: Not used
 52    17 
 53    1A    0x1A: Not used
 54    6C 
 55    1B    0x1B: Not used
 56    43 
 57    1C    0x1C: Not used
 58    40 
 59    1D    0x1D: Not used
 60    91 
 61    1E    0x1E: Not used
 62    87 
 63    1F    0x1F: Not used
 64    6B 
 65    20    0x20: RESERVED (Table 5-38)
 66    FB 
 67    21    0x21: Not used
 68    56 
 69    22    0x22: FREND0 - Front End TX Configuration (Table 5-39)
 70    10          -> 0x10
 71    23    0x23: FSCAL3 - Frequency Synthesizer Calibration (Table 5-40)
 72    E9          -> 0xE9
 73    24    0x24: FSCAL2 - Frequency Synthesizer Calibration (Table 5-41)
 74    2A          -> 0x2A
 75    25    0x25: FSCAL1 - Frequency Synthesizer Calibration (Table 5-42)
 76    00          -> 0x00
 77    26    0x26: FSCAL0 - Frequency Synthesizer Calibration (Table 5-43)
 78    1F          -> 0x1F
 79    27    0x27: Not used
 80    41 
 81    28    0x28: Not used
 82    00 
 83    29    0x29: RESERVED (Table 5-44)
 84    59 
 85    2A    0x2A: RESERVED (Table 5-45)
 86    7F 
 87    2B    0x2B: RESERVED (Table 5-46)
 88    3F 
 89    2C    0x2C: TEST2 - Various Test Settings (Table 5-47)
 90    81          -> 0x81
 91    2D    0x2D: TEST1 - Various Test Settings (Table 5-48)
 92    35          -> 0x35
 93    2E    0x2E: TEST0 - Various Test Settings (Table 5-49)
 94    09          -> 0x09
 95    09    0x09: Not used
 96    00 
 97    7E    0x7E -> 0111 1110 ==> (WriteBit, StatusBit, 0x3E=PATABLE)
 98    C0    0xC0 = PA_LongDistance
 99    36    0x36 -> 0011 0110 ==> (WriteBit, StrobeBit, 0x36=SIDLE)
100    F1    0xF1 -> 1111 0001 ==> (ReadBit,  StatusBit, 0x31=ChipVersion)
101    00         
102    39    0x39 -> 0011 1001 ==> (WriteBit, StrobeBit, 0x39=SPWD)
                                   Enter power down mode when CSn goes high.
#-----------------------------------------------------------------------------
# Sending packet 1
#
# Timeline position = 18,8 sec
#
#-----------------------------------------------------------------------------

SPI    MOSI  Comment
===    ====  ==================================================================
103    36    SIDLE ==> Enter IDLE state 
104    7F    Burst access to TX FIFO
105    11    ----
106    49        \
107    00         |
108    07         |
109    0F         |
110    A2         |
111    76         |
112    17         |
113    0E          \        Len  ID  Cnt Status  Fixed    PCnt AvgTime PulseCnt    Crc16
114    CF           >------ 11   49   00 070f    a276170e cfa2 8148    47cfa27ed3  _____
115    A2          /        
116    81         |
117    48         |
118    47         |
119    CF         |
120    A2         |
121    7E        /
122    D3    ----
123    35    STX  ==> In IDLE state: Enable TX. Perform calibration first if MCSM0.FS_AUTOCAL=1.
124    39    SPWD ==> Enter power down mode when CSn goes high.
#-----------------------------------------------------------------------------
# Sending packet 2
#
# Timeline position = 33,8 sec
#
#-----------------------------------------------------------------------------

SPI    MOSI  Comment
===    ====  ==================================================================
125    36    SIDLE ==> Enter IDLE state 
126    7F    Burst access to TX FIFO
127    11    ----
128    49        \
129    01         |
130    07         |
131    0F         |
132    A2         |
133    76         |
134    17         |
135    0E          \        Len  ID  Cnt Status  Fixed    PCnt AvgTime PulseCnt    Crc16
136    CF           >------ 11   49   01 070f    a276170e cfa3 8148    47cfa27ed3  _____
137    A3          /
138    81         |
139    48         |
140    47         |
141    CF         |
142    A2         |
143    7E        /
144    D3    ----
145    35    STX  ==> In IDLE state: Enable TX. Perform calibration first if MCSM0.FS_AUTOCAL=1.
146    39    SPWD ==> Enter power down mode when CSn goes high.

SmartRF Studio

We could continue to look up exactly what each configured register value corresponds to in the CC115L datasheet. This will provide us with the best understanding of things. However, Texas Instruments develops a tool called SmartRF Studio. It is the recommended tool for configuring devices in the Texas CCxxxx-series. We can feed the register settings into this application to observe some of the details quite easliy:

SmartRF Studio

Now, we can note some new observations:

  • Base Frequency 867.999939 MHz is not exactly 868 MHz (just as we saw in the Inspectrum analysis) but very very close.
  • Deviation of 20.629883 kHz was quite close to our Inspectrum analysis (20.0 kHz)
  • Data Rate of 38.3835 kBaud was quite close to our Inspectrum analysis (38.391 kBaud)
  • However, Modulation Format is set to GFSK, not FSK. We should look into how we could have made this distinction earlier.
  • Also, Channel Spacing is not set to the theoretical Deviation * 2 (40.0 kHz in our Inspectrum analysis) found in FSK-literature, but instead to 199.951172 kHz. We should look into the theory behind this.

SmartRF Studio enables us to export the registers in a pretty HTML-table:

CC115L registers as sent by the MSP430G2433 MCU
NameAddressValue Description
IOCFG20x00000x0BGDO2 Output Pin Configuration
IOCFG00x00020x06GDO0 Output Pin Configuration
FIFOTHR0x00030x47TX FIFO Thresholds
SYNC10x00040xD2Sync Word, High Byte
SYNC00x00050x01Sync Word, Low Byte
PKTLEN0x00060x28Packet Length
PKTCTRL00x00080x05Packet Automation Control
CHANNR0x000A0x0AChannel number
FREQ20x000D0x21Frequency Control Word, High Byte
FREQ10x000E0x62Frequency Control Word, Middle Byte
FREQ00x000F0x76Frequency Control Word, Low Byte
MDMCFG40x00100xCAModem Configuration
MDMCFG30x00110x83Modem Configuration
MDMCFG20x00120x11Modem Configuration
DEVIATN0x00150x35Modem Deviation Setting
MCSM00x00180x18Main Radio Control State Machine Configuration
RESERVED_0X200x00200xFBUse setting from SmartRF Studio
FSCAL30x00230xE9Frequency Synthesizer Calibration
FSCAL20x00240x2AFrequency Synthesizer Calibration
FSCAL10x00250x00Frequency Synthesizer Calibration
FSCAL00x00260x1FFrequency Synthesizer Calibration
TEST20x002C0x81Various Test Settings
TEST10x002D0x35Various Test Settings
TEST00x002E0x09Various Test Settings

In order to investigate whether the radio configuration varies between Sparsnäs-devices, we repeat the whole process of recording the SPI bus once again, and end up with this data. The previous device is 400 565 321 and the new device is 400 710 424. SPI entry 0 -> 102 is the initial setup of the radio. SPI entry 103 -> 124 is the burst send of the first packet. SPI entry 125 -> 146 is the burst send of the second packet.

Comparing SPI bus on two different Sparsnäs-devices

As we can see in the data, the setup of the CC115L-registers are identical in both cases. Only some parts of the packet payload data differs, which is expected since the devices are operating with different XOR-Keys.

We should update our RfCat-script to reflect these findings. (Note to self: Do this at a later time).

SPI analysis of the receiving display

The receiver consists of a Texas Instruments CC113L receiver, an NXP LPC1785FBD208 microcontroller, a Flash Memory, and a display. The microcontroller uses SPI to communicate with the CCL113L receiver and the flash memory. Here's an outline of the schematics:

The receiving display

We solder the SPI-connectors onto the board, and hook up the logic analyzer.

The receiving display hooked up to the logic analyzer

When diff'ing the result of the sender (CC115L) compared to the receiver (CC113L) we see the following:

(Both sender & receiver are of serial number 400 565 321)

Diff the setup of CC113L and CC115L

The setups are identical except for the last bytes.

Debug interfaces of the receiving display

If you look careful to the right of the processor, you see a row of holes which might be used for something. What if we could solder on a few headers there... By using our multimeter in beep-mode we can follow the wires to the processor and lookup the pin-descriptions in the documentation. Can you see the set of resistors just below the row of holes? These are PullUp/PullDown resistors used to configure the pins to their default values.

Debug interfaces on the receiving display

This is the result:

Debug interfaces on the receiving display

Now, we also notice the five solder-pads just above the processor:

Debug interfaces on the receiving display

Using the multimeter in beep-mode, we come up with the following scheme:

Debug interfaces on the receiving display

Analysing the Serial Port

Lets hook up the logic analyser to TX on UART0: Serial Port Hook Up After an initial test capture, we see the following bits:

Serial Port

We measure the bit width, and calculate the baudrate based upon that data: BaudRate = 1 / PulseWidth = 1 / 0.00002608 = 38343.56, which rounded off to nearest commonly known baudrate is 38400 bps.

Now we can configure the UART-decoder in the logic analyser, which decodes the bits into the text string "Starting up ELIS": Serial Port

Analysing the JTAG Port

In order to communicate with the JTAG port, we need some sort of device which is able to talk JTAG. There are the vendor specific devices, but here we will use a generic tool called AdaFruit FS232H which costs about $15. This device is able to inteface with many different technologies and can be seen as a little swiss army knife for serial protocols. By looking into the datasheet of the FS232H, we can figure out how to connect the FT232H to our Ikea Sparsnäs:

JTAG Setup JTAG Setup

We connect the AdaFruit FT232H to our computer using a USB-cable, and install OpenOCD which is an open source On-Chip debugger toolbox. Using OpenOCD, we can do disassembly, read/write memory etc.

Connecting using OpenOCD

  1. Install OpenOCD
     sudo apt-get install openocd gdb-multiarch

  2. Make sure the AdaFruit FT232H is connected
     dmesg

  3. Start OpenOCD
     openocd -f ft232h.cfg -f /usr/share/openocd/scripts/target/lpc1850.cfg

We now have JTAG-access to the board. Lets attach a more capable debugger:

Connecting GDB using OpenOCD

  4. Connect to our running OpenOCD-instance
     telnet localhost 4444

  5. Halt the execution on the board
     halt

  6. Connect to our running OpenOCD-instance with GDB
     gdb-multiarch
       set arch arm
       target remote localhost:3333
       x/10i $pc

Looking into the memory map of the LPC 1785

According to the data sheet, the LPC 1785 have the following specifications:

 * CPU:                         ARM Cortex-M3 120 MHz
 * On-Chip Flash:               256 kB
 * Main SRAM Data memory:        64 kB
 * Peripheral SRAM Data memory:  16 kB
 * Total SRAM:                   80 kB
 * On-Chip EEPROM:             4032 Bytes

Lets investigate the memory map of the LPC 1785 microcontroller. Section 2.1 in the LPC 1785 User Manual outlines the address space:

LPC 1785 Memory Map

We can use the dump_image-command in OpenOCD to save memory to a file:

Command    Filename       StartAddr  NumberOfBytesToSave
---------- -------------- ---------- -------------------
dump_image 0x00000000.bin 0x00000000 0x0003FFFF
dump_image 0x00100000.bin 0x00100000 0x0003FFFF
dump_image 0x10000000.bin 0x10000000 0x0000FFFF
dump_image 0x1FFF0000.bin 0x1FFF0000 0x00014000
...

Analysing the memory

Now, lets see if we can find the XOR-key in our saved data. We're working with the device having the serial number 400 565 321. As you might recall, we have figured out the XOR-key of this device in our analysis above. Reviewing that text we see the key being 47 cf a2 7e b7.

Searching for a few bytes of the XOR-key results in one hit (in endian-permutated form):

user@user-virtual-machine:~/OpenOCD$ bgrep2 -C 1 -x cfa2 0x10000000.bin 
==== 0x00003652 (0x00003640-0x00003670) ====
0x00003640: e0b20200 eeeeee00 8c360010 e8de2d00    .... .... .6.. ..-.
0x00003650: 7eb7cfa2 a27eb747 cf800920 11430307    ~... .~.G .... .C..
                ^-                           
0x00003660: 0e000e5e 43510307 db002dde e8570300    ...^ CQ.. ..-. .W..
====
user@user-virtual-machine:~/OpenOCD$ 

This address, 0x10003650, is located on the stack. Lets set a WatchPoint (i.e. a BreakPoint on data access) in OpenOCD on that address:

> wp 0x10003650 4 r

> resume

After a few seconds, our WatchPoint is triggered:

target halted due to watchpoint, current mode: Thread 
xPSR: 0x81000000 pc: 0x000383ba msp: 0x10003650

> mdw 0x10003650 
0x10003650: a2cfb77e 

> reg
===== arm v7m registers
(0) r0 (/32): 0x10003661
(1) r1 (/32): 0xA2CFB77E
(2) r2 (/32): 0x0000E0CD
(3) r3 (/32): 0x40000000
(4) r4 (/32): 0x1000368C
(5) r5 (/32): 0x0008A049
(6) r6 (/32): 0x10000640
(7) r7 (/32): 0x20098030
(8) r8 (/32): 0x10002F01
(9) r9 (/32): 0x00000000
(10) r10 (/32): 0x00201000
(11) r11 (/32): 0x00201000
(12) r12 (/32): 0x4BFB472A
(13) sp (/32): 0x10003650
(14) lr (/32): 0x000383AF
(15) pc (/32): 0x000383BA
(16) xPSR (/32): 0x81000000
(17) msp (/32): 0x10003650
(18) psp (/32): 0xDB725EDC
(19) primask (/1): 0x00
(20) basepri (/8): 0x00
(21) faultmask (/1): 0x00
(22) control (/2): 0x00

Notice that register r1 contains part of our XOR-key. The PC-register (i.e. Program Counter) is set to 0x000383BA. We can continue the analysis in OpenOCD & GDB. However, to make our life a little bit easier, we load the memory dump into a tool where we can anotate the assembler code. There are lots of options out there; Radare2, Binary Ninja, Hopper, IDA, JEB, etc, some of which are Open Source.

I will spare you the digging details and just show you the annotated result:

//
// Prototype: int __fastcall XOR_function(uint8_t *r0_pktBuf, uint8_t *r1_pSerialNumber)
//
ROM:0003839E XorSeed         = -0x30
ROM:0003839E XorKey          = -0x2C
ROM:0003839E localDataBuffer = -0x24
ROM:0003839E
ROM:0003839E          #
ROM:0003839E          # Copy SPI pktdata to a local stack buffer
ROM:0003839E          #
ROM:0003839E          PUSH            {R4-R6,LR}
ROM:000383A0          MOV             R4, R1                            ; R4 = pSerialNumber
ROM:000383A2          SUB             SP, SP, #0x20
ROM:000383A4          MOV             R1, R0                            ; R1 = src SPI pktdata
ROM:000383A6          MOVS            R2, #0x12                         ; R2 = src SPI pktdata bufLen
ROM:000383A8          ADD             R0, SP, #0x30+localDataBuffer     ; R0 = dstBuf
ROM:000383AA          BL              Copy_SPI_pktdata
ROM:000383AA
ROM:000383AA          #
ROM:000383AA          # Derive XorSeed from serial number
ROM:000383AA          #
ROM:000383AE          LDR             R5, [R4,#ResultCtx]
ROM:000383B0          LDR             R1, =0xA2C71735                    <--- Hardcoded XOR-seed constant
ROM:000383B2          ADDS            R1, R1, R5
ROM:000383B4          STR             R1, [SP,#0x30+XorSeed]
ROM:000383B6          
ROM:000383B6          #
ROM:000383B6          # Get ptr to where XOR-data is located in the SPI pktdata
ROM:000383B6          #
ROM:000383B6          ADD.W           R0, SP, #0x30+localDataBuffer+5
ROM:000383BA          
ROM:000383BA          #
ROM:000383BA          # Derive XOR-key (which is 5 bytes) from XorSeed (which is 4 bytes)
ROM:000383BA          #
ROM:000383BA          LDRB.W          R1, [SP,#0x30+XorSeed+3]        <--- PC_here_when_break_on_first_WP
ROM:000383BE          LDRB.W          R2, [SP,#0x30+XorSeed]
ROM:000383C2          STRB.W          R1, [SP,#0x30+XorKey]
ROM:000383C6          ADD             R1, SP, #0x30+XorKey
ROM:000383C8          STRB            R2, [R1,#1]
ROM:000383CA          LDRB.W          R2, [SP,#0x30+XorSeed+1]
ROM:000383CE          STRB            R2, [R1,#2]
ROM:000383D0          MOVS            R2, #0x47 ; 'G'
ROM:000383D2          STRB            R2, [R1,#3]
ROM:000383D4          LDRB.W          R2, [SP,#0x30+XorSeed+2]
ROM:000383D8          STRB            R2, [R1,#4]
ROM:000383DA          
ROM:000383DA          #
ROM:000383DA          # Do the XOR_LOOP on buffer R0=pXorData with the 5-bytes rolling XOR-key R1=pXorKey
ROM:000383DA          #
ROM:000383DA          MOVS            R2, #0              ; R2 = LoopCnt
ROM:000383DA                                                R1 = pXorKey
ROM:000383DA                                                R0 = pXorData
ROM:000383DA
ROM:000383DC XOR_Loop:                   
ROM:000383DC          MOVS            R6, #5
ROM:000383DE          SDIV.W          R6, R2, R6          ; Calc modulus 5 for XOR-key index 'R2'
ROM:000383E2          ADD.W           R6, R6, R6,LSL#2
ROM:000383E6          SUBS            R6, R2, R6          ; R6 = R2++ % 5
ROM:000383E8          LDRB            R3, [R0]            ; R3 = *pXorData
ROM:000383EA          LDRB            R6, [R6,R1]         ; R6 = XorKey[R2++ % 5]
ROM:000383EC          EORS            R3, R6              ; v = *pXorData ^ XorKey[R2++ % 5];
ROM:000383EE          ADDS            R2, R2, #1
ROM:000383F0          STRB.W          R3, [R0],#1         ; *pXorData++ = v;
ROM:000383F4          CMP             R2, #13
ROM:000383F6          BCC             XOR_Loop
ROM:000383F6
ROM:000383F8          #
ROM:000383F8          # Copy result data into the buffer 'ResultCtx'
ROM:000383F8          #
ROM:000383F8          ADD.W           R0, SP, #0x30+localDataBuffer+5
ROM:000383FC          BL              swap32
ROM:000383FC
ROM:00038400          STR             R0, [R4,#ResultCtx]
ROM:00038402          ADD.W           R0, SP, #0x30+localDataBuffer+9
ROM:00038406          BL              swap16
ROM:00038406
ROM:0003840A          STRH            R0, [R4,#ResultCtx.PCnt]
ROM:0003840C          ADD.W           R0, SP, #0x30+localDataBuffer+0xB
ROM:00038410          BL              swap16
ROM:00038410
ROM:00038414          STRH            R0, [R4,#ResultCtx.AvgTime]
ROM:00038416          ADD.W           R0, SP, #0x30+localDataBuffer+0xD
ROM:0003841A          BL              swap32
ROM:0003841A
ROM:0003841E          STR             R0, [R4,#ResultCtx.PulseCnt]
ROM:00038420          LDRB.W          R0, [SP,#0x30+localDataBuffer+0x11]
ROM:00038424          STRB            R0, [R4,#ResultCtx.Power]
ROM:00038426          LDR             R0, [R4,#ResultCtx]
ROM:00038428          CMP             R5, R0
ROM:0003842A          BEQ             skip
ROM:0003842A
ROM:0003842C          MOVS            R0, #0
ROM:0003842E          MOVS            R1, #0
ROM:00038430          MOV             R2, R0
ROM:00038432          MOV             R3, R0
ROM:00038434          STMIA           R4!, {R0-R3}
ROM:00038436
ROM:00038436 skip                      
ROM:00038436          ADD             SP, SP, #0x20
ROM:00038438          POP             {R4-R6,PC}
ROM:00038438

00000000 ResultCtx     struc ; (sizeof=0xD)
00000000 FixedSerial   DCD ?
00000004 PCnt          DCW ?
00000006 AvgTime       DCW ?
00000008 PulseCnt      DCD ?
0000000C Power         DCB ?
0000000D ResultCtx     ends

To visualize the function input buffers, we set a BreakPoint on address 0x00383aa, and dump the memory of the following registers:

  • Src SPI PktData (R1 = 0x10002DCC): mdb 0x10002DCC 0x12
  • SerialNumber (R4 = 0x1000368C): mdw 0x1000368c
> bp 0x000383aa 2
breakpoint set at 0x000383aa

> resume
target halted due to breakpoint, current mode: Thread 
xPSR: 0x01000000 pc: 0x000383aa msp: 0x10003650

> reg r1
r1 (/32): 0x10002DCC

> mdb 0x10002DCC 0x12
0x10002dcc: 11 49 00 07 0f a2 76 17 0e cf a2 81 48 47 cf a2 7e d3 

> reg r4
r4 (/32): 0x1000368C

> mdw 0x1000368c
0x1000368c: 0008a049

The SPI pktdata bytes, 11 49 00 ..., looks just like packets we have seen before. The serial number, 0008a049, when converted to decimal form, is 565321, which is part of our device serial number 400 565 321.

To investigate further, we remove our BreakPoint at 0x000383AA, and add two new ones at 0x000383da where the XOR-loop begins, and 0x000383f8 where the XOR-loop is done.

  • R0 = pXorData
  • R1 = pXorKey
> rbp 0x00383AA

> bp 0x383da 2                                                   <--- Set first BreakPoint before XOR-loop
breakpoint set at 0x000383da

> bp 0x383f8 2                                                   <--- Set second BreakPoint after XOR-loop
breakpoint set at 0x000383f8

> resume
target halted due to breakpoint, current mode: Thread            <--- First BreakPoint Hit
xPSR: 0x01000000 pc: 0x000383da msp: 0x10003650

> reg r0
r0 (/32): 0x10003661

> reg r1
r1 (/32): 0x10003654

> mdb 0x10003661 0x0d
0x10003661: a2 76 17 0e cf a2 81 48 47 cf a2 7e d3               <--- pXorData before XOR-loop

> mdb 0x10003654 0x05
0x10003654: a2 7e b7 47 cf                                       <--- pXorKey before XOR-loop

> resume
target halted due to breakpoint, current mode: Thread            <--- Second BreakPoint Hit
xPSR: 0x61000000 pc: 0x000383f8 msp: 0x10003650

> mdb 0x10003661 20
0x10003661: 00 08 a0 49 00 00 ff ff 00 00 00 00 64               <--- pXorData after XOR-loop

> mdb 0x10003654 5
0x10003654: a2 7e b7 47 cf                                       <--- pXorKey after XOR-loop

Now we can manually follow the ARM assembly code to calculate the XOR-key at 0x000383AE.

  R5 = 0x0008a049   # Device SerialNumber
  R1 = 0xA2C71735   # Hardcoded XOR-seed constant
  R1 = 0xA2C71735 + 0x0008a049 = 0xA2CFB77E ---ByteOrder---> 7E B7 CF A2 
  XorSeed = R1

  XorBufferStart = SPI pktdata buffer + 5

  XorKey[0] = XorSeed[3]       
  XorKey[1] = XorSeed[0]       
  XorKey[2] = XorSeed[1]
  XorKey[3] = 0x47
  XorKey[4] = XorSeed[2]
  
  -> XorKey: A2 7E B7 47 CF 

                              ------------- XOR'ed data ------------
  SPI pktdata: 11 49 00 07 0f a2 76 17 0e cf a2 81 48 47 cf a2 7e d3 
  XOR key:                    A2 7E B7 47 CF A2 7E B7 47 CF A2 7E B7
  Result:      11 49 00 07 0f 00 08 a0 49 00 00 ff ff 00 00 00 00 64

Now that we understand the assembly code, we can rewrite it in in C:

ReWrite the XOR-decoder in C

/*----------------------------------------------------------------------------
 * Description: ReWrite of NXP LPC 1785 ARM assembler XOR-decoder
 * Compile: gcc -Wall -o PktDecoder PktDecoder.c
 *----------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------
 * Include Files
 *----------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>

/*----------------------------------------------------------------------------
 * Function argument container
 *----------------------------------------------------------------------------*/
typedef struct SpiPkt 
{
  uint8_t  LengthField;
  uint8_t  AddressField;
  uint8_t  Cnt;
  uint16_t Status;
  uint32_t FixedSerial; // XOR'ed data
  uint16_t PCnt;        // XOR'ed data
  uint16_t AvgTime;     // XOR'ed data
  uint32_t PulseCnt;    // XOR'ed data
  uint8_t  Power;       // XOR'ed data
  uint16_t Crc16;
} SpiPkt;

typedef struct Ctx 
{
  uint32_t FixedSerial;
  uint16_t PCnt;
  uint16_t AvgTime;
  uint32_t PulseCnt;
  uint8_t  Power;
} Ctx;

/*----------------------------------------------------------------------------
 * Helper functions
 *----------------------------------------------------------------------------*/
uint16_t swap16(uint8_t *x)
{
  uint16_t tmp = *((uint16_t *)x);
  return ((((tmp) >> 8) & 0xffu) | (((tmp) & 0xffu) << 8)) ;
}

uint32_t swap32(uint8_t *x)
{
  uint32_t tmp = *((uint32_t *)x);
  return ((((tmp) & 0xff000000u) >> 24) | (((tmp) & 0x00ff0000u) >>  8) | (((tmp) & 0x0000ff00u) <<  8) | (((tmp) & 0x000000ffu) << 24));
}

void Copy_SPI_pktdata(uint8_t *SPI_PktData, uint8_t *localDataBuffer)
{
  memcpy(localDataBuffer, SPI_PktData, 0x18);
}

/*----------------------------------------------------------------------------
 * Decoder function
 *----------------------------------------------------------------------------*/
uint32_t XOR_function(uint8_t *SPI_PktData, Ctx *ResultCtx)
{
  size_t    i                     = 0; 
  uint8_t   localDataBuffer[18]   = { 0 };
  uint8_t   XorSeed[4]            = { 0 };
  uint8_t   XorKey[5]             = { 0 };
  uint32_t  FixedSerialOrg        = ResultCtx->FixedSerial;

  Copy_SPI_pktdata(SPI_PktData, localDataBuffer);
  uint8_t *XorData = &localDataBuffer[5];

  *((uint32_t *)XorSeed) = ResultCtx->FixedSerial + (uint32_t)0xA2C71735;

  XorKey[0] = XorSeed[3];      
  XorKey[1] = XorSeed[0];      
  XorKey[2] = XorSeed[1];
  XorKey[3] = 0x47;
  XorKey[4] = XorSeed[2];

  do
  {
    uint8_t v;
    v = *XorData ^ XorKey[i++ % 5];
    *XorData++ = v;
  } while ( i < 0xD );

  ResultCtx->FixedSerial  = swap32(&localDataBuffer[5]);
  ResultCtx->PCnt         = swap16(&localDataBuffer[9]);
  ResultCtx->AvgTime      = swap16(&localDataBuffer[11]);
  ResultCtx->PulseCnt     = swap32(&localDataBuffer[13]);
  ResultCtx->Power        = localDataBuffer[17];

  if (FixedSerialOrg != ResultCtx->FixedSerial)
  {
    ResultCtx->FixedSerial  = 0;
    ResultCtx->PCnt         = 0;
    ResultCtx->AvgTime      = 0;
    ResultCtx->PulseCnt     = 0;
    ResultCtx->Power        = 0;
  }
  return ResultCtx->FixedSerial;

}

/*----------------------------------------------------------------------------
 * Parent function feeding the decoder function with data
 *----------------------------------------------------------------------------*/
int sub_33532(void)
{
  uint8_t   SPI_PktData[18] = { 0x11, 0x49, 0x00, 0x07, 0x0f, 0xa2, 0x76, 0x17, 0x0e, 0xcf, 0xa2, 0x81, 0x48, 0x47, 0xcf, 0xa2, 0x7e, 0xd3 };
  Ctx       ctx             = { 0 };
  uint32_t  ret             = 0;

  ctx.FixedSerial = 565321;

  ret = XOR_function(SPI_PktData, &ctx);
  if (ret == 565321)
  {
    printf("FixedSerial = 0x%04x (%d)\n", ctx.FixedSerial, ctx.FixedSerial);
    printf("PCnt        = 0x%02x\n", ctx.PCnt);
    printf("AvgTime     = 0x%02x\n", ctx.AvgTime);
    printf("PulseCnt    = 0x%04x\n", ctx.PulseCnt);
    printf("Power       = 0x%01x\n", ctx.Power);
  } else {
    printf("Failed!\n");
  }

  return ret;
}

/*----------------------------------------------------------------------------
 * Test App Main
 *----------------------------------------------------------------------------*/
int main(void)
{
  sub_33532();
  return 0;
}

Final analysis comments

In this article we have analysed the reception of signals sent by the IKEA Sparsnäs radio transmitter. We have covered two approaches:

  • Radio analysis using SDR and numeric processing.
  • Reading the receiver device flash memory using JTAG, and analysing the assembler code in order to create a C-implementation.

The two approaches have both pros and cons. While the Radio is quite easy to start with, it quickly comes down to numeric analysis which requires time, patience and some luck. The hardware board approach on the other side requires another set of skills, but once you master those skills you can read the code and understand exactly how it works.

Build a hardware receiver using a CC1101

This section is Work-In-Progress

Schematics of WeMos D1 mini & CC1101

Build list

Schematics of WeMos D1 mini & CC1101 using a WeMos Proto board Hardware Receiver prototype

Source code

Insert link here

Ideas for the future

  • Build a software receiver using GNU Radio
  • Attempt to modify the flash firmware and write it back