This artifact parses the FSEvents log files.
We can filter on Path, Flags or use time box on source file
An interesting hunt may be 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.
NOTE:
name: MacOS.Forensics.FSEvents
description: |
This artifact parses the FSEvents log files.
We can filter on Path, Flags or use time box on source file
An interesting hunt may be 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.
NOTE:
- FSEvents do not have timestamps so we specify source file Mtime and
Btime.
- default timeout is only 600 seconds - you will need to increase for this
colection to finish.
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 e given.
- name: PathRegex
description: Filter the path by this regexp
default: .
type: regex
- name: FlagsRegex
description: Filter by flags
type: regex
default: .
- 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"
- name: LogSource
type: bool
description: "Adds the Source file OSPath into logs"
export: |
LET FSEventProfile = '''[
["Header", 0, [
["Signature", 0, "String", {
length: 4
}],
["StreamSize", 8, uint32],
["Items", 12, "Array", {
count: 10000,
max_count: 10000,
type: FSEventEntry,
sentinel: "x=>len(list=x.path) = 0",
}]
]],
["FSEventEntry", "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
}
}],
]]
]'''
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)
SELECT * FROM foreach(row=files,
query={
SELECT
path as EntryPath,
id as EntryId,
join(array=flags, sep=", ") AS EntryFlags,
OSPath.Basename as SourceFile,
Mtime as SourceMtime,
Btime as SourceBtime
FROM foreach(row=parse_binary(
filename=read_file(filename=OSPath, accessor="gzip", length=1000000),
accessor="data",
profile=FSEventProfile, struct="Header").Items)
})
WHERE EntryPath =~ PathRegex AND EntryFlags =~ FlagsRegex