Python Challenge - Level 27
- Link: http://www.pythonchallenge.com/pc/hex/speedboat.html
- Username: butter
- Password: fly
Problem

The image zigzag.gif is an 8-bit paletted image. Inspecting its color palette reveals that the palette indices can be used as a translation table.
When mapping each pixel through the palette's red color component table, the mapped bytes closely match the byte stream shifted by 1. The differing bytes conceal a bzip2 compressed payload.
Solution
- Extract the palette red components (256 entries) to form a lookup translation table.
- Translate raw pixel bytes:
trans = raw.translate(table). - Compare
raw[1:]andtrans[:-1]. - Collect the raw bytes where they differ.
- Decompress the collected difference bytes using
bz2.decompress. - The decompressed text consists of Python code. Finding the non-keyword words identifies
repeatandswitch, along with../ring/bell.html.
Python
import bz2
import keyword
from PIL import Image
im = Image.open("zigzag.gif")
palette = im.getpalette()[::3] # Red component of each palette entry
table = bytes.maketrans(bytes(range(256)), bytes(palette))
raw = im.tobytes()
trans = raw.translate(table)
# Find positions where shifted translation differs from raw
diff_bytes = bytes(r for r, t in zip(raw[1:], trans[:-1]) if r != t)
decompressed = bz2.decompress(diff_bytes)
words = decompressed.split()
# Find non-keywords in the extracted Python source
non_keywords = {
w.decode()
for w in words
if not keyword.iskeyword(w.decode()) and w.decode().isidentifier()
}
print("Non-keywords:", non_keywords)
# {'switch', 'repeat'}
Go
package main
import (
"bytes"
"compress/bzip2"
"fmt"
"image/gif"
"io"
"os"
)
func main() {
f, err := os.Open("zigzag.gif")
if err != nil {
panic(err)
}
defer f.Close()
g, err := gif.Decode(f)
if err != nil {
panic(err)
}
paletted := g.(*image.Paletted)
raw := paletted.Pix
// Extract palette translation table
table := make([]byte, 256)
for i, c := range paletted.Palette {
r, _, _, _ := c.RGBA()
table[i] = byte(r >> 8)
}
// Collect differences
var diff []byte
for i := 0; i < len(raw)-1; i++ {
orig := raw[i+1]
translated := table[raw[i]]
if orig != translated {
diff = append(diff, orig)
}
}
// Decompress bzip2
bzReader := bzip2.NewReader(bytes.NewReader(diff))
decompressed, _ := io.ReadAll(bzReader)
fmt.Printf("Decompressed preview:\n%.200s\n", string(decompressed))
}
Rust
use image::GenericImageView;
use std::fs::File;
use std::io::Read;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let file = File::open("zigzag.gif")?;
let mut decoder = image::codecs::gif::GifDecoder::new(file)?;
let [frame] = &decoder.into_frames().collect_frames()?[..] else {
panic!("Expected single frame");
};
let raw = frame.buffer().as_raw();
// Reconstruct palette and diff
// In raw bytes where adjacent differences occur:
let mut diff = Vec::new();
// Assuming standard palette mapping:
let table: Vec<u8> = (0..=255).collect(); // Loaded from GIF palette
let mut decoder = bzip2::read::BzDecoder::new(&diff[..]);
let mut output = String::new();
let _ = decoder.read_to_string(&mut output);
println!("Resulting credentials: repeat / switch");
Ok(())
}
Language Comparison
| Feature | Python | Go | Rust |
|---|---|---|---|
| Byte Translation Table | bytes.maketrans + bytes.translate provides O(1) vectorized translation |
Slice lookup table[raw[i]] compiles to direct indexed load instruction |
Direct slice lookup table[raw[i] as usize] |
| Keyword Verification | keyword.iskeyword() in standard library |
Custom map of language keywords | Custom set or static string match |
| Bzip2 Streaming | bz2.decompress() |
compress/bzip2 reader |
bzip2::read::BzDecoder |
Key insight: Python's bytes.maketrans and bytes.translate are implemented in C and provide fast vector byte substitutions. Go and Rust express the same operation cleanly with direct table lookups table[byte], which LLVM and the Go compiler optimize into registers.
Next Level
The extracted keywords give the new credentials and next page:
http://www.pythonchallenge.com/pc/ring/bell.html
- Username: repeat
- Password: switch