# BreizhCTF 2026 - Seems Empty


# Seems Empty

- Difficulty: Very Easy
- Category: Reverse
- Author: AntwortEinesLebens

## Description

> During an audit, a strange binary was only displaying a bland message. No network activity, no suspicious writes — nothing but perfectly harmless output. Dismissed among unremarkable artifacts, it was nonetheless tampered with by a malware group.
>
> Even what seems empty can hide a secret.

Files:
- `seems-empty.pyc` — Python bytecode compiled for CPython 3.12

## Solve

### Step 1 — Reconnaissance

```bash
file seems-empty.pyc
# seems-empty.pyc: Byte-compiled Python module for CPython 3.12 or newer,
# timestamp-based, .py timestamp: Sun Apr  5 12:20:40 2026 UTC, .py size: 1132 bytes
```

A `.pyc` is a compiled Python file (bytecode). The source text is not directly readable. First reflex: run `strings` to look for readable hints.

```bash
strings seems-empty.pyc
```

```
Franchement, s'ils sont assez limit
s pour s'arr
ter 
 une phrase aussi vide, tant mieux pour nous.
On leur montre trois mots sans int
t, et ils appelleront 
a une analyse.uq
There's nothing 
to see here...c
J'ai planqu
 la charge dans les caract
res invisibles.
Vu le niveau habituel en face, ils vont encore conclure que c'est "juste une string".
TODO :
1. R
rer le contenu cach
 avec StegCloak.
2. Utiliser "empty" comme mot de passe.
3. Extraire le secret sans ab
mer le leurre ; il ne faudrait pas les brusquer intellectuellement.
main.py
get_secretr
On affiche 
a proprement, et ils pourront croire qu'ils ont fait le tour.N)
print
messager
mainr
__main__N)
__doc__r
__name__r
<module>r
```

Three key pieces of information:
1. The payload is hidden in **invisible characters**
2. The tool used is **StegCloak**
3. The password is **`"empty"`**

Between `"There's nothing "` and `"to see here..."`, `strings` shows what looks like a blank space but actually contains dozens of invisible Unicode characters — that's where the flag is hidden.

### Step 2 — Extracting constants from the bytecode

CPython 3.12 is not supported by standard decompilers like `uncompyle6`. However, Python can read its own bytecode using the `marshal` module:

```python
import marshal

with open("seems-empty.pyc", "rb") as f:
    f.read(16)  # skip header (magic + flags + timestamp + source size)
    code = marshal.load(f)

for c in code.co_consts:
    print(repr(c))
```

Output:

```
"Franchement, s'ils sont assez limités pour s'arrêter à une phrase aussi vide, tant mieux pour nous.\nOn leur montre trois mots sans intérêt, et ils appelleront ça une analyse."
"There's nothing \u2062\u200d\u200c\u200c\u2062\u2062\u2062\u2062\u200d\u200c\u2062\u200d\u2064\u200c\u2061\u2062\u2063\u2064\u200d\u200c\u200c\u200c\u200d\u200c\u200d\u2062\u2061\u2062\u2062\u2063\u2062\u2061\u200c\u2064\u200c\u2062\u200c\u2062\u2061\u2062\u2061\u200c\u200d\u2062\u200c\u200d\u2064\u2061\u200d\u2061\u2062\u2064\u200c\u2061\u200d\u2062\u2062\u2061\u200c\u200c\u200c\u200d\u200c\u200d\u2062\u200c\u2062\u2062\u2062\u2062\u2061\u2062\u200c\u2062\u2062\u2063\u200d\u2062\u200d\u200c\u2064\u200c\u200c\u2063\u2062\u200c\u200c\u2062\u2061\u2063\u2061\u200c\u200c\u2063\u2062\u2063\u2063\u2061\u200c\u200c\u2062\u200c\u200d\u2062\u2061\u2062\u2064\u2062\u2064\u200c\u2062\u2063\u2061to see here..."
<code object get_secret at 0x7bce79f139e0, file "main.py", line 7>
<code object main at 0x7bce79f464c0, file "main.py", line 20>
'__main__'
None
```

The cover string contains **113 invisible Unicode characters** inserted between `"nothing "` and `"to"`. They all belong to the 6 codepoints used by StegCloak:

| Codepoint | Character | Role |
|-----------|-----------|------|
| U+200C | ZERO WIDTH NON-JOINER | value 0 |
| U+200D | ZERO WIDTH JOINER | value 1 |
| U+2061 | FUNCTION APPLICATION | value 2 |
| U+2062 | INVISIBLE TIMES | value 3 |
| U+2063 | INVISIBLE SEPARATOR | value 4 |
| U+2064 | INVISIBLE PLUS | value 5 |

Save the string to `/tmp/steg_input.txt` to keep it alongside the StegCloak install:

```python
for c in code.co_consts:
    if isinstance(c, str) and '⁢' in c:
        with open("/tmp/steg_input.txt", "w", encoding="utf-8") as f:
            f.write(c)
        print("Saved.")
```

### Step 3 — Understanding StegCloak

[StegCloak](https://github.com/KuroLabs/stegcloak) is a Node.js library that hides messages inside text using invisible "zero-width" Unicode characters.

**Encoding:**
1. The secret message is **compressed** then optionally **encrypted** (AES)
2. The result is encoded at **2 bits per ZWC** (6 codepoints → 4 data combinations + 2 flag characters)
3. The ZWC sequence is **inserted just before a word** in the cover text

**Decoding:** StegCloak splits the text on spaces, finds the word preceded by ZWC characters, and extracts the sequence. That's why the payload sits just before `"to"` in `"nothing [ZWC]to see here..."`.

### Step 4 — Installing StegCloak via Docker

```bash
docker run --rm -v /tmp:/tmp node:18-alpine sh -c \
  'cd /tmp && npm install stegcloak'
```

### Step 5 — Extracting the flag

Create `/tmp/decode.js`:

```javascript
const StegCloak = require("/tmp/node_modules/stegcloak");
const fs = require("fs");

const stegcloak = new StegCloak(true, false);
const text = fs.readFileSync("/tmp/steg_input.txt", "utf8");

// Note: the argument order is reveal(coverText, password)
// NOT reveal(password, coverText)
const secret = stegcloak.reveal(text, "empty");
console.log("FLAG:", secret);
```

```bash
node /tmp/decode.js
# FLAG: BZHCTF{n07_r3411y_3mp7y}
```

> **Trap to avoid:** the method is declared `reveal(secret, password)` in StegCloak's source where `secret` refers to the *cover text* (not the hidden secret). Swapping the arguments fails silently with `"Invisible stream not detected!"`.

**Flag : `BZHCTF{n07_r3411y_3mp7y}`**

