Windows.NTFS.MFT

This artifact parses $MFT files and returns rows of each in scope MFT record. This artifact can be used as the basis for other artifacts where the MFT needs to be queried or for deleted file recovery.

For deleted file recovery: Take the MFT ID of a file of interest and provide it to the Windows.NTFS.Recover artifact.

To query all attached ntfs drives: check the AllDrives switch.

I have added several filters to uplift search capabilities from the original MFT artifact. Due to the multi-drive features, the MFTPath will output the MFT path of the entry.

Available filters include:

  • PathRegex (OSPath): e.g ^C:\\folder\\file\.ext$ or partial \\folder\\folder2\\ or string|string2|string3
  • Fileregex: ^filename.ext$ or partial string1|string2
  • Time bounds to select files with a timestamp within time ranges
  • FileSize bounds
  • MFTDrive: drive to target collection and show as source in results during offline pricessing.
  • MFTPath: optional filter for offline MFT processing.

NOTE: Generally more efficient to filter on filename. Multiple filters are cumulative. OSPath output now uses expected Windows backslash “\”.


name: Windows.NTFS.MFT
author: "Matt Green - @mgreen27"
description: |
  This artifact parses $MFT files and returns rows of each in scope  MFT record.
  This artifact can be used as the basis for other artifacts where the MFT needs
  to be queried or for deleted file recovery.

  For deleted file recovery: Take the MFT ID of a file of interest and provide
  it to the Windows.NTFS.Recover artifact.

  To query all attached ntfs drives: check the AllDrives switch.

  I have added several filters to uplift search capabilities from the original
  MFT artifact. Due to the multi-drive features, the MFTPath will output the MFT
  path of the entry.

  Available filters include:

  - PathRegex (OSPath): e.g `^C:\\folder\\file\.ext$` or partial `\\folder\\folder2\\` or `string|string2|string3`
  - Fileregex: `^filename.ext$` or partial `string1|string2`
  - Time bounds to select files with a timestamp within time ranges
  - FileSize bounds
  - MFTDrive: drive to target collection and show as source in results during offline pricessing.
  - MFTPath: optional filter for offline MFT processing.

  NOTE: Generally more efficient to filter on filename.
  Multiple filters are cumulative.
  OSPath output now uses expected Windows backslash "`\`".

parameters:
  - name: MFTDrive
    description: |
      The path to to the drive that holds the MFT file (can be a pathspec). This
      drive is also used for results for offline processing.
    default: "C:"
  - name: MFTPath
    description: Optional path to MFT file for offline processing.
    default:
  - name: Accessor
    default: ntfs
  - name: AllNtfs
    type: bool
    description: "Return all NTFS metadata with results."
  - name: PathRegex
    description: "Regex search over OSPath."
    default: "."
    type: regex
  - name: FileRegex
    description: "Regex search over File Name"
    default: "."
    type: regex
  - name: DateAfter
    type: timestamp
    description: "search for events after this date. YYYY-MM-DDTmm:hh:ssZ"
  - name: DateBefore
    type: timestamp
    description: "search for events before this date. YYYY-MM-DDTmm:hh:ssZ"
  - name: SizeMax
    type: int64
    description: "Entries in the MFT under this size in bytes."
  - name: SizeMin
    type: int64
    description: "Entries in the MFT over this size in bytes."
  - name: AllDrives
    type: bool
    description: "Select MFT search on all attached ntfs drives."
  - name: NTFS_INCLUDE_SHORT_NAMES
    description: See all names referencing the file including short names.
    type: bool

sources:
  - query: |
      -- Cater for older clients which do not have the Links column.
      LET parse_mft_version(filename, accessor, prefix) = SELECT *
      FROM if(condition=version(plugin="parse_mft") > 1,
              then={ SELECT *
                     FROM parse_mft(
                         filename=filename, accessor=accessor, prefix=prefix)
              },

              -- Older versions do not have the prefix parameter in
              -- the plugin and need the prefix prepended to the
              -- OSPath
              else={ SELECT *,
                            prefix + OSPath AS Links,
                            prefix + OSPath AS OSPath
                     FROM parse_mft(
                         filename=filename, accessor=accessor)
              })

      -- The path to to the drive that holds the MFT file (can be a pathspec)
      LET Drive <= pathspec(parse=MFTDrive, path_type="ntfs")

      -- time testing
      LET time_test(stamp) =
            if(condition= DateBefore AND DateAfter,
                then= stamp < DateBefore AND stamp > DateAfter,
                else=
            if(condition=DateBefore,
                then= stamp < DateBefore,
                else=
            if(condition= DateAfter,
                then= stamp > DateAfter,
                else= True
            )))

      -- find all ntfs drives
      LET ntfs_drives = SELECT
        OSPath AS Drive,
        OSPath + '$MFT' AS MFTFilename
      FROM glob(globs="/*", accessor="ntfs")
      WHERE log(message="Processing " + MFTFilename)

      -- function returning MFT entries
      -- Only check the filename - should be very quick
      LET mftsearch_with_filename(Drive, MFTPath) =
        SELECT EntryNumber, InUse, ParentEntryNumber,
            OSPath,
            Links AS _Links,
            FileName, FileSize, ReferenceCount, IsDir,
            Created0x10, Created0x30,
            LastModified0x10, LastModified0x30,
            LastRecordChange0x10, LastRecordChange0x30,
            LastAccess0x10,LastAccess0x30,
            HasADS, SI_Lt_FN, uSecZeros, Copied,
            FileNames, FileNameTypes
        FROM parse_mft_version(filename=MFTPath,
                       accessor=Accessor, prefix=Drive)
        WHERE FileName =~ FileRegex
          AND Links =~ PathRegex

      -- Check only one date bound
      LET mftsearch_after_date(Drive, MFTPath) =
        SELECT
            EntryNumber, InUse, ParentEntryNumber,
            OSPath,
            Links AS _Links,
            FileName, FileSize, ReferenceCount, IsDir,
            Created0x10, Created0x30,
            LastModified0x10, LastModified0x30,
            LastRecordChange0x10, LastRecordChange0x30,
            LastAccess0x10,LastAccess0x30,
            HasADS, SI_Lt_FN, uSecZeros, Copied,
            FileNames, FileNameTypes
        FROM parse_mft_version(filename=MFTPath,
                       accessor=Accessor, prefix=Drive)
        WHERE
             ( Created0x10 > DateAfter
              OR Created0x30 > DateAfter
              OR LastModified0x10 > DateAfter
              OR LastModified0x30 > DateAfter
              OR LastRecordChange0x10 > DateAfter
              OR LastRecordChange0x30 > DateAfter)
            AND FileName =~ FileRegex
            AND Links =~ PathRegex

      LET mftsearch_before_date(Drive, MFTPath) =
        SELECT EntryNumber, InUse, ParentEntryNumber,
            OSPath,
            Links AS _Links,
            FileName, FileSize, ReferenceCount, IsDir,
            Created0x10, Created0x30,
            LastModified0x10, LastModified0x30,
            LastRecordChange0x10, LastRecordChange0x30,
            LastAccess0x10,LastAccess0x30,
            HasADS, SI_Lt_FN, uSecZeros, Copied,
            FileNames, FileNameTypes
        FROM parse_mft_version(filename=MFTPath,
                       accessor=Accessor, prefix=Drive)
        WHERE
             ( Created0x10 < DateBefore
              OR Created0x30 < DateBefore
              OR LastModified0x10 < DateBefore
              OR LastModified0x30 < DateBefore
              OR LastRecordChange0x10 < DateBefore
              OR LastRecordChange0x30 < DateBefore)
            AND FileName =~ FileRegex
            AND Links =~ PathRegex

      -- Check everything can be slow.
      LET mftsearch_full(Drive, MFTPath) =
        SELECT EntryNumber, InUse, ParentEntryNumber,
            OSPath,
            Links AS _Links,
            FileName, FileSize, ReferenceCount, IsDir,
            Created0x10, Created0x30,
            LastModified0x10, LastModified0x30,
            LastRecordChange0x10, LastRecordChange0x30,
            LastAccess0x10,LastAccess0x30,
            HasADS, SI_Lt_FN, uSecZeros, Copied,
            FileNames, FileNameTypes
        FROM parse_mft_version(filename=MFTPath,
                       accessor=Accessor, prefix=Drive)
        WHERE FileName =~ FileRegex
            AND Links =~ PathRegex
            AND if(condition=SizeMax,
                then=FileSize < atoi(string=SizeMax),
                else=TRUE)
            AND if(condition=SizeMin,
                then=FileSize > atoi(string=SizeMin),
                else=TRUE)
            AND
             ( time_test(stamp=Created0x10)
            OR time_test(stamp=Created0x30)
            OR time_test(stamp=LastModified0x10)
            OR time_test(stamp=LastModified0x30)
            OR time_test(stamp=LastRecordChange0x10)
            OR time_test(stamp=LastRecordChange0x30)
            OR time_test(stamp=LastAccess0x10)
            OR time_test(stamp=LastAccess0x30))

      -- Choose a query to run depending on the user's choices.
      LET mftsearch(Drive, MFTPath) = SELECT * FROM if(
       -- only need to do a filename comparison
       condition=NOT DateAfter AND NOT DateBefore AND NOT SizeMin AND NOT SizeMax,
       then={ SELECT *
              FROM mftsearch_with_filename(Drive=Drive, MFTPath=MFTPath) },
       else={ SELECT * FROM if(

          -- Only DateAfter is set
          condition=NOT DateBefore AND NOT SizeMin AND NOT SizeMax,
          then={ SELECT *
                 FROM mftsearch_after_date(Drive=Drive, MFTPath=MFTPath)},
          else={ SELECT * FROM if(

             -- Only Date Before is set
             condition=NOT DateAfter AND NOT SizeMin AND NOT SizeMax,
             then={ SELECT *
                    FROM mftsearch_before_date(Drive=Drive, MFTPath=MFTPath)},
             else={ SELECT *
                    FROM mftsearch_full(Drive=Drive, MFTPath=MFTPath)})
          })
       })

      -- include all attached drives
      LET all_drives = SELECT * FROM foreach(row={
           SELECT * FROM ntfs_drives
        },
        query={
           SELECT *, Drive
           FROM mftsearch(
              Drive=Drive,
              MFTPath=MFTFilename)
        })

      -- return results
      LET results = SELECT *
          FROM if(condition=AllDrives,
            then={
              SELECT * FROM all_drives
            },
            else={
              SELECT * FROM mftsearch(Drive=Drive,
                                MFTPath=if(condition= MFTPath ,
                                            then= MFTPath,
                                            else= Drive + "$MFT"))
            })
      -- enrich results with NtfsMetadata is requests
      LET enriched_results = SELECT *,
            parse_ntfs(mft=EntryNumber, device=Drive ) as NtfsMetadata
        FROM results

      -- return rows
      SELECT * FROM if(condition=AllNtfs,
        then= enriched_results,
        else= results)