Devices on networks often communicate with software designed to send and receive messages that are encapsulated in packets. Packets typically contain information like the addresses of the sender and receiver and one or more messages. Examples include browser-server communication as well as phone-app-to-server communication. In such systems, sender and receiver software are designed to exchange packets in a compatible, expected format. But what happens when a packet is sent that is not in the correct format?
If new software or technologies are deployed that are not hardened against malformed packets, cyberattacks can end up exploiting their vulnerabilities. These kinds of attacks can cause a device or server to stop working. A malformed packet attack can even serve as a step toward gaining access to a network and its protected resources.
In this activity, you will use a pair of micro:bit modules communicating on a radio network to explore the kind of problems malformed packet attacks can cause. You will also learn how to update your scripts to prevent these kinds of attacks from interfering with your applications.
You will need:
Complete these tutorials first:
You will be able to:
Preventing fellow students from successfully mounting a malformed packet attack on one of your micro:bit apps might be a key ingredient in winning a networked robot race. If you end up writing other software apps, the habit of testing for exceptions and incorporating exception handling into your apps can also help keep those apps safe from attackers.
In this first activity, you will experiment with the countdown timer app from the Send and Receive Packets activity. Its original form is vulnerable to malformed packet attacks you will learn how to harden it against them. The malformed packet attack will cause the receiver to throw an exception. At that point, it will become unresponsive until its script is restarted or reloaded with the Flash button.
This activity requires two micro:bit modules running different scripts, a Sender script and a Receiver script.
Running 2 micro:bit modules: You can either connect each micro:bit to a separate computer or connect both to separate USB ports on the same computer.
# countdown_sender_w_malformed_packet_option from microbit import * import radio radio.on() radio.config(channel=7,length=128) sleep(1000) print("Countdown App") print("micro:bit sender") while True: text = input("Enter countdown start: ") value = int(text) message = input("Enter message after countdown: ") dictionary = { } dictionary['start'] = value dictionary['after'] = message # Sends malformed packet if value is -1 if value is -1: dictionary['start'] = "Not a number!" packet = str(dictionary) print("Send: ", packet) radio.send(packet) print()
# countdown_receiver.py from microbit import * import radio radio.on() radio.config(channel=7,length=128) sleep(1000) print("Countdown App") print("micro:bit receiver\n") while True: packet = radio.receive() if packet is not None: print("Receive: ", packet) print() print("Parse: ") dictionary = eval(packet) value = dictionary['start'] message = dictionary['after'] print("value = ", value) print("message = ", message, "\n") while value >= 0: print(value) sleep(1000) value = value - 1 print(message) print()
Let’s first make sure the sender and receiver micro:bit modules work as expected with normal packets. If the terminal doesn’t display what you type or display messages, try closing and reopening the terminal by clicking Close Serial and then clicking Open Serial again.
See Texting with Terminals if you need a reminder on how to set up the serial monitor and radio connections between the two micro:bit modules.
Now, it’s time to examine the effect a malformed packet can have on a script. At this point, the Sender serial terminal should be prompting you to enter another number of seconds to count down.
For background info on how the scripts work without the malformed packets, see How Radio Data Packets Work from the Cybersecurity: Radio Data tutorial.
Here is an example of a properly formed packet, sent by the sender micro:bit:
{'after': 'All done!', 'start': 3}
Even when you type -1 for the countdown start, the dictionary that’s created is this:
{'after': 'All done!', 'start': -1}
…but then, the script reaches these statements:
# Sends malformed packet if value is -1 if value is -1: dictionary['start'] = "Not a number!"
After that, the packet is changed to this:
{'after': 'Add done!', 'start': 'Not a number!'}
In the receiver script, the value corresponding to the dictionary’s ’start’ key is stored in a variable named value. When the receiver script gets to this line:
value = value - 1
…it tries to subtract 1 from the ’Not a number!’ string, and throws this runtime exception: TypeError: can’t convert ’int’ object to str implicitly. After the exception, the receiver micro:bit will not respond until it is restarted.
The receiver’s runtime exception from the malformed packet is easily preventable with exception handling. For the basics of how exception handling works, try or review the Exception Handling Primer.
After placing the susceptible portion of the script into a try: block and handling the exception with except:, the receiver will no longer stop running its script when it receives that kind of malformed packet.
This modified receiver script puts the while value >= 0: countdown loop into a try-except statement.
Applications don’t normally have just one vulnerability, so it’s best to test for more than one scenario. For example, what if the dictionary packet is changed to some other object, like maybe a string? This is just one example of the kinds of questions that application security validation specialists help with as part of software development.
Your receive script is hardened against one kind of malformed packet attack, but what if there are more? In the first example, the dictionary contained an unexpected term. What if the dictionary is instead not a dictionary? That would cause another kind of exception that the receiver script is not yet hardened against.
In Cybersecurity: Sniffing Attacks & Defenses, the Share Something Personal – Unencrypted? activity involved testing unencrypted transmission of emoji from one micro:bit to another.
In this activity, the application is a little more versatile, allowing you to scroll through the emoji with the micro:bit module’s A button and send it with the B button.
This application also has a routine for sending a malformed packet. In this activity, you will again study how the malformed packet can cause problems, mitigate them, and then also encrypt the communication.
You can either connect each micro:bit to a separate computer, or both to separate USB ports on the same computer.
# radio_send_receive_images_w_buttons from microbit import * import radio radio.on() radio.config(channel=7) n = 0 emoji = [ 'Image.YES', 'Image.NO', 'Image.HEART', 'Image.SKULL' ] image = eval(emoji[n]) display.show(image) while True: if button_a.was_pressed(): n = n + 1 n = n % len(emoji) image = eval(emoji[n]) display.show(image) if button_b.was_pressed(): packet = emoji[n] print('packet:', packet) if image is Image.SKULL: # Sends malformed packet packet = 'malformed packet' # Sends malformed packet radio.send(packet) packet = radio.receive() if packet: print('packet:', packet) n = emoji.index(packet, 0, len(emoji)) image = eval(packet) display.show(image)
Here you will first send correctly formed data by sending the YES, NO, or HEART images. After that, you will send the SKULL to trigger the malformed packet and observe the effect on the receiver micro:bit.
The script starts with its name as a comment, followed by importing the microbit and radio modules. Then, initialization starts by setting the radio to on and the channel to 7.
# radio_send_receive_images_w_buttons from microbit import * import radio radio.on() radio.config(channel=7)
The initialization continues by creating a global variable n and setting it to zero. Then, a list of emoji is created and named emoji. The image = eval(emoji[n]) statement uses eval to convert the string emoji[0] = ’Image.YES’ to the object Image.YES, and stores the result in a variable named image. Then, display.show(image) makes the micro:bit modules LED matrix display the Image.YES image, which is a checkmark.
n = 0 emoji = [ 'Image.YES', 'Image.NO', 'Image.HEART', 'Image.SKULL' ] image = eval(emoji[n]) display.show(image)
The main loop starts with a statement checking if the A button has been pressed. If so, it adds 1 to n. The statement n = n % len(emoji) is a trick to keep the value of n cycling through index values of n that never go out of range. With four emoji in the list, repeatedly pressing and releasing the A button result in n incrementing 0, 1, 2, 3, 0, 1, 2, 3,… and so on. After the first press of A, the value of n has been incremented from 0 to 1. So, instead of Image.YES (a checkmark), eval(emoji[n]) returns Image.NO (an X), which is stored in the image variable and then displayed by display.show(image).
while True: if button_a.was_pressed(): n = n + 1 n = n % len(emoji) image = eval(emoji[n]) display.show(image)
If the B button is pressed, the packet variable is set to emoji[n]. For example, if n is 2, packet will be set to ’Image.HEART’, or if n is 3, packet will be set to ’Image.SKULL’. Both of those are strings that represent the image to transmit. The print(’packet:’, packet) statement displays the string that packet stores in the Serial terminal. Back when button A was pressed, the image variable was set to the corresponding Image object. So, when n was 2, image became Image.HEART or when n was 3, image was set to Image.SKULL. Basically, the image variable stores the Image object that is displayed, and packet stores the corresponding string that will be sent to the receiver. The receiver will convert that string to an Image object and display it.
if button_b.was_pressed(): packet = emoji[n] print('packet:', packet) if image is Image.SKULL: # Sends malformed packet packet = 'malformed packet' # Sends malformed packet radio.send(packet)
The if image is Image.SKULL: statement above compares the image the transmitting micro:bit displays to Image.SKULL. If it’s a match, the packet is changed from ’Image.SKULL’ to a string that simply says ’malformed packet’. If the string does not contain an image that the receiver recognizes, it will cause a runtime exception, so the string could instead contain something else, like ’abcdefg…’, for example, and it would have the same effect.
Every time through the while True: loop, the packet = radio.receive() statement checks for incoming packets. If one is waiting in the radio receiver’s buffer, the if packet: statement processes and displays it. First, it displays it in the Serial terminal with a print statement. Then, it uses n = emoji.index(packet, 0, len(emoji)) to find the index value that matches the incoming string.
For example, if the incoming string is ’Image.HEART’, the emoji.index method will return 2, which gets stored in n. Without this statement, you might have received an image and press B to try to send it back since it’s displaying, but without updating the value of n like this, it might send a different image. Then, image = eval(packet) changes a string like ’Image.HEART’ to an Image object like Image.HEART, and finally, display.show(image) displays the incoming image on the receiving micro:bit.
packet = radio.receive() if packet: print('packet:', packet) n = emoji.index(packet, 0, len(emoji)) image = eval(packet) display.show(image)
As with the first activity, exception handling can prevent the micro:bit from freezing in response to a malformed packet.
What does a micro:bit terminal now display when it receives a malformed packet?
Neither encryption nor exception handling alone can entirely protect an app, but used together, they improve its chances of not being intercepted AND not halting script execution due to an exception.
Now it is time to implement exception handling + encryption with the Share Something Personal app.
Many robots and other manufacturing machines have their work orchestrated by computers on a factory network. If there is any kind of connection to the outside world, a malformed packet might provide a cyberattack entry and then cause malfunctions.
Similarly, in a remote-controlled navigation contest, your cyber:bot robot might also be vulnerable to malformed packets in addition to sniffing attacks. So, we’ll now enhance the application from Cybersecurity: Navigation Control from a Keyboard.
In Your Turn: Handle Transmitter Exceptions, you already hardened the sender against accidentally entering malformed packet data, but the receiver is not yet hardened against intentional attacks. So, in this activity, you will harden the Terminal Control — Go Wireless! app against both kinds of attacks.
# terminal_bot_controller_wireless_malfomred_packet_attack from microbit import * import radio radio.on() radio.config(channel=7,length=64) sleep(1000) print("\nSpeeds are -100 to 100\n") while True: try: vL = int(input("Enter left speed: ")) vR = int(input("Enter right speed: ")) ms = int(input("Enter ms to run: ")) dictionary = { } dictionary['vL'] = vL dictionary['vR'] = vR dictionary['ms'] = ms # Sends malformed packet attack if ms to run is negative. if ms < 0: dictionary = { 'vL': -20, 'vR': 20, 'ms': "Not a number!" } packet = str(dictionary) print("Send: ", packet) radio.send(packet) print() except: print("Error in value entered.") print("Please try again. \n")
# terminal_controlled_bot_wireless from cyberbot import * import radio radio.on() radio.config(channel=7,length=64) sleep(1000) print("Ready...\n") while True: packet = radio.receive() if packet is not None: print("Receive: ", packet) dictionary = eval(packet) vL = dictionary['vL'] vR = dictionary['vR'] ms = dictionary['ms'] bot(18).servo_speed(vL) bot(19).servo_speed(-vR) sleep(ms) bot(18).servo_speed(None) bot(19).servo_speed(None)
Let’s first make sure the sender micro:bit and receiver micro:bit in the cyber:bot work as expected with normal packets. Since we are going to leave the cyber:bot tethered with USB, we’ll use some short distance maneuvers with low speeds and short run times.
Now it is time to see the effect that a malformed packet can have.
If your micro:bit was not tethered, it would be stuck until a human came and restarted it by pressing and releasing the micro:bit module’s reset button! That is not always possible in a competition, but you can do it now.
The original Sender script terminal_bot_controller_wireless.py is explained here: How the Wireless Controller Script Works.
The exception handling added to the Sender script is explained here: Your Turn: Handle Transmitter Exceptions.
The cyber:bot Receiver script terminal_controlled_bot_wireless is explained here: How the Wireless Controlled Bot Script Works.
With that aside, let’s look at how the Receiver script terminal_controlled_bot_wireless was modified to send a malformed packet in response to a negative value.
At this point in the Sender script, the maneuver values have all been collected, and before the statements below, the dictionary looks like this: { ’vL’: 25, ’vR’: 25, ’ms’: -1 }. If the ms variable receives -1 from ms = int(input(“Enter ms to run: “)) earlier in the script, the if ms < 0: statement detects it, and changes the dictionary to dictionary = { ’vL’: -20, ’vR’: 20, ’ms’: “Not a number!” }.
# Sends malformed packet attack if ms to run is negative. if ms < 0: dictionary = { 'vL': -20, 'vR': 20, 'ms': "Not a number!" }
If you guessed that the receiver script also needs protection with a try-except statement, you were right!
As mentioned earlier, devices on a network need both encryption AND protection from malformed packets. This does not normally take many lines of code. Like the earlier example, both Sender and Receiver scripts need the encrypt/decrypt function (ascii_shift in our example).
def ascii_shift(key, text): result = '' for letter in text: ascii = ( ord(letter) + key - 32 ) % 94 + 32 result = result + chr(ascii) return result
The Sender script also needs to encrypt the packet before sending:
packet = ascii_shift(17, packet)
…and the Receiver needs to decrypt the packet after receiving:
packet = ascii_shift(-17, packet)
In Python for computers (as opposed to MicroPython for the micro:bit), an encryption/decryption module would be imported. Instead of adding a comparatively weak encryption function, you’d import a module like PyCryptodome, Cryptography, or PyNaCl and use its stronger cryptography methods to encrypt and decrypt.
The last task in hardening the keyboard-controlled cyber:bot is to implement both encryption and exception handling.