digital forensics

Recovering Deleted Files with PhotoRec

Recover deleted files from disk images and storage media using PhotoRec's file signature-based carving engine regardless of file system damage.

data-recoveryevidence-recoveryfile-carvingfile-recoveryforensicsphotorec
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

When to Use

  • When recovering deleted files from a forensic disk image or storage device
  • When the file system is corrupted, formatted, or overwritten
  • During investigations requiring recovery of documents, images, videos, or databases
  • When file system metadata is unavailable but raw data sectors remain intact
  • For recovering files from memory cards, USB drives, and hard drives

Prerequisites

  • PhotoRec installed (part of TestDisk suite)
  • Forensic disk image or direct device access (read-only)
  • Sufficient output storage space (potentially larger than source)
  • Write-blocker if working with original media
  • Root/sudo privileges for device access
  • Knowledge of target file types for focused recovery

Workflow

Step 1: Install PhotoRec and Prepare the Environment

# Install TestDisk (includes PhotoRec) on Debian/Ubuntu
sudo apt-get install testdisk
 
# On RHEL/CentOS
sudo yum install testdisk
 
# On macOS
brew install testdisk
 
# Verify installation
photorec --version
 
# Create output directory structure
mkdir -p /cases/case-2024-001/recovered/{all,documents,images,databases}
 
# Verify the forensic image
file /cases/case-2024-001/images/evidence.dd
ls -lh /cases/case-2024-001/images/evidence.dd

Step 2: Run PhotoRec in Interactive Mode

# Launch PhotoRec against a forensic image
photorec /cases/case-2024-001/images/evidence.dd
 
# Interactive menu steps:
# 1. Select the disk image: evidence.dd
# 2. Select partition table type: [Intel] for MBR, [EFI GPT] for GPT
# 3. Select partition to scan (or "No partition" for whole disk)
# 4. Select filesystem type: [ext2/ext3/ext4] or [Other] for NTFS/FAT
# 5. Choose scan scope: [Free] (unallocated only) or [Whole] (entire partition)
# 6. Select output directory: /cases/case-2024-001/recovered/all/
# 7. Press C to confirm and begin recovery
 
# For direct device scanning (with write-blocker)
sudo photorec /dev/sdb

Step 3: Run PhotoRec with Command-Line Options for Targeted Recovery

# Non-interactive mode with specific file types
photorec /d /cases/case-2024-001/recovered/documents/ \
   /cmd /cases/case-2024-001/images/evidence.dd \
   partition_table,options,mode,fileopt,search
 
# Recover only specific file types using photorec command mode
photorec /d /cases/case-2024-001/recovered/documents/ \
   /cmd /cases/case-2024-001/images/evidence.dd \
   options,keep_corrupted_file,enable \
   fileopt,everything,disable \
   fileopt,doc,enable \
   fileopt,docx,enable \
   fileopt,pdf,enable \
   fileopt,xlsx,enable \
   search
 
# Recover only image files
photorec /d /cases/case-2024-001/recovered/images/ \
   /cmd /cases/case-2024-001/images/evidence.dd \
   fileopt,everything,disable \
   fileopt,jpg,enable \
   fileopt,png,enable \
   fileopt,gif,enable \
   fileopt,bmp,enable \
   fileopt,tif,enable \
   search
 
# Recover database files
photorec /d /cases/case-2024-001/recovered/databases/ \
   /cmd /cases/case-2024-001/images/evidence.dd \
   fileopt,everything,disable \
   fileopt,sqlite,enable \
   fileopt,dbf,enable \
   search

Step 4: Organize and Catalog Recovered Files

# PhotoRec outputs files into recup_dir.1, recup_dir.2, etc.
ls /cases/case-2024-001/recovered/all/
 
# Count recovered files by type
find /cases/case-2024-001/recovered/all/ -type f | \
   sed 's/.*\.//' | sort | uniq -c | sort -rn > /cases/case-2024-001/recovered/file_type_summary.txt
 
# Sort recovered files into directories by extension
cd /cases/case-2024-001/recovered/all/
for ext in jpg png pdf docx xlsx pptx zip sqlite; do
   mkdir -p /cases/case-2024-001/recovered/sorted/$ext
   find . -name "*.$ext" -exec cp {} /cases/case-2024-001/recovered/sorted/$ext/ \;
done
 
# Generate SHA-256 hashes for all recovered files
find /cases/case-2024-001/recovered/all/ -type f -exec sha256sum {} \; \
   > /cases/case-2024-001/recovered/recovered_hashes.txt
 
# Generate file listing with metadata
find /cases/case-2024-001/recovered/all/ -type f \
   -printf "%f\t%s\t%T+\t%p\n" | sort > /cases/case-2024-001/recovered/file_listing.txt

Step 5: Validate and Filter Recovered Files

# Verify file integrity using file signatures
find /cases/case-2024-001/recovered/all/ -type f -exec file {} \; \
   > /cases/case-2024-001/recovered/file_signatures.txt
 
# Find files with mismatched extension/signature
while IFS= read -r line; do
   filepath=$(echo "$line" | cut -d: -f1)
   filetype=$(echo "$line" | cut -d: -f2-)
   ext="${filepath##*.}"
   if [[ "$ext" == "jpg" ]] && ! echo "$filetype" | grep -qi "JPEG"; then
      echo "MISMATCH: $filepath -> $filetype"
   fi
done < /cases/case-2024-001/recovered/file_signatures.txt > /cases/case-2024-001/recovered/mismatches.txt
 
# Filter out known-good files using NSRL hash comparison
hashdeep -r -c sha256 /cases/case-2024-001/recovered/all/ | \
   grep -vFf /opt/nsrl/nsrl_sha256.txt > /cases/case-2024-001/recovered/unknown_files.txt
 
# Remove zero-byte and corrupted files
find /cases/case-2024-001/recovered/all/ -type f -empty -delete
find /cases/case-2024-001/recovered/all/ -name "*.jpg" -exec jpeginfo -c {} \; 2>&1 | \
   grep "ERROR" > /cases/case-2024-001/recovered/corrupted_images.txt

Key Concepts

Concept Description
File carving Recovering files from raw data using file header/footer signatures
File signatures Magic bytes at the start of files identifying their type (e.g., FF D8 FF for JPEG)
Unallocated space Disk sectors not assigned to any active file; may contain deleted data
Fragmented files Files stored in non-contiguous sectors; harder to carve completely
Cluster/Block size Minimum allocation unit on a file system; affects carving granularity
File footer Byte sequence marking the end of a file (not all formats have footers)
Data remanence Residual data remaining after deletion until sectors are overwritten
False positives Carved artifacts that match signatures but contain corrupted or partial data

Tools & Systems

Tool Purpose
PhotoRec Open-source file carving tool supporting 300+ file formats
TestDisk Companion tool for partition recovery and repair
Foremost Alternative file carver originally developed by US Air Force OSI
Scalpel High-performance file carver based on Foremost
hashdeep Recursive hash computation and audit tool
jpeginfo JPEG file integrity verification
file Unix utility identifying file types by magic bytes
exiftool Extract metadata from recovered image and document files

Common Scenarios

Scenario 1: Recovering Deleted Evidence from a Suspect's USB Drive Image the USB drive with dcfldd, run PhotoRec targeting document and image formats, organize by file type, hash all recovered files, compare against known-bad hash sets, extract metadata from images for GPS and timestamp information.

Scenario 2: Formatted Hard Drive Recovery Run PhotoRec in "Whole" mode against the entire formatted partition, recover all file types, expect higher false positive rate due to file fragmentation, validate recovered files with signature checking, catalog and hash for evidence chain.

Scenario 3: Memory Card from a Surveillance Camera Recover deleted video files (AVI, MP4, MOV) from the memory card image, use targeted file type selection to speed recovery, verify video files are playable, extract frame timestamps, document recovery in case notes.

Scenario 4: Corrupted File System on Evidence Drive When file system metadata is destroyed, PhotoRec bypasses the file system entirely and carves from raw sectors, recover maximum possible data, accept that file names and directory structure will be lost, rename files based on content during review.

Output Format

PhotoRec Recovery Summary:
  Source Image:     evidence.dd (500 GB)
  Partition:        NTFS (Partition 2)
  Scan Mode:        Free space only
 
  Files Recovered:  4,523
    Documents:      234 (doc: 45, docx: 89, pdf: 67, xlsx: 33)
    Images:         2,145 (jpg: 1,890, png: 198, gif: 57)
    Videos:         34 (mp4: 22, avi: 12)
    Archives:       67 (zip: 45, rar: 22)
    Databases:      12 (sqlite: 8, dbf: 4)
    Other:          2,031
 
  Data Recovered:   12.4 GB
  Corrupted Files:  312 (flagged for review)
  Output Directory: /cases/case-2024-001/recovered/all/
  Hash Manifest:    /cases/case-2024-001/recovered/recovered_hashes.txt
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 1

api-reference.md2.0 KB

API Reference: Recovering Deleted Files with PhotoRec Agent

Overview

Wraps PhotoRec via subprocess for forensic file recovery from disk images, with automated file cataloging, SHA-256 hashing for evidence integrity, and categorized sorting.

Dependencies

Package Version Purpose
hashlib stdlib SHA-256 hashing for evidence integrity
subprocess stdlib PhotoRec command execution
pathlib stdlib File extension handling

External Tools Required

Tool Purpose
photorec File carving and recovery from disk images
file File type identification

Core Functions

run_photorec(image_path, output_dir, file_types, partition)

Executes PhotoRec with optional file type filtering and partition selection.

  • Timeout: 14400 seconds (4 hours)
  • Returns: dict with command, returncode, output_dir

catalog_recovered_files(output_dir)

Catalogs all recovered files by extension with counts and sizes.

  • Returns: dict with total_files, total_mb, by_extension

hash_recovered_files(output_dir, extensions)

Generates SHA-256 hashes for recovered files, optionally filtered by extension.

  • Returns: list[dict] with file path, sha256, size

sort_recovered_files(output_dir, sorted_dir)

Sorts recovered files into categories: documents, images, databases, archives, executables, email, web, other.

  • Returns: dict[str, int] - category to file count

full_recovery_pipeline(image_path, output_dir, file_types)

End-to-end: image info -> PhotoRec recovery -> catalog -> sort.

File Categories

Category Extensions
documents .doc, .docx, .pdf, .xls, .xlsx, .ppt, .txt, .csv
images .jpg, .png, .gif, .bmp, .tiff, .svg
databases .db, .sqlite, .mdb, .sql
archives .zip, .rar, .7z, .tar, .gz
executables .exe, .dll, .bat, .ps1, .sh
email .eml, .msg, .pst, .ost

Usage

python agent.py /cases/evidence.dd /cases/recovered/ jpg,pdf,doc

Scripts 1

agent.py7.4 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Deleted file recovery agent using PhotoRec subprocess wrapper."""

import subprocess
import os
import sys
import json
import hashlib
from pathlib import Path
from collections import defaultdict
from datetime import datetime


def verify_photorec():
    """Check that PhotoRec is installed and available."""
    result = subprocess.run(
        ["photorec", "--version"], capture_output=True, text=True,
        timeout=120,
    )
    if result.returncode == 0:
        return {"installed": True, "version": result.stdout.strip()}
    return {"installed": False}


def get_image_info(image_path):
    """Get forensic image information."""
    file_result = subprocess.run(
        ["file", image_path], capture_output=True, text=True,
        timeout=120,
    )
    size = os.path.getsize(image_path) if os.path.exists(image_path) else 0
    return {
        "path": image_path,
        "size_bytes": size,
        "size_gb": round(size / (1024 ** 3), 2),
        "type": file_result.stdout.strip(),
    }


def run_photorec(image_path, output_dir, file_types=None, partition=None):
    """Run PhotoRec for file recovery using command-line interface."""
    os.makedirs(output_dir, exist_ok=True)
    cmd = ["photorec", "/d", output_dir, "/cmd", image_path]
    options = []
    if partition:
        options.append(f"partition_i_end,{partition}")
    if file_types:
        enable_list = ",".join(file_types)
        options.append(f"fileopt,everything,disable,{enable_list},enable")
    options.append("search")
    cmd.append(",".join(options) if options else "search")
    result = subprocess.run(
        cmd, capture_output=True, text=True, timeout=14400
    )
    return {
        "command": " ".join(cmd),
        "returncode": result.returncode,
        "stdout": result.stdout[-1000:] if result.stdout else "",
        "stderr": result.stderr[-500:] if result.stderr else "",
        "output_dir": output_dir,
    }


def catalog_recovered_files(output_dir):
    """Catalog all recovered files by type, size, and hash."""
    catalog = defaultdict(list)
    total_files = 0
    total_bytes = 0
    for root, dirs, files in os.walk(output_dir):
        for filename in files:
            filepath = os.path.join(root, filename)
            ext = Path(filename).suffix.lower()
            try:
                size = os.path.getsize(filepath)
            except OSError:
                continue
            total_files += 1
            total_bytes += size
            entry = {
                "path": filepath,
                "filename": filename,
                "extension": ext,
                "size": size,
            }
            catalog[ext].append(entry)
    summary = {
        "total_files": total_files,
        "total_bytes": total_bytes,
        "total_mb": round(total_bytes / (1024 * 1024), 2),
        "by_extension": {
            ext: {"count": len(files), "total_bytes": sum(f["size"] for f in files)}
            for ext, files in sorted(catalog.items(), key=lambda x: -len(x[1]))
        },
    }
    return summary


def hash_recovered_files(output_dir, extensions=None):
    """Generate SHA-256 hashes for recovered files for evidence integrity."""
    hashes = []
    for root, dirs, files in os.walk(output_dir):
        for filename in files:
            filepath = os.path.join(root, filename)
            ext = Path(filename).suffix.lower()
            if extensions and ext not in extensions:
                continue
            try:
                with open(filepath, "rb") as f:
                    sha256 = hashlib.sha256(f.read()).hexdigest()
                hashes.append({
                    "file": filepath,
                    "sha256": sha256,
                    "size": os.path.getsize(filepath),
                })
            except (OSError, PermissionError):
                pass
    return hashes


def sort_recovered_files(output_dir, sorted_dir):
    """Sort recovered files into categorized directories."""
    categories = {
        "documents": [".doc", ".docx", ".pdf", ".xls", ".xlsx", ".ppt", ".pptx",
                      ".odt", ".ods", ".txt", ".rtf", ".csv"],
        "images": [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".svg", ".webp"],
        "databases": [".db", ".sqlite", ".mdb", ".accdb", ".sql"],
        "archives": [".zip", ".rar", ".7z", ".tar", ".gz", ".bz2"],
        "executables": [".exe", ".dll", ".bat", ".ps1", ".sh", ".msi"],
        "email": [".eml", ".msg", ".pst", ".ost", ".mbox"],
        "web": [".html", ".htm", ".css", ".js", ".json", ".xml"],
    }
    os.makedirs(sorted_dir, exist_ok=True)
    for cat in categories:
        os.makedirs(os.path.join(sorted_dir, cat), exist_ok=True)
    os.makedirs(os.path.join(sorted_dir, "other"), exist_ok=True)
    moved = defaultdict(int)
    for root, dirs, files in os.walk(output_dir):
        if root.startswith(sorted_dir):
            continue
        for filename in files:
            src = os.path.join(root, filename)
            ext = Path(filename).suffix.lower()
            target_cat = "other"
            for cat, exts in categories.items():
                if ext in exts:
                    target_cat = cat
                    break
            dst = os.path.join(sorted_dir, target_cat, filename)
            counter = 1
            while os.path.exists(dst):
                name, extension = os.path.splitext(filename)
                dst = os.path.join(sorted_dir, target_cat, f"{name}_{counter}{extension}")
                counter += 1
            try:
                os.rename(src, dst)
                moved[target_cat] += 1
            except OSError:
                pass
    return dict(moved)


def full_recovery_pipeline(image_path, output_dir, file_types=None):
    """Run complete file recovery pipeline."""
    results = {"timestamp": datetime.now().isoformat()}
    results["image_info"] = get_image_info(image_path)
    recovery_dir = os.path.join(output_dir, "recovered")
    results["recovery"] = run_photorec(image_path, recovery_dir, file_types=file_types)
    if os.path.exists(recovery_dir):
        results["catalog"] = catalog_recovered_files(recovery_dir)
        sorted_dir = os.path.join(output_dir, "sorted")
        results["sorting"] = sort_recovered_files(recovery_dir, sorted_dir)
    return results


def print_report(results):
    print("File Recovery Report")
    print("=" * 50)
    print(f"Date: {results.get('timestamp', 'N/A')}")
    img = results.get("image_info", {})
    print(f"Image: {img.get('path', 'N/A')} ({img.get('size_gb', 0)} GB)")
    cat = results.get("catalog", {})
    print(f"\nRecovered: {cat.get('total_files', 0)} files ({cat.get('total_mb', 0)} MB)")
    print("\nBy Extension:")
    for ext, info in list(cat.get("by_extension", {}).items())[:15]:
        print(f"  {ext:8s}: {info['count']:>5} files ({info['total_bytes'] // 1024:>8} KB)")
    sorting = results.get("sorting", {})
    if sorting:
        print("\nSorted Categories:")
        for cat_name, count in sorting.items():
            print(f"  {cat_name:15s}: {count} files")


if __name__ == "__main__":
    if len(sys.argv) < 3:
        print("Usage: python agent.py <disk_image> <output_dir> [file_types]")
        print("  file_types: comma-separated (e.g., jpg,pdf,doc)")
        sys.exit(1)
    image = sys.argv[1]
    output = sys.argv[2]
    types = sys.argv[3].split(",") if len(sys.argv) > 3 else None
    results = full_recovery_pipeline(image, output, file_types=types)
    print_report(results)
Keep exploring