Skip to content

An update to our prior work within Lagging for the Win, now reporting all sms.db missing ROWID values up to the message sequence number.

License

Notifications You must be signed in to change notification settings

MetadataForensics/RowIDetective

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 

Repository files navigation

RowIDetective

An update to our prior work within Lagging for the Win: Querying for Negative Evidence in the sms.db. This version reports all missing ROWID values in the sms.db up to the message sequence number.

From the initial query we've:

  • Moved the timestamps calculations earlier in the query.
  • Added guid support.

The core logic of the query remains the same.

Detecting Missing Rows in the message Table

This SQL query identifies missing rows in the message table by comparing consecutive ROWID values and calculating the number of skipped rows. It also provides timestamp and guid values for the beginning and end of each missing interval.

Query Overview

SELECT * FROM (
  SELECT 
  CASE 
    WHEN length(DATE) = 18 
    THEN LAG(DATETIME(DATE/1000000000 + 978307200, 'UNIXEPOCH'),1) OVER (ORDER BY ROWID) 
    WHEN length(DATE) = 9
    THEN LAG(DATETIME(DATE + 978307200, 'UNIXEPOCH'),1) OVER (ORDER BY ROWID)
    END AS "Beginning Timestamp",
  CASE 
    WHEN length(DATE) = 18 
    THEN DATETIME(DATE/1000000000 + 978307200, 'UNIXEPOCH') 
    WHEN length(DATE) = 9
    THEN DATETIME(DATE + 978307200, 'UNIXEPOCH')	
    END  AS "Ending Timestamp",
  LAG (guid,1) OVER (ORDER BY ROWID) AS "Previous guid", 
  guid AS "guid", 
  LAG (ROWID,1) OVER (ORDER BY ROWID) AS "Previous ROWID", 
  ROWID AS "ROWID",
  (ROWID - (LAG (ROWID,1) OVER (ORDER BY ROWID)) - 1) AS "Number of Missing Rows"
  FROM message) list
WHERE ROWID - "Previous ROWID" > 1

Query Overview

Column Name Description
Beginning Timestamp Timestamp of the row before the missing interval. Handles both 9-digit (seconds since Apple Epoch) and 18-digit (nanoseconds since Apple Epoch) values.
Ending Timestamp Timestamp of the current row following the missing interval. Also adapts to both timestamp formats.
Previous guid The guid from the previous row, obtained via LAG(guid,1) OVER (ORDER BY ROWID). Can be used as a search reference within the overall dataset.
guid The unique identifier for the current message row.
Previous ROWID ROWID of the preceding row, retrieved using LAG function.
ROWID ROWID of the current row.
Number of Missing Rows Calculated as (ROWID - Previous ROWID - 1) to represent the count of missing rows.

How It Works

Inner Query

  • Uses the LAG function to access the previous row’s timestamp, guid, and ROWID.
  • Retrieves the current row’s timestamp, guid, and ROWID.
  • Calculates the number of missing rows by subtracting consecutive ROWID values:
    (ROWID - (LAG(ROWID, 1) OVER (ORDER BY ROWID)) - 1)

Outer Query

  • Filters only the rows where gaps exist: ROWID - "Previous ROWID" > 1.
    WHERE ROWID - "Previous ROWID" > 1

Example Output

Beginning Timestamp Ending Timestamp Previous guid guid Previous ROWID ROWID Number of Missing Rows
2025-10-07 22:43:01 2025-10-08 01:05:47 4013A733-D32A-404C-801D-950203036B2A 101D77A2-877D-434D-BAE3-DE057FE61B43 352105 352108 2
2025-10-08 02:34:20 2025-10-08 12:53:32 DFDE17CA-2A59-4EFD-8741-76D640005741 BE6283EB-1B37-2B9A-6080-E3899A5752C1 352123 352125 1

Extended Logic (Trailing Row Handling)

Purpose / Highlights:

  • Ensures the query detects gaps at the end of the message table that the original missing-ROWID logic could miss, depending on the data.
  • Adds a final row if the last message ROWID does not match the maximum sequence number (sqlite_sequence.seq).
  • Uses placeholders ("Unknown" for GUID, "Time of Extraction" for timestamp) to represent a single inferred row for missing data at the end of the table.

Detailed Explanation:

  1. WITH LastROWID AS (...) Common Table Expression (CTE)

    • Retrieves the last assigned ROWID from the sqlite_sequence table for the message table.
    • Serves as the maximum sequence reference to detect any trailing missing rows.
    WITH LastROWID AS (
    	SELECT seq AS last_rowid
    	FROM sqlite_sequence
    	WHERE sqlite_sequence.name = 'message'
    )
  2. Final Row Logic via UNION ALL

    • Uses the CTE LastROWID to obtain the table’s maximum ROWID (last_rowid) as a constant reference value.
    • When the maximum ROWID in the table is less than the reference last_rowid, adds a single synthetic “tail” row to represent the missing messages at the end.
    • Fills missing values with placeholders to indicate inferred data:
      • "Unknown" for the guid
      • "Time of Extraction" for the ending timestamp
    • Calculates Number of Missing Rows as (last_rowid - message.ROWID) to reflect the count of missing rows at the end of the table.
  3. Outer Query Behavior

    • The new outermost SELECT * FROM (...) WHERE "ROWID" IS NOT NULL ensures that only valid rows are returned.
    • Key consequence: if the max ROWID equals the last sequence value, no row of null values is returned which prevents a potentially misleading empty row.

Key Takeaway:

  • This extension guarantees complete coverage of missing rows, including any trailing gaps, without generating an additional empty row when the table is already complete up to the last sequence value.

Full Query

WITH LastROWID AS (
  SELECT seq AS last_rowid
  FROM sqlite_sequence
  WHERE sqlite_sequence.name = 'message'
)
SELECT * FROM (
  SELECT * FROM (
    SELECT 
    CASE 
      WHEN length(DATE) = 18 
      THEN LAG(DATETIME(DATE/1000000000 + 978307200, 'UNIXEPOCH'),1) OVER (ORDER BY ROWID) 
      WHEN length(DATE) = 9
      THEN LAG(DATETIME(DATE + 978307200, 'UNIXEPOCH'),1) OVER (ORDER BY ROWID)
      END AS "Beginning Timestamp",
    CASE 
      WHEN length(DATE) = 18 
      THEN DATETIME(DATE/1000000000 + 978307200, 'UNIXEPOCH') 
      WHEN length(DATE) = 9
      THEN DATETIME(DATE + 978307200, 'UNIXEPOCH')	
      END  AS "Ending Timestamp",
    LAG (guid,1) OVER (ORDER BY ROWID) AS "Previous guid", 
    guid AS "guid", 
    LAG (ROWID,1) OVER (ORDER BY ROWID) AS "Previous ROWID", 
    ROWID AS "ROWID",
    (ROWID - (LAG (ROWID,1) OVER (ORDER BY ROWID)) - 1) AS "Number of Missing Rows"
    FROM message) list
  WHERE ROWID - "Previous ROWID" > 1

  UNION ALL

  SELECT
  CASE
    WHEN message.ROWID != (SELECT last_rowid FROM LastROWID)
    THEN MAX(CASE
          WHEN length(DATE) = 18 THEN DATETIME(DATE/1000000000 + 978307200, 'UNIXEPOCH')
          WHEN length(DATE) = 9  THEN DATETIME(DATE + 978307200, 'UNIXEPOCH')
        END)
    END AS "Beginning Timestamp",
  CASE
    WHEN message.ROWID != (SELECT last_rowid FROM LastROWID)
    THEN "Time of Extraction"
    END AS "Ending Timestamp",
  CASE
    WHEN message.ROWID != (SELECT last_rowid FROM LastROWID)
    THEN guid
    END AS "Previous guid",
  CASE
    WHEN message.ROWID != (SELECT last_rowid FROM LastROWID)
    THEN "Unknown" 
    END AS "guid",
  CASE
    WHEN message.ROWID != (SELECT last_rowid FROM LastROWID)
    THEN MAX(ROWID)
    END AS "Previous ROWID",
  CASE
    WHEN message.ROWID != (SELECT last_rowid FROM LastROWID)
    THEN (SELECT last_rowid FROM LastROWID)
    END AS "ROWID",
  CASE
    WHEN message.ROWID != (SELECT last_rowid FROM LastROWID)
    THEN ((SELECT last_rowid FROM LastROWID) - message.ROWID)
    END AS "Number of Missing Rows"
  FROM message)
WHERE "ROWID" IS NOT NULL;

Example Output with Extended Logic

Beginning Timestamp Ending Timestamp Previous guid guid Previous ROWID ROWID Number of Missing Rows
2025-10-07 22:43:01 2025-10-08 01:05:47 4013A733-D32A-404C-801D-950203036B2A 101D77A2-877D-434D-BAE3-DE057FE61B43 352105 352108 2
2025-10-08 02:34:20 2025-10-08 12:53:32 DFDE17CA-2A59-4EFD-8741-76D640005741 BE6283EB-1B37-2B9A-6080-E3899A5752C1 352123 352125 1
2025-11-10 14:08:32 Time of Extraction 614BE4B5-552A-42E6-B3CF-27BEC58507B1 Unknown 354318 354325 7

Summary of Extended Output

With the addition of the trailing-row logic and the LastROWID CTE, the query now captures metadata for gaps at the end of the message table that were previously invisible.

For example, the row:

Beginning Timestamp Ending Timestamp Previous guid guid Previous ROWID ROWID Number of Missing Rows
2025-11-10 14:08:32 Time of Extraction 614BE4B5-552A-42E6-B3CF-27BEC58507B1 Unknown 354318 354325 7
  • Shows a gap of seven missing messages at the end of the table.
  • Provides useful metadata, including the previous message’s guid and timestamp, usable either as search terms or as contextual guidance for analysis.
  • This ensures complete coverage of all missing messages, including data that would have been omitted in the original query.

Where to Use:

  • The SQL queries will work in most SQLite database viewers able to execute SQL queries.
  • The RowIDetective.xml file is a Magnet AXIOM Custom Artifact, which can be added to Magnet AXIOM Process through Tools > Manage custom artifacts > ADD NEW CUSTOM ARTIFACT.
  • The iLEAPP artifact will be updated also, available here.

File Location

This query operates on sms.db, the database storing iMessage, SMS, and RCS data on Apple devices. It is found in all iPhone extractions and iTunes backups.

About

An update to our prior work within Lagging for the Win, now reporting all sms.db missing ROWID values up to the message sequence number.

Resources

License

Stars

Watchers

Forks