← Back to Writeups
HTBEasyMisc

Dust to Dust

D3v0o0Nu119/2/20265 min read
#misc#htb#easy#bitmap_reconstruction_from_pac#reverse_custom_encoding#six_bit_unpacking#source_guided_decoder_reimplem

Dust to Dust

Platform: Dawgctf | Category: Misc | Type: Challenge | Difficulty: Easy | OS: NA | Author: D3v0o0Nu11 | Date: 2026-04-10 | Status: Solved Techniques: bitmap_reconstruction_from_packed_text, reverse_custom_encoding, six_bit_unpacking, source_guided_decoder_reimplementation

Summary

Task: a C encoder compresses a binary image by packing each 2x3 pixel block into one printable ASCII byte, with } as row separators and ~ as EOF. Solution: subtract 0x20, expand every byte back to 6 bits, rebuild paired bitmap rows, and render the recovered image to read the handwritten flag.

Recon

Port scan

bash
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: dawgctf | ID: 20260410_dawgctf_dust_to_dust
  • Tags: custom_encoding, dawgctf, bitmap_reconstruction, binary_image, printable_ascii, six_bit_packing, source_review
  • Indicators: Source code reads input.txt as rows of only 0 and 1 characters, Image width must be a multiple of 3 and height must be a multiple of 2, Each output byte is forced into printable ASCII via 0x20 + value, Row separators are explicit non-image delimiters (}) and the stream ends with ~, Recovered data looks like a monochrome bitmap rather than text or ciphertext
  • Source: 20260410_dawgctf_dust_to_dust.md

Foothold

Vulnerability / Misconfiguration

  1. bitmap_reconstruction_from_packed_text
  2. reverse_custom_encoding
  3. six_bit_unpacking
  4. source_guided_decoder_reimplementation
bash
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

bash
sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
bash
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • bitmap_reconstruction_from_packed_text
  • reverse_custom_encoding
  • six_bit_unpacking
  • source_guided_decoder_reimplementation
  • Tags: custom_encoding, dawgctf, bitmap_reconstruction, binary_image, printable_ascii, six_bit_packing, source_review

Original Writeup

<details><summary>Click to expand original content</summary>

Description

File: chal.zip

The repository entry only exposes the archive, so the practical challenge is to reverse the included encoder and decode output.txt. The recovered data is not normal plaintext: it is a packed monochrome bitmap that must be reconstructed into an image before the flag becomes readable.

Summary

encoder.c shows that the challenge takes a binary image, packs every 2x3 pixel block into one printable ASCII character, separates compressed rows with }, and terminates the stream with ~. Reversing that packing reconstructs the original bitmap, and rendering it reveals a handwritten DawgCTF flag.

Analysis

1. Read the encoder instead of guessing the format

The key artifact is encoder.c. Its input validation immediately tells us what the original data looked like:

  • input.txt must contain only 0 and 1
  • every row length must be a multiple of 3
  • all rows must have equal length
  • the number of rows must be a multiple of 2

That is a strong sign that the encoder is treating the file as a binary image or bitmap, not as text.

2. Understand the 2x3 packing rule

Inside compressArray, the encoder reads pixels in this order:

c
buffer[0] = arr[l*2][w*3];
buffer[1] = arr[l*2][w*3 + 1];
buffer[2] = arr[l*2][w*3 + 2];
buffer[3] = arr[l*2 + 1][w*3];
buffer[4] = arr[l*2 + 1][w*3 + 1];
buffer[5] = arr[l*2 + 1][w*3 + 2];

So each encoded character represents a 2-row by 3-column block:

text
top row:    b0 b1 b2
bottom row: b3 b4 b5

The six bits are parsed as a binary integer and shifted into the printable ASCII range:

c
long bin = strtol(buffer, NULL, 2);
c = (char)(0b00100000 + bin);

So the mapping is:

text
encoded_char = chr(int(six_bits, 2) + 0x20)

3. Understand the row delimiters

writeArray writes each compressed row followed by 0x7d (}), then appends 0x7e (~) once at the end:

c
fprintf(output, "%s%c", arr[i], (char)0b01111101);
...
fprintf(output, "%c", (char)0b01111110);

That means the decoding procedure is deterministic:

  1. remove the final ~
  2. split the stream on }
  3. for each character in a compressed row, compute ord(ch) - 32
  4. convert that value back to a 6-bit binary string
  5. use the first 3 bits for the top row chunk and the last 3 bits for the bottom row chunk
  6. append those chunks to reconstruct the full bitmap

4. Rebuild and render the bitmap

Once the rows are reconstructed, the output is a large black-and-white image. Writing it back as plain bitmap text or as a PBM image makes the content visible immediately.

The recovered image contains handwritten text spelling the flag. A few letters are visually ambiguous in the raw handwriting, but the correct final flag is the user-confirmed one below.

Solution

The decoder can be implemented in a few lines of Python:

python
#!/usr/bin/env python3

from pathlib import Path

def decode(path="output.txt"):
    data = Path(path).read_text()
    assert data.endswith("~"), "missing stream terminator"

    rows = []
    for packed_row in data[:-1].split("}"):
        if not packed_row:
            continue

        top = []
        bottom = []
        for ch in packed_row:
            value = ord(ch) - 0x20
            bits = f"{value:06b}"
            top.append(bits[:3])
            bottom.append(bits[3:])

        rows.append("".join(top))
        rows.append("".join(bottom))

    return rows

def save_outputs(rows):
    Path("recovered_input.txt").write_text("\n".join(rows) + "\n")

    width = len(rows[0])
    height = len(rows)
    with open("recovered.pbm", "w") as f:
        f.write(f"P1\n{width} {height}\n")
        for row in rows:
            f.write(" ".join(row) + "\n")

if __name__ == "__main__":
    bitmap_rows = decode("output.txt")
    save_outputs(bitmap_rows)
    print(f"Recovered bitmap: {len(bitmap_rows)} rows x {len(bitmap_rows[0])} cols")
    print("Saved: recovered_input.txt, recovered.pbm")

Step-by-step

  1. Open encoder.c and notice it reads input.txt as a binary bitmap.
  2. Observe that compression operates on 2x3 pixel blocks, producing 6-bit values.
  3. Notice the printable ASCII bias: each 6-bit value is stored as value + 0x20.
  4. Notice the structural delimiters: } separates compressed rows and ~ marks end-of-stream.
  5. Reverse the transform by subtracting 0x20, re-expanding to 6 bits, and splitting the bits back into top and bottom 3-pixel chunks.
  6. Save the reconstructed rows and render them as a monochrome image.
  7. Read the handwritten flag from the recovered bitmap.
</details>

signed by XESXOR