Cursed Map

BearcatCTFby smothy

Cursed Map - Forensics

Points: 436 | Flag: BCCTF{00H_1M_bR07l1_f33ls_S0_g0Od!} | Solved by: Smothy @ 0xN1umb

brotli bomb go brrrr

what we got

A single file: map.pcap (876KB). Challenge description:

"Legend has it that there exists a map that leads to the greatest treasure of all, only the map is cursed so that anyone who opens it dies..."

lmao ok so already getting cursed artifact vibes. let's see what's in this pcap.

initial analysis

Opened it up and it's dead simple traffic-wise - one TCP conversation:

  • Client: 172.29.73.206:60002
  • Server: 10.11.157.174:80 (nginx/1.25.3)

Just a single HTTP request:

GET /flag.txt HTTP/1.1 Host: 10.11.157.174 Accept-Encoding: gzip, deflate, br

And the response:

HTTP/1.1 200 OK Content-Type: text/plain Content-Length: 860207 Content-Encoding: br

So we got ~840KB of brotli-compressed data. Cool, should be easy right? just decompress and grab the flag...

the curse reveals itself

bash
# extract the brotli body from the pcap
tshark -r map.pcap -qz follow,tcp,raw,0 | # extract TCP stream
# ... parse out the HTTP body ...
# save to flag_br.bin (860,207 bytes)

# try to decompress
brotli -d flag_br.bin -o flag.txt
brotli: flag.txt: No space left on device

wait what?? i have 8GB free... tried python next:

python
import brotli
data = open("flag_br.bin", "rb").read()
result = brotli.decompress(data)  # hangs forever, eats all RAM

ngl this had me confused for a sec. then it clicked - it's a brotli bomb. same concept as a zip bomb. 840KB of compressed data that decompresses to an absolutely absurd amount.

that's the "curse" - anyone who tries to open (decompress) it, their system dies. classic.

this is fine

how big is this thing??

wrote a C program using libbrotli to stream-decompress without storing anything:

c
#include <stdio.h>
#include <stdlib.h>
#include <brotli/decode.h>

int main() {
    FILE *f = fopen("flag_br.bin", "rb");
    fseek(f, 0, SEEK_END);
    size_t file_size = ftell(f);
    fseek(f, 0, SEEK_SET);
    uint8_t *input = malloc(file_size);
    fread(input, 1, file_size, f);
    fclose(f);

    BrotliDecoderState *state = BrotliDecoderCreateInstance(NULL, NULL, NULL);
    size_t available_in = file_size;
    const uint8_t *next_in = input;
    size_t out_size = 64 * 1024 * 1024;  // 64MB output buffer
    uint8_t *output = malloc(out_size);
    size_t available_out, total_out = 0;

    while (1) {
        available_out = out_size;
        uint8_t *next_out = output;
        BrotliDecoderResult result = BrotliDecoderDecompressStream(
            state, &available_in, &next_in,
            &available_out, &next_out, &total_out);

        if (result == BROTLI_DECODER_RESULT_SUCCESS) {
            printf("Total: %zu bytes (%zuGB)\n",
                total_out, total_out/(1024*1024*1024));
            break;
        } else if (result == BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT) {
            continue;  // just discard output buffer and keep going
        } else {
            printf("Error!\n");
            break;
        }
    }
    // cleanup...
}
bash
gcc -O2 -o br_analyze br_analyze.c -lbrotlidec -lbrotlicommon
./br_analyze

Result:

Total: 1099511627811 bytes (1024GB) Consumed: 860207/860207

840KB compresses to exactly 1,024 GB (1 TB + 35 bytes). compression ratio of 1,278,194:1. absolutely insane.

the key detail: it's 1TB + 35 bytes. that extra 35 bytes is sus af.

the rabbit holes (what didn't work)

before finding the right approach, tried a bunch of stuff:

1. covert channels in packet headers

  • IP ID gaps from server: only values 1-4 (looked like base-4 encoding). tried every combination of bit mappings. nope, just garbage.
  • TTL values: constant (64 client, 126 server). nothing.
  • TCP timestamps, window sizes, MAC addresses: all normal.
  • Retransmissions, overlapping segments: none.

2. steganography in brotli structure

the compressed data had a 105-byte repeating block pattern (8192 blocks + 47 byte remainder). each block had 56 byte positions with exactly 2 possible values across all blocks. tried interpreting each as a bit for steganography - no flag found in any position.

3. brotli metadata blocks

brotli format supports metadata blocks (arbitrary data the decompressor ignores). parsed the format looking for hidden metadata. found none - it's all compressed data blocks.

4. checking tail bytes

wrote a program to capture the last 4096 bytes of decompressed output (hoping the 35 extra bytes had the flag). after decompressing the full 1TB... the tail was all zeros. the 35 extra bytes were nulls too. rip.

the actual solve

the flag isn't at the end. it's not in the headers. it's not steganography. it's buried deep inside 1TB of null bytes.

wrote a C scanner that decompress the entire stream while checking every single byte for non-null values:

c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <brotli/decode.h>

int main() {
    FILE *f = fopen("flag_br.bin", "rb");
    fseek(f, 0, SEEK_END);
    size_t file_size = ftell(f);
    fseek(f, 0, SEEK_SET);
    uint8_t *input = malloc(file_size);
    fread(input, 1, file_size, f);
    fclose(f);

    BrotliDecoderState *state = BrotliDecoderCreateInstance(NULL, NULL, NULL);
    size_t available_in = file_size;
    const uint8_t *next_in = input;
    size_t out_size = 64 * 1024 * 1024;
    uint8_t *output = malloc(out_size);
    size_t available_out, total_out = 0;
    char flag_buf[4096];
    int flag_idx = 0;

    while (1) {
        available_out = out_size;
        uint8_t *next_out = output;
        BrotliDecoderResult result = BrotliDecoderDecompressStream(
            state, &available_in, &next_in,
            &available_out, &next_out, &total_out);

        // scan every output byte for non-null
        size_t produced = out_size - available_out;
        for (size_t i = 0; i < produced; i++) {
            if (output[i] != 0) {
                size_t abs_pos = total_out - produced + i;
                fprintf(stderr, "NON-NULL at offset %zu: 0x%02x '%c'\n",
                    abs_pos, output[i],
                    (output[i] >= 32 && output[i] < 127) ? output[i] : '.');
                if (flag_idx < 4095) flag_buf[flag_idx++] = output[i];
            }
        }

        if (result == BROTLI_DECODER_RESULT_SUCCESS) {
            flag_buf[flag_idx] = 0;
            printf("DONE: %zu bytes, Non-null: %d\n", total_out, flag_idx);
            if (flag_idx > 0) printf("Flag: %s\n", flag_buf);
            break;
        } else if (result == BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT) {
            continue;
        } else {
            printf("Error\n"); break;
        }
    }
    // cleanup...
}
bash
gcc -O2 -o br_scan br_scan.c -lbrotlidec -lbrotlicommon
./br_scan 2>&1

waited a few minutes... scanning through hundreds of GBs of nulls... then:

Scanned: 50GB, consumed=42014/860207, non-null=0 Scanned: 100GB, consumed=84014/860207, non-null=0 Scanned: 150GB, consumed=126014/860207, non-null=0 Scanned: 200GB, consumed=168014/860207, non-null=0 NON-NULL at offset 223338299392: 0x42 'B' NON-NULL at offset 223338299393: 0x43 'C' NON-NULL at offset 223338299394: 0x43 'C' NON-NULL at offset 223338299395: 0x54 'T' NON-NULL at offset 223338299396: 0x46 'F' NON-NULL at offset 223338299397: 0x7b '{' NON-NULL at offset 223338299398: 0x30 '0' NON-NULL at offset 223338299399: 0x30 '0' NON-NULL at offset 223338299400: 0x48 'H' NON-NULL at offset 223338299401: 0x5f '_' NON-NULL at offset 223338299402: 0x31 '1' NON-NULL at offset 223338299403: 0x4d 'M' ... NON-NULL at offset 223338299425: 0x21 '!' NON-NULL at offset 223338299426: 0x7d '}'

THERE IT IS. 35 bytes of flag, hiding at exactly 208GB into the stream (20.3% through the 1TB). the rest of the 1TB after the flag? all nulls again.

the numbers

metricvalue
compressed size840 KB
decompressed size1,024 GB (1 TB + 35 bytes)
compression ratio1,278,194 : 1
flag locationoffset 223,338,299,392 (208 GB in)
flag percentage20.3% into stream
non-null bytes in 1TBexactly 35 (the flag)
RAM used~128 MB (streaming decompression)

key takeaways

  1. brotli bombs are real - same concept as zip bombs but using brotli's insane compression of repetitive data. 840KB to 1TB is wild.

  2. you can't skip ahead in brotli - it's a streaming format with sliding window context. you HAVE to decompress sequentially. there's no "jump to offset" shortcut.

  3. stream it, don't store it - the trick is using a fixed-size output buffer and discarding data after scanning. you only need ~128MB RAM regardless of decompressed size.

  4. use C, not Python - python's brotli bindings are way too slow for scanning 1TB. the C libbrotli library with -O2 can push through at ~50GB/min on a decent CPU.

  5. needle in a haystack - the entire 1TB was null bytes except for 35 bytes of flag hidden 208GB in. the challenge author really said "good luck finding this without a proper scanner" lmao

  6. the challenge name is the hint - "Cursed Map" = brotli bomb (the curse kills anyone who opens it). you weren't supposed to fully decompress it to disk, you were supposed to stream-scan it.

flag

BCCTF{00H_1M_bR07l1_f33ls_S0_g0Od!}

"OOH I'M brotli feels SO good!"

fr tho, finding that flag after scanning through 208GB of nothing hit different

celebration


smothy out ✌️