TIP
Scripts are stored here: click
cut_frames.py
Visual explanation
How the frames are cropped:
.crop() takes 1 tuple as parameter with coords:
img.crop((left, top, right, bottom)).crop() parameters, that will be passed to the method:
frame = img.crop((0, y_start, frame_width, y_end))
# ↑ ↑ ↑ ↑
# │ │ │ └─ bottom (y2)
# │ │ └─ right (x2) = 1280
# │ └─ top (y1) = y_start
# └─ left (x1) = 0Original image (1280x47520):
0 1280
0 ┌─────────────────────────────────┐ (0,0) (1280,0)
│ │
│ Frame 1 │ ← crop(0, 0, 1280, 720)
│ │
720 ├─────────────────────────────────┤ (0,720) (1280,720)
│ │
│ Frame 2 │ ← crop(0, 720, 1280, 1440)
│ │
1440 ├─────────────────────────────────┤ (0,1440) (1280,1440)
│ │
│ Frame 3 │ ← crop(0, 1440, 1280, 2160)
│ │
2160 ├─────────────────────────────────┤ (0,2160) (1280,2160)
│ │
│ ... │
│ │
47520 └─────────────────────────────────┘ (0,47520) (1280,47520)
As we move through the frames from top to bottom, we can imagine that we are moving at -720, -1440, -2160 pixels, and so on. Like at the math.
But PIL (Python Imaging Library) coords are always Positive and always go down:
PIL coordinates (always positive):
0 ┌─────────────→ X (to the right)
│
│ (100, 50) ← x=100, y=50
│ ●
│
↓
Y (down)
this would be wrong:
frame = img.crop((0, -720, 1280, -1440))in the script:
# concat_v.png 1280x47520, 66 frames with 720 pixel height
frame_width = 1280
frame_height = 720
for i in range(3): # ex. first 3 frames
# Calculating the position of the first frame
y_start = i * 720 # 0, 720, 1440
y_end = y_start + 720 # 720, 1440, 2160
print(f"Frame {i+1}: y={y_start} to {y_end}")
# cropping (left=0, top=y_start, right=1280, bottom=y_end)
frame = img.crop((0, y_start, 1280, y_end))
# Result: frame sized 1280x720
print(f"Result size: {frame.size}") # (1280, 720)zsteg_to_each_frame.py
for frame in frame_*.png;Here bash searches for all files matching the pattern and extends frame_*.png to file-list.
frame_*.png → frame_1.png frame_2.png frame_3.png ... frame_66.pngit equals to
echo frame_*.pngoutput.
2>/dev/null
Streams in bash:
0 = stdin (standard input)
1 = stdout (standard output)
2 = stderr (error stream)
zsteg $frame 2>/dev/null | grep -iE "pico|ctf|flag"So what;s going on:
zsteghas bugs with some PNG- errors go to
stderr(stream 2) grepfilters only stdout (stream 1)- errors go through
grepass well and clog the outlet
Here 2>/dev/null is needed to avoid zsteg stderr about it’s buffer overloaded. Without it we would see the following:
NOTHING YET
NOTHING YET
NOTHING YET
NOTHING YET
NOTHING YET
NOTHING YET
/var/lib/gems/3.3.0/gems/zsteg-0.2.13/lib/zsteg/checker/wbstego.rb:41:in `to_s': stack level too deep (SystemStackError)
from /var/lib/gems/3.3.0/gems/iostruct-0.5.0/lib/iostruct.rb:180:in `inspect'
from /var/lib/gems/3.3.0/gems/zsteg-0.2.13/lib/zsteg/checker/wbstego.rb:41:in `to_s'
from /var/lib/gems/3.3.0/gems/iostruct-0.5.0/lib/iostruct.rb:180:in `inspect'
from /var/lib/gems/3.3.0/gems/zsteg-0.2.13/lib/zsteg/checker/wbstego.rb:41:in `to_s'
from /var/lib/gems/3.3.0/gems/iostruct-0.5.0/lib/iostruct.rb:180:in `inspect'
from /var/lib/gems/3.3.0/gems/zsteg-0.2.13/lib/zsteg/checker/wbstego.rb:41:in `to_s'
from /var/lib/gems/3.3.0/gems/iostruct-0.5.0/lib/iostruct.rb:180:in `inspect'
from /var/lib/gems/3.3.0/gems/zsteg-0.2.13/lib/zsteg/checker/wbstego.rb:41:in `to_s'
... 10906 levels...
from /var/lib/gems/3.3.0/gems/zsteg-0.2.13/lib/zsteg.rb:26:in `run'
from /var/lib/gems/3.3.0/gems/zsteg-0.2.13/bin/zsteg:8:in `<top (required)>'
from /usr/local/bin/zsteg:25:in `load'
from /usr/local/bin/zsteg:25:in `<main>'
NOTHING YET
NOTHING YETwith 2>dev/null we’ll have:
zsteg frame_1.png 2>/dev/null | grep -iE "pico|ctf|flag"
├── stdout: "b1,r,lsb,xy .. text: "picoCTF{..." → PASSED to grep
└── stderr: "SystemStackError..." → NOT PASSED to grep (will be IGNORED due 2>dev/null)