All Articles

Greycat CTF 2023 Write-up

This page has been machine-translated from the original page.

I took part in Greycat CTF, which started on 5/19.

This time our team members’ schedules did not line up, so we participated casually and finished in 131st place.

I’ll keep this write-up brief.

Table of Contents

Web-Assembly(Rev)

Handwriting assembly code is a bad idea…

The challenge binary was accompanied by the following JavaScript code.

The function wasmModule.instance.exports.check() that validates the flag appears to be implemented in WebAssembly.

const readline = require('readline');

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});

rl.question('Gimme something: ', (flag) => {
    const wasmBinBuf = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 127, 2, 11, 1, 2, 106, 115, 3, 109, 101, 109, 2, 0, 1, 3, 2, 1, 0, 7, 9, 1, 5, 99, 104, 101, 99, 107, 0, 0, 10, 122, 1, 120, 1, 3, 127, 65, 0, 33, 0, 65, 1, 33, 2, 3, 64, 2, 64, 2, 64, 2, 64, 2, 64, 2, 64, 32, 0, 65, 4, 112, 14, 3, 3, 2, 1, 0, 11, 65, 137, 2, 33, 1, 12, 3, 11, 65, 59, 33, 1, 12, 2, 11, 65, 41, 33, 1, 12, 1, 11, 65, 31, 33, 1, 12, 0, 11, 32, 1, 65, 255, 1, 32, 0, 40, 2, 0, 113, 108, 65, 255, 1, 113, 32, 0, 65, 192, 0, 106, 40, 2, 0, 65, 255, 1, 113, 115, 65, 0, 70, 32, 2, 108, 33, 2, 32, 0, 65, 1, 106, 33, 0, 32, 0, 65, 46, 72, 13, 0, 11, 32, 2, 11])
    const wasmMem = new WebAssembly.Memory({ initial: 10, maximum: 100 });
    var strBuf = new TextEncoder().encode(flag.slice(0, 64));
    const memBuf = new Uint8Array(wasmMem.buffer);
    
    for (let i = 0; i < strBuf.length; i++) {
        memBuf[i] = strBuf[i];
    }

    data = [121, 66, 71, 65, 229, 176, 150, 150, 43, 107, 209, 212, 12, 217, 16, 222, 129, 189, 55, 185, 82, 127, 229, 47, 45, 178, 252, 11, 107, 43, 31, 114, 20, 97, 229, 185, 237, 55, 252, 87, 12, 168, 75, 222, 121, 5]

    for (let i = 0; i < data.length; i++) {
        memBuf[i + 64] = data[i] 
    }

    WebAssembly.instantiate(wasmBinBuf, {js: {mem: wasmMem}}).then(wasmModule => {
        result = wasmModule.instance.exports.check();
        if (result) {
            console.log("Correct flag!");
        } else {
            console.log("?")
        }
    });
    rl.close();
});

The WebAssembly code to be loaded was defined as the byte array in wasmBinBuf, so I decoded it with CyberChef and saved it as a .wasm file.

Next, I decompiled the obtained wasm file with Ghidra and got the following function.

int export::check(void)
{
  uint *puVar1;
  int iVar2;
  int iVar3;
  uint uVar4;
  
  puVar1 = (uint *)0x0;
  iVar3 = 1;
  do {
    uVar4 = (uint)puVar1 % 4;
    if (uVar4 == 0) {
      iVar2 = 0x1f;
    }
    else if (uVar4 == 1) {
      iVar2 = 0x29;
    }
    else if (uVar4 == 2) {
      iVar2 = 0x3b;
    }
    else {
      iVar2 = 0x109;
    }
    iVar3 = (uint)((iVar2 * (*puVar1 & 0xff) & 0xff) == (puVar1[0x10] & 0xff)) * iVar3;
    puVar1 = (uint *)((int)puVar1 + 1);
  } while ((int)puVar1 < 0x2e);
  return iVar3;
}

Since the byte array used for flag verification was defined as data in JavaScript, I wrote the following solver to recover the flag from it.

base = [121, 66, 71, 65, 229, 176, 150, 150, 43, 107, 209, 212, 12, 217, 16, 222, 129, 189, 55, 185, 82, 127, 229, 47, 45, 178, 252, 11, 107, 43, 31, 114, 20, 97, 229, 185, 237, 55, 252, 87, 12, 168, 75, 222, 121, 5]
flag = ""

for i,b in enumerate(base):
    if i % 4 == 0:
        t = 0x1f
    elif i % 4 == 1:
        t = 0x29
    elif i % 4 == 2:
        t = 0x3b
    elif i % 4 == 3:
        t = 0x109
    
    # (t * (x & 0xff) & 0xff) = (b & 0xff)
    for x in range(0xff):
        if (t * (x & 0xff) & 0xff) == (b & 0xff):
            result = x
    
    print(chr(result), end="")

    # grey{0bfusc4t10n_u51ng_w3b4s53mbly_1s_4_th1ng}

With this, I was able to obtain the flag.

ReService(Rev)

I found this file that was installed by the virus. Can you find out what it does?

It seems to connect to a c2 server and makes use of the current time.

By manually reconstructing the decompiled Go binary, I found that it appended an 8-character hex string generated by the following logic to the URL and issued a request with http.Get().

package main

import (
	"fmt"
	"hash/crc32"
	"math/rand"
	"time"
)

func main() {
	now := time.Now().Unix()
	rand.Seed(now)
	hash := crc32.ChecksumIEEE([]byte(fmt.Sprintf("%x", rand.Int())))
	result := fmt.Sprintf("%x", hash)
	fmt.Println(result)
}

I thought I would need to brute-force the time when this file was created, but that wasn’t the case at all; simply looking at the packet stream when the binary was executed revealed the flag immediately.

img

The binary analysis turned out to be completely unrelated, and I still was not really sure what the intended challenge was…