Reverse engineering Outernet: modulation, coding and framing

Outernet is a company whose goal is to ease worldwide access to internet content. They aim to provide a downlink of selected internet content via geostationary satellites. Currently, they provide data streams from three Inmarsat satellites on the L-band (roughly around 1.5GHz). This gives them almost worldwide coverage. The downlink bitrate is about 2kbps or 20MB of content per day.

The downlink is used to stream files, mostly of educational or informational content, and recently it also streams some APRS data. As this is a new radio technology to play with, it is starting to get the attention of some Amateur Radio operators and other tech-savvy people.

Most of the Outernet software is open-source, except for some key parts of the receiver, which are closed-source and distributed as freeware binaries only. The details of the format of the signal are not publicly known, so the only way to receive the content is to use the Outernet closed-source binaries. Why Outernet has decided to do this escapes me. I find that this is contrary to the principles of broadcasting internet content. The protocol specifications should be public. Also, as an Amateur Radio operator, I find that it is not acceptable to work with a black box receiver of which I can't know what kind of signal receives and how it does it. Indeed, the Amateur Radio spirit is quite related in some aspects to the Free Software movement philosophy.

For this reason, I have decided to reverse engineer the Outernet signal and protocol with the goal of publishing the details and building an open-source receiver. During the last few days, I've managed to reverse engineer all the specifications of the modulation, coding and framing. I've being posting all the development updates to my Twitter account. I've built a GNUradio Outernet receiver that is able to get Outernet frames from the L-band signal. The protocols used in these frames is still unknown, so there is still much reverse engineering work to do.

The only two closed-source pieces of the Outernet software are called sdr100 and ondd. sdr100 is the receiver. It uses an SDR dongle to receive the L-band signal and decode Outernet frames. Then it passes the Outernet frames to ondd, which is the daemon in charge of doing something useful with the frames. Its main job is to reconstruct and decompress the files that are being streamed.

In particular, sdr100 is a bit polemical because it seems to me that it violates the GPL licence, as it links librltsdr and libmirisdr, which are GPL (not LGPL) software. I've tried to write to the Outernet developers, but they don't seem to care.

In any case, my GNUradio Outernet receiver is now able to substitute sdr100 and send Outernet frames to ondd. I'm starting to do this to reverse engineer the protocols used in the frames, as the goal is to replace ondd as well (or at least come up with some open-source software that does something useful with the Outernet frames).

In my reverse engineering effort, the help of Scott Chapman K4KDR and Balint Seeber has being invaluable. Scott has being making SDR recordings for me of the Outernet signal, as I don't have an Outernet receiver. The work of Balint has being very inspirational for me, in particular, his slides about blind signal analysis and his Auto FEC GNUradio block from gr-baz.

The first thing we note when reverse engineering the signal is that it is a bit more than 4kHz wide. We know that it is probably a PSK signal of some sort, but BPSK and QPSK are both good candidates. To see which type of PSK it is, we study the powers of the signal. We raise the complex baseband PSK signal to the power 2 and observe if there is a large DC component in the signal we get. In this case, there is, so the signal is BPSK. If there wasn't, we would raise the signal to the power 4, where a DC component would indicate QPSK, and so on in order to detect higher order PSK. This trick works because of the symmetry of the PSK constellations. The BPSK symbols are 180º apart (i.e., they are oposite) while the QPSK symbols are 90º apart (i.e., they are related by the 4 roots of unity of order 4). Thus, when raising to the power 2 the BPSK symbols they become the same symbol, so the resulting signal has DC. Similarly, when raising to the power 4 a QPSK signal, the four symbols collapse.

Next, we use cyclostationary analysis to deduce the symbol rate. We multiply the signal by the complex conjugate of the signal delayed one sample. The resulting signal will have a strong DC component. The next strong frequency component will be at a frequency equal to the symbol rate. This works because the signal and the delayed signal are more or less equal most of the time, since both samples are still in the same symbol, thus we get 1 most of time. However, when the signal just jumps from one symbol to a different symbol, the signal and the delayed signal will be very different and we get something that is not 1. Some sort of spike. Thus, we get a spike every time a symbol change happens, so the resulting signal has a strong frequency component at the symbol rate. In the cyclostationary analysis for the Outernet signal I got a symbol rate of 4200baud, which is a nice round number and hence seemed to be right.

In hindsight, it was already known that the signal is 4200baud BPSK, but running these sort of tests allows us to confirm that the modulation has not changed recently.

Since we know that the signal is 4200baud BPSK but the data stream is only quoted to be around 2kbps by Outernet, we suspect that an r=1/2 FEC is in use, probably the usual r=1/2, k=7 convolutional code with CCSDS polynomials. Since this code admits some variations, the Auto FEC block from gr-baz is very useful, as it tries many combinations until a low bit error rate is achieved, to try to detect automatically the variation used. This block needs a patched version of GNUradio, because it is necessary to modify the Viterbi decoder to make it output bit error statistics. The patch given by Balint is for an older version of GNUradio, so I had to modify it. Here is a patch that works with GNUradio 3.7.10.1, in case anyone needs it.

The Auto FEC block works only with QPSK. I modified it to make it work with BPSK (only). Probably the best thing to do would be to make it support several PSK constellations. Here is the BPSK patch for Auto FEC.

The Auto FEC block found that the convolutional code used is the same that the "Decode CCSDS 27" GNUradio block expects but with the polynomial order swapped (first POLYB and then POLYA). Therefore, each pair of soft symbols needs to be swapped before the Viterbi decoder. As with any BPSK signal coded with an r=1/2 convolutional code, there is the ambiguity of how to make the pairs in the soft symbol stream. Thus, we run one swap + Viterbi chain on the soft symbol stream and another chain on the soft symbol stream delayed one symbol.

Using the patched Viterbi decoder, we can see that our Viterbi decoder is working because of the low bit error (which corresponds to a positive and almost constant output in the statistics output of the modified "Decode CCSDS 27" block). We suspect that we need to use a descrambler after the Viterbi decoder and a raster plot of the bitstream confirms it. We can try popular asynchronous descramblers to see if there is any luck and we get some structure in the raster plot. For instance, the polynomial used in G3RUH 9k6 packet radio is a good first choice. In this case, there wasn't any luck.

Since I have the binaries for the Outernet closed-source receiver, there is another way to attack this problem: to reverse engineer the assembler code. I'm using the Linux x86_64 L-band receiver binaries. These are not the latest version, but they seem to work. The latest version is only available for Linux on ARM, since Outernet targets single board computers such as the Raspberry Pi 3 and the CHIP to be used as the receiver. They now advise to run their ARM software in a virtual machine if one wants to use a desktop computer. I disassembled sdr100 (this is done with objdump -D) and quickly found that the scrambler is implemented in a function called scrambler308, which is not very long. I translated the assembler code back to C. This is a slow process which requires concentration. The code I got was pretty close to the code that has finally made it into gr-outernet.

As you can see in the code, the descrambler has some sort of counter that gets reset sometimes. The counter also influences the output bit sometimes. This is something I hadn't seen before, as I'm used to the multiplicative scramblers I've described in a previous post. Using "308" as a keyword for my search, I found out that this scrambler is called IESS-308 scrambler. It is described in an Intelsat document which is not publicly available. However, I managed to find a description of the scrambler in another document (see page 28). As you can see, the diagram in this document matches the C code obtained from descrambler308, except for the fact that descrambler308 inverts the output bit.

At this point, we have to worry about signal polarity. As you may know, when receiving a BPSK signal there is a phase ambiguity of 180º which translates to the fact that we have ambiguity on the signal polarity. We don't know if we are receiving the original bitstream or an inverted version of it. Generally, differential coding is used to resolve this ambiguity, but when using a Viterbi soft decoder, the differential decoding is done after Viterbi decoding, just because the Viterbi decoder works on soft symbols, and the differential decoder can't provide soft symbols.

The question now is whether differential decoding should come before or after the descrambler or whether it is used at all in the Outernet signal (there are other ways to resolve the polarity ambiguity). When thinking about this, it is good to know what happens with the various processing blocks if we feed in an inverted version of the signal we expect. It is well known that for a Viterbi decoder with the usual CCSDS polynomials we just get an inverted output (except for a few bits at the beginning of the stream). This is just because the CCSDS polynomials have an odd number of nonzero coefficients.

The IESS-308 descrambler has the same property, because the reset line for the counter is obtained by XORing an even number of bits in the stream, while the output depends on an odd number of bits in the stream. Thus, if we hook up the descrambler directly after the Viterbi decoder we still have a polarity ambiguity on the signal.

Figuring out the differential coding issue was probably the hardest part. It was a matter of blind trial and error. Most tries will yield some kind of noticeable structure on the raster plot of the output. This means that the descrambler is working and we're on the right track.

By examining the assembler code of sdr100 we know that HDLC is used in some way for framing, as several of the names of the functions refer to HDLC. However, I didn't manage to get any valid HDLC frames with my deframer from gr-kiss. I also reverse engineered the checksum functions of sdr100 just in case a different checksum was used. It turned out to be a table-based implementation of CRC16-CCITT, which is the checksum specified for HDLC, and bit-endianness was handled correctly. So, nothing unexpected here.

The solution turned out to be something really simple: no differential coding is used. Since there is an ambiguity on the polarity of the bitstream, an HDLC deframer is run both on the bitstream and on the inverted bitstream. One of these two deframers will successfully get the frames. Thus, there is a total of 4 HDLC deframers, since we also have 2 Viterbi decoders. With this decoder setup, I started to get correct HDLC frames from the Outernet signal.

To sum up, the specifications for modulation, coding and framing of the Outernet L-band signal are:

  • 4200baud BPSK
  • r=1/2, k=7 convolutional code with CCSDS polynomials (polynomials swapped)
  • IESS-308 scrambler
  • No differential coding
  • HDLC framing

With these specifications, the decoder in gr-outernet is able to get Outernet frames from the L-band signal. I still don't know much about the protocols used in the Outernet frames, since I need lots of them to try to detect some patterns, scan for plain text contents and so on. However, I have already noted a few things.

This is a typical Outernet frame:

pdu_length = 276
contents = 
0000: ff ff ff ff ff ff 00 30 18 c1 dc a8 8f ff 01 04 
0010: 3c 02 00 00 18 00 01 00 00 00 08 11 10 e5 21 4b 
0020: 48 2c e0 77 00 86 4d 14 06 3c 24 f7 30 e7 19 4c 
0030: ed 60 d4 44 94 6a 4a 18 34 ad b2 b5 92 01 b7 87 
0040: 06 ba 80 61 a5 87 06 80 f6 04 12 f6 d9 12 13 02 
0050: 64 0b 68 94 21 36 01 ab af 01 50 d0 13 4b dc b6 
0060: 92 90 6b f4 76 27 73 3d 91 f5 84 3d 75 d9 77 90 
0070: d2 74 15 49 66 e5 9a 57 df df 72 28 32 48 97 ed 
0080: 9a 46 6e 68 8e 72 b3 54 5f 52 ce f6 f5 de c1 fd 
0090: e4 e6 f8 a2 bd bb bb 65 cf 9e d0 ed 80 1e ad 8c 
00a0: 0c b8 59 28 41 cf 27 d3 cf a9 9e 28 06 8e c0 c8 
00b0: 42 7a bd ea da ae 7e 41 ee 24 c2 f9 28 b7 35 f6 
00c0: 8b 12 13 23 1f fb 0d 3e 32 49 b9 75 4b 31 d3 29 
00d0: 11 c1 48 a2 3b d4 8b 40 e6 2c 69 02 59 f2 f8 c8 
00e0: d2 ea aa ce 63 57 ed f7 25 42 8e 9b 21 d4 64 07 
00f0: 89 59 d0 47 d6 7b c7 3c c7 11 2c 91 d3 ca b1 52 
0100: ea ba be e3 00 39 fb be 6a 02 52 e3 8f ac ba 30 
0110: b7 d1 c2 3f

I expect that this contains a chunk of a compressed file, since this is most of the Outernet traffic. Almost all the frames are 276 bytes long. At a rate of almost 2100bps (you would have to take bit-stuffing into account), a frame takes about 1.05 seconds to transmit. This is pretty good. Each second you get a new packet if the signal is good enough for the Viterbi decoder to do its job. If the lock on the signal is lost momentarily, you only loose one second of data.

The thing that intrigues me most about the frames is that they look very much like Ethernet L2 frames. The destination MAC would be the broadcast MAC ff:ff:ff:ff:ff:ff,
which makes sense, and the source MAC would be 00:30:18:c1:dc:a8, which turns out to be a valid universally administered MAC address with an OUI assigned to Jetway Information Co., Ltd. There is some company called Jetway Computer which makes embedded computers for industrial applications, so perhaps this makes sense. However, the ethertype would be 0x8fff, which is not used for any standard protocol. I think that it is likely that this is the case: the frames are actual ethernet frames and the contents are some lightweight UDP-like protocol that rides just on top of ethernet (without an IP layer). I haven't found a standard protocol that does such a thing and matches the structure of these packets. Probably Outernet has come up with some simple ad-hoc protocol. I don't know why would anyone waste 5% of the bandwidth of an already low bitrate signal to send Ethernet headers (which are useless to the receiver), but it's fun to see Ethernet frames being downlinked from geostationary orbit.

I've also being playing with injecting the few frames I have (Scott's recordings were only around 1 minute long) into ondd to see if it does anything useful with them. ondd listens on a SOCK_SEQPACKET Unix socket at /tmp/run/ondd.data and sdr100 writes the frames it decodes to this socket. Unfortunately, socat doesn't have good support for SOCK_SEQPACKET sockets, but it's easy to write a little program that listens for the frames from the GNUradio decoder by UDP and writes them to the ondd socket. Here you have such a program. I'll probably publish it in a better manner in the future. strace is a great tool to do this kind of research. That's what I used to discover the type of socket that ondd uses and I also use it to keep an eye over ondd to see if it does something.

Using this technique I managed to spot two special packets:

pdu_length = 60
contents = 
0000: ff ff ff ff ff ff 00 30 18 c1 dc a8 8f ff 00 1c 
0010: 3c 00 00 00 81 00 00 18 01 04 6f 64 63 32 02 08 
0020: 00 00 00 00 57 f6 94 20 48 3a ca 8d 00 00 00 00 
0030: 00 00 00 00 00 00 00 00 00 00 00 00

pdu_length = 60
contents = 
0000: ff ff ff ff ff ff 00 30 18 c1 dc a8 8f ff 00 1c 
0010: 3c 00 00 00 81 00 00 18 01 04 6f 64 63 32 02 08 
0020: 00 00 00 00 57 fa a0 b0 11 56 ab ab 00 00 00 00 
0030: 00 00 00 00 00 00 00 00 00 00 00 00

As you can see, these packets are shorter than the regular packets. When ondd receives one of them, it tries to set the system clock (of course it fails, since I don't run it as root). The timestamp it uses is 0x57f69420 for the first packet and 0x57faa0b0 for the second packet, which are at position 0x24 inside the packets. Moreover, these timestamps correspond to "Thu, 06 Oct 2016 18:12:48 GMT" and "Sun, 09 Oct 2016 19:55:28 GMT", which match well the time that Scott's recordings where made.

Therefore, it's pretty clear what these packets are: they are just a time packet that is used to set the clock on the receivers. This is a very good idea, since the single board computers used as receivers do not have a real-time clock and of course using NTP is not an option. I expect that a time packet is transmitted every minute more or less.

By now, I don't know what are the other 4 nonzero bytes that follow the timestamp. I don't know why there are so many zero bytes either. The beginning of the packet is the same for both, but I expect that this is some sort of header that identifies the service, probably using some concept of ports.

So there you have it, someone could use all this information to build a fully functional Outernet clock. I hope that in the future we will know how to pull out more useful data from the frames.

13 Replies to “Reverse engineering Outernet: modulation, coding and framing”

  1. What a great development,

    Regarding the ethernet frames, good it be some kind of mpls technology ?
    This would add the ability to have multiple subnets using the same infrastructure.

    Thanks Daniel!

  2. Hi Daniel,

    Kudos and thanks for the remarkable signal forensics work. I am building a feed for a small dish antenna for receiving Outernet and will certainly play with the gr-outernet block.

    73, Edson PY2SDR

  3. Very impressed Dani. It's really helpful to have this level of detail and explanation. Your efforts on all of these projects both inform and inspire. Will await further developments!

  4. Something that pops out at me right away, is that the bytes after ethertype are payload length (2 bytes). (0x001c for frames with pdu length = 60, looks like the data is padded out and only 0x001c bytes of it are valid; length 0x0104 for the first bigger packet also checks out as packet length excluding the packet header (16 bytes)). Hopefully it helps with building fake frames to send to ondd.

    Probably reverse engineering/disassembling the ondd binary will give good clues as to the packet structure.

    1. Correct. I'm now pretty sure that the 2 bytes after the ethertype are payload length. I've reverse engineered the protocols to a point where it is feasible to construct a file receiver. More on this on a future post.

      The problem about the ondd binary is that it's C++ (sdr100 was C), which tends to be much more verbose (in terms of assembler produced) than C, and you need to get a good view of which classes are used and how.

      The only serious problem I have right now is that most files are transmitted using an LDPC code. This allows the receiver to reconstruct the whole file even if some blocks are missing. The problem is that I don't know which LDPC codes are being used, so I might have to disassemble ondd for that.

  5. This is very impressive work. I have one question. I am not clear the reasoning of Complex to Real after Costas Loop. I expected "Complex to Float" then both real and img parts going to an "Interleaver" instead. May be I am not clear what "Decode CCSDS 27" is looking for. Can you please explain

    1. The signal is BPSK, so the imaginary part only contains noise. Your description doesn't seem to make sense even if the signal was QPSK, unless you multiply the first by 1+i or something like that.

  6. I think the large amount of 0x00 at the end of the time packet is due to Ethernet padding. An Ethernet packet should be at least 60 bytes long for the collision detection to work with the maximum cable length. The packets shown are exactly 60 bytes. It seems they are just uplinked directly as-is.

    1. Thanks! Indeed that's the case. All the packets are padded to 60 bytes at least. It's good that you pointed out that this is because of a limitation of physical Ethernet packets.

  7. I really enjoyed reading the process of analyzing and reverse engineering these signals even though I understood less than half of it. I obviously should have paid more attention during maths in high-school. Reading this has made me curious enough to want to make a go of it even though I'm only a novice with Linux.
    Looks like I need to learn a bit more GNUradio and some more Python. Thanks Daniel for your effort and sharing. 73 vk2byf

Leave a Reply

Your email address will not be published. Required fields are marked *