Investigative Reversing 2: Bitmap LSB Deconstruction & Shift Recovery

“We have recovered another binary and an image. See what was modified this time.”
picoCTF{...}💡 THE INTUITIVE ANALOGY (The Even/Odd Morse Code)
Suppose a spy wants to send a secret number. Instead of writing the number down, he gives you a list of 8 large numbers. If a number is Even, it represents a 0. If a number is Odd, it represents a 1. By reading the even/odd state of 8 consecutive pixels in an uncompressed Bitmap image, you get an 8-bit binary number. Then add 5 to reveal the secret letter!
1. Inspecting the `mystery` Binary
// Ghidra Decompilation: mystery (main & codedChar)
int main(void) {
FILE *orig_bmp = fopen("original.bmp", "r");
FILE *enc_bmp = fopen("encoded.bmp", "a");
// 1. Copy first 2000 bytes directly (Offset 0x7D0)
for (int i = 0; i < 2000; i++) {
char b = fgetc(orig_bmp);
fputc(b, enc_bmp);
}
char flag[50];
fread(flag, 50, 1, flag_file);
// 2. Encode 50 characters using LSB Steganography
for (int i = 0; i < 50; i++) {
char val = flag[i] - 5; // Subtracted 5 from each character!
for (int bit = 0; bit < 8; bit++) {
char orig_b = fgetc(orig_bmp);
char enc_b = codedChar(bit, val, orig_b);
fputc(enc_b, enc_bmp);
}
}
// ...
}
char codedChar(int bit, char val, char orig_byte) {
char bit_val = (val >> bit) & 1; // Extract 1 bit
char result = (orig_byte & 0xFE) | bit_val; // Replace lowest bit of image byte
return result;
}2. Manual LSB Bit Assembly for Character 0 ('p')
Jump to offset 0x7D0 (Decimal: 2000) in HxD and inspect the first 8 bytes: BC C1 BC BD BC BD C1 BC.
| Byte Position | Hex Byte | Even or Odd? | LSB Bit | Bit Weight | Value |
|---|---|---|---|---|---|
| Byte 0 (Bit 0) | BC | Even | 0 | 2⁰ = 1 | 0 |
| Byte 1 (Bit 1) | C1 | Odd | 1 | 2¹ = 2 | 2 |
| Byte 2 (Bit 2) | BC | Even | 0 | 2² = 4 | 0 |
| Byte 3 (Bit 3) | BD | Odd | 1 | 2³ = 8 | 8 |
| Byte 4 (Bit 4) | BC | Even | 0 | 2⁴ = 16 | 0 |
| Byte 5 (Bit 5) | BD | Odd | 1 | 2⁵ = 32 | 32 |
| Byte 6 (Bit 6) | C1 | Odd | 1 | 2⁶ = 64 | 64 |
| Byte 7 (Bit 7) | BC | Even | 0 | 2⁷ = 128 | 0 |
Calculation: 0 + 2 + 0 + 8 + 0 + 32 + 64 + 0 = 106 (+ 5 shift) = 112 = 'p'.
3. Automated Extraction Script (`solve.py`)
# Read the encoded BMP image
with open('encoded.bmp', 'rb') as f:
data = f.read()
offset = 2000 # Starting offset identified from decompilation
flag = []
# 50 characters, 8 bytes per character (LSB)
for i in range(50):
val = 0
for bit in range(8):
byte = data[offset + i * 8 + bit]
lsb = byte & 1
val |= (lsb << bit)
flag.append(chr(val + 5)) # Apply inverse math (+5)
print("🎉 Decoded Flag:", ''.join(flag))python -c "d=open('encoded.bmp','rb').read(); print(''.join(chr(sum(((d[2000+i*8+b]&1)<<b) for b in range(8))+5) for i in range(50)))"4. Decoded Flag
Extracted Secret Flag:
5. The Complete Investigation Path & Mental Roadmap
Here is the step-by-step mental roadmap from binary decompilation to LSB flag recovery:
Opened mystery in Ghidra. Traced the initial 2000-byte loop to locate the starting payload offset: 2000 (0x7D0).
Analyzed codedChar(): spotted (orig & 0xFE) | bit_val confirming Least Significant Bit (LSB) embedding across 8 bytes per character.
Identified flag[i] - 5 in the encoding loop, requiring an addition of 5 during bitstream recovery.
Submitted recovered 50-character flag: picoCTF{n3xt_0n30000000000000000000000000ce8d5cad}.