Reverse engineering of XBee Pro PHY layer, part 1


For another side-project, I have bought three Xbee Pro 900HP. Here is a description of these modules, according to the manufacturer:

XBee-PRO 900HP embedded modules provide best-in-class range wireless connectivity to devices. They take advantage of the DigiMesh® networking protocol, featuring dense network operation and support for sleeping routers, and are also available in a proprietary point-to-multipoint configuration. Supporting RF line-of-sight ranges up to 28 miles (with high-gain antennas), and data rates of up to 200 Kbps, these modules are ideal for extended-range applications requiring increased data throughput.


With such a long range, the number of possible applications is immense. On their blog, Digi wrote:

Companies have utilized this module to develop some amazing products, like intelligent wireless street lighting systems, mission-critical radiation detection systems at power plants, and even beacon systems used in firefighting suits.

Although, a large amount of Xbee models are based on the ZigBee protocol, the 900HP uses its own proprietary protocol. This series of posts has the objective to reverse this protocol but also, to provide general guidelines on how to reverse any unknown RF stack.

It must be noted that this analysis has been possible because the price of SDR has dramatically decreased in recent years. As proof, the RTL2832U (~20 AUD) has been used for most of this work.

Reading the documentation

The first step to reverse the physical layer is to read the documentation that has been released by the manufacturer. It may give us some insight on the underlying technologies. If not available, extracts from the FCC Test Report may help. In our case, the User Manual is freely available and contains more than we need to start.

First stop is the Key Features of the Xbee section. FHSS (Frequency Hopping Spread Spectrum) is mentioned in the Advanced Networking & Security subsection. The focus of this first post will be on that feature. Let’s continue with the manual. A bit further down, the documentation for the read-only command AF states:

This command returns a bitfield. Each bit corresponds to a physical channel. Channels are spaced 400 kHz apart:

Bit 0 – 902.400 MHz
Bit 1 – 902.800 MHz
Bit 63 – 927.600 MHz

That’s 64 channels. Depending on which version you have, only a subset of these is active. For the Australian version (the one I am using):

Australia: 0x00FFFFFFFE00000000 (channels 33 – 63)

A bit further down:

A minimum of 25 channels must be made available for the module to communicate on. The module will choose the 25 lowest enabled frequencies as its active channels if more than 25 are enabled.

So only the first 25 channels will be used. For us, that is channels 33 to 57, from 915.6MHz to 925.2MHz. To be able to capture these 25 channels we would need about 10MHz of bandwidth. Unfortunately, the RTL2832U is limited to 2.56MS/s (without sample loss). With such bandwidth, we should be able to capture around 4 channels at a time.

Lab set up


For the testing, I’m using an XBee Pro 900HP (right) Australian version. To communicate with my laptop, I’ve used the XBee Explorer USB from Sparkfun.

For the sniffing, I’ve used a customised RTL-SDR (RTL 2382U/E4000) (left).

The XBee operates in two modes: either transparent (every character sent to the serial port is forwarded) or API (the serial port expect a structured binary flow that encapsulates the data). Using the transparent mode is the easiest. Here is a Python script that opens ttyUSB0 and send “A”s:

import time
import serial

ser = serial.Serial('/dev/ttyUSB0', 57600, timeout=1)

while True:
  ser.write("A" * 1024)
  print "."

At this stage, we are not sure how much is effectively sent (potential buffering or waiting for acknowledgement before data transmission). By default, the XBee is configured to broadcast its input (destination address=0xFFFF). When run, the transmission LED of the breakout board is almost always on, that is a good sign that some packets might be flowing.

First capture

It is time to test if our assumptions drawn from the user’s manual hold. Although, a considerable amount of tools exist to interact with the RTL-SDR, GNURadio is generally the preferred method, because of its flexibility. Here is the first program, plotting the FFT of our full bandwidth:


The important part of this diagram is the settings of the modules. Mainly, the bandwidth (2.048MS/s) and the capture frequency (916.2MHz). We are hoping to capture between channel 34 and 35. Here is the output:


The FFT is plotted in blue, the peak values are in green. The first four channels are clearly visible with the peak values between -20 and -10 dB. As predicted, the channels are separated by 400kHz. The first channel is centered around 915.6MHz, the second around 916MHZ, the third around 916.4MHz and the forth around 916.8MHz. Note that we can even see half of the fifth channel on the right. At the time of capture, you can see that channel 35 (the fourth one) was being used.

Channel Isolation

We are now going to try to isolate one channel from the full bandwidth. To do so, we use the Xlating filter. For an introduction on that module and how to use it, I recommend watching GNURadio Tutorial 3 or reading this tutorial.

From the first FFT, it seems that one channel is about 200kHz large. The tap of the Xlating filter is a low-pass filter with a cutoff frequency of 100kHz and a transition bandwidth of 50kHz. These are just defaults, we will be using GUI sliders to modify them while the code is running:


Here are the peak values observed:


In that sample, our centre frequency was 916MHz, which should be the centre of channel 34. However, it is quite clear that the signal is shifted on the left. To correct this, we will use an offset of -30kHz. The same correction will be applied to the other channels.

Here is our shifted and trimmed channel (cutoff = 100kHz, transition bw = 50kHz):


Back to the time domain, we may use an oscilloscope to visualise the signal. In its current form, the signal is a complex value (I/Q). We will plot its magnitude:


At that stage, we can note that this channel is being used for about 16ms. We will dig deeper into the signal itself in a subsequent post. For now, let’s focus on the relationship between the channels.

Reversing the FHSS pattern

Now that we know how to isolate one channel, we can repeat the filtering to isolate the neighbours channels:


When run:


Channel 33 is blue, 34 green, 35 red and 36 purple. We can see that each frequency is utilised in a specific order. This exact same pattern is being repeated over time.

The order in which each channel is being used is called the hopping pattern. In our case, there is obviously some space between channel 34 (green) and 36 (purple). We can assume that some channels (two to be precise) are being used during that time. Unfortunately, because of our capture limit, we cannot see these. To solve that issue, we simply need to repeat this operation with different capture frequencies that overlap. Once done, it is about solving the puzzle of frequencies’ order. Below is the reconstructed order:


This is the end of the first part. In the next post, we will analyse one packet in particular and try to reconstruct its content.