Blog

When Athletic Abilities Just Aren't Enough - Scoreboard Hacking Part 3

10/03/2022

This is the final installment of a three part series on scoreboard hacking. Please go read parts 1 and 2 first if you have not been following along; they provide much of the background and context of this post. The sections are listed below:

  • Introduction & Signal Analysis - Part 1
  • Stealing the AES Key via Hardware Hacking - Part 2
  • Full Protocol replication in Python and GNU Radio - Part 3 (This Post)

In the previous post, we found the AES key used for encrypting the packets via hardware hacking. To end the series, we will go through creating legitimate packets with Python and using GNU Radio to put the bits in the air. With the ability to create arbitrary packets, we will launch attacks against the scoreboard to impact real sporting events. Enjoy all of the shenanigans required to get this working :)

For a packet to be accepted, everything has to be perfect. Encryption, data whitening, CRC, baud rate... everything. Since we do not have a packet in an intermediate state, such as encrypted but not whitened, this complicates the matter further. We will have to perform both de-whitening and decryption consecutively to get our original data back. From a reverse engineering perspective, this is less than ideal but we must move forward! To start with this task, we will dive into the data whitening process.

Data Whitening

Data whitening is the process of randomly permuting bits being transmitted; the goal of the randomization is to guarantee no consecutive long string of only 0s or only 1s. In the case of the RF module, the benefit of the lack of consecutive strings is better clock synchronization between the transmitter and receiver. To replicate the effect of data whitening on the RF module, we will need to learn how the data whitening is performed. This is shown in Figure 1, which is from the RF module documentation.

Data Whitening Documentation Scoreboard Hack
Figure 1: Data Whitening in Documentation

Random data (more on this generation later) is XORed with the data to be transmitted ("transmit data" in Figure 1) that we want to send. The resulted is the whitened byte, which is the byte that is sent over the air. For example, 0x19 ^ 0xFF = 0xE6, where 0x19 is the transmit data, 0xFF is the random data and 0xE6 is the whitened(output) byte. The data whitening is why the length of the packets was wrong originally in part 1.

The documentation shows that a Linear Feedback Shift Register (LFSR) with a polynomial of X9 + X5 + 1 is used generate the random numbers for data whitening. "Woah woah woah," I hear you saying "What is an LFSR anyway?" Well, let's find out.

Linear Feedback Shift Registers (LFSR)

Linear Feedback Shift Registers (LFSR) Movement Demonstration
Figure 2: LFSR in Action

An LFSR is an old school random number generator that is still used throughout the world. An LFSR is made up of registers which hold a single bit of data. The shift is moving a bit from one register into another. The linear feedback is that the new input (X2) is generated from the other registers. There you go: the whole acronym broken down!

How does the LFSR really work though? Let's break down Figure 2. Notice that these operations are unordered, as everything should happen simultaneously.

  • Bits from the registers (the boxes labeled as X2, X1 and X0) are taken as input. In the case of Figure 2, this is XORing registers (bits) X0 and X2 in order to produce the new input bit that goes back into X2.
  • Shift all of the bits into the next register. The final bit is known as the output bit. The output bit is our random data.

The gif in Figure 2 has a polynomial of X2 + 1 (using addition modulo 2). All the polynomial indicates is which registers (bits) are used to generate the input. In the case of the RF module, it uses a much larger amount of registers (9) in its operation to generate much more random data. With the LFSR polynomial used in the RF module(X9 + X5 + 1), it can generate 511 random bits (≈64 bytes) before looping around all possible states. This information helps us solve certain problems later in this post. For more on LFSRs and maximum polynomials, please read this article.

LFSR Initialization Value

Why is the maximum amount of numbers 511 instead of 512 or 29? Since the LFSR uses the XOR operation, we cannot end with a state of all 0s. Otherwise, it will lead to an infinite repeating of 0s; this is because 0 ^ 0 = 0. As a result, we end up with only 511 possible outputs. Since the initialization value is nowhere to be found in the documentation and cannot be zero, we need to uncover the starting value in order to implement the LFSR ourselves.

Brute Forcing the Initialization Value

RF module packet format for scoreboard
Figure 3: Packet Format from documentation - Length Emphasis

One major challenge of writing the code is that we have to do the data whitening and encryption properly in the same step, since we have no intermediate steps. However, this is not entirely the case. If you look at Figure 3, two data sections are whitened but not encrypted: the length byte and the CRC. Additionally, the length byte is the first byte that gets whitened. This is a gold mine of a discovery!

We know BOTH the whitened and de-whitened values of the length of the packet. We know the de-whitened value from sniffing it off the SPI bus in part 2. From the bus sniffing, the size of the smaller packet is 0x19 and the larger packet is 0x34. Additionally, we know the whitened value by reading it from the wireless signals in part 1. The smaller packet has a whitened value of 0xE6 and the larger packet has a whitened value of 0xCB. Again, we know both the whitened and de-whitened length values of the length of the packet.

Initialization Brute Forcing Table
Figure 4: LFSR Initialization Brute Forcing Table

Since we know the inputs and outputs of the system, we can brute force the initialization value with a guess-and-check scheme on the length byte! By attempting all 511 possible starting states for the LFSR, we can find the initialization value. Simply put, whichever initialization value gives us the proper output for both the small and large packet must be the correct value. The easiest way to do this is writing a Python script to try each initialization value until the outputs are what we expect.

A table showing the brute force attempts can be found in Figure 4; the Python script just goes through all possible initialization values and checks the output to see if it matches what we want. After running the script we get a few outputs: 0x001, 0x0FF & 0x1FF. Common sense tells us that the initialization value is likely all 1s (0x1FF). If I were a developer, this is probably what I would have gone with. Once we have the decryption working, we can dwindle the options down further by running the whitening process on the next (2nd) byte as well, which only will work with the correct initialization value. For now, we will assume that the initial state is all 1's but keep the 3 possible values in mind. Major S/O to Max Arnold for the help on the LFSRs as well.

The code for implementing the LFSR and data whitening is too large for a code snippet. But, it can be found on Github at scoreboard_hacking/lfsr.py.

Encryption/Decryption

Now that the data whitening is being performed properly (mostly), all we have to do is get the encryption/decryption process correct. For the purposes of this post, we will go through the easier process of decryption.

AES Decryption Code scoreboard hack
Figure 5: AES Decryption Code

From reading the documentation (and part 2 of this blog post), we know this is using AES128-ECB mode. All we have to do is fiddle with the settings until the encryption/decryption works, such as the endianness of the key. The key is 0x010203...0x8. Eventually, all of the stars align to get the de-whitened and unencrypted data from the packet! We had to try out the three different whitening initialization values; the only correct one was 0x1FF. But, how do we know when the output is correct? Since we sniffed the data off the SPI bus in part 2, we have plaintext to compare against. Additionally, a randomness test (entropy) on the bits could be performed to determine if the decryption was successful or not.

The code used for decryption is shown in Figure 5. Once we know the decrypted data, we can find the padding (the bytes used for block alignment) that was used for AES encryption. As one would expect, this was just 0x0. However, it helps to verify all assumptions as you go along.

Cyclic Redundancy Check (CRC)

A Cyclic Redundancy Check (CRC) is used for detecting errors with the transferring of data. For instance, network packets contain a CRC to verify that the packet has arrived intact without corruption. The actual check is the data verification that is being done, ie. comparing two values and ‘checking’ they’re the same. The redundancy comes from the idea of appending ‘redundant’ information to the packet that can then be used to verify that it has not been corrupted. Cyclic describes the type of code used; meaning, that a cyclic algorithm is used to perform the check on the data that has been redundantly attached to the end of the payload.

In our case, the CRC is 2 bytes long, tagged to the end of the packet and uses the standard polynomial X16 + X12 + X5 + 1. This is used widely for CRC implementations and the polynomial can be found in the documentation of the RF module. However, simply plugging in our input to a CRC calculator online with this polynomial is not working. Why is this?

Recreating the CRC

By pure happenstance, I had been listening to Smart Meter hacker Hash on the Unnamed Reverse Engineering podcast. While talking about reverse engineering the meters, they were talking about how to find the CRC used by the device. Hash laughed and mentioned he just uses the tool CRC RevEng; it's a cheat code for finding the algorithm and parameters of CRCs! If you plug in the raw bytes of your data, it will magically find out the length of the CRC, the polynomial and everything else about it. Neat!

Reverse Engineer CRC RevEng
Figure 6: CRC RevEng Tool

Naturally, after hearing about this magic CRC tool, you would download it! If you plug in an entire dewhitened packet (since the CRC is generated prior to whitening), we may be able to find out the parameters being used. In Figure 6, the blue line is setting flags and calling the binary. The red lines are 4 inputs for the system, including the CRC at the end. The tool needs multiple inputs to narrow down the parameters used. Finally the pink line shows the CRC parameters outputted from the tool. Wow, this is truly magic!

The only odd thing in the output is the initialization value. This particular initialization value was on none of the several online sites for calculating CRCs; apparently, the RF module uses a non-standard CRC starting value. To implement this in software, we just import a module to do that for us: crcmod. We plug in the newly discovered parameters and we have a fully functioning CRC!

Incorrect 3 Bytes

Everything works now, right? Well, only the smaller packets are correct after going through the whitening and encryption. Whenever a large packet is generated, it is always wrong. Why is this?

Debugging the 3 Bytes

Upon further inspection, the final 3 bytes for a large packet are always wrong. In particular, the final byte of the encrypted message and the final two bytes of the CRC are causing problems. At least, they were wrong according to the received packets. Is our CRC wrong? Is the whitening off? Let's get to the bottom of this.

Upon banging your head against a wall for a while, a strange realization would be made: the mistake happens at exactly 64 bytes of data! The mistake being at 64 bytes is interesting because the cycle for the LFSR is at 64 bytes as well. The mistake appears to be in the whitening implementations wrap around of the LFSR. How do we solve this problem though?

Finding the 3 Bytes

When an LFSR wraps around correctly, it should continue on its same cycle as before. Even though our implementation wraps around, the LFSR on the RF module must be doing something different on the wrap around. In our case, we do not care about a perfect LFSR implementation; we only care about mimicking the RF modules implementation.

LFSR Magic Bytes
Figure 7: LFSR Magic Byte Finding

How do we figure out what the RF module is doing? Math! The data whitening XORs with the random bit from the LFSR and the message to send. With XOR, if you know two values, you can always recover the third. In our case, we know the message data (sniffed off of SPI bus) and we know the output of the packet (received as a wireless signal). All we have to do is XOR the whitened packet byte and expected output byte to recover the LFSR bits used for whitening. For instance, 0x57 is the whitened byte and 0xA8 is the unwhitened packet byte. 0x57 ^ 0xA8 will give us the byte used for whitening: 0xFF! Figure 7 shows the math performed for each of the bytes in order to recover the bytes being used for data whitening. For the results: the final byte of the encrypted data is whitened using the value 0xFF. The two bytes of the CRC are 0x01 and 0xC3 respectively. Now, we can successfully craft large packets!

I realized an error in my thoughts while writing this blog post. Technically, the cycle is 511 bits and not 512 (64 bytes). This means that our trick is off by 1. By sheer luck, this mistake does not matter because the generation of our LFSR is the same as the bit from the LFSR on the RF module. However, working with subsections of bytes in diagrams would be horrible. So, for simplicity, I kept the off-by-1 bit mistake in the diagrams. Additionally, the code was left alone since it works and looks cleaner this way. So keep thy snarky corrective comments to thyself on this, redditors.

GNU Radio Integration

SDR & GNU Radio

At this point, this entire blog post has been code written in Python that stays on the computer. In order to impersonate the controller to change the scoreboard, we will need to translate the bytes into wireless signals. How is this done? Welcome to the world of Software Defined Radios (SDR) and GNU Radio!

SDR is exactly what it sounds like: a radio communication system where the components are implemented in software. The software controls the output of the radio in order to produce arbitrary radio signals. Honestly, I have no idea how the SDR works but they are pretty damn amazing. A few examples of SDRs are the HackRF (what Jesse and I used for this project), BladeRF and USRPs.

GNU Radio is software to build digital signal processing (DSP) systems on top of SDRs. There is a Python SDK for this that implements everything under the sun required for low level processing, such as filters, amplifiers and much more. However, the primer way to use GNU Radio is to use the companion software. The companion has blocks that can be visually added and connected for easy programming. The companion makes life much easier for programming DSP applications but still requires a fairly intimate knowledge of how DSP actually works to use. There is no Frequency Shift Keying block that just works, for instance. So, we will have to create our own using lower level components from scratch.

Creating a Working Flow Graph

Most of the post explains how everything works with a step by step walk through. However, to do that for the flow graph would require an additional blog post and expertise that I do not have. So, the flow graph (shown in Figure 8) is explained below in a very matter-of-fact way with only the important components. S/O to Derek Kozel from GNU radio for significantly helping Jesse and I with this project. Additionally, thanks to Jesse for these explanations and the nice comments on the flow graph.

Gnu Radio Flow Graph
Figure 8: Gnu Radio Flow Graph For Scoreboard

The top layer of the flow graph is a list of variables for various things. The important variables are listed below:

  • digital_sequence: The raw list of 0s and 1s to send to the scoreboard.
  • bits_per_second: The baud rate of the transmission. This is how fast an individual bit should be understood from the digital_sequence. From part 1 of this post, this is 250K bits per second.
  • sample_rate: The rate at which the SDR should receive data from GNU Radio to output. Although this seems unintuitive, we have to ensure the rate of the bits going through the system is the same as the bits being outputted to the SDR.
  • FSK_offset_MHz: The distance between the two frequencies used for frequency shift keying (FSK). In our case, this is 922MHz-923MHz for a 1MHz offset.
The flow graph itself has a few main components with several supporting ones. Only the main components are described below. The rotator and low pass filter improve the signal but are too complicated to explain in this post.
  1. Vector Source: The input for the system. In our case, this a list of 0s and 1s that each represent the data being sent for the full packet. If we want to change the data being sent, we simply edit the variable digital_sequence to do so.
  2. Repeat: Repeat the digital sequence of 0s and 1s in order to match the sample rate of the HackRF. We need to ensure that the baud rate coming from the flow graph matches the rate in which the SDR handles it.
  3. Voltage Controlled Oscillator (VCO): This is the real magic of the graph. With this component, the more amplitude that is provided, the higher the frequency is. For instance, 0 from the vector source is a lower frequency and a 1 is a higher frequency. This is how we convert the sequence of 0s and 1s to be frequency modulated. The variable FSK_offset_Mhz is the separation between a low and high frequency for FSK modulation.
  4. Osmocom Source: The output of the flow graph. This will send the data to the HackRF (SDR) that we are using to send the bits over the air.

So, how do we hook our previously created Python code into this? When a flow graph is compiled, it simply spits out Python code that can be used externally. So, we can import this into our main file and use the module in its own thread. To change the data being sent wirelessly, we edit the digital_sequence variable within the flow graph at runtime to change the data being sent. This allows our packet creation (whitening, encryption, formatting, etc.) to be completely independent of the flow graph! One function creates the packet and the other edits the data being sent in GNU Radio. Neat! For more information on the components used to create the wireless modem, please reference the flow graph or picture of the flow graph on Github and/or and visit the GNU radio documentation.

Mutually Assured Destruction

Alright, back to the walkthrough... One problem we have is the real scoreboard controller will be functioning while our hacker controller is trying to attack it. It is easy to say "Just get a more powerful signal." However, let's actually think about this. The HackRF is very low power. So, we'll need to buy a power amplifier with a 40dB gain in order to compete with the actual controller. Even with this though, we are around the same loudness as the scoreboard. Will this work? Even if this is high enough power, what happens when the two signals are sent at the same time? Let me introduce you to mutually assured destruction.

Attacker Packet Acceptance Mutually Assured Destruction
Figure 9: Attacker Packet Acceptance

The scoreboard transmitter sends short bursts of updates; it does not send data constantly. This can be seen in Figure 9 with the Controller at the top of the image with the pink color in the white box. This means we could send our signal during the breaks to get the receiver to output our malicious packet. Additionally, if we send data at the same time as the real controller, the packet CRC will get corrupted and fail! This is what I mean by "mutually assured destruction".

The real controller and our attacker controller can be around the same power with our attacker controller almost always being the received data. This is because of the pauses in transmission from the real controller and the mutually assured destruction. If our packet is sent at the same time as the real scoreboard, we corrupt the data, causing the scoreboard to not be updated with either controllers packet. If our packet is sent during the pause of the real controller, it will update the scoreboard with our result. An image of this concept can be seen in Figure 9 showing the real controller, the attacker controller and the received result on the scoreboard. Remember, if there is a corrupted packet, it simply gets dropped and nothing happens. Real neat!

Easy System Attacks

We have spent all of this time creating a custom controller in GNU Radio. Everything has led to this moment! These next few sections dive into the practical attacks that can be launched as a result of this work. Some of them are obvious and brutish, while others are sly and scary. Enjoy!

Replay

A replay attack is using previously observed information and repeating it. In the case of the scoreboard, the packets can be recorded and replayed to change the scoreboard display. Using Universal Radio Hacker (URH) it is trivial to record a packet and replay it with an SDR. This attack can be done without having any understanding about the underlying packet format or anything else about the signal.

Building off of the replay attack, a specific kind of replay called block swapping could be used as well. The encrypted scoreboard message is split into blocks of either 2 (small packet) or 4 (large packet). By using the encrypted data, with the known values on the scoreboard, we can match different blocks from separate packets to create arbitrary packets. This is possible because the encryption uses ECB mode, as covered in part 1. At this point, the CRC could be recalculated and the packet would be received. This ended up not being useful because of the sheer complexity and orientation of the blocks but is still interesting to call out.

Horn Denial of Service (DoS)

While testing the replay attack on our scoreboard, Jesse and I ran into a very strange bug. If one of the packets had the horn on and the other one did not, then the scoreboard would crash! The sound of this happening is hilarious. For a vocal on what this sounds like, listen here.

From the reverse engineering of the C# application, the crash appears to happen because these horn on/horn off calls are simply bash scripts. By alternating horn on/horn off calls, we are turning on and off the audio of the horn extremely rapidly. Eventually, this eats up all of the resources on the operating system and the system crashes. One interesting note is that a power amplifier is not even needed for this attack. An attacker could send a low power signal during the break of the actual transmitter, only for it to be toggled off again by the actual controller. Neat!

Complete Control - Changing the Score and Other fields

Figure 10: Controlling All of the Fields

From all of the previous steps, we have codified the packet creation (encryption, whitening, CTC, etc.) and the sending of packets in GNU radio. Now, we can create a simple program that acts as a controller to change the contents of the packet, such as the score and time. In reality, we can control anything on the display! This was the original goal from day 1 and it has been met. Arbitrary control of the scoreboard is game over :) Now, we have taken an 8-3 in the baseball game, as there is no coming back from this deficit. A video of a simple CLI tool alongside changes on the scoreboard is shown above in Figure 10.

Subtle Attacks

Instead of explaining this attack, you should try to find it for yourself first. Enjoy.

Figure 11: Fast Clock Attack

Did you see it? The clock is running at 0.9 seconds instead of 1 for each second. Damn, that hurts my brain... we can manipulate time! A human's perception of time is very bad. From our testing, most people do not notice a change of 0.1 seconds in either direction. Although these slight differences do not seem substantial, sporting events are commonly decided in the last fractions of a second. Taking away a few seconds could be the difference between a layout and a half court shot.

In many sports, such as basketball and football, time decides when the game is over and many other big decisions. Changing this to benefit one team would have drastic consequences. For instance, speeding up the shot/play clock for the one team could push them into bad decision making. In the NBA, speeding up the 24 second shot clock to 21.6 seconds for one team would give the other team a major advantage. Or, at the end of the game, we could extend the clock in order to give the losing team an edge. Or stopping the clock for a few seconds before continuing, giving the team an extra second to work with at the end of a game. This extra second could be the difference between a goal and no goal! Or… The possibilities for this attack are endless; frankly, it is really scary that this could effect an NFL, NBA or any other timed sports event.

Other Non-Obvious Attacks

The reason for the time manipulation working is that the scoreboard holds the source of truth about the clock. There are several other fields where the source of truth lives on a scoreboard. In a similar way to the time, if an attacker controlled these, it could influence the game. A few of these ideas are listed below:

  • Possession Arrow: In lower levels of basketball, if two players control the ball at the same time, this is called a jumpball. Instead of actually jumping, the possession of the ball alternates between teams. Since this happens regularly throughout the game, the possession is commonly kept track of on the scoreboard. If the possession arrow was changed at halftime or in the middle of a game, the referees may not notice, giving the ball to the wrong team on the next jumpball.
  • Team Foul Count: In basketball, when a team has reached a specific amount of fouls in the half or quarter, the other team shots free throws. By manipulating the total foul count for a team on the scoreboard, the referees may give a team free throws faster than they should. This would give the team shooting free throws a substantial advantage since they are getting free points.
  • Intentional Malfunction: Since the scoreboard holds the source of truth for many things, if the scoreboard stops working, the game stops as well. In basketball, a team may need a timeout to discuss strategy or to get a rest. By glitching out the scoreboard, an attacker could force a pause in the game. Since the malfunctioning is a common occurrence, no one would realize that something malicious was going on. This can be done with the horn as well.

Conclusion

All in all, this project pushed our skillsets to the limit. Jesse Victors was a badass partner in crime on this project; without his knowledge of radios, this project wouldn't have succeeded. Along the way, Jesse and I both had to learn how radios work and use many different skillsets, such as hardware hacking, GNU radio programming and a multitude of other concepts. From starting down the radio hacking path to project completion, this took a year and a few months to do. Finally, I had accomplished a childhood goal: change the score on the scoreboard. If you want more information on this, you can read through the source code on the Scoreboard Hacking controller repository and/or watch the extra YouTube videos at scoreboard hacking playlist.

Thanks for joining us in the first hack of a wireless scoreboard. I hope there are more interesting hacks to come! A special S/O goes to drtychai and Nathan Kirkland for reviewing these posts with wonderful feedback. We hope you found this interesting and learned all about radios, hardware hacking and GNU radio along the way. Feel free to reach out to me (contact information is in the footer) if you have any questions or comments about this article or anything else. Cheers from Maxwell "ꓘ" Dulin.