Skip to content

Fix/datetime filetime decoding - #29

Merged
scudette merged 2 commits into
Velocidex:masterfrom
sec-pc:fix/datetime-filetime-decoding
May 25, 2026
Merged

Fix/datetime filetime decoding#29
scudette merged 2 commits into
Velocidex:masterfrom
sec-pc:fix/datetime-filetime-decoding

Conversation

@sec-pc

@sec-pc sec-pc commented May 25, 2026

Copy link
Copy Markdown
Contributor

Pull Request: fix: auto-detect DateTime encoding for Flags=0 columns

Title

fix: auto-detect DateTime encoding for Flags=0 columns


Description

Problem

When parsing Active Directory Certificate Services (ADCS) CA databases (CertLog\*.edb) using parse_ese, all DateTime column fields — SubmittedWhen, ResolvedWhen, RevokedWhen, RevokedEffectiveWhen in the Requests table and NotBefore, NotAfter in the Certificates table — return 1899-12-30T00:00:00Z instead of the correct timestamps. The same database parsed by certutil and esedbexport produces correct timestamps.

Root Cause

ADCS CA databases store DateTime column values as Windows FILETIMEs (64-bit integers, 100-nanosecond intervals since 1601-01-01 UTC) but declare those columns with Flags=0 in the ESE catalog.

The existing Flags=0 decoder calls math.Float64frombits() on the raw bytes, treating them as an OLE variant double. A real FILETIME value for a date in 2025–2026 is approximately 134,000,000,000,000,000. When those 8 bytes are reinterpreted as an IEEE 754 double, the result is a subnormal float on the order of 10^-299. Multiplying by 86400 yields essentially zero, so the computed Unix timestamp is always approximately -2,209,334,400 — which is 1899-12-30 — regardless of the actual date stored. This corruption is irreversible.

Why Not Just Force FILETIME for All Flags=0 Columns

The initial fix unconditionally decoded all Flags=0 DateTime columns as FILETIMEs. This was reverted after the test suite revealed that SRUDB.dat (Windows SRUM) also uses DateTime columns with Flags=0 but stores genuine OLE variant doubles. Both databases have identical catalog metadata — same type byte (0x08), same ColumnFlags (0x00000000). The encoding is determined by the application that wrote the data, not the ESE schema.

The Fix — Self-Discriminating Value Inspection

Although catalog metadata cannot distinguish the two encodings, the raw 8-byte values occupy completely different regions of the IEEE 754 float64 number space:

Encoding Example (bytes) Interpreted as float64 Range
OLE double 2E D8 82 2D A1 B7 E5 40 44477.04 [2.0, ~73050] — normal float
FILETIME 30 20 6E C1 09 11 DC 01 ~1.05e-299 [0, ~10^-295] — subnormal float
  • A valid OLE double for any date 1900–2100 produces a normal IEEE 754 float in the range [2.0, ~73050] (high byte 0x40).
  • A valid FILETIME for any date 1601–9999, misread as a float64, produces a subnormal float on the order of 10^-299 (high byte 0x01 or lower).
  • The gap between these two ranges spans hundreds of orders of magnitude — there is no overlap.

The fix adds a single if days_since_1900 > 1.0 check inside the existing Flags=0 branch in both the fixed-column and tagged-column decoders:

case 0:
    value_int := ParseUint64(reader, offset)
    days_since_1900 := math.Float64frombits(value_int)

    if days_since_1900 > 1.0 {
        // Genuine OLE variant double — existing path unchanged
        result.Set(column.Name,
            time.Unix(int64(days_since_1900*24*60*60)+
                -2208988800-2*24*60*60, 0).UTC())
    } else {
        // Windows FILETIME (e.g. ADCS CA databases)
        result.Set(column.Name, WinFileTime64(reader, offset))
    }

Validation

ADCS CA database (ESSOS-CA.edb) — previously broken, now fixed:

Column Raw bytes Before After
SubmittedWhen 30 20 6E C1 09 11 DC 01 1899-12-30T00:00:00Z 2025-08-19T13:04:11Z
SubmittedWhen 20 DC 98 CC 09 11 DC 01 1899-12-30T00:00:00Z 2025-08-19T13:04:29Z
SubmittedWhen 20 3B 91 BD 9B D1 DC 01 1899-12-30T00:00:00Z 2026-04-21T14:32:54Z

SRUM database (SRUDB.dat) — must remain correct, and does:

Column Raw bytes float64 value Result
TimeStamp 2E D8 82 2D A1 B7 E5 40 44477.04 (> 1.0 → OLE path) 2021-10-08T00:53:00Z

All existing test fixtures pass without modification.

Files Changed

  • parser/catalog.go — added if days_since_1900 > 1.0 branch in both the fixed-column and tagged-column DateTime decoders under Flags=0

No imports were added or removed. No other files were modified.

sec-pc added 2 commits May 6, 2026 15:58
ESE DateTime columns with Flags=0 are ambiguous: some applications
(e.g. SRUM) store genuine OLE variant doubles while others (e.g. ADCS
CA) store Windows FILETIMEs in the same column type with identical
catalog metadata. The previous code unconditionally used the OLE double
decoder for Flags=0, which corrupted ADCS timestamps to 1899-12-30.

The two encodings are structurally distinguishable by examining the raw
bytes as a float64:

  - A valid OLE double for any date 1900-2100 produces a normal IEEE 754
    float in the range [2.0, ~73050] (high byte 0x40).
  - A valid FILETIME for any date 1601-9999 produces a subnormal float
    on the order of 10^-299 when misread as a double (high byte <= 0x01).

The gap between these ranges is many orders of magnitude with no
overlap. A threshold of 1.0 is used: values above it are decoded as OLE
doubles, values at or below it are decoded as Windows FILETIMEs.

Zero values (unset fields) produce 0.0 in both interpretations and
fall through to the FILETIME path, returning the FILETIME epoch
(1601-01-01) rather than the OLE epoch (1899-12-30).

Confirmed against:
  - ADCS CA (ESSOS-CA.edb): SubmittedWhen, ResolvedWhen, NotBefore,
    NotAfter now decode correctly (e.g. 2025-08-19T13:04:11Z)
  - SRUM (SRUDB.dat): TimeStamp continues to decode correctly
  - UAL (Current.mdb): DateTime columns continue to decode correctly
  - WebCache (WebCacheV01.dat): unaffected (uses LongLong not DateTime)
Changes.md was added as part of the DateTime fix investigation but is
not appropriate for the repository root. The fix rationale and
technical details are documented in the commit message and PR
description instead.
@CLAassistant

CLAassistant commented May 25, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@scudette
scudette merged commit 56639b0 into Velocidex:master May 25, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants