Final Round

Welcome back to my writeup! With Trend Micro's uCTF 2025 now complete, I’m sharing my writeup for the finals. Please note that I’ll only cover two challenges, the ones I personally solved, since some of the others were handled by my team members. These two challenges were key to helping our team secure 3rd place.

C2 An Obfuscated Script

For this challenge, we’re given an obfuscated JavaScript code. Essentially, it’s a cryptography challenge, but with a few twists to make it more difficult.

C2_an_obfuscated_script.js

const cRyPtO = require("crypto"); //library

function _0x5a93d2(_0x1ca7d3, _0xb3fde3, _0x323f78, _0xd89189) {
  const _0x58965c = cRyPtO.pbkdf2Sync(_0xb3fde3, Buffer.from(_0x323f78, "base64"), 100000, 32, "sha512");
  const _0x3b71c4 = cRyPtO.createDecipheriv("aes-256-cbc", _0x58965c, Buffer.from(_0xd89189, "base64"));

  let _0xe13a10 = _0x3b71c4.update(_0x1ca7d3, "base64", "utf8");
  _0xe13a10 += _0x3b71c4.final("utf8");

  new Function("require", _0xe13a10)(require);
}

const _0x1b50eb = "5BaJWs4GlyJV2SlS3li+6oNesISAk+gOoVQkB9z98eUUN+/I9N5FJSkxQFPI7SqjC+/znRGSbfF8dYv/bJUC1ENT9yNp37Ff3Ecln0OKkbnwL4FB3KWpEbzUByAbnh6V47+AIJ7NkaGEiCIB1k2qmBeDBKHPOBWSTaMinYql3Aw=";
const _0xb3fde3 = "NgzBrZ2LvJmBDcoB8jy75+aSMRYG4vhP";
const _0x28f6c4 = "Yx4gw7KaMEqdcyDTjRXnnw==";
const _0x466dee = "8IhD/HFZmalUu9GzVK1KmA==";

_0x5a93d2(_0x1b50eb, _0xb3fde3, _0x28f6c4, _0x466dee);

This JavaScript code is an obfuscated self-decrypting script that uses cryptography to hide its true functionality until runtime. It first derives a 256-bit AES key from a password (_0xb3fde3) and a Base64-encoded salt (_0x28f6c4) using PBKDF2 (Password-Based Key Derivation Function 2) with SHA-512 as the hashing function and 100,000 iterations. PBKDF2 essentially stretches a password into a fixed-length cryptographic key by repeatedly hashing it with the salt, mathematically defined as:

DK=PBKDF2H(P,S,c,dkLen)DK = PBKDF2_H(P, S, c, dkLen)

where PP is the password, SS is the salt, CC is the iteration count, dkLendkLen is the desired key length, and HH is the hash function (SHA-512). Using this derived key, the script sets up an AES-256-CBC cipher with the provided initialization vector (_0x466dee) to decrypt the Base64-encoded ciphertext (_0x1b50eb). AES-CBC divides the plaintext into blocks and encrypts each block while chaining it with the previous ciphertext block; mathematically, each ciphertext block is Ci=EK(PiCi1)Ci=EK(PiCi1)Ci=EK(PiCi1)Ci=EK(Pi⊕Ci−1)C_i = E_K(P_i \oplus C_{i-1})Ci​=EK​(Pi​⊕Ci−1​) with C0=IVC0=IVC0=IVC0=IVC_0 = IVC0​=IV. After decryption, the result is dynamically executed using new Function("require", ...), meaning the hidden payload is run as JavaScript code. Essentially, this script hides its real functionality behind strong cryptography and executes it only when the correct key and IV are used.

To solve this challenge, we first identify the ciphertext, password, salt, and IV from the script. Using the password and salt, we derive a key and decrypt the ciphertext with AES-256-CBC to reveal the flag.

solve.py

import base64, hashlib
from Crypto.Cipher import AES

# Decrypt AES-256-CBC ciphertext using PBKDF2-HMAC-SHA512 derived key
decrypt = lambda c, p, s, iv: (lambda d: d[:-d[-1]].decode('utf-8'))( 
    AES.new(
        hashlib.pbkdf2_hmac('sha512', p.encode(), base64.b64decode(s), 100_000, 32), # Derive 32-byte AES key using PBKDF2-HMAC-SHA512
        AES.MODE_CBC, # Use AES in CBC mode
        base64.b64decode(iv) # Decode the Initialization Vector from Base64
    ).decrypt(base64.b64decode(c)) # Decode the ciphertext from Base64 and decrypt
)

print(decrypt(
    "5BaJWs4GlyJV2SlS3li+6oNesISAk+gOoVQkB9z98eUUN+/I9N5FJSkxQFPI7SqjC+/znRGSbfF8dYv/bJUC1ENT9yNp37Ff3Ecln0OKkbnwL4FB3KWpEbzUByAbnh6V47+AIJ7NkaGEiCIB1k2qmBeDBKHPOBWSTaMinYql3Aw=",
    "NgzBrZ2LvJmBDcoB8jy75+aSMRYG4vhP",
    "Yx4gw7KaMEqdcyDTjRXnnw==",
    "8IhD/HFZmalUu9GzVK1KmA=="
))

Here's the output

C10 Invisible Challenge

For this challenge, we were also given a straightforward JavaScript code.

C10_invisible_challenge.js

// Invisible Secrets - Can you find the hidden flag?
// Hint: Something invisible is hiding in plain sight...

const secret = {
  message: "ᅠㅤᅠᅠᅠㅤᅠᅠᅠㅤᅠᅠᅠㅤᅠㅤᅠㅤᅠᅠᅠᅠㅤㅤᅠㅤᅠᅠㅤㅤㅤㅤᅠㅤᅠInvisible Stringᅠㅤᅠᅠᅠㅤᅠᅠᅠㅤᅠᅠᅠㅤᅠㅤᅠㅤㅤㅤㅤᅠㅤㅤᅠᅠㅤㅤᅠᅠᅠㅤᅠㅤㅤᅠㅤㅤㅤᅠᅠㅤᅠㅤᅠㅤㅤᅠᅠᅠㅤㅤᅠᅠᅠㅤᅠㅤㅤㅤᅠᅠㅤㅤᅠᅠㅤㅤᅠᅠᅠㅤᅠㅤㅤᅠᅠᅠㅤᅠᅠㅤㅤᅠㅤㅤᅠᅠᅠᅠㅤㅤᅠᅠㅤㅤᅠㅤㅤㅤㅤㅤᅠㅤ"
};

// The flag is right in front of your eyes, but can you see it?
console.log("Find the invisible flag!");

The code defines a secret.message string full of unusual characters and prints "Find the invisible flag!". To most editors and on normal screens, these characters appear as blank spaces because they are full-width spaces and Hangul filler characters, which render almost invisibly. That’s why the string seems empty, even though it actually contains data.

The hidden message is encoded in these invisible characters: typically represents 0 and ㅤ1. By mapping each character to a bit and converting the resulting binary string to ASCII, the real content which is the flag can be revealed. Essentially, the flag is right in front of your eyes, but standard text rendering hides it, requiring careful decoding to uncover.

To decode the invisible string, you need to map each character to a binary value—commonly, as 0 and ㅤ1 then concatenate these bits into a binary string. Next, split the binary string into bytes (8 bits each) and convert each byte to its ASCII character. Doing this for the entire string will reveal the hidden message or flag that’s encoded within the seemingly invisible characters.

solve.py

# Invisible character message from JS
secret = "ᅠㅤᅠᅠᅠㅤᅠᅠᅠㅤᅠᅠᅠㅤᅠㅤᅠInvisible String"

# Map invisible chars to bits and convert every 8 bits to a character
flag = "".join(
    chr(int("".join("01"[c=="ㅤ"] for c in secret[i:i+8]), 2))
    for i in range(0, len(secret), 8)
)

print(flag)

Here's the output

And that’s a wrap! Participating in such a large-scale CTF competition has been both incredibly enjoyable and a significant personal achievement. The challenges were tough, but they offered a lot of learning opportunities. Securing 3rd place with our team feels like a major accomplishment and is something I’m truly proud of.

Last updated