Linux Shellcoding

RED TEAM
8 min readJul 22, 2024

--

“Linux Shellcoding for Security Professionals”

Hello everyone,

Today we are going to learn about Linux Shellcoding and go through with Practical knowledges.

shellcode

Writing shellcode is a great way to learn more about assembly language and how a program interacts with the operating system.

Why do red teamers and penetration testers write shellcode? Because in real situations, shellcode can be injected into a running program to make it do something it wasn’t designed to do, such as buffer overflow attacks.

Therefore, shellcode is often used as the “payload” in an exploit.

Why is it called “shellcode”? Historically, shellcode is machine code that, when executed, opens a shell.

Shellcode plays a crucial role in penetration testing and red teaming for several reasons:

  1. Understanding Low-Level Operations: Writing shellcode requires a deep understanding of assembly language and how programs interact with the operating system at a low level.
  2. Payload for Exploits : Shellcode is often used as the payload in exploits. When a vulnerability such as a buffer overflow is exploited, shellcode can be injected into a running process to execute arbitrary commands.
  3. Evasion and Obfuscation: Crafting effective shellcode involves techniques to evade detection by antivirus and intrusion detection systems.
  4. Advanced Post-Exploitation: Beyond simply spawning a shell, modern shellcode can be designed to perform complex post-exploitation tasks.
  5. Versatility: Modern shellcode is not limited to spawning shells. It can be crafted to perform a wide range of actions, such as downloading additional malware, creating backdoors.

Shellcode Testing

When testing shellcode, it’s convenient to simply insert it into a program and execute it.

The following C program (run.c) will be used to test all our code:

run.c

A strong understanding of C and Assembly is highly recommended. Additionally, knowing how the stack operates is a significant advantage.

Disable ASLR

Address Space Layout Randomization (ASLR) is a security measure implemented in most contemporary operating systems. It alters the memory addresses utilized by programs, including the stack, heap, and libraries, making it more difficult for attackers to exploit vulnerabilities. In Linux, ASLR can be configured using the /proc/sys/kernel/randomize_va_space file.

Address Space Layout Randomization (ASLR) security feature that makes it more difficult for attackers to predict the location of specific functions or memory addresses, thus mitigating certain types of attacks

The following values are supported:

0 — no randomization, Everything is static.

1 — conservative randomization, Shared libraries, stack, mmap(), VDSO, and heap are randomized.

2 — full randomization

To check the current ASLR setting, you can use the following command:

cat /proc/sys/kernel/randomize_va_space

To disable ASLR, run:

echo 0 > /proc/sys/kernel/randomize_va_space

To enable ASLR, run:

echo 2 > /proc/sys/kernel/randomize_va_space

Assembly

Let’s start by going over some more introductory information again.

The x86 Intel Register Set.

EAX, EBX, ECX, and EDX are all 32-bit general-purpose registers.

AH, BH, CH, and DH access the upper 16 bits of these general-purpose registers, while AL, BL, CL, and DL access the lower 8 bits.

EAX, AX, AH, and AL are referred to as “Accumulator” registers and can be utilized for I/O port access, arithmetic operations, interrupt calls, etc. These registers are also useful for implementing system calls.

EBX, BX, BH, and BL are known as “Base” registers, serving as base pointers for memory access. This register is commonly used to store pointers for system call arguments and sometimes to store the return value from an interrupt.

ECX, CX, CH, and CL are called “Counter” registers.

EDX, DX, DH, and DL are known as “Data” registers and can be employed for I/O port access, arithmetic operations, and certain interrupt calls.

Assembly instructions. There are some instructions that are important in assembly programming:

mov eax, 32 ; assign: eax = 32
xor eax, eax ; exclusive OR
push eax ; push something onto the stack
pop ebx ; pop something from the stack
; (what was on the stack in a register/variable)
call mysuperfunc ; call a function
int 0x80 ; interrupt, kernel command

Linux system calls serve as APIs that bridge the user space and kernel space. To utilize Linux system calls in assembly programs, follow these steps:

1. Load the system call number into the EAX register.
2. Place the arguments for the system call into the EBX, ECX, and other registers as needed.
3. Invoke the appropriate interrupt (80h).
4. The result is typically returned in the EAX register.

A comprehensive list of x86 system calls can be found in `/usr/include/asm/unistd_32.h`.

Example of how libc wraps syscalls:

Let’s compile and disassembly:

gcc -masm=intel -static -m32 -o exit0 exit0.c
gdb -q ./exit0

0xfc = exit_group() and 0x1 = exit()

Nullbytes

Let’s go to investigate simple program:

compile and run:

gcc -m32 -w -o woow woow.c
./woow

When delivering shellcode for exploits targeting C code, null bytes (\x00) must be avoided because they terminate the instruction chain. This is crucial since shellcode is often included in NUL-terminated strings. If the shellcode contains null bytes, the C code being exploited may ignore and discard any subsequent code starting from the first null byte.

This challenge is specifically related to machine code. For example, to invoke a system call with the number 0xb, you need to set the EAX register to 0xb without using machine code that includes null bytes.

Avoid null bytes (\x00) in shellcode for C code exploits, as they prematurely terminate the code, causing the rest to be ignored.

Let’s go to compile and run two equivalent code.

First exit1.asm:

compile and investigate exit1.asm:

nasm -f elf32 -o exit1.o exit1.asm
ld -m elf_i386 -o exit1 exit1.o
./exit1
objdump -M intel -d exit1

as you can see we have a zero bytes in the machine code.

Next exit2.asm:

compile and investigate exit2.asm:

nasm -f elf32 -o exit2.o exit2.asm
ld -m elf_i386 -o exit2 exit2.o
./exit2
objdump -M intel -d exit2

As you can see, there are no embedded zero bytes in it.

The EAX register in a computer’s CPU can be split into smaller parts: AX, AH, and AL. AX is the lower half of EAX, AL is the lower quarter, and AH is the upper quarter of that lower half.

This matters when writing shellcode (small, special-purpose code) because we need to avoid “null bytes” (0x00), which can mess up the code.

Using the smaller parts of the register can help us do this. For example, using `mov al, 0x1` changes just a small part of EAX and avoids creating a null byte, unlike `mov eax, 0x1`, which could create unwanted null bytes in our code.

  • EAX is a 32-bit register.
  • AX is the lower 16 bits of EAX.
  • AL is the lower 8 bits of AX (which means it’s also part of EAX).
  • AH is the upper 8 bits of AX.

Both these programs are functionally equivalent.

Normal exit

Let’s begin with simplest example. Let’s use our exit.asm code as the first example for shellcoding (example1.asm):

Extract byte code:

nasm -f elf32 -o example1.o example1.asm
ld -m elf_i386 -o example1 example1.o
objdump -M intel -d example1

Here is how it looks like in hexadecimal.

So, the bytes we need are 31 c0 b0 01 cd 80. Replace the code at the top (run.c) with:

Now, compile and run:

gcc -z execstack -m32 -o run run.c
./run
echo $?

-z execstack Turn off the NX protection to make the stack executable

Our program returned 0 instead of 1, so our shellcode worked.

Spawning a linux shell.

Let’s go to writing a simple shellcode that spawns a shell (example2.asm):

To compile it use the following commands:

nasm -f elf32 -o example2.o example2.asm
ld -m elf_i386 -o example2 example2.o
./example2

Using system("/bin/sh") would be a straightforward approach, but it has a drawback: system drops the user's privileges.

Instead, we can use execve, which is a bit more complex but doesn’t have this issue. execve needs three pieces of information:

  1. The program to run (this goes into the EBX register),
  2. Arguments for the program (this goes into the ECX register, which can be null if there are no arguments),
  3. Environment variables (this goes into the EDX register, which can also be null if not needed).

In the example code (example3.asm), we’re avoiding null bytes and using the stack to hold these values directly.

Now, let’s assemble it and check if it properly works and does not contain any null bytes:

nasm -f elf32 -o example3.o example3.asm
ld -m elf_i386 -o example3 example3.o
./example3
objdump -M intel -d example3

Then, extract byte code via some bash hacking and objdump:

objdump -d ./example3|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'

So, our shellcode is:

Then, replace the code at the top (run.c) with:

Compile and run:

gcc -z execstack -m32 -o run run.c
./run

As you can see, everything work perfectly.

Conclusion:
In malware development, shellcoding means creating small assembly programs that perform tasks like opening a command shell. This requires knowing how to write and manage code that directly interacts with the operating system while avoiding issues like null bytes that can break the code.

Credit : zhassulan zhussupov

RED TEAM

--

--

RED TEAM
RED TEAM

Written by RED TEAM

I'm a 19-year-old malware developer with 1 year of experience. Passionate about learning new techniques, sharing knowledge, and creating malware tools.