DEV Community

Sylvain Kerkour
Sylvain Kerkour

Posted on • Originally published at kerkour.com

 

How to Write and Compile a Shellcode in Rust

Last week, we saw how to execute a shellcode from memory in Rust. What if we could write the actual shellcode in Rust?

Writing shellcodes is usually done directly in assembly. It gives you absolute control over what you are crafting, however, it comes with many, many drawbacks:

  • It requires a lot of deep knowledge that is not transferable
  • Shellcodes are not portable across different architectures
  • Assembly is like regexps: (barely) easy to write, impossible to read
  • Assembly code is a nightmare to compose and reuse
  • It's extremely easy to introduce bugs that are hard to debug
  • It's a nightmare to maintain over time and across teams of many developers

What if instead, we could write our shellcodes in a language that is high-level and thanks to a highly advanced compiler, gives us precise, low-level control. A language that would make our shellcodes portables across architectures and easy to reuse.

Sounds too good to be true?

Without further ado, here is how to write shellcodes in Rust, so you will be able to judge by yourself.

This post is an excerpt from my book Black Hat Rust

Here is the assembly equivalent of the "Hello world" shellcode that we are about to craft in Rust:

_start:
    jmp short string

code:
    pop rsi
    xor rax, rax
    mov al, 1
    mov rdi, rax
    mov rdx, rdi
    add rdx, 12
    syscall

    xor rax, rax
    add rax, 60
    xor rdi, rdi
    syscall

string:
    call code
    db  'hello world',0x0A
Enter fullscreen mode Exit fullscreen mode

Rust shellcode

First, we need to configure the linker to produce a bloat-free binary:

shellcode.ld

ENTRY(_start);

SECTIONS
{
    . = ALIGN(16);
    .text :
    {
        *(.text.prologue)
        *(.text)
        *(.rodata)
    }
    .data :
    {
        *(.data)
    }

    /DISCARD/ :
    {
        *(.interp)
        *(.comment)
        *(.debug_frame)
    }
}
Enter fullscreen mode Exit fullscreen mode

And tell cargo to use this file:

shellcode/.cargo/config.toml

[build]
rustflags = ["-C", "link-arg=-nostdlib", "-C", "link-arg=-static", "-C", "link-arg=-Wl,-T./shellcode.ld,--build-id=none", "-C", "relocation-model=pic"]
Enter fullscreen mode Exit fullscreen mode

Then, we need to configure Rust to optimize the final binary for size:
shellcode/Cargo.toml

[package]
name = "shellcode"
version = "0.1.0"
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]


[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"
opt-level = "z"
lto = true
codegen-units = 1
Enter fullscreen mode Exit fullscreen mode

Now the configuration is done, we can start crafting the shellcode.

First, the boilerplate:

shellcode/main.rs

#![no_std]
#![no_main]

use core::arch::asm;

#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! {
    loop {}
}

const SYS_WRITE: usize = 1;
const SYS_EXIT: usize = 60;
const STDOUT: usize = 1;
static MESSAGE: &str = "hello world\n";
Enter fullscreen mode Exit fullscreen mode

Then, we implement the syscalls. It's the only part that requires assembly and is not architecture-agnostic:

unsafe fn syscall1(syscall: usize, arg1: usize) -> usize {
    let ret: usize;
    asm!(
        "syscall",
        in("rax") syscall,
        in("rdi") arg1,
        out("rcx") _,
        out("r11") _,
        lateout("rax") ret,
        options(nostack),
    );
    ret
}

unsafe fn syscall3(syscall: usize, arg1: usize, arg2: usize, arg3: usize) -> usize {
    let ret: usize;
    asm!(
        "syscall",
        in("rax") syscall,
        in("rdi") arg1,
        in("rsi") arg2,
        in("rdx") arg3,
        out("rcx") _,
        out("r11") _,
        lateout("rax") ret,
        options(nostack),
    );
    ret
}
Enter fullscreen mode Exit fullscreen mode

And finally, we can write the entry point of the shellcode:

#[no_mangle]
fn _start() {
    unsafe {
        syscall3(
            SYS_WRITE,
            STDOUT,
            MESSAGE.as_ptr() as usize,
            MESSAGE.len(),
        );

        syscall1(SYS_EXIT, 0)
    };
}
Enter fullscreen mode Exit fullscreen mode

It can be compiled with:

.PHONY: build_shellcode
build_shellcode:
    cd shellcode && cargo +nightly build --release
    strip -s shellcode/target/release/shellcode
    objcopy -O binary shellcode/target/release/shellcode shellcode.bin
Enter fullscreen mode Exit fullscreen mode

and examined with:

.PHONY: dump_shellcode
dump_shellcode: build_shellcode
    objdump -D -b binary -mi386 -Mx86-64 -Mintel -z shellcode.bin
Enter fullscreen mode Exit fullscreen mode
$ make dump_shellcode
Disassembly of section .data:

00000000 <.data>:
   0:   48 8d 35 13 00 00 00    lea    rsi,[rip+0x13]  # 0x1a
   7:   6a 01                   push   0x1
   9:   58                      pop    rax
   a:   6a 0c                   push   0xc
   c:   5a                      pop    rdx
   d:   48 89 c7                mov    rdi,rax
  10:   0f 05                   syscall
  12:   6a 3c                   push   0x3c
  14:   58                      pop    rax
  15:   31 ff                   xor    edi,edi
  17:   0f 05                   syscall
  19:   c3                      ret
  1a:   68 65 6c 6c 6f          push   0x6f6c6c65    # "hello world\n"
  1f:   20 77 6f                and    BYTE PTR [rdi+0x6f],dh
  22:   72 6c                   jb     0x90
  24:   64                      fs
  25:   0a                      .byte 0xa
Enter fullscreen mode Exit fullscreen mode

38 bytes! It's even better than our hand-crafted shellcode!

The only imperfection is the useless ret instruction.

$ make execute_shellcode
hello world
Enter fullscreen mode Exit fullscreen mode

The code is on GitHub

As usual, you can find the code on GitHub: github.com/skerkour/kerkour.com (please don't forget to star the repo 🙏).

Want to learn more Rust, Offensive Security and Applied Cryptography? Take a look at my book Black Hat Rust where, among other things, you will learn how to craft more advanced shellcodes with Rust.

Top comments (0)

DEV

Thank you.

 
Thanks for visiting DEV, we’ve worked really hard to cultivate this great community and would love to have you join us. If you’d like to create an account, you can sign up here.