PICOCTF 2019 • REVERSE ENGINEERING • MULTI-IMAGE CARVING

Investigative Reversing 1: Multi-PNG Overlay Carving

By Abdo•Aug 31, 2026
Investigative Reversing 1 Analysis
Official Challenge Prompt

“We have recovered a binary and a few images. See if you can recover the flag.”

● Category: Forensics / Reverse Engineering● Points: 350 PTS● Flag Format: picoCTF{...}
Dissector: Ghidra + HxD

Forensic Analysis: Multi-File Trailing Overlay Splitting

The binary reads flag.txt and fragments the 26-byte flag string across three separate target PNG images (mystery.png, mystery2.png, and mystery3.png). Each fragment is appended past the standard IEND end-of-file marker. Additionally, specific character transformations are applied (such as adding 21 to index 7 before writing to the second image). Extracting the full flag requires carving the trailing bytes from all three images and reversing the assignment indices.

STEP 1: REVERSE ENGINEERING

1. Decompiling `mystery` in Ghidra

Opening mystery in Ghidra and inspecting main() reveals the crucial clue: all three PNG files are opened with mode "a" (Append mode).

int main(void) {
    FILE *flag_file = fopen("flag.txt", "r");
    FILE *f1 = fopen("mystery.png", "a");   // Append mode (writes past IEND)
    FILE *f2 = fopen("mystery2.png", "a");
    FILE *f3 = fopen("mystery3.png", "a");

    char flag[26];
    fread(flag, 26, 1, flag_file);

    // Distribution Sequence:
    fputc(flag[1], f3);          // mystery3.png gets flag[1]
    fputc(flag[0] + 21, f2);     // mystery2.png gets flag[0] + 21 (0x15)
    fputc(flag[2], f3);          // mystery3.png gets flag[2]
    fputc(flag[5], f3);          // mystery3.png gets flag[5]
    fputc(flag[4], f1);          // mystery.png  gets flag[4]

    // Indices 6..9 to mystery.png
    for (int i = 6; i <= 9; i++) {
        fputc(flag[i], f1);
    }
    fputc(flag[3] + 4, f2);      // mystery2.png gets flag[3] + 4

    // Indices 10..14 to mystery3.png
    for (int i = 10; i <= 14; i++) {
        fputc(flag[i], f3);
    }

    // Indices 15..25 to mystery.png
    for (int i = 15; i <= 25; i++) {
        fputc(flag[i], f1);
    }

    return 0;
}
Mathematical Byte Distribution Summary:
  • mystery.png receives 16 bytes: flag[4], flag[6..9], flag[15..25]
  • mystery2.png receives 2 bytes: flag[0] + 21, flag[3] + 4
  • mystery3.png receives 8 bytes: flag[1], flag[2], flag[5], flag[10..14]
  • Total: 16 + 2 + 8 = 26 bytes (Exact match for the 26-character flag).
METHOD A: HANDS-ON MANUAL RECONSTRUCTION

2. Manual Solution (Hex Editor & Pen & Paper Table)

How a beginner can solve this by hand using just HxD / HexEd.it and an ASCII decimal chart.

Step 1: Extract Trailing Bytes Past `IEND` in Each Image

In any Hex Editor, jump to the end of each image file. Look immediately past the standard PNG end marker IEND (49 45 4E 44 AE 42 60 82):

1. mystery.png (16 Bytes):
43 46 7B 41 6E 31 5F 38 61 34 34 38 63 62 32 7D
ASCII: CF{An1_8a448cb2}
2. mystery2.png (2 Bytes):
85 73
ASCII: \x85 and 's'
3. mystery3.png (8 Bytes):
69 63 54 30 74 68 61 5F
ASCII: icT0tha_

Step 2: Solve the Math for the 2 Modified Characters

Slot 0 Calculation:
mystery2[0] = 0x85 (Decimal 133)
Slot 0 = 133 - 21 = 112 = 'p'
Slot 3 Calculation:
mystery2[1] = 's' (Decimal 115)
Slot 3 = 115 - 4 = 111 = 'o'

Step 3: Complete 26-Slot Reconstruction Matrix

Slot IndexSource File & PositionEncoded ByteMath / OperationDecoded Character
0mystery2.png [0]0x85 (133)133 - 21p
1mystery3.png [0]'i' (105)Unchangedi
2mystery3.png [1]'c' (99)Unchangedc
3mystery2.png [1]'s' (115)115 - 4o
4mystery.png [0]'C' (67)UnchangedC
5mystery3.png [2]'T' (84)UnchangedT
6mystery.png [1]'F' (70)UnchangedF
7mystery.png [2]'{' (123)Unchanged{
8mystery.png [3]'A' (65)UnchangedA
9mystery.png [4]'n' (110)Unchangedn
10mystery3.png [3]'0' (48)Unchanged0
11mystery3.png [4]'t' (116)Unchangedt
12mystery3.png [5]'h' (104)Unchangedh
13mystery3.png [6]'a' (97)Unchangeda
14mystery3.png [7]'_' (95)Unchanged_
15mystery.png [5]'1' (49)Unchanged1
16..25mystery.png [6..15]'_8a448cb2}'Unchanged_8a448cb2}
METHOD B: AUTOMATED SCRIPT & ONELINER

3. Automated Python Solver (`solve.py`)

Automating the 26-slot reconstruction with a clean Python script:

with open('mystery.png', 'rb') as f:
    m1 = f.read()[-16:]
with open('mystery2.png', 'rb') as f:
    m2 = f.read()[-2:]
with open('mystery3.png', 'rb') as f:
    m3 = f.read()[-8:]

flag = [None] * 26

# Slot arithmetic from binary reverse engineering
flag[0] = chr(m2[0] - 0x15)  # 133 - 21 = 112 ('p')
flag[1] = chr(m3[0])         # 'i'
flag[2] = chr(m3[1])         # 'c'
flag[3] = chr(m2[1] - 4)     # 115 - 4 = 111 ('o')
flag[4] = chr(m1[0])         # 'C'
flag[5] = chr(m3[2])         # 'T'

for i in range(4):
    flag[6 + i] = chr(m1[1 + i])

for i in range(5):
    flag[10 + i] = chr(m3[3 + i])

for i in range(11):
    flag[15 + i] = chr(m1[5 + i])

print("[+] Decoded Flag:", ''.join(flag))
Terminal One-Liner (PowerShell / Bash):
python -c "m1=open('mystery.png','rb').read()[-16:]; m2=open('mystery2.png','rb').read()[-2:]; m3=open('mystery3.png','rb').read()[-8:]; f=[chr(m2[0]-21), chr(m3[0]), chr(m3[1]), chr(m2[1]-4), chr(m1[0]), chr(m3[2])] + [chr(m1[1+i]) for i in range(4)] + [chr(m3[3+i]) for i in range(5)] + [chr(m1[5+i]) for i in range(11)]; print(''.join(f))"

4. Verified Flag

Decoded Flag Output:

picoCTF{An0tha_1_8a448cb2}

5. Key Takeaways & Lessons

PrincipleTechnical RuleWhy It Matters
Append Mode ("a")fopen(..., "a")Writes directly past the end of the file, creating trailing overlay data.
PNG TerminatorIEND (49 45 4E 44 ...)Standard PNG parsers stop reading at IEND; anything past it is hidden payload.
Byte Count Verification16 + 2 + 8 = 26 bytesAlways sum extracted pieces against the initial buffer size to confirm zero data loss.

6. The Complete Investigation Path & Mental Roadmap

Here is the step-by-step mental roadmap from binary decompilation to manual jigsaw assembly:

STEP 1
Decompiling the Encoding Binary

Opened mystery in Ghidra. Traced main() and spotted fopen(..., "a") appending 26 bytes across mystery.png, mystery2.png, and mystery3.png.

STEP 2
Carving Trailing Overlays Past IEND

Opened all 3 images in HxD / HexEd.it. Jumped past the IEND marker (49 45 4E 44 AE 42 60 82) and extracted 16 bytes from mystery.png, 2 bytes from mystery2.png, and 8 bytes from mystery3.png (Total: 26 bytes).

STEP 3
Reverse Math & Scrambled Byte Recovery

Calculated the 2 shifted bytes from mystery2.png: Slot 0 was $133 - 21 = 112$ ('p'), and Slot 3 was $115 - 4 = 111$ ('o').

STEP 4
Jigsaw Assembly & Automated Verification

Constructed the 26-slot table mapping each byte slice back to its original index. Validated the assembly using our automated Python script solve.py.

STEP 5
Flag Capture

Submitted the verified flag string: picoCTF{An0tha_1_8a448cb2}.