Skip to content
Please update to the latest release 0.77.2 to address Multiple CVEs.
MacOS.Forensics.FSEvents

MacOS.Forensics.FSEvents

Reads macOS FSEvents logs to enumerate file creation, deletion, rename, and modification events.

OPtionally filter on Path, Flags or use time box on source file.

An interesting hunt may filter for Entries of plist files modified or created on a specific date. Malware often creates plist files in /Library/LaunchAgents, Library/Preferences, /Library/LaunchDaemons, or /Library/Internet Plugins.

NOTES

  • FSEvents do not have timestamps so we specify source file Mtime and Btime.
  • The default timeout is only 600 seconds - you will probably need to increase it to allow the collection to finish.

name: MacOS.Forensics.FSEvents
description: |
  Reads macOS FSEvents logs to enumerate file creation, deletion,
  rename, and modification events.

  OPtionally filter on Path, Flags or use time box on source file.

  An interesting hunt may filter for Entries of plist files modified
  or created on a specific date. Malware often creates plist files in
  `/Library/LaunchAgents`, `Library/Preferences`,
  `/Library/LaunchDaemons`, or `/Library/Internet Plugins`.

  **NOTES**

  - FSEvents do not have timestamps so we specify source file Mtime and
    Btime.
  - The default timeout is only 600 seconds - you will probably need to
    increase it to allow the collection to finish.

author: |
  Mike Cohen, Matt Green - @mgreen27, Yogesh Khatri (@swiftforensics), CyberCX

reference:
- https://www.osdfcon.org/presentations/2017/Ibrahim-Understanding-MacOS-File-Ststem-Events-with-FSEvents-Parser.pdf
- https://www.crowdstrike.com/blog/using-os-x-fsevents-discover-deleted-malicious-artifact/

type: CLIENT

parameters:
   - name: GlobTable
     type: csv
     default: |
       Glob
       /.fseventsd/*
       /System/Volumes/Data/.fseventsd/*
   - name: Glob
     type: string
     description: Instead of providing the globs in a table, a single glob may be given.
   - name: PathRegex
     description: Filter the path by this regexp
     default: .
     type: regex
   - name: FlagsRegex
     description: Filter by flags
     type: regex
     default: .
   - name: MaxFileSize
     type: int
     default: "10000000"
     description: "Read up to that many bytes"
   - name: DateAfter
     type: timestamp
     description: "search for source files with Btime after this date. YYYY-MM-DDTmm:hh:ssZ"
   - name: DateBefore
     type: timestamp
     description: "search for source files with Mtime before this date. YYYY-MM-DDTmm:hh:ssZ"

export: |
    LET FSEventProfile = '''[
    ["FSEventsProfile", 0, [
      ["Entries", 0, "Array", {
         type: "Header",
         count: 10000,
      }],
    ]],
    ["Header", "x=>x.Info.StreamSize", [
      ["Version", 0, "Enumeration", {
         type: "unsigned int",
         choices: {
           "1145852721": "V1",
           "1145852722": "V2",
           "1145852723": "V3"
         }
      }],
      ["Info", 8, "Union", {
         selector: "x=>x.Version",
         choices: {
             "V1": "FS1",
             "V2": "FS2",
             "V3": "FS3",
         }
      }],
    ]],
    ["FS1", "x=>x.StreamSize - 8", [
      ["StreamSize", 0, uint32],
      ["Items", 4, "Array", {
          count: 10000,
          max_count: 10000,
          type: FSEventEntry1,
          sentinel: "x=>this.EndOf < x.EndOf",
      }],
    ]],
    ["FS2", "x=>x.StreamSize - 8", [
      ["StreamSize", 0, uint32],
      ["Items", 4, "Array", {
          count: 10000,
          max_count: 10000,
          type: FSEventEntry2,
          sentinel: "x=>this.EndOf < x.EndOf",
      }],
    ]],
    ["FS3", "x=>x.StreamSize - 8", [
      ["StreamSize", 0, uint32],
      ["Items", 4, "Array", {
          count: 10000,
          max_count: 2336,
          type: FSEventEntry3,
          sentinel: "x=>this.EndOf < x.EndOf",
      }],
    ]],
    ["FSEventEntry1", "x=>len(list=x.path) + 13", [
      ["path", 0, "String"],
      ["id", "x=>len(list=x.path) + 1", "uint64"],
      ["flags", "x=>len(list=x.path) + 9", "Flags", {
          type: "uint32",
          bitmap: {
            FSE_CREATE_FILE: 0,
            FSE_DELETE: 1,
            FSE_STAT_CHANGED: 2,
            FSE_RENAME: 3,
            FSE_CONTENT_MODIFIED: 4,
            FSE_EXCHANGE: 5,
            FSE_FINDER_INFO_CHANGED: 6,
            FSE_CREATE_DIR: 7,
            FSE_CHOWN: 8,
            FSE_XATTR_MODIFIED: 9,
            FSE_XATTR_REMOVED: 10,
            FSE_DOCID_CREATED: 11,
            FSE_DOCID_CHANGED: 12,
            FSE_UNMOUNT_PENDING: 13,
            FSE_CLONE: 14,
            FSE_MODE_CLONE: 16,
            FSE_TRUNCATED_PATH: 17,
            FSE_REMOTE_DIR_EVENT: 18,
            FSE_MODE_LAST_HLINK: 19,
            FSE_MODE_HLINK: 20,
            IsSymbolicLink: 22,
            IsFile: 23,
            IsDirectory: 24,
            Mount: 25,
            Unmount: 26,
            EndOfTransaction: 29
          }
      }],
      ["file_id", 0, "Value", {"value": ""}],
    ]],
    ["FSEventEntry2", "x=>len(list=x.path) + 21", [
      ["path", 0, "String"],
      ["id", "x=>len(list=x.path) + 1", "uint64"],
      ["flags", "x=>len(list=x.path) + 9", "Flags", {
          type: "uint32",
          bitmap: {
            FSE_CREATE_FILE: 0,
            FSE_DELETE: 1,
            FSE_STAT_CHANGED: 2,
            FSE_RENAME: 3,
            FSE_CONTENT_MODIFIED: 4,
            FSE_EXCHANGE: 5,
            FSE_FINDER_INFO_CHANGED: 6,
            FSE_CREATE_DIR: 7,
            FSE_CHOWN: 8,
            FSE_XATTR_MODIFIED: 9,
            FSE_XATTR_REMOVED: 10,
            FSE_DOCID_CREATED: 11,
            FSE_DOCID_CHANGED: 12,
            FSE_UNMOUNT_PENDING: 13,
            FSE_CLONE: 14,
            FSE_MODE_CLONE: 16,
            FSE_TRUNCATED_PATH: 17,
            FSE_REMOTE_DIR_EVENT: 18,
            FSE_MODE_LAST_HLINK: 19,
            FSE_MODE_HLINK: 20,
            IsSymbolicLink: 22,
            IsFile: 23,
            IsDirectory: 24,
            Mount: 25,
            Unmount: 26,
            EndOfTransaction: 29
          }
      }],
      ["file_id", "x=>len(list=x.path) + 13", "int64"],
    ]],
    ["FSEventEntry3", "x=>len(list=x.path) + 25", [
      ["path", 0, "String"],
      ["id", "x=>len(list=x.path) + 1", "uint64"],
      ["flags", "x=>len(list=x.path) + 9", "Flags", {
          type: "uint32",
          bitmap: {
            FSE_CREATE_FILE: 0,
            FSE_DELETE: 1,
            FSE_STAT_CHANGED: 2,
            FSE_RENAME: 3,
            FSE_CONTENT_MODIFIED: 4,
            FSE_EXCHANGE: 5,
            FSE_FINDER_INFO_CHANGED: 6,
            FSE_CREATE_DIR: 7,
            FSE_CHOWN: 8,
            FSE_XATTR_MODIFIED: 9,
            FSE_XATTR_REMOVED: 10,
            FSE_DOCID_CREATED: 11,
            FSE_DOCID_CHANGED: 12,
            FSE_UNMOUNT_PENDING: 13,
            FSE_CLONE: 14,
            FSE_MODE_CLONE: 16,
            FSE_TRUNCATED_PATH: 17,
            FSE_REMOTE_DIR_EVENT: 18,
            FSE_MODE_LAST_HLINK: 19,
            FSE_MODE_HLINK: 20,
            IsSymbolicLink: 22,
            IsFile: 23,
            IsDirectory: 24,
            Mount: 25,
            Unmount: 26,
            EndOfTransaction: 29
          }
      }],
      ["file_id", "x=>len(list=x.path) + 13", "int64"],
      ["unknown_id", "x=>len(list=x.path) + 21", "int32"],
    ]],
    ]'''

sources:
  - query: |
      LET files = SELECT OSPath, Mtime, Btime
        FROM glob(globs=(Glob || GlobTable.Glob))
        WHERE   if(condition=DateAfter, then= Btime > DateAfter, else= True )
            AND if(condition=DateBefore, then= Mtime < DateBefore, else= True )
            AND log(message=OSPath)

      LET x = SELECT * FROM foreach(row=files,
        query={
            SELECT
                Version,
                Info.Items as items,
                OSPath.Basename as SourceFile,
                Mtime as SourceMtime,
                Btime as SourceBtime
            FROM
                foreach(row=parse_binary(
                    filename=read_file(filename=OSPath, accessor="gzip", length=MaxFileSize),
                    accessor="data",
                    profile=FSEventProfile, struct="FSEventsProfile").Entries)
        })

      SELECT
        items.path as EntryPath,
        items.id as EntryId,
        join(array=items.flags, sep=", ") AS EntryFlags,
        items.file_id as FileId,
        SourceFile,
        SourceMtime,
        SourceBtime,
        Version
      FROM
        flatten(query=x)
      WHERE EntryPath =~ PathRegex AND EntryFlags =~ FlagsRegex