This post outlines a security assessment of the new Sena Wifi Adapter I performed last summer for fun.
With the world on lockdown due to COVID-19, I spent a lot of time last summer escaping the city going on motorcycle rides through the mountains and forests surrounding the bay area. It's the perfect social distance activity because if you get within 6ft of someone you are likely to crash. One of my favorite motorcycle accessories is my Sena headset. It allows me to listen to navigation or music from my phone over Bluetooth while riding, and talk to other riders in my group.
I was in the market for a new headset and Sena had just released the Sena 50R. It has USB-C, mesh, Bluetooth 5, voice activated assistant, and a built in FM radio. A huge upgrade from my older SMH10R.
The 50R is meant to be controlled via a companion app that allows the user to change the settings. The USB-C port is used for charging and firmware updates. The 50R comes with a WiFi Adapter cable to simultaneously charge the headset and install any available firmware updates.
This WiFi Adapter cable is actually a USB-C cable with a WiFi enabled computer inside the cable. This sounds an a lot like the COTTONMOUTH implant from the NSA ANT catalog, but much cheaper.
To use the WiFi cable, after plugging it in and waiting for it to boot, it creates its own WiFi access point, which the app connects to allowing the user to configure the cable to connect to their home's WiFi network. Now every time a headset is plugged into the USB-C end of the cable, it will check for firmware updates and install them.
Sena must really want everyone to have up to date firmware to build a WiFi enabled firmware updating computer inside of a USB cable.
Being the curious security minded person that I am, I decided to investigate this computer inside my new USB cable.
Removing 4 screws and disconnecting the USB upstream and downstream cables is all it takes to see the internal components.
The main SOC (System On Chip) is a SONiX SN98600 containing an ARM9 CPU and 64MB of RAM. There is a 16MB Winbond 25Q128JVP0 SPI flash storage chip, and a Realtek RTL8188EU WiFi interface.
There are some test points, three of which are conveniently labeled GND, TX, and RX. Which looks like a lot like UART! Connecting these test points to a logic analyzer and monitoring the signals while applying power confirms that it is an async serial configured for 115200 8N1.
Monitoring the UART while rebooting reveals that the device is using the U-BOOT boot loader to boot Linux kernel 126.96.36.199. While the Linux kernel boots, the boot logs provide more details on the hardware and its configuration on the device and then drops to a login prompt.
Dumping the Flash
In order to dump the contents of the SPI flash chip to better examine the system's file-system there are two options.
- Connect the SPI flash chip to a device that speaks SPI like the FT232H and use the flashrom tool to dump the contents.
- Find a way to read and dump the flash contents from within the device.
The first option is cleaner and is my preferred method. I connected GND, MISO, MOSI, CS, CLK, and 3.3 VCC to my SPI dumper and tried to identify the chip with
flashrom -p ft2232_spi:type=232h --flash-name. However providing power to the SPI chip also supplied power to the main SOC and other components on the board causing them to boot and also send commands to the SPI flash, making a clean read impossible. I could have desoldered the chip and read it without powering the rest of the device, but that runs the risk that I might physically damage something in the process, so I decided to give the second option a try.
I did not know any valid logins to get past the Linux login prompt (I tried a lot of the common defaults), but what about the boot loader? The U-BOOT boot loader was configured to immediately start the Linux kernel after loading. My colleague Matthew Garrett proposed an ingenious idea, glitch U-BOOT into dropping to a shell. The idea is that after U-BOOT itself loads, it loads the Linux kernel from the SPI flash into RAM, and then boots it. If this process can be tricked into erroring then then it may drop to an interactive U-BOOT shell. This can be accomplished by interfering with the SPI MISO signal while U-BOOT is loading the kernel so that it loads a corrupt kernel and is unable to boot it. I did this by watching the console output and shorting the SPI MISO to ground very briefly while it was loading the kernel image. This took a few tries to get right, but it did result in U-BOOT entering an interactive shell.
U-BOOT can be compiled to include different utilities or commands to get the system booting. This version included the following two commands of interest:
spi read addr offset len - read 'len' bytes starting at 'offset' to memory at 'addr' md [.b, .w, .l] address [# of objects]
spi readreads data from the SPI chip into memory at a specified location.
mddisplays the contents of memory at the provided location in a hexdump like format.
I could tell from the boot logs that the Linux kernel is loaded into memory starting at address
0x00008000 so that address should be safe to load flash data to, and the entire flash is 16M, which is
0x1000000 byes in hex.
Using these two commands it is possible to load all of the flash to memory and then dump it over the UART. I used
screen -L as the serial console so that all of the UART results would be saved to a file.
screen -L /dev/ttyUSB0 115200 spi read 0x00008000 0 0x1000000 md 0x00008000 1000000
spi read operation took about one second to complete, however due to the inefficiency of loading 16M of data in hexdump format over a 115200 baud connection the
md operation took the better part of a day. Reading the SPI chip directly would have been much faster, but I did not want to risk it.
Once the dump finishes
xxd can be used to convert it from the hexdump format to binary.
xxd -r hexdump.txt flash.bin
md command started at offset
0x8000, xxd will prepend nulls (0x00) to the file to make the offsets align.
dd can be used to trim the extra data added for the memory offsets leaving just the flash data with the correct offsets.
dd if=flash.bin of=flash_trim.bin bs=1 skip=$((0x8000))
Since the flash was dumped from RAM, it needs to be converted from big-endian to little-endian. This can be done with
objcopy -I binary -O binary --reverse-bytes=4 flash_trim.bin sena_wifi.bin
The boot logs contain the partition table, which can be used to extract the individual partitions on the flash.
hw-setting=0x00000000 0x00000FFF u-boot=0x00003000 0x0007DFFF u-env=0x0007E000 0x0007E000 flash-layout=0x0007F000 0x0007FFFF factory=0x00080000 0x000BFFFF kernel=0x000C0000 0x003BFFFF rootfs-r=0x003C0000 0x00ABFFFF rootfs-app=0x00ABFFFF 0x00ABFFFF rootfs-rw =0x00EC0000 0x00FBFFFF user=0x00FC0000 0x00FFFFFF u-logo=0x00FFFFFF 0x00FFFFFF rescue=0x00AC0000 0x00EBFFFF
dd if=sena_wifi.bin of=hw_setting.bin bs=1 skip=$((0x00000000)) count=$((0x00000FFF-0x00000000)) dd if=sena_wifi.bin of=u-boot.bin bs=1 skip=$((0x00003000)) count=$((0x0007DFFF-0x00003000)) dd if=sena_wifi.bin of=u-env.bin bs=1 skip=$((0x0007E000)) count=$((0x0007E000-0x0007E000)) dd if=sena_wifi.bin of=flash-layout.bin bs=1 skip=$((0x0007F000)) count=$((0x0007FFFF-0x0007F000)) dd if=sena_wifi.bin of=factory.bin bs=1 skip=$((0x00080000)) count=$((0x000BFFFF-0x00080000)) dd if=sena_wifi.bin of=kernel.bin bs=1 skip=$((0x000C0000)) count=$((0x003BFFFF-0x000C0000)) dd if=sena_wifi.bin of=rootfs-r.bin bs=1 skip=$((0x003C0000)) count=$((0x00ABFFFF-0x003C0000)) dd if=sena_wifi.bin of=rootfs-app.bin bs=1 skip=$((0x00ABFFFF)) count=$((0x00ABFFFF-0x00ABFFFF)) dd if=sena_wifi.bin of=rootfs-rw.bin bs=1 skip=$((0x00EC0000)) count=$((0x00FBFFFF-0x00EC0000)) dd if=sena_wifi.bin of=user.bin bs=1 skip=$((0x00FC0000)) count=$((0x00FFFFFF-0x00FC0000)) dd if=sena_wifi.bin of=u-logo.bin bs=1 skip=$((0x00FFFFFF)) count=$((0x00FFFFFF-0x00FFFFFF)) dd if=sena_wifi.bin of=rescue.bin bs=1 skip=$((0x00AC0000)) count=$((0x00EBFFFF-0x00AC0000))
With the individual partitions extracted they can be explored with tools like
strings, Binwalk, or mounted on another Linux system. The
rootfs-rw seem to the the only partitions with real file-systems and can be mounted locally with the following commands.
mount -t cramfs -o loop,ro rootfs-r.bin rootfs-r/ modprobe mtdblock dd if=rootfs-rw.bin of=/dev/mtdblock0 mount -t jffs2 -o ro /dev/mtdblock0 rootfs-rw/
Now the file-systems can be explored locally.
There are two
/etc/shadow files, one on
rootfs-r and another on
rootfs-rw. One is mounted over the other during boot. The root password from
rootfs-r was hashed with md5crypt which the online service OnlineHashCrack.com was able to crack for free revealing it was
1234. However this password did not work for the UART Linux login prompt, meaning the
rootfs-rw partition is likely mounted over it.
/etc/shadow root password was hashed with the much more secure DEScrypt algorithm. I was able to submit this hash to crack.sh and have it cracked in a day revealing the password
snowtalk. They say a picture is worth 1000 words:
It worked! Root shell acquired! It's now possible to poke around the running system which is much more valuable than just exploring the file-system dump created earlier.
Sena has another product called the Snowtalk, which makes me wonder if aside from poor password choice if there is any password reuse going on...
The Sena WiFi adapter can act as its own AP (wireless access point) for initial configuration; or join an existing wireless network (for updating firmware). In both modes the same services appear to be running on the device. The only service of interest is an HTTP server running on port 8000, that the companion app sends requests to, but more on that later.
When in normal mode (as a client connected to WiFi), the WiFi adapter sends out periodic requests to check for firmware updates for itself, and any headset that may be connected.
In order to view and tamper with the outbound requests I made a simple DNS and HTTP MITM server that would log and respond to any requests the device made on the network. Using this tool I was able to discover that all of its outbound requests are to
http://firmware.sena.com. This is interesting because it only used HTTP and not HTTPS, which means the traffic can be easily inspected and it is possible to serve different content to the adapter without it knowing.
First, periodic requests are made to
http://firmware.sena.com/senabluetoothmanager/WiFiCradle/check.cdat to check for internet connectivity. Once internet connectivity is established, a request to
http://firmware.sena.com/senabluetoothmanager/WiFiCradle/fw_restore_ver_adapter.cdat is made to determine if the WiFi adapter needs to update its own firmware, if so it will download an ARM ELF executable from the same server and run it. If there is a newer adapter firmware available it will download the binary
http://firmware.sena.com/senabluetoothmanager/WiFiCradle/FIRMWARE_660R_X.X_ADAPTER.bin and run it, where
X.X is the firmware version.
After the WiFi adapter is satisfied that it is running the latest firmware, it will make a request to
http://firmware.sena.com/senabluetoothmanager/Firmware to get a list of all the firmwares available for all of Sena's headset products. If it determines that there is a newer firmware available for the connected headset, it will download and run
http://firmware.sena.com/senabluetoothmanager/WiFiCradle/loader.capp which is another ARM ELF executable to start the firmware updating process. This
loader.capp will determine which headset is connected by looking at the USB Product ID (PID), and then download
0x#### is the USB PID in hex. This is another binary file that is run, specific to the particular headset that is connected. For my 50R, this was
updater_0x3134.capp. Finally, the updater program will download the latest firmware image and flash it to the headset using DFU. In my case this was
What's important to note here is that all the binaries are downloaded over HTTP, and there is no signature validation. This means anyone able to intercept network requests can provide other binaries and the WiFi adapter will blindly download and run them. To test this I created my own versions of the binaries, but I used the following shell script:
#!/bin/sh ping -c 1 hacked.com
Surprisingly this worked even though it was a script and not an ELF binary. As I was still monitoring the network traffic, I could see the DNS request being made for
hacked.com and then ICMP requests being sent to it. This worked for all of the previously mentioned binary files. This allows for Remote Code Execution as long as the attacker is on the same network as the adapter and able to impersonate the
firmware.sena.com HTTP server. Combined with ARP Spoofing, an attacker only needs to be on the same network to make the adapter think that the attacker is the gateway allowing then to impersonate the HTTP update server and send malicious updates.
But can the attack be better?
With access to the file-system and binaries that are running on the device from the prior work, it is now possible to examine the HTTP API server
query.cgi listening on port 8000. This API is used by the companion apps.
query.cgi HTTP server runs when the adapter is in AP mode, and when connected to another network, It is possible to make calls to it from any computer on the same network it is connected to, and even instruct it to switch to AP mode, or from AP mode and connect to another network from the Android app. There is no authentication for the API or for the network when in AP mode making the API a great target.
Monitoring the network traffic between the Android app and the WiFi adapter (IP:
10.42.0.1) reveals that all the API calls are HTTP GET request with parameters and values in the query-string, and responses returned in the HTTP body.
http://10.42.0.1:8000/cgi-bin/query.cgi?cmd=getaplist http://10.42.0.1:8000/cgi-bin/query.cgi?cmd=app_version http://10.42.0.1:8000/cgi-bin/query.cgi?cmd=setssid&value=WiFiNetworkName http://10.42.0.1:8000/cgi-bin/query.cgi?cmd=getlog
This appears to be a very simple RPC API where the
cmd parameter is the function to call and any arguments are provided with the
Some API calls reveal a lot of information. For example, one such call returns the user's WiFi password, which could be used by an attacker to get onto a victim's home network if they de-auth the WiFi adapter putting it into AP mode first.
In order to determine how the API calls are implemented and if there are any other hidden API calls available, the
query.cgi can be examined with a software reverse engineering tool like Ghidra.
Taking a look at setssid
I looked at the
setssid command first to see how the user supplied data was being processed. After setting the WiFi SSID in the
wpa_supplicant.conf file it also sets the hostname of the WiFi adapter to the provided value with the following code:
At first pass this may seem fine, but upon further inspection I noticed that the user supplied value,
param_1 in this case, was being put into a string with
sprintf() and then passed to
system(), which will pass the command to the shell. This is a vulnerability.
If I pass the value
sena1337";ping -c1 "hacked.com, then the string passed to
system() will end up being
/bin/hostname "sena1337";ping -c1 "hacked.com" which will set the hostname to "sena1337" and then run the second command provided, in this case, ping. This works because the
; character tells the shell to treat everything after it as a separate command.
Since setting the SSID is a feature provided by the mobile app, it might be possible to use it to perform run commands, however it seems Sena thought of that and prevented it.
But this is only the Android app sanitizing the input and preventing the use of special characters in this API call. It is possible to bypass the app and make the call directly with curl and URL encoding the parameters:
After running the above command while the adapter was on my home network, I observed both a DNS query for
hacked.com and an ICMP ping request sent as well. This is known as Command Injection, and it worked here because the user input was only validated on the client (Android app) and not the server (HTTP API). While the ping here is harmless, it can be replaced with any other command which the WiFi adapter will run as root. Another RCE!
Finding a Backdoor
After further examination of the HTTP API code I found a few more interesting API calls. The WiFi adapter has two test modes that change the endpoints the adapter uses to check for firmware updates. These are likely used internally at Sena for development and debugging. When in test mode the WiFi adapter also starts a telnet server listing on port 23. And of course, there is a API call to just enable the telnet server directly in the normal mode too. A HTTP GET request to any of these endpoints will start the telnet server:
http://10.42.0.1:8000/cgi-bin/query.cgi?cmd=telnetd http://10.42.0.1:8000/cgi-bin/query.cgi?cmd=setconf&key=testmode&value=1 http://10.42.0.1:8000/cgi-bin/query.cgi?cmd=setconf&key=testmode&value=2
Once started, anyone on the same network can connect to it. The telnet server does require authentication, but the previously acquired password works providing another root shell. More RCE!
Looking at Another System Call
There are many places that the HTTP API will make a
system() call to
wget in order to download a file from the internet such as the previously mentioned firmware updates or binaries that it runs as part of the update process. All of the calls to
wget look like this:
Here the code clearly shows us that
wget is always called with the
--no-check-certificate flag, which means that even if HTTPS was used, the certificate would not be checked, still allowing an attacker to provide any binary or firmware they want.
After reviewing more of the code, I suspect that the same code and hardware that are in this WiFi adapter are also in Sena's WiFi Docking Station, Implying it is vulnerable to the same attacks. There were even references to unreleased and unannounced Sena products in the reversed code as well.
These findings provide a potential attacker multiple ways to remotely get root shell access to the WiFi Adapter cable. From here they can flash malicious firmware to the adapter and helmet, or use the adapter as a pivot point and attack other devices on the user's home network. If the attacker was able to flash malicious firmware onto a headset, it could lead to injury or even loss of life if the bad firmware was able to distract or incapacitate a driver.
Unfortunately these types of vulnerabilities are common on IoT devices like this where security is not a high concern.
- weak dictionary word password
- no signature verification for update images or binaries loaded over the network
- no TLS used for remote connections
- command injection on HTTP API
- SSL certificate checking disabled
- telnet backdoor
One hypothetical full attack chain could go like this: an attacker scans for Sena WiFi adapters around them identifying them by MAC address OUI prefix. Once found, send a WiFi de-auth frame to the adapter causing it to go into AP mode, if not already. Then connect to the WiFi adapter, and get a root shell using either the telnet backdoor and password, or command injection. From here the attacker can steal the user's home WiFi password, use the adapter as a pivot to attack other devices on the victim's network, backdoor the firmware to provide malicious firmware updates to any connected headsets, and more. All wirelessly without physical access to the WiFi adapter.
As a long time user and advocate of Sena products I'd like to work with them to fix these issues and make them more secure for everyone. Unfortunately that was more difficult than ideal. I was unable to get in contact with anyone at Sena to disclose the vulnerabilities when I initially found them in September 2020. It was not until after I gave them 90 days and submitted my original draft of this post to them in December that they eventually respond to me and started working to fix the issues. I gave them an additional few months to fix the issues as it was my goal to get the problems fixed.
- 09/24/2020 - I had great difficulty finding any contact information for Sena to report these findings. I started by emailing [email protected] and [email protected], and was told to file a support ticket.
- 09/25/2020 - Sent a Twitter DM to @senabluetooth, who also told me to file a support ticket. I filed support ticket #390170 to start the conversation for disclosure. The support reps said they would pass along my claim to upper management and then closed the ticket. However I was unable to provide details of the vulnerabilities to them.
- 09/26/2020 - I emailed [email protected] (bounced), [email protected], [email protected], and [email protected], but got no responses. I even found what I believed to be their CIO's email and emailed them directly and got no response. Still I decide to give them 90 days before I publish this post in case they are working internally on a fix.
- 12/23/2020 - I sent a draft of this post to the Sena support team and CIO.
- 12/24/2020 - Sena's CIO finally responds to my email and forwards the information to the product team.
- 12/25/2020 - Sena's CTO reached out to me to let me know their development team is investigating the issues.
- 12/29/2020 - Sena confirms everything in the above report to be correct, and asks that I postpone publishing until the end of February 2021. I agree.
- 02/26/2021 - Sena releases WiFi Adapter firmware v1.1 and updates to their mobile apps with fixes to the findings above.
- 03/08/2021 - I publish this blog post.
Note: I have not gotten around to testing the Sena updates to verify that all of the issues described here are sufficiently mitigated. I'll update this post if I get around to testing them.
Sena was nice enough to send me a free Sena 50R for submitting the issue to them and for waiting for them to release a fix before publishing this post.