Task 387: .MDB File Format

Task 387: .MDB File Format

1. List of all the properties of this file format intrinsic to its file system

The .MDB file format is a proprietary binary format used by the Microsoft Jet Database Engine for Microsoft Access databases (versions up to 2003). It is structured as a series of fixed-size pages (2KB for Jet 3.x, 4KB for Jet 4.x), with the first page containing the main header. The format has no official public specification from Microsoft, but reverse-engineering (from sources like MDB Tools and the Unofficial MDB Guide) reveals the following intrinsic properties, which define the file's structure, encoding, security, and metadata. These are parsed from the header (first page), including an encrypted section starting at offset 0x18 (RC4 with key 0x6b39dac7 for 126 bytes in Jet 3 or 128+ bytes in Jet 4). Properties are listed with offsets (in hex), sizes, and descriptions where known:

  • Signature: Offset 0x00, 4 bytes (0x00 0x01 0x00 0x00). Identifies the file as a Jet database.
  • Format ID String: Offset 0x04, 20 bytes ("Standard Jet DB\0" for .MDB). Confirms the format type.
  • Jet Version: Offset 0x14, 1 byte (0x00 for Jet 3.x, 0x01 for Jet 4.x). Determines compatibility and page size.
  • File Type: Offset 0x15, 1 byte (typically 0x00). Indicates general file category.
  • System Collation/Sort Order: Offset 0x16, 2 bytes. Defines default sorting and comparison rules for text data.
  • Code Page: Offset 0x1C, 2 bytes. Specifies the character encoding (e.g., CP1252 for Western European).
  • Database Key: Offset 0x1E, 4 bytes. Used for encryption; non-zero indicates the file is encrypted.
  • Encryption Flag: Derived from Database Key (if non-zero, file is encrypted). Indicates if data pages are RC4-encrypted.
  • Page Size: Derived from Jet Version (2048 bytes for Jet 3.x, 4096 bytes for Jet 4.x). Defines the block size for all pages in the file.
  • Database Password: Offset 0x42, 20 bytes (Jet 3) or 40 bytes (Jet 4). Obfuscated storage for password (masked in Jet 4 with creation date).
  • Default Sort Order: Offset 0x66, 2 bytes. Specifies the default collation for new tables.
  • Creation Date/Time: Offset 0x72, 8 bytes (Jet 4; similar in Jet 3 but offset may vary). Timestamp when the database was created.
  • Number of Pages: Offset 0x08 (in first page usage map section, after decryption), 4 bytes. Total pages in the file.
  • Creating Program Name: Variable offset in header (after decryption, typically near 0x7A in Jet 4). String indicating the application that created the file (e.g., "MS Access").
  • Free Page Map: Variable offset in header (part of usage map after decryption). Bitmap tracking free/used pages in the file.

These properties are "intrinsic to its file system" as they govern how the file is read, decoded, and managed (e.g., page allocation, encoding, security). The rest of the file consists of table definition pages, data pages, index pages, and system tables (e.g., MSysObjects), but those are not header-intrinsic. Note: Offsets after 0x18 require decryption.

3. Ghost blog embedded HTML JavaScript for drag and drop .MDB file dump

Here's an HTML snippet with embedded JavaScript that can be embedded in a Ghost blog post. It creates a drag-and-drop area. When an .MDB file is dropped, it reads the binary data, parses the header, decrypts the encrypted section (using a simple RC4 implementation), extracts the properties from list 1, and dumps them to the screen in a preformatted block. This runs in the browser (no server needed).

Drag and drop an .MDB file here

4. Python class for .MDB file

Here's a Python class that can open an .MDB file, decode the header (including RC4 decryption), read and print the properties, and write modifications (e.g., update creation date). It uses built-in modules only. Run with python script.py file.mdb.

import struct
import datetime

def rc4(key, data):
    s = list(range(256))
    j = 0
    for i in range(256):
        j = (j + s[i] + key[i % len(key)]) % 256
        s[i], s[j] = s[j], s[i]
    i = j = 0
    output = bytearray()
    for byte in data:
        i = (i + 1) % 256
        j = (j + s[i]) % 256
        s[i], s[j] = s[j], s[i]
        output.append(byte ^ s[(s[i] + s[j]) % 256])
    return output

class MDBParser:
    def __init__(self, filename):
        self.filename = filename
        self.data = None
        self.properties = {}

    def open(self):
        with open(self.filename, 'rb') as f:
            self.data = f.read()
        if len(self.data) < 4096:
            raise ValueError("File too small for .MDB")

    def decode(self):
        if not self.data:
            raise ValueError("File not opened")
        # Non-encrypted
        sig = self.data[0:4].hex(' ')
        format_id = self.data[4:24].decode(errors='ignore')
        jet_ver = self.data[0x14]
        file_type = self.data[0x15]
        sys_collation = struct.unpack_from('<H', self.data, 0x16)[0]
        code_page = struct.unpack_from('<H', self.data, 0x1C)[0]
        db_key = struct.unpack_from('<I', self.data, 0x1E)[0]
        page_size = 2048 if jet_ver == 0 else 4096
        encrypted = 'Yes' if db_key != 0 else 'No'

        # Decrypt
        key = struct.pack('<I', 0x6b39dac7)
        enc_length = 126 if jet_ver == 0 else 128
        enc_data = self.data[0x18:0x18 + enc_length]
        dec_data = rc4(key, enc_data)

        # Decrypted fields
        pwd_length = 20 if jet_ver == 0 else 40
        password = dec_data[0x42 - 0x18:0x42 - 0x18 + pwd_length].hex(' ')
        default_sort = struct.unpack_from('<H', dec_data, 0x66 - 0x18)[0]
        # Creation date approx as double (8 bytes, Windows FILETIME)
        creation_raw = struct.unpack_from('<Q', dec_data, 0x72 - 0x18)[0]
        creation_date = datetime.datetime(1601, 1, 1) + datetime.timedelta(microseconds=creation_raw / 10)
        num_pages = struct.unpack_from('<I', self.data, 0x08)[0]  # Approx

        self.properties = {
            'Signature': sig,
            'Format ID': format_id,
            'Jet Version': jet_ver,
            'File Type': file_type,
            'System Collation': sys_collation,
            'Code Page': code_page,
            'Database Key': db_key,
            'Encrypted': encrypted,
            'Page Size': page_size,
            'Database Password (hex)': password,
            'Default Sort Order': default_sort,
            'Creation Date': creation_date.isoformat(),
            'Number of Pages': num_pages,
        }

    def print_properties(self):
        for key, value in self.properties.items():
            print(f"{key}: {value}")

    def write(self, new_filename=None, modify=None):
        if not self.data:
            raise ValueError("File not opened")
        data = bytearray(self.data)
        if modify:
            # Example: modify creation date (re-encrypt after)
            if 'creation_date' in modify:
                new_time = int((modify['creation_date'] - datetime.datetime(1601, 1, 1)).total_seconds() * 10000000)
                enc_length = 126 if data[0x14] == 0 else 128
                enc_data = data[0x18:0x18 + enc_length]
                key = struct.pack('<I', 0x6b39dac7)
                dec_data = rc4(key, enc_data)
                struct.pack_into('<Q', dec_data, 0x72 - 0x18, new_time)
                new_enc = rc4(key, dec_data)
                data[0x18:0x18 + enc_length] = new_enc
        filename = new_filename or self.filename + '.modified'
        with open(filename, 'wb') as f:
            f.write(data)
        print(f"Written to {filename}")

# Example usage
if __name__ == "__main__":
    import sys
    parser = MDBParser(sys.argv[1])
    parser.open()
    parser.decode()
    parser.print_properties()
    # Example write: parser.write(modify={'creation_date': datetime.datetime.now()})

5. Java class for .MDB file

Here's a Java class with similar functionality. Compile and run with javac MDBParser.java and java MDBParser file.mdb.

import java.io.*;
import java.nio.*;
import java.nio.file.*;
import java.time.*;

public class MDBParser {
    private byte[] data;
    private final String filename;
    private java.util.Map<String, Object> properties = new java.util.HashMap<>();

    public MDBParser(String filename) {
        this.filename = filename;
    }

    private byte[] rc4(byte[] key, byte[] input) {
        int[] s = new int[256];
        for (int i = 0; i < 256; i++) s[i] = i;
        int j = 0;
        for (int i = 0; i < 256; i++) {
            j = (j + s[i] + (key[i % key.length] & 0xFF)) % 256;
            int temp = s[i];
            s[i] = s[j];
            s[j] = temp;
        }
        int i = 0; j = 0;
        byte[] output = new byte[input.length];
        for (int k = 0; k < input.length; k++) {
            i = (i + 1) % 256;
            j = (j + s[i]) % 256;
            int temp = s[i];
            s[i] = s[j];
            s[j] = temp;
            output[k] = (byte) (input[k] ^ s[(s[i] + s[j]) % 256]);
        }
        return output;
    }

    public void open() throws IOException {
        data = Files.readAllBytes(Paths.get(filename));
        if (data.length < 4096) throw new IOException("File too small");
    }

    public void decode() {
        if (data == null) throw new IllegalStateException("File not opened");
        StringBuilder sigSb = new StringBuilder();
        for (int b = 0; b < 4; b++) sigSb.append(String.format("%02x ", data[b]));
        String sig = sigSb.toString().trim();
        String formatId = new String(data, 4, 20);
        int jetVer = data[0x14] & 0xFF;
        int fileType = data[0x15] & 0xFF;
        ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
        int sysCollation = bb.getShort(0x16) & 0xFFFF;
        int codePage = bb.getShort(0x1C) & 0xFFFF;
        int dbKey = bb.getInt(0x1E);
        int pageSize = jetVer == 0 ? 2048 : 4096;
        String encrypted = dbKey != 0 ? "Yes" : "No";

        byte[] key = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(0x6b39dac7).array();
        int encLength = jetVer == 0 ? 126 : 128;
        byte[] encData = new byte[encLength];
        System.arraycopy(data, 0x18, encData, 0, encLength);
        byte[] decData = rc4(key, encData);

        int pwdLength = jetVer == 0 ? 20 : 40;
        StringBuilder pwdSb = new StringBuilder();
        for (int b = 0x42 - 0x18; b < 0x42 - 0x18 + pwdLength; b++) pwdSb.append(String.format("%02x ", decData[b]));
        String password = pwdSb.toString().trim();
        int defaultSort = ByteBuffer.wrap(decData).order(ByteOrder.LITTLE_ENDIAN).getShort(0x66 - 0x18) & 0xFFFF;
        long creationRaw = ByteBuffer.wrap(decData).order(ByteOrder.LITTLE_ENDIAN).getLong(0x72 - 0x18);
        Instant creationDate = Instant.ofEpochMilli((creationRaw / 10000) - 11644473600000L); // FILETIME to epoch
        int numPages = bb.getInt(0x08);

        properties.put("Signature", sig);
        properties.put("Format ID", formatId);
        properties.put("Jet Version", jetVer);
        properties.put("File Type", fileType);
        properties.put("System Collation", sysCollation);
        properties.put("Code Page", codePage);
        properties.put("Database Key", dbKey);
        properties.put("Encrypted", encrypted);
        properties.put("Page Size", pageSize);
        properties.put("Database Password (hex)", password);
        properties.put("Default Sort Order", defaultSort);
        properties.put("Creation Date", creationDate.toString());
        properties.put("Number of Pages", numPages);
    }

    public void printProperties() {
        properties.forEach((k, v) -> System.out.println(k + ": " + v));
    }

    public void write(String newFilename, java.util.Map<String, Object> modify) throws IOException {
        if (data == null) throw new IllegalStateException("File not opened");
        byte[] newData = data.clone();
        if (modify != null && modify.containsKey("creation_date")) {
            Instant newTime = (Instant) modify.get("creation_date");
            long newRaw = (newTime.toEpochMilli() + 11644473600000L) * 10000;
            int jetVer = newData[0x14] & 0xFF;
            int encLength = jetVer == 0 ? 126 : 128;
            byte[] encData = new byte[encLength];
            System.arraycopy(newData, 0x18, encData, 0, encLength);
            byte[] key = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(0x6b39dac7).array();
            byte[] decData = rc4(key, encData);
            ByteBuffer.wrap(decData).order(ByteOrder.LITTLE_ENDIAN).putLong(0x72 - 0x18, newRaw);
            byte[] newEnc = rc4(key, decData);
            System.arraycopy(newEnc, 0, newData, 0x18, encLength);
        }
        String outFile = newFilename != null ? newFilename : filename + ".modified";
        Files.write(Paths.get(outFile), newData);
        System.out.println("Written to " + outFile);
    }

    public static void main(String[] args) throws IOException {
        if (args.length < 1) return;
        MDBParser parser = new MDBParser(args[0]);
        parser.open();
        parser.decode();
        parser.printProperties();
        // Example: java.util.Map<String, Object> modify = new java.util.HashMap<>(); modify.put("creation_date", Instant.now()); parser.write(null, modify);
    }
}

6. JavaScript class for .MDB file

Here's a Node.js JavaScript class. Run with node script.js file.mdb. Requires no dependencies.

const fs = require('fs');

function rc4(key, data) {
  let s = new Array(256).fill(0).map((_, i) => i);
  let j = 0;
  for (let i = 0; i < 256; i++) {
    j = (j + s[i] + key[i % key.length].charCodeAt(0)) % 256;
    [s[i], s[j]] = [s[j], s[i]];
  }
  let i = 0; j = 0;
  let output = new Uint8Array(data.length);
  for (let k = 0; k < data.length; k++) {
    i = (i + 1) % 256;
    j = (j + s[i]) % 256;
    [s[i], s[j]] = [s[j], s[i]];
    output[k] = data[k] ^ s[(s[i] + s[j]) % 256];
  }
  return output;
}

class MDBParser {
  constructor(filename) {
    this.filename = filename;
    this.data = null;
    this.properties = {};
  }

  open() {
    this.data = fs.readFileSync(this.filename);
    if (this.data.length < 4096) throw new Error('File too small');
  }

  decode() {
    if (!this.data) throw new Error('File not opened');
    const sig = [...this.data.subarray(0, 4)].map(b => b.toString(16).padStart(2, '0')).join(' ');
    const formatId = new TextDecoder().decode(this.data.subarray(4, 24));
    const jetVer = this.data[0x14];
    const fileType = this.data[0x15];
    const sysCollation = this.data.readUInt16LE(0x16);
    const codePage = this.data.readUInt16LE(0x1C);
    const dbKey = this.data.readUInt32LE(0x1E);
    const pageSize = jetVer === 0 ? 2048 : 4096;
    const encrypted = dbKey !== 0 ? 'Yes' : 'No';

    const key = Buffer.alloc(4);
    key.writeUInt32LE(0x6b39dac7, 0);
    const encLength = jetVer === 0 ? 126 : 128;
    const encData = this.data.subarray(0x18, 0x18 + encLength);
    const decData = rc4(key.toString('binary'), encData);

    const pwdLength = jetVer === 0 ? 20 : 40;
    const password = [...decData.subarray(0x42 - 0x18, 0x42 - 0x18 + pwdLength)].map(b => b.toString(16).padStart(2, '0')).join(' ');
    const defaultSort = decData.readUInt16LE(0x66 - 0x18);
    const creationRaw = Number(decData.readBigUInt64LE(0x72 - 0x18));
    const creationDate = new Date((creationRaw / 10000) - 11644473600000).toISOString();
    const numPages = this.data.readUInt32LE(0x08);

    this.properties = {
      'Signature': sig,
      'Format ID': formatId,
      'Jet Version': jetVer,
      'File Type': fileType,
      'System Collation': sysCollation,
      'Code Page': codePage,
      'Database Key': dbKey,
      'Encrypted': encrypted,
      'Page Size': pageSize,
      'Database Password (hex)': password,
      'Default Sort Order': defaultSort,
      'Creation Date': creationDate,
      'Number of Pages': numPages,
    };
  }

  printProperties() {
    Object.entries(this.properties).forEach(([key, value]) => console.log(`${key}: ${value}`));
  }

  write(newFilename = null, modify = null) {
    if (!this.data) throw new Error('File not opened');
    let newData = Buffer.from(this.data);
    if (modify && modify.creation_date) {
      const newTime = BigInt((Date.parse(modify.creation_date) + 11644473600000) * 10000);
      const jetVer = newData[0x14];
      const encLength = jetVer === 0 ? 126 : 128;
      const encData = newData.subarray(0x18, 0x18 + encLength);
      const key = Buffer.alloc(4);
      key.writeUInt32LE(0x6b39dac7, 0);
      let decData = rc4(key.toString('binary'), encData);
      decData.writeBigUInt64LE(newTime, 0x72 - 0x18);
      const newEnc = rc4(key.toString('binary'), decData);
      newEnc.copy(newData, 0x18);
    }
    const outFile = newFilename || this.filename + '.modified';
    fs.writeFileSync(outFile, newData);
    console.log(`Written to ${outFile}`);
  }
}

// Example usage
if (process.argv.length > 2) {
  const parser = new MDBParser(process.argv[2]);
  parser.open();
  parser.decode();
  parser.printProperties();
  // Example: parser.write(null, { creation_date: new Date().toISOString() });
}

7. C class for .MDB file

Here's a C++ class (since "c class" likely means C++ for class support). Compile with g++ -o mdbparser script.cpp and run ./mdbparser file.mdb.

#include <iostream>
#include <fstream>
#include <vector>
#include <iomanip>
#include <ctime>
#include <map>
#include <cstdint>

std::vector<uint8_t> rc4(const std::vector<uint8_t>& key, const std::vector<uint8_t>& data) {
  std::vector<int> s(256);
  for (int i = 0; i < 256; ++i) s[i] = i;
  int j = 0;
  for (int i = 0; i < 256; ++i) {
    j = (j + s[i] + key[i % key.size()]) % 256;
    std::swap(s[i], s[j]);
  }
  int i = 0; j = 0;
  std::vector<uint8_t> output(data.size());
  for (size_t k = 0; k < data.size(); ++k) {
    i = (i + 1) % 256;
    j = (j + s[i]) % 256;
    std::swap(s[i], s[j]);
    output[k] = data[k] ^ s[(s[i] + s[j]) % 256];
  }
  return output;
}

class MDBParser {
private:
  std::string filename;
  std::vector<uint8_t> data;
  std::map<std::string, std::string> properties;

public:
  MDBParser(const std::string& fn) : filename(fn) {}

  void open() {
    std::ifstream file(filename, std::ios::binary);
    if (!file) throw std::runtime_error("Cannot open file");
    file.seekg(0, std::ios::end);
    size_t size = file.tellg();
    file.seekg(0);
    data.resize(size);
    file.read(reinterpret_cast<char*>(data.data()), size);
    if (size < 4096) throw std::runtime_error("File too small");
  }

  void decode() {
    if (data.empty()) throw std::runtime_error("File not opened");
    std::stringstream sigSs;
    for (int b = 0; b < 4; ++b) sigSs << std::hex << std::setw(2) << std::setfill('0') << (int)data[b] << " ";
    std::string sig = sigSs.str(); sig.pop_back();
    std::string formatId(reinterpret_cast<char*>(&data[4]), 20);
    uint8_t jetVer = data[0x14];
    uint8_t fileType = data[0x15];
    uint16_t sysCollation = *reinterpret_cast<uint16_t*>(&data[0x16]);
    uint16_t codePage = *reinterpret_cast<uint16_t*>(&data[0x1C]);
    uint32_t dbKey = *reinterpret_cast<uint32_t*>(&data[0x1E]);
    int pageSize = (jetVer == 0) ? 2048 : 4096;
    std::string encrypted = (dbKey != 0) ? "Yes" : "No";

    std::vector<uint8_t> key = {0xc7, 0xda, 0x39, 0x6b}; // Little-endian 0x6b39dac7
    int encLength = (jetVer == 0) ? 126 : 128;
    std::vector<uint8_t> encData(data.begin() + 0x18, data.begin() + 0x18 + encLength);
    std::vector<uint8_t> decData = rc4(key, encData);

    int pwdLength = (jetVer == 0) ? 20 : 40;
    std::stringstream pwdSs;
    for (int b = 0x42 - 0x18; b < 0x42 - 0x18 + pwdLength; ++b) pwdSs << std::hex << std::setw(2) << std::setfill('0') << (int)decData[b] << " ";
    std::string password = pwdSs.str(); password.pop_back();
    uint16_t defaultSort = *reinterpret_cast<uint16_t*>(&decData[0x66 - 0x18]);
    uint64_t creationRaw = *reinterpret_cast<uint64_t*>(&decData[0x72 - 0x18]);
    time_t epoch = (creationRaw / 10000000) - 11644473600LL;
    char dateBuf[32];
    std::strftime(dateBuf, sizeof(dateBuf), "%Y-%m-%dT%H:%M:%S", std::gmtime(&epoch));
    uint32_t numPages = *reinterpret_cast<uint32_t*>(&data[0x08]);

    properties["Signature"] = sig;
    properties["Format ID"] = formatId;
    properties["Jet Version"] = std::to_string(jetVer);
    properties["File Type"] = std::to_string(fileType);
    properties["System Collation"] = std::to_string(sysCollation);
    properties["Code Page"] = std::to_string(codePage);
    properties["Database Key"] = std::to_string(dbKey);
    properties["Encrypted"] = encrypted;
    properties["Page Size"] = std::to_string(pageSize);
    properties["Database Password (hex)"] = password;
    properties["Default Sort Order"] = std::to_string(defaultSort);
    properties["Creation Date"] = dateBuf;
    properties["Number of Pages"] = std::to_string(numPages);
  }

  void printProperties() {
    for (const auto& [key, value] : properties) {
      std::cout << key << ": " << value << std::endl;
    }
  }

  void write(const std::string& newFilename = "", std::map<std::string, std::string> modify = {}) {
    if (data.empty()) throw std::runtime_error("File not opened");
    std::vector<uint8_t> newData = data;
    if (modify.count("creation_date")) {
      struct tm tm = {};
      strptime(modify["creation_date"].c_str(), "%Y-%m-%dT%H:%M:%S", &tm);
      uint64_t newTime = (static_cast<uint64_t>(mktime(&tm)) + 11644473600LL) * 10000000;
      uint8_t jetVer = newData[0x14];
      int encLength = (jetVer == 0) ? 126 : 128;
      std::vector<uint8_t> encData(newData.begin() + 0x18, newData.begin() + 0x18 + encLength);
      std::vector<uint8_t> key = {0xc7, 0xda, 0x39, 0x6b};
      std::vector<uint8_t> decData = rc4(key, encData);
      *reinterpret_cast<uint64_t*>(&decData[0x72 - 0x18]) = newTime;
      std::vector<uint8_t> newEnc = rc4(key, decData);
      std::copy(newEnc.begin(), newEnc.end(), newData.begin() + 0x18);
    }
    std::string outFile = newFilename.empty() ? filename + ".modified" : newFilename;
    std::ofstream out(outFile, std::ios::binary);
    out.write(reinterpret_cast<char*>(newData.data()), newData.size());
    std::cout << "Written to " << outFile << std::endl;
  }
};

int main(int argc, char** argv) {
  if (argc < 2) return 1;
  try {
    MDBParser parser(argv[1]);
    parser.open();
    parser.decode();
    parser.printProperties();
    // Example: std::map<std::string, std::string> modify; modify["creation_date"] = "2025-09-29T12:00:00"; parser.write("", modify);
  } catch (const std::exception& e) {
    std::cerr << e.what() << std::endl;
  }
  return 0;
}