Search Results for

# Common Forensic Workflows

This guide provides step-by-step tutorials for common forensic operations using FEX Core SDK.

## Prerequisites

Before starting these tutorials:

1. FEX Core SDK extracted to a local directory
2. Python 3.14+ installed (64-bit)
3. Valid license key (or trial key)
4. Sample forensic image (`sample-data/sample-image.dd`)

## Workflow 1: Open and Verify an Image

Goal: Open a forensic image and verify it loaded correctly.

### Use Case

Before processing files, you need to open the forensic image and confirm FEX Core can read it. This validates the image format is supported and the file is accessible.

### Python Implementation

    import ctypes
    import json
    import os

    # Load the DLL
    dll_path = os.path.join("bin", "Win64", "FEX.Core.dll")
    dll = ctypes.CDLL(dll_path)

    # Define function signatures
    dll.ImageOpen.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
    dll.ImageOpen.restype = ctypes.c_int

    dll.GetImageInfoByIdAsJSON.argtypes = [
        ctypes.c_int,
        ctypes.POINTER(ctypes.c_void_p),
        ctypes.POINTER(ctypes.c_uint32)
    ]
    dll.GetImageInfoByIdAsJSON.restype = ctypes.c_int

    dll.FreeAllocatedBuffer.argtypes = [ctypes.c_void_p]
    dll.FreeAllocatedBuffer.restype = None

    dll.ImageClose.argtypes = [ctypes.c_int]
    dll.ImageClose.restype = ctypes.c_int

    # Open the image
    image_path = b"sample-data/sample-image.dd"
    license_key = b"YOUR_LICENSE_KEY"

    image_id = dll.ImageOpen(image_path, license_key)
    if image_id < 0:
        print(f"Failed to open image: error code {image_id}")
        exit(1)

    print(f"Image opened successfully! ID: {image_id}")

    # Get image info
    buffer = ctypes.c_void_p()
    size = ctypes.c_uint32()

    result = dll.GetImageInfoByIdAsJSON(image_id, ctypes.byref(buffer), ctypes.byref(size))
    if result == 0:
        try:
            json_data = ctypes.string_at(buffer, size.value).decode('utf-8')
            info = json.loads(json_data)
            print(f"Image Type: {info.get('ImageType', 'Unknown')}")
            print(f"Total Size: {info.get('TotalSize', 0):,} bytes")
        finally:
            dll.FreeAllocatedBuffer(buffer)

    # Close image when done
    dll.ImageClose(image_id)
    print("Image closed.")

### Expected Output

    Image opened successfully! ID: 1
    Image Type: DD
    Total Size: 52,428,800 bytes
    Image closed.

### Error Handling

Error Code | Meaning | Solution
--- | --- | ---
-4 | Image not supported | Check file format, use supported type
-6 | Could not open | Verify path, check permissions
-20 | Invalid license | Check license key

## Workflow 2: List All Files

Goal: Enumerate all files and folders in a forensic image.

### Use Case

File listing is the foundation for most forensic operations - finding specific files, building timelines, or exporting file inventories.

### Python Implementation

    import ctypes
    import json
    import os

    # Assume DLL is loaded and image is open (image_id from Workflow 1)

    # Define GetFileSystemRecords_V2
    dll.GetFileSystemRecords_V2.argtypes = [
        ctypes.c_int,  # image_id
        ctypes.c_int,  # partition_index (0 for first)
        ctypes.POINTER(ctypes.c_void_p),  # buffer
        ctypes.POINTER(ctypes.c_uint32)   # size
    ]
    dll.GetFileSystemRecords_V2.restype = ctypes.c_int

    def list_files(image_id, partition_index=0):
        """List all files in a partition."""
        buffer = ctypes.c_void_p()
        size = ctypes.c_uint32()

        result = dll.GetFileSystemRecords_V2(
            image_id,
            partition_index,
            ctypes.byref(buffer),
            ctypes.byref(size)
        )

        if result != 0:
            print(f"Error listing files: {result}")
            return []

        try:
            json_data = ctypes.string_at(buffer, size.value).decode('utf-8')
            records = json.loads(json_data)
            return records
        finally:
            dll.FreeAllocatedBuffer(buffer)

    # Get and display files
    files = list_files(image_id)
    print(f"Found {len(files)} files/folders\n")

    # Display first 10 files
    print(f"{'Name':<30} {'Size':>12} {'Type':<10}")
    print("-" * 55)

    for record in files[:10]:
        name = record.get('Name', 'Unknown')
        size = record.get('Size', 0)
        is_folder = record.get('Status', 0) & 0x04  # FILESTATUS_FOLDER
        file_type = "Folder" if is_folder else "File"
        print(f"{name:<30} {size:>12,} {file_type:<10}")

### Expected Output

    Found 47 files/folders

    Name                                  Size Type
    -------------------------------------------------------
    .                                        0 Folder
    ..                                       0 Folder
    Documents                                0 Folder
    Pictures                                 0 Folder
    report.txt                           1,234 File
    invoice.pdf                         45,678 File
    photo.jpg                          234,567 File
    ...

### Filtering Files

To filter results, process the returned list:

    # Find only deleted files
    deleted_files = [f for f in files if f.get('Status', 0) & 0x02]

    # Find files larger than 1MB
    large_files = [f for f in files if f.get('Size', 0) > 1_000_000]

    # Find files by extension
    pdf_files = [f for f in files if f.get('Name', '').lower().endswith('.pdf')]

    # Find files modified in date range
    from datetime import datetime
    def parse_date(timestamp):
        # FEX Core returns Windows FILETIME or Unix timestamp
        return datetime.fromtimestamp(timestamp) if timestamp else None

    recent_files = [
        f for f in files
        if parse_date(f.get('Modified', 0)) and
           parse_date(f.get('Modified', 0)).year >= 2024
    ]

## Workflow 3: Extract File Content

Goal: Read the contents of a specific file from the forensic image.

### Use Case

Extract files for analysis, export evidence, or preview file contents during investigation.

### API choice

FEX Core exposes two ways to read file data:

Function | When to use
--- | ---
`ReadFileData` | One-off reads of a few files. Simple, but each first-use of a `FileIndex` allocates an internal reader that is not freed until `CloseImage()` — do not use in a loop over many files.
`RequestFileStream` → `ReadFileStream` → `ReleaseFileStream` | Bulk extraction or anything multi-threaded. Backed by an LRU pool (256 slots per image); `ReleaseFileStream` returns the slot immediately.

The extraction example below uses the handle-based stream API. See V2 API Reference for full signatures.

### Python implementation (handle-based stream)

    import ctypes

    # Wire up the handle-based stream API
    dll.RequestFileStream.argtypes = [
        ctypes.c_int32,                     # image_id
        ctypes.c_int32,                     # file_index
        ctypes.POINTER(ctypes.c_int32),     # out: stream_handle
    ]
    dll.RequestFileStream.restype = ctypes.c_int32

    dll.ReadFileStream.argtypes = [
        ctypes.c_int32,                     # image_id
        ctypes.c_int32,                     # stream_handle
        ctypes.c_int64,                     # offset
        ctypes.POINTER(ctypes.c_ubyte),     # buffer (caller-allocated)
        ctypes.c_int32,                     # buffer_size
        ctypes.POINTER(ctypes.c_int32),     # out: bytes_read
    ]
    dll.ReadFileStream.restype = ctypes.c_int32

    dll.ReleaseFileStream.argtypes = [ctypes.c_int32, ctypes.c_int32]
    dll.ReleaseFileStream.restype = ctypes.c_int32

    dll.GetFileSize.argtypes = [ctypes.c_int32, ctypes.c_int32, ctypes.POINTER(ctypes.c_uint64)]
    dll.GetFileSize.restype = ctypes.c_int32

    CHUNK = 1024 * 1024  # 1 MB

    def extract_file(image_id, file_index, output_path):
        """Extract a file from the forensic image to disk by its file index."""

        # Logical size — how many bytes of content to write. GetFileSize returns
        # logicalSize, not the allocated-on-disk physicalSize.
        logical_size = ctypes.c_uint64(0)
        rc = dll.GetFileSize(image_id, file_index, ctypes.byref(logical_size))
        if rc != 0 or logical_size.value == 0:
            return False

        handle = ctypes.c_int32(0)
        rc = dll.RequestFileStream(image_id, file_index, ctypes.byref(handle))
        if rc != 0:
            print(f"RequestFileStream failed: {rc}")
            return False

        try:
            buf = (ctypes.c_ubyte * CHUNK)()
            bytes_read = ctypes.c_int32(0)
            offset = 0
            remaining = logical_size.value

            with open(output_path, "wb") as out_file:
                while remaining > 0:
                    to_read = min(CHUNK, remaining)
                    rc = dll.ReadFileStream(
                        image_id, handle, offset, buf, to_read, ctypes.byref(bytes_read)
                    )
                    if rc != 0 or bytes_read.value <= 0:
                        break
                    out_file.write(bytes(buf[: bytes_read.value]))
                    offset += bytes_read.value
                    remaining -= bytes_read.value
                    print(f"\rExtracted {offset:,} bytes...", end="")

            print(f"\nFile extracted to: {output_path}")
            return True
        finally:
            # ALWAYS release — the pool slot is held until you do.
            dll.ReleaseFileStream(image_id, handle)

    # file_index comes from GetFileSystemRecords_V2() (see Workflow 2).
    extract_file(image_id, 42, "extracted_report.txt")

### Expected output

    Extracted 1,234 bytes...
    File extracted to: extracted_report.txt

### Hex-dump preview

For a quick preview without writing to disk — same handle pattern, single read:

    def hex_dump(image_id, file_index, num_bytes=256):
        handle = ctypes.c_int32(0)
        if dll.RequestFileStream(image_id, file_index, ctypes.byref(handle)) != 0:
            return
        try:
            buf = (ctypes.c_ubyte * num_bytes)()
            bytes_read = ctypes.c_int32(0)
            rc = dll.ReadFileStream(image_id, handle, 0, buf, num_bytes, ctypes.byref(bytes_read))
            if rc == 0 and bytes_read.value > 0:
                data = bytes(buf[: bytes_read.value])
                for i in range(0, len(data), 16):
                    hex_part = " ".join(f"{b:02x}" for b in data[i:i+16])
                    ascii_part = "".join(chr(b) if 32 <= b < 127 else "." for b in data[i:i+16])
                    print(f"{i:08x}  {hex_part:<48}  {ascii_part}")
        finally:
            dll.ReleaseFileStream(image_id, handle)

    hex_dump(image_id, 42)

## Workflow 4: Batch Processing Multiple Images

Goal: Process multiple forensic images efficiently.

### Use Case

Forensic labs often need to process dozens of images. This workflow shows how to handle multiple images with proper error handling and progress tracking.

### Python Implementation

    import ctypes
    import os
    from concurrent.futures import ThreadPoolExecutor, as_completed

    def process_image(image_path, license_key):
        """Process a single image and return summary."""

        result = {
            'path': image_path,
            'status': 'success',
            'file_count': 0,
            'total_size': 0,
            'error': None
        }

        # Open image
        image_id = dll.ImageOpen(
            image_path.encode('utf-8'),
            license_key.encode('utf-8')
        )

        if image_id < 0:
            result['status'] = 'error'
            result['error'] = f"Failed to open: error code {image_id}"
            return result

        try:
            # Get file list
            files = list_files(image_id)  # From Workflow 2

            result['file_count'] = len(files)
            result['total_size'] = sum(f.get('Size', 0) for f in files)

        except Exception as e:
            result['status'] = 'error'
            result['error'] = str(e)

        finally:
            dll.ImageClose(image_id)

        return result

    def batch_process(image_paths, license_key, max_workers=4):
        """Process multiple images in parallel."""

        results = []

        print(f"Processing {len(image_paths)} images...")
        print("-" * 60)

        # Sequential processing (safer for DLL)
        for i, path in enumerate(image_paths, 1):
            print(f"[{i}/{len(image_paths)}] Processing: {os.path.basename(path)}")

            result = process_image(path, license_key)
            results.append(result)

            if result['status'] == 'success':
                print(f"    Files: {result['file_count']:,}, "
                      f"Size: {result['total_size']:,} bytes")
            else:
                print(f"    ERROR: {result['error']}")

        # Summary
        print("-" * 60)
        successful = sum(1 for r in results if r['status'] == 'success')
        print(f"Completed: {successful}/{len(image_paths)} images processed successfully")

        return results

    # Process multiple images
    image_list = [
        "evidence/case001.E01",
        "evidence/case002.dd",
        "evidence/case003.L01"
    ]

    results = batch_process(image_list, "YOUR_LICENSE_KEY")

### Expected Output

    Processing 3 images...
    ------------------------------------------------------------
    [1/3] Processing: case001.E01
        Files: 1,234, Size: 45,678,901 bytes
    [2/3] Processing: case002.dd
        Files: 567, Size: 12,345,678 bytes
    [3/3] Processing: case003.L01
        ERROR: Failed to open: error code -4
    ------------------------------------------------------------
    Completed: 2/3 images processed successfully

### Error Handling Patterns

    def robust_process(image_path, license_key, retries=3):
        """Process with retry logic for transient failures."""

        for attempt in range(retries):
            try:
                result = process_image(image_path, license_key)
                if result['status'] == 'success':
                    return result

                # Don't retry certain errors
                if result['error'] and 'not supported' in result['error']:
                    return result  # Format issue, won't help to retry

            except Exception as e:
                if attempt < retries - 1:
                    print(f"Attempt {attempt + 1} failed, retrying...")
                    continue
                result = {
                    'path': image_path,
                    'status': 'error',
                    'error': str(e)
                }

        return result

## Cross-Platform Notes

### Windows vs Linux Path Handling

    import os

    # Use os.path for cross-platform compatibility
    dll_name = "FEX.Core.dll" if os.name == 'nt' else "libfexcore.so"
    dll_path = os.path.join("bin", "Win64" if os.name == 'nt' else "Linux64", dll_name)

    # Normalize paths
    image_path = os.path.normpath(image_path)

### DLL Loading Differences

    import ctypes
    import os

    if os.name == 'nt':
        # Windows: CDLL or WinDLL
        dll = ctypes.CDLL(dll_path)
    else:
        # Linux: Use CDLL with full path
        dll = ctypes.CDLL(os.path.abspath(dll_path))

## Next Steps

- Best Practices - Optimization and patterns
- V2 API Reference - Complete function documentation
- Troubleshooting - Common problems and solutions

Back to Documentation Index