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 socket
connect()- Establish a TCP connection to chosen remote host
dup2()x3 - Redirect STDIN, STDOUT, STDERR to the network socket
execve()- 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.
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.
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.
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.