Backdooring Embedded Devices

Discussing various ways to apply persistence to embedded devices

Introduction

If you have ever gotten a shell on an embedded device, you may have noticed that it is a wildly different experience when compared to your more common flavors of Linux distributions. Embedded devices while commonly run on a version of Linux, is more than likely significantly different than any Linux operating system you have used before.

Embedded devices commonly run a light-weight version of Linux that only contains the absolute most core packages needed to function. You may be thinking well how stripped down are we talking? Well in my experience, you are lucky if the operating system even recognizes the famous "whoami" command.

Introducing Busybox

However, embedded Linux systems commonly have what is referred to as "Busybox" installed on the system. Busybox is described as "combining tiny versions of many common UNIX utilities into a single small executable". Busybox is often thought of as The Swiss Army Knife of Embedded Linux.

Typically when running commands on an embedded device, it is actually just a soft link to the Busybox binary.

Busybox has a number of different versions that have been compiled for a variety of different architectures including MIPS and ARM. This comes in handy after getting an initial shell on a embedded device, as sometimes it may not even have Busybox binary installed or maybe it will only have a version of Busybox with limited commands.

This makes it possible to easily download a compatible version online and uploading it to the target device to give us more control.

Figure 7: Busybox binary download page

After uploading a compatible Busybox binary to the target one could script a Netcat bind shell to initialize on startup.

While this may work in a crunch, it is definitely not the most reliable way to maintain persistence remote access. Additionally, this is not a full TTY shell, which is much more powerful then this "dumb" Netcat shell.

A TTY shell is basically a shell that gives us access to the terminal in addition to displaying STDOUT, it will also display streams such as STDERR. Some commands produce error messages that is outputted through a stream other than STDOUT. It benefits attackers greatly to have a shell that will display these streams of output.

Cross-Compiling for ARM

This brings me to the main topic of this blog, which is cross-compiling binaries for embedded devices. Because embedded devices frequently run on ARM architecture, you can't just use your standard GCC on Ubuntu to compile your C program. This requires a cross-compiler to compile a ARM compatible binary.

Prerequisites

In my example I used a fully updated Ubuntu 22.04 virtual machine to cross-compile on. Before starting there are some packages that need to be installed.

The GCC cross-compiler supporting programs can be installed like below.

Afterwards, the ARM cross-compiler needs to be installed like so.

sudo apt install gcc-arm-linux-gnueabi

Payload Source Code

For the bind shell, I will be using a slightly modified version of this program. In order to make the backdoor more persistent and allow me to be able to connect back multiple times, the following changes were implemented.

Compiling

Cross-compiling the program for the embedded device is easy as the following code snippet.

arm-linux-gnueabi-gcc backdoor.c -static -o backdoor

Because every embedded device is different, I found it imperative that the -static flag be used to compile the needed libraries within the binary. While this makes the payload significantly larger, it makes it much more versatile and less dependent on the environment.

After successfully compiling the binary, we upload it to the target device to test the compatibility.

No errors upon executing. This is looking really good. Let's try to connect to it from our attacking machine.

Looks like it is working as intended.

Persistence

Creating a Backdoor as a Service

Assuming the target embedded device is utilizing systemd for services, you should be able to setup the backdoor binary to run as a service. This creates a cushion of redundancy in the chance that the backdoor dies.

Add the following script to the following directory as a file called backdoor.service:

/etc/systemd/system/backdoor.service

Afterwards run the following commands to enabel and start the service.

You can check the status of your service by either running netstat to see if port 9999 is being exposed or systemctl status backdoor to see if the service is active.

Last updated