← Back to Writeups
CUSTOMMediumReversing

Data Needs Splitting

XESXOR9/10/20265 min read
#reversing#custom#medium#decimal_stream_backtracking#dns_chunk_reassembly#java_bytecode_reversing#xor_mask_reversal

Data Needs Splitting

Platform: CUSTOM | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: decimal_stream_backtracking, dns_chunk_reassembly, java_bytecode_reversing, xor_mask_reversal

Summary

Walkthrough of the CUSTOM Reversing Data Needs Splitting. Task: a MetaCTF domain hides its real payload in numbered DNS TXT records instead of a normal host response. Solution: rebuild the base64 JAR from TXT chunks, reverse the dynamically loaded Java valid

Description

Target domain: data-needs-splitting.umbccd.net

English summary: the challenge hides its actual artifact inside DNS TXT records. Reassembling those records yields a Java archive whose validator must be reversed to recover the correct flag string.

Challenge Overview

The main trick is that the domain does not behave like a normal service endpoint. Instead of returning a useful A record, it stores the payload across many numbered TXT records. Those chunks reconstruct a JAR file, and the JAR hides its real validator logic in assets/file.dat, which is dynamically loaded as a Java class.

One important oddity is the final flag format: the challenge was solved under MetaCTF, but the valid flag is clearly DawgCTF-formatted:

text
DawgCTF{REDACTED}

That mismatch should not be ignored during solving, because it is a strong hint that the backend content was reused or shared even though the event metadata says MetaCTF.

Reconnaissance

Initial DNS resolution did not produce a useful normal host record. Querying TXT records immediately revealed multiple long strings prefixed with two-digit indices such as 00, 01, 02, and so on.

Useful enumeration commands:

bash
nslookup -type=TXT data-needs-splitting.umbccd.net
curl -s -H "accept: application/dns-json" "https://dns.google/resolve?name=data-needs-splitting.umbccd.net&type=TXT"

This strongly suggests chunked data exfiltration or staged storage over DNS. The numbered prefixes indicate the intended recovery process: strip the index, sort by index, and concatenate the remaining base64 data.

DNS Chunk Reconstruction

Each TXT answer starts with a two-digit sequence number followed by a base64 fragment. After removing the index bytes, sorting numerically, and concatenating the rest, the result decodes cleanly into a ZIP/JAR file.

Recovery workflow:

  1. Query the TXT records.
  2. For each record, split it into index = chunk[:2] and data = chunk[2:].
  3. Sort by index.
  4. Concatenate all data pieces.
  5. Base64-decode the result into challenge_blob.bin.

The reconstructed file is actually a Java archive even though it has a .bin extension. Listing it shows:

text
META-INF/MANIFEST.MF
Loader.class
Main.class
assets/file.dat

Useful inspection commands:

bash
unzip -l challenge_blob.bin
javap -classpath challenge_blob.bin -c -p Main
javap -classpath challenge_blob.bin -c -p Loader
unzip -p challenge_blob.bin assets/file.dat > file.dat
cp file.dat Validator.class
javap -c -p Validator

Java Reversing Steps

Main loads /assets/file.dat through Loader and invokes validate(). Although the file is named like data, it is actually a compiled Java class named Validator.

Disassembly of Validator.validate() shows the whole check:

  • it prompts with Enter the flag:
  • it reads one line of input
  • for each character at position i, it extracts two rotating 16-bit masks from the long constants
    • 2194307438957234483
    • 148527584754938272
  • the rotation uses (i % 4) * 16, so the masks repeat every four characters
  • it computes:
text
transformed = ord(ch) ^ mask1 ^ mask2
  • it appends each transformed value as a decimal string to a StringBuilder
  • it compares the final concatenated decimal string against the hardcoded target:
text
145511939249997195145441944550467175145531942549987228145401943650017203145451934650207244145651934650127169

The critical detail is that the program does not store separators between numbers. So reversing requires both undoing the XOR and segmenting the big decimal string correctly.

Recovering the Valid Flag String

Because the masks are fully known and repeat every four positions, each character can be tested independently once we choose how many decimal digits belong to that position. A small backtracking script over printable characters recovers the unique string whose transformed values concatenate to the target decimal blob.

Full working solve script:

python
#!/usr/bin/env python3
import json
import base64
import zipfile
from functools import lru_cache
from pathlib import Path

DOMAIN = "data-needs-splitting.umbccd.net"
DNS_JSON = Path("dns_txt.json")
JAR_PATH = Path("challenge_blob.bin")
TARGET = "145511939249997195145441944550467175145531942549987228145401943650017203145451934650207244145651934650127169"
CONST_A = 2194307438957234483
CONST_B = 148527584754938272


def rebuild_jar_from_dns() -> None:
    data = json.loads(DNS_JSON.read_text())
    answers = data["Answer"]
    txt_chunks = []

    for answer in answers:
        if answer.get("type") != 16:
            continue
        chunk = answer["data"]
        txt_chunks.append((int(chunk[:2]), chunk[2:]))

    blob_b64 = "".join(piece for _, piece in sorted(txt_chunks))
    JAR_PATH.write_bytes(base64.b64decode(blob_b64))


def mask_at(index: int) -> int:
    shift = (index % 4) * 16
    m1 = (CONST_A >> shift) & 0xFFFF
    m2 = (CONST_B >> shift) & 0xFFFF
    return m1 ^ m2


@lru_cache(maxsize=None)
def recover(pos: int, idx: int):
    if pos == len(TARGET):
        return [""]

    out = []
    mask = mask_at(idx)
    for ch in range(32, 127):
        encoded = str(ch ^ mask)
        if TARGET.startswith(encoded, pos):
            for rest in recover(pos + len(encoded), idx + 1):
                out.append(chr(ch) + rest)
    return out


def main() -> None:
    rebuild_jar_from_dns()

    with zipfile.ZipFile(JAR_PATH) as zf:
        print("JAR entries:")
        for name in zf.namelist():
            print(f" - {name}")

    candidates = recover(0, 0)
    if len(candidates) != 1:
        raise SystemExit(f"Expected one solution, got {len(candidates)}: {candidates}")

    flag = candidates[0]
    print(f"Recovered flag: {flag}")


if __name__ == "__main__":
    main()

Running the logic yields exactly one valid candidate:

text
DawgCTF{REDACTED}

Verification

The recovered string can be checked directly against the rebuilt JAR:

bash
printf 'DawgCTF{REDACTED}\n' | java -jar challenge_blob.bin

Observed output:

text
Enter the flag:
Correct!

Key Takeaways / Lessons

  • Mastering decimal_stream_backtracking, dns_chunk_reassembly, java_bytecode_reversing, xor_mask_reversal was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-04-10 • CUSTOM • Data Needs Splitting • Difficulty: Medium