Recover JPEGs from SD Card

CS50 Problem Set 4 Implementation
Captain X Cipher
July 2025
CS50, C Programming, File Recovery

The Recover program is a forensic tool that recovers deleted JPEG images from a memory card. Digital cameras often format memory cards such that "deleted" photos are simply hidden from the index but remain on the card. This program scans the raw data block by block to identify JPEG signatures and reconstructs images.

Project Requirements

  • Implement a program that recovers JPEGs from a forensic image
  • Accept a single command-line argument (the forensic image)
  • Open the memory card image file
  • Read the file in 512-byte blocks
  • Identify JPEG signatures in file headers
  • Create new JPEG files for each found image
  • Write consecutive blocks to the current JPEG file
  • Generate unique filenames in sequence (000.jpg, 001.jpg, etc.)
  • Properly close all files when finished

SD Card Structure

Memory cards are structured in 512-byte blocks. JPEG files are stored contiguously with specific headers:

FAT File System
JPEG Header Block
Image Data Block
Image Data Block
Free Space
JPEG Header Block
Image Data Block
File System Metadata
JPEG File Data
Free Space/Other Data

found()

Checks if a 512-byte block starts with a valid JPEG signature.

File Creation

Creates new JPEG files with sequential naming (000.jpg, 001.jpg, etc.)

Block Writing

Writes 512-byte blocks to the current JPEG file until a new signature is found.

Step 1: Opening the Memory Card

The program starts by validating command-line arguments and opening the memory card file for reading.

recover.c
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdint.h>

// Function prototype
bool found(uint8_t header[]);

int main(int argc, char *argv[])
{
    // Check for correct usage
    if (argc != 2)
    {
        printf("Usage: ./recover FILE\n");
        return 1;
    }

    // Open memory card file in binary mode
    FILE *card = fopen(argv[1], "rb");
    if (card == NULL)
    {
        printf("Card not opened\n");
        return 2;
    }
    
    // Rest of the code...

Important Note

The file is opened in binary mode ("rb") to ensure no data transformation occurs during reading, which is critical for forensic recovery.

Step 2: Reading Blocks and Detecting JPEG Signatures

The program reads the memory card 512 bytes at a time and checks for the JPEG signature in each block.

recover.c
// Buffer for reading 512-byte blocks
uint8_t buffer[512];

// Array to store the first 4 bytes (potential JPEG header)
uint8_t header[4];

// Output file pointer
FILE *output = NULL;

// Filename buffer (8 chars for 000.jpg\0)
char filename[8];
int file_count = 0;

// Read through the card block by block
while (fread(buffer, 1, 512, card) == 512)
{
    // Extract the first 4 bytes
    for (int i = 0; i < 4; i++)
    {
        header[i] = buffer[i];
    }
    
    // Check if we have a JPEG signature
    if (found(header))
    {
        // Handle JPEG found...
    }
    // Continue writing if we're in the middle of a JPEG
    else if (output != NULL)
    {
        fwrite(buffer, 1, 512, output);
    }
}

JPEG Signature Structure

JPEG files start with a specific 4-byte sequence:

Byte Position Value Description
Byte 0 0xFF Start of JPEG marker
Byte 1 0xD8 JPEG file start marker
Byte 2 0xFF Additional marker
Byte 3 0xE0-0xEF Specific JPEG format identifier

Step 3: Creating JPEG Files

When a new JPEG signature is found, the program closes any open file and creates a new JPEG file.

recover.c
if (found(header) == true)
{
    // Close previous JPEG if open
    if (output != NULL)
    {
        fclose(output);
    }

    // Create new filename (000.jpg, 001.jpg, etc.)
    sprintf(filename, "%03i.jpg", file_count);
    
    // Open new JPEG file in binary write mode
    output = fopen(filename, "wb");
    file_count++;
    
    // Write the first block of the new JPEG
    fwrite(buffer, 1, 512, output);
}

Filename Formatting

The sprintf function formats filenames with 3-digit numbers padded with zeros (e.g., 000.jpg, 001.jpg). This ensures proper sorting and meets the project requirements.

Step 4: JPEG Signature Detection

The found() function checks if a byte sequence matches the JPEG signature.

recover.c
// Check if header matches JPEG signature
bool found(uint8_t header[])
{
    // First 3 bytes must be 0xff, 0xd8, 0xff
    uint8_t signature[] = {0xff, 0xd8, 0xff};
    
    // Fourth byte must be in this range
    uint8_t sign2[] = {
        0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 
        0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xeb, 
        0xec, 0xed, 0xee, 0xef
    };

    // Check first 3 bytes
    for (int i = 0; i < 3; i++)
    {
        if (header[i] != signature[i])
        {
            return false;
        }
    }
    
    // Check fourth byte
    for (int i = 0; i < 16; i++)
    {
        if (header[3] == sign2[i])
        {
            return true;
        }
    }
    
    return false;
}

Full Implementation

The complete JPEG recovery implementation with all functions and main logic:

recover.c
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdint.h>

// Check if header matches JPEG signature
bool found(uint8_t header[]);

int main(int argc, char *argv[])
{
    // Validate command line arguments
    if (argc != 2)
    {
        printf("Usage: ./recover FILE\n");
        return 1;
    }

    // Open memory card file
    FILE *card = fopen(argv[1], "rb");
    if (card == NULL)
    {
        printf("Card not opened\n");
        return 2;
    }

    // Buffer for reading 512-byte blocks
    uint8_t buffer[512];
    
    // Array to store header bytes
    uint8_t header[4];
    
    // Output file pointer
    FILE *output = NULL;
    
    // Filename buffer
    char filename[8];
    int file_count = 0;

    // Read through card block by block
    while (fread(buffer, 1, 512, card) == 512)
    {
        // Extract first 4 bytes
        for (int i = 0; i < 4; i++)
        {
            header[i] = buffer[i];
        }
        
        // Check for JPEG signature
        if (found(header))
        {
            // Close previous file if open
            if (output != NULL)
            {
                fclose(output);
            }

            // Create new filename
            sprintf(filename, "%03i.jpg", file_count);
            output = fopen(filename, "wb");
            file_count++;
            
            // Write the first block
            fwrite(buffer, 1, 512, output);
        }
        // Continue writing if we're in a JPEG
        else if (output != NULL)
        {
            fwrite(buffer, 1, 512, output);
        }
    }

    // Close any remaining files
    if (output != NULL)
    {
        fclose(output);
    }
    
    // Close card file
    fclose(card);
    
    return 0;
}

// Function to detect JPEG signature
bool found(uint8_t header[])
{
    uint8_t signature[] = {0xff, 0xd8, 0xff};
    uint8_t sign2[] = {
        0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 
        0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xeb, 
        0xec, 0xed, 0xee, 0xef
    };

    // Check first 3 bytes
    for (int i = 0; i < 3; i++)
    {
        if (header[i] != signature[i])
        {
            return false;
        }
    }
    
    // Check fourth byte
    for (int i = 0; i < 16; i++)
    {
        if (header[3] == sign2[i])
        {
            return true;
        }
    }
    
    return false;
}

Key Considerations

  • Binary Mode: Files are opened in binary mode ("rb"/"wb") to prevent data corruption
  • JPEG Signatures: The detection function checks for the specific 4-byte sequence that identifies JPEG files
  • Block Reading: The program reads the card in 512-byte blocks, which is the standard size for storage devices
  • File Management: Properly closing files prevents resource leaks and ensures all data is written
  • Sequential Naming: Files are named with 3-digit numbers for proper sorting and identification

Important Note

To use this program on a physical SD card, you must first create a raw image using tools like Win32 Disk Imager. The input file should be in .raw or .img format containing the exact binary data from the memory card.

Example Execution

Input: ./recover card.raw

Output:

  • 000.jpg
  • 001.jpg
  • 002.jpg
  • ...

Result: Recovered JPEG images from the memory card image