SLAE A.2 - TCP Reverse Shellcode
This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification: http://securitytube-training.com/online-courses/securitytube-linux-assembly-expert/
Student ID: SLAE-1294
This second assignment is to create an original x86 Linux shellcode for a TCP reverse shell. It is built on the TCP bind shell discussed in Assignment 1. I won’t repeat the details of locating syscall numbers and related constants. The full source code is available on GitHub.
A reverse shell needs to connect back to the attacker’s host so it must contain the remote IP address and port. In this shellcode, these are inserted at assembly-time with -D
parameters to nasm. These parameters are generated by the python build script.
The shellcode must perform these syscalls in order:
socket()
- Get a socketconnect()
- Establish a TCP connection to chosen remote hostdup2()
x3 - Redirect STDIN, STDOUT, STDERR to the network socketexecve()
- Launch /bin/sh
Some of it is similar to the bind shell but this time I’ve made an effort to minimise the shellcode length, which is 75 bytes in total. Samples on shell-storm.org are 67, 72 and 92 bytes so it’s not bad. There is room for improvement of course.
Obtaining a socket
Register values could contain anything at the shellcode entry point so they must be cleared. Then we can set the parameters to get an AF_INET socket. This is the same as the bind shell, except we will leave the resulting file descriptor (fd) in EAX for the moment.
Connecting to the remote host
To establish an outgoing TCP connection we must use the connect()
syscall. The struct sockaddr
contains the remote IP and port in a packed format.
We will push the contents of the struct sockaddr
onto the stack and take its address from ESP. The addrlen
is expected to be at least 16. In this case it will be parsed as a struct sockaddr_in
, which has all required information in the first 8 bytes. To save instructions we will only push 8 bytes (in reverse order) and leave the second half filled with whatever junk is already on the stack.
These REMOTE_IP
and REPORT_PORT
constants are the parameters provided by -D
. They are expected to be numbers already in the required format—32-bit big endian for IP address and 16-bit big endian for port. This conversion is handled by the build script.
With ESP now pointing to the head of the struct, the call to connect()
can be set up. The fd that was returned from socket()
is still sitting in EAX so it must be moved to the EBX parameter. ECX is the addr
pointer, taken from ESP. EDX is addrlen
and we will pretend that it’s 16 bytes long.
Redirecting stdin, stdout, stderr
The shell’s input and output will be redirected to the TCP connection by replacing fds 0, 1 and 2 with the connected network socket. In the bind shell I simply performed three calls to dup2()
one after the other. In an effort to be more efficient this is now a loop.
ECX is initialised to 3. Each loop subtracts 1 then uses it as the newfd parameter for dup2()
. After this has run for ECX = 0, execution continues onward.
Spawning /bin/sh
Finally, the shell is executed using execve()
. The required data structures are pushed onto the stack—the null-terminated string “/bin//sh”, and pointers for the argv and envp arrays. This is fundamentally the same as the bind shell but it has been streamlined a little. ECX contains 0 after the dup2()
loops so it is used to create the NULL entries on the stack.
Demonstration
It compiles and builds. Any valid IP and port can be used, provided the resulting big-endian structure does not contain a zero byte. The build script generates an error if it does.
To test it, a netcat listener is first run on localhost port 4444. After that, shell_test
is executed and we see that nc
catches a connection.
The remote user can run interactive commands in the shell.
Once the remote user exits the shell, the shellcode ends and the wrapper terminates.