Investigative Reversing 1: Multi-PNG Overlay Carving

“We have recovered a binary and a few images. See if you can recover the flag.”
picoCTF{...}💡 THE INTUITIVE ANALOGY (The Shredded Letter)
Imagine an author writing a 26-letter secret password. Instead of mailing one letter, they tear the password into 3 pieces. They slip piece #1 into an envelope labeled mystery.png, piece #2 into mystery2.png (and add 21 to the first letter just to scramble it), and piece #3 into mystery3.png. They tape these scraps to the very outside bottom of each envelope (past the IEND seal). To solve it, we simply inspect the bottom of all 3 envelopes and solve the 26-slot jigsaw puzzle!
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;
}mystery.pngreceives 16 bytes:flag[4], flag[6..9], flag[15..25]mystery2.pngreceives 2 bytes:flag[0] + 21, flag[3] + 4mystery3.pngreceives 8 bytes:flag[1], flag[2], flag[5], flag[10..14]- Total: 16 + 2 + 8 = 26 bytes (Exact match for the 26-character flag).
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):
Step 2: Solve the Math for the 2 Modified Characters
Step 3: Complete 26-Slot Reconstruction Matrix
| Slot Index | Source File & Position | Encoded Byte | Math / Operation | Decoded Character |
|---|---|---|---|---|
| 0 | mystery2.png [0] | 0x85 (133) | 133 - 21 | p |
| 1 | mystery3.png [0] | 'i' (105) | Unchanged | i |
| 2 | mystery3.png [1] | 'c' (99) | Unchanged | c |
| 3 | mystery2.png [1] | 's' (115) | 115 - 4 | o |
| 4 | mystery.png [0] | 'C' (67) | Unchanged | C |
| 5 | mystery3.png [2] | 'T' (84) | Unchanged | T |
| 6 | mystery.png [1] | 'F' (70) | Unchanged | F |
| 7 | mystery.png [2] | '{' (123) | Unchanged | { |
| 8 | mystery.png [3] | 'A' (65) | Unchanged | A |
| 9 | mystery.png [4] | 'n' (110) | Unchanged | n |
| 10 | mystery3.png [3] | '0' (48) | Unchanged | 0 |
| 11 | mystery3.png [4] | 't' (116) | Unchanged | t |
| 12 | mystery3.png [5] | 'h' (104) | Unchanged | h |
| 13 | mystery3.png [6] | 'a' (97) | Unchanged | a |
| 14 | mystery3.png [7] | '_' (95) | Unchanged | _ |
| 15 | mystery.png [5] | '1' (49) | Unchanged | 1 |
| 16..25 | mystery.png [6..15] | '_8a448cb2}' | Unchanged | _8a448cb2} |
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))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:
5. Key Takeaways & Lessons
| Principle | Technical Rule | Why It Matters |
|---|---|---|
| Append Mode ("a") | fopen(..., "a") | Writes directly past the end of the file, creating trailing overlay data. |
| PNG Terminator | IEND (49 45 4E 44 ...) | Standard PNG parsers stop reading at IEND; anything past it is hidden payload. |
| Byte Count Verification | 16 + 2 + 8 = 26 bytes | Always 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:
Opened mystery in Ghidra. Traced main() and spotted fopen(..., "a") appending 26 bytes across mystery.png, mystery2.png, and mystery3.png.
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).
Calculated the 2 shifted bytes from mystery2.png: Slot 0 was $133 - 21 = 112$ ('p'), and Slot 3 was $115 - 4 = 111$ ('o').
Constructed the 26-slot table mapping each byte slice back to its original index. Validated the assembly using our automated Python script solve.py.
Submitted the verified flag string: picoCTF{An0tha_1_8a448cb2}.