Last active
January 2, 2026 16:14
-
-
Save Hamid-K/6f4328a8cd4f1e34798e63a8b82aa428 to your computer and use it in GitHub Desktop.
PoC script to detect steganography and hidden files in video files
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| import argparse | |
| import json | |
| import math | |
| import subprocess | |
| import sys | |
| def run_ffprobe(path): | |
| cmd = [ | |
| "ffprobe", | |
| "-v", | |
| "error", | |
| "-select_streams", | |
| "v:0", | |
| "-show_entries", | |
| "stream=width,height,nb_frames,avg_frame_rate,duration", | |
| "-of", | |
| "json", | |
| path, | |
| ] | |
| try: | |
| out = subprocess.check_output(cmd, text=True) | |
| except subprocess.CalledProcessError: | |
| return None | |
| try: | |
| info = json.loads(out) | |
| stream = info["streams"][0] | |
| except Exception: | |
| return None | |
| width = int(stream.get("width")) | |
| height = int(stream.get("height")) | |
| nb_frames = str(stream.get("nb_frames", "")) | |
| avg_rate = str(stream.get("avg_frame_rate", "")) | |
| duration = str(stream.get("duration", "")) | |
| frames = None | |
| if nb_frames.isdigit(): | |
| frames = int(nb_frames) | |
| else: | |
| # Try to estimate frames from duration and avg_frame_rate. | |
| try: | |
| num, den = avg_rate.split("/") | |
| fps = float(num) / float(den) if float(den) != 0 else None | |
| dur = float(duration) | |
| if fps and dur: | |
| frames = int(round(fps * dur)) | |
| except Exception: | |
| frames = None | |
| return width, height, frames | |
| def entropy(data): | |
| if not data: | |
| return 0.0 | |
| counts = [0] * 256 | |
| for b in data: | |
| counts[b] += 1 | |
| total = len(data) | |
| ent = 0.0 | |
| for c in counts: | |
| if c: | |
| p = c / total | |
| ent -= p * math.log2(p) | |
| return ent | |
| def extract_b_lsb_bytes(video_path, want_bytes): | |
| cmd = [ | |
| "ffmpeg", | |
| "-v", | |
| "error", | |
| "-i", | |
| video_path, | |
| "-f", | |
| "rawvideo", | |
| "-pix_fmt", | |
| "rgb24", | |
| "-", | |
| ] | |
| proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) | |
| out = bytearray() | |
| cur = 0 | |
| cnt = 0 | |
| try: | |
| while len(out) < want_bytes: | |
| chunk = proc.stdout.read(1024 * 1024) | |
| if not chunk: | |
| break | |
| for i in range(2, len(chunk), 3): | |
| b = chunk[i] & 1 | |
| cur = (cur << 1) | b | |
| cnt += 1 | |
| if cnt == 8: | |
| out.append(cur) | |
| if len(out) >= want_bytes: | |
| break | |
| cur = 0 | |
| cnt = 0 | |
| finally: | |
| if proc.stdout: | |
| proc.stdout.close() | |
| proc.terminate() | |
| return bytes(out) | |
| def main(): | |
| ap = argparse.ArgumentParser( | |
| description="Detect hide.py-style video steganography without a password." | |
| ) | |
| ap.add_argument("video", help="Path to the video file (mp4, mkv, etc.)") | |
| ap.add_argument( | |
| "--dump", | |
| metavar="OUT", | |
| help="Write extracted encrypted payload blob to this file", | |
| ) | |
| args = ap.parse_args() | |
| info = run_ffprobe(args.video) | |
| if not info: | |
| print("ffprobe failed to read video metadata.", file=sys.stderr) | |
| sys.exit(2) | |
| width, height, frames = info | |
| if not frames: | |
| print("Could not determine frame count; detection may be weaker.") | |
| else: | |
| capacity_bits = width * height * frames | |
| capacity_bytes = capacity_bits // 8 | |
| print(f"Video size: {width}x{height}, frames={frames}, B-LSB capacity={capacity_bytes} bytes") | |
| # Read first 4 bytes to get payload length. | |
| hdr = extract_b_lsb_bytes(args.video, 4) | |
| if len(hdr) < 4: | |
| print("Not enough data to read header; no detection.") | |
| sys.exit(1) | |
| payload_len = int.from_bytes(hdr, "big") | |
| print(f"Header length field: {payload_len} bytes") | |
| plausible = True | |
| if frames: | |
| capacity_bytes = (width * height * frames) // 8 | |
| if payload_len <= 16 or payload_len > capacity_bytes - 4: | |
| plausible = False | |
| if payload_len % 16 != 0: | |
| plausible = False | |
| if not plausible: | |
| print("Length field is not plausible for hide.py-style AES-CBC payload.") | |
| sys.exit(0) | |
| # Extract full payload if requested, otherwise sample for entropy. | |
| if args.dump: | |
| blob = extract_b_lsb_bytes(args.video, 4 + payload_len) | |
| if len(blob) < 4 + payload_len: | |
| print("Reached EOF before extracting full payload.", file=sys.stderr) | |
| sys.exit(1) | |
| with open(args.dump, "wb") as f: | |
| f.write(blob) | |
| print(f"Wrote encrypted payload blob: {args.dump}") | |
| else: | |
| sample = extract_b_lsb_bytes(args.video, 4 + min(payload_len, 4096)) | |
| ent = entropy(sample[4:]) | |
| print(f"Payload entropy (sample): {ent:.2f} bits/byte") | |
| print("Likely hide.py-style steganography: YES") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment