BreizhCTF 2024 - Rust Golfing writeup

May 23, 2024

This year, BreizhCTF had a nice MISC challenge that asked us to do some rust golf to get a flag.

To win the flag, you had to:

  • create a rust code that can listen to a port given as first argument of your program ;
  • read a shellcode from this port ;
  • map it, and execute it, without crashing ;
  • do everything with a source code that does not exceed 415 bytes.

The main difficulty about this challenge was to map the code in an executable section without any external crate.

But first things first, let’s write a code that listens to a given port:

fn main() -> std::io::Result<()> {
    std::net::TcpListener::bind(format!("127.1:{}", std::env::args().last().unwrap()))?;
    Ok(())    
}

Since we are golfing, I will be using 127.1 as a local IP address, which allows us to save bytes over 127.0.0.1.

Next, we need to create an executable memory page that can hold some shellcode. The only way to do so, assuming that our target runs on Linux, is to use the mmap syscall. According to this syscall table, this syscall takes quite a lot of arguments. Since we want to minimize the number of bytes of our source code, I will be ignoring some arguments that aren’t mandatory for the syscall to be valid:

let l: u64;
unsafe {
    asm!("
        mov rax,9
        xor rdi,rdi
        mov rsi,0xffff
        mov rdx,7
        mov r10,34 
        syscall
        mov {},rax",
        out(reg) rwx_mem_addr, // Saving mmap'ed address to variable `rwx_mem_addr'
    )
}

Since we are mapping MMAP_ANONYMOUS code, mmap’s fd and offset arguments are ignored, which allows us to skip r8 and r9 register initialization. r10 holds the flags, which are MMAP_ANONYMOUS | MMAP_PRIVATE, while rdx holds the protections, which are PROT_EXEC | PROT_READ | PROT_WRITE (see mmap.h).

Next, we need to read the incoming shellcode on our previously opened port, and copy it all to our newly created RWX memory. This should be fairly easy using std::ptr::copy:

let mut buffer = [0; 0x100];
socket.accept()?.0.read(&mut buffer)?;
std::ptr::copy(&mut buffer[0], rwx_mem_addr as *mut u8, 0x100);

Last, we call our freshly copied shellcode using asm!("call {}", in(reg) rwx_mem_addr,).

Here is the final code:

use std::io::Read;
use std::arch::asm;

fn main() -> std::io::Result<()>{
    let mut buffer = [0; 0x100];
    let socket = std::net::TcpListener::bind(format!("127.1:{}",std::env::args().last().unwrap()))?;
    let l:u64;
    unsafe {
        asm!("
            mov rax,9
            xor rdi,rdi
            mov rsi,0xffff
            mov rdx,7
            mov r10,34
            syscall
            mov {},rax",
            out(reg) l,
        );
        socket.accept()?.0.read(&mut buffer)?;
        std::ptr::copy(&mut buffer[0], rwx_mem_addr as *mut u8,0x100);
        asm!("call {}", in(reg) rwx_mem_addr,)
    }
    
    Ok(())
}

Now all we need to do is minify it:

use std::io::Read;use std::arch::asm;fn main() -> std::io::Result<()>{let mut b=[0;0x100];let x=std::net::TcpListener::bind(format!("127.1:{}",std::env::args().last().unwrap()))?;let l:u64;unsafe {asm!("
mov rax,9
xor rdi,rdi
mov rsi,0xffff
mov rdx,7
mov r10,34
syscall
mov {},rax",out(reg)l,);x.accept()?.0.read(&mut b)?;std::ptr::copy(&mut b[0],l as *mut u8,0x100);asm!("call {}",in(reg) l,);}Ok(())}