Common Forensic Workflows
This guide provides step-by-step tutorials for common forensic operations using FEX Core SDK.
Prerequisites
Before starting these tutorials:
- FEX Core SDK extracted to a local directory
- Python 3.14+ installed (64-bit)
- Valid license key (or trial key)
- 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