Detection rules › Kusto
DLL Hijacking: Loading from an Unusual Directory
Below query detects DLL Hijacking scenario of planting a DLL having an invalid signature in a different folder and making an application load it instead of the original DLL.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Execution | |
| Stealth |
Telemetry coverage
Rule body
// Author: Cyb3rMonk(https://twitter.com/Cyb3rMonk, https://mergene.medium.com)
// Link to original post: https://posts.bluraven.io/detecting-dll-hijacking-attacks-part-1-bdb354685164
// Description: This query detects DLL Hijacking scenario of planting a DLL having an invalid signature in a different
// folder and making an application load it instead of the original DLL.
//
// Query parameters:
let WinDevices = materialize (
DeviceInfo
| where Timestamp > ago(8d)
| where OSPlatform startswith_cs "Windows"
| summarize make_set(DeviceId)
)
;
// Get Filenames that have more than 1 SHA1 and loaded by the same process
let FileNames = materialize (
DeviceImageLoadEvents
| where Timestamp > ago(8d)
| where DeviceId in (WinDevices)
| where isnotempty(SHA1) and isnotempty(InitiatingProcessFileName)
| project FileName = tolower(FileName), SHA1, Process = tolower(InitiatingProcessFileName), DLLDir = tolower(tostring(parse_path(FolderPath).DirectoryPath)), ProcessDir = tolower(tostring(parse_path(InitiatingProcessFolderPath).DirectoryPath))
| where DLLDir in ("c:\\windows", "c:\\windows\\system32", "c:\\windows\\syswow64", "c:\\windows\\winsxs") or (not(DLLDir startswith "c:\\windows"))
| where ProcessDir in ("c:\\windows", "c:\\windows\\system32", "c:\\windows\\syswow64", "c:\\windows\\winsxs") or ProcessDir has_all ("Users","AppData") or ProcessDir has_any ("Program Files")
| summarize hint.strategy=shuffle dcount(SHA1) by FileName, Process
| where dcount_SHA1 > 1
)
;
// From the Filenames, get SHA1 values and filter Filename-SHA1 if its loaded by a few proecsses and on a few devices
// Also, get first and last time of load of the file(based on SHA1, filename)
let Files = materialize (
FileNames
| join hint.strategy=shuffle kind=rightsemi (
DeviceImageLoadEvents
| where Timestamp > ago(8d)
| where DeviceId in (WinDevices)
| project Timestamp, DeviceName, FileName = tolower(FileName), SHA1, Process = tolower(InitiatingProcessFileName), DLLDir = tolower(tostring(parse_path(FolderPath).DirectoryPath)), ProcessDir = tolower(tostring(parse_path(InitiatingProcessFolderPath).DirectoryPath))
| where DLLDir in ("c:\\windows", "c:\\windows\\system32", "c:\\windows\\syswow64", "c:\\windows\\winsxs") or (not(DLLDir startswith "c:\\windows"))
| where ProcessDir in ("c:\\windows", "c:\\windows\\system32", "c:\\windows\\syswow64", "c:\\windows\\winsxs") or ProcessDir has_all ("Users","AppData") or ProcessDir has_any ("Program Files")
| summarize hint.strategy=shuffle dcount(DeviceName), Process = make_set(Process), FirstLoad = min(Timestamp), LastLoad = max(Timestamp), count() by FileName, SHA1
| where dcount_DeviceName < 3 and array_length(Process) < 3
| mv-expand Process to typeof(string)
)
on FileName, Process
)
;
// Files: Potentially suspicious files seen in the last 8d
// Next step: suspicious file (based on SHA1) should have been loaded from a location different than the other files(SHA1s) based on the same filename
// The suspicious file should have also been loaded recently
// Get files loaded in the last day (from the potentially suspicious files)
let Hashes = materialize (
Files
| where FirstLoad > ago(1d)
| join (
DeviceFileCertificateInfo
| where Timestamp > ago(30d)
| where not(IsTrusted )
| summarize arg_max(Timestamp,*) by SHA1
| project-away Timestamp, DeviceId, DeviceName) on SHA1
| project-away SHA11
)
;
// Get all image loads of the files that have the same name with the files in Hashes table(Hashes table only has the suspicious hash with its name from the last day)
Hashes
| join hint.strategy=shuffle kind=inner (
DeviceImageLoadEvents
| where Timestamp > ago(8d)
| where DeviceId in (WinDevices)
| project Timestamp, DeviceName, SHA1, FolderPath, FileName = tolower(FileName), Process = tolower(InitiatingProcessFileName), DLLDirectory = strcat(tolower(tostring(parse_path(FolderPath).DirectoryPath)), '\\'), ProcessDir = tolower(tostring(parse_path(InitiatingProcessFolderPath).DirectoryPath))
| where DLLDirectory in ("c:\\windows\\", "c:\\windows\\system32\\", "c:\\windows\\syswow64\\", "c:\\windows\\winsxs\\") or (not(DLLDirectory startswith "c:\\windows\\"))
| where ProcessDir in ("c:\\windows", "c:\\windows\\system32", "c:\\windows\\syswow64", "c:\\windows\\winsxs") or ProcessDir has_all ("Users","AppData") or ProcessDir has_any ("Program Files")
| extend NormalizedDLLDirectory = replace(@'(c|d):\\users\\.*?\\', @'c:\\users\\userxx\\',DLLDirectory)
| extend NormalizedDLLDirectory = replace(@'\{.*\}', @'\{xxxxxxxxxx\}',NormalizedDLLDirectory) //{fe07d7-d438-4dd9-bb0f-5721658f4f}
| extend NormalizedDLLDirectory = replace(@'\\[A-Za-z0-9-]+-[A-Za-z0-9]+\\', @'\\xxxxxxxxxx\\',NormalizedDLLDirectory ) //\fe07d7-d438-4dd9-bb0f-5721658f4f\
| extend NormalizedDLLDirectory = replace(@'\d+\.\d+\.\d+\.\d+', @'X.Y.Z.T',NormalizedDLLDirectory) // ex: Edge\Application\104.0.1293.47\process.exe
| extend NormalizedDLLDirectory = replace(@'-\d+\.\d+\.\d+', @'-X.Y.Z',NormalizedDLLDirectory)
| extend NormalizedDLLDirectory = replace(@'c:\\windows\\assembly\\nativeimages_v\d\.\d\.\d+_\d{2}\\.*', @'c:\\windows\\assembly\\nativeimages_vX.Y.Z_T\\oneoffewsubfolders\\', NormalizedDLLDirectory)
| extend NormalizedDLLDirectory = replace(@'c:\\programdata\\.*?\\microsoft\\teams\\',@'c:\\programdata\\userxxx\\microsoft\\teams\\',NormalizedDLLDirectory)
| summarize hint.strategy=shuffle by FileName, SHA1, NormalizedDLLDirectory
)
on FileName
| project-rename OtherSHA1 = SHA11
// Flag suspicious hash
| extend Suspicious = iff(SHA1==OtherSHA1, 'TRUE', 'FALSE')
// group properties of suspicious and previous files separately
// we are looking for a filename that was loaded from previously unknown location
| summarize hint.strategy=shuffle PreviousDirs = make_set_if(NormalizedDLLDirectory, Suspicious == 'FALSE'),
NewDir = make_set_if(NormalizedDLLDirectory, Suspicious == 'TRUE'),
PreviousSHA1s = make_set_if(OtherSHA1, Suspicious == 'FALSE'),
NewSHA1 = make_set_if(SHA1, Suspicious == 'TRUE')
by FileName
// compare the directory of the suspicous file with the previous directories
| extend diff = set_difference(NewDir, PreviousDirs)
// filter if the new(suspicious) file is loaded from previously known directory
| where diff != '[]'
| order by FileName
// if you get lots of false positives, uncomment the below section.
// this section compares the directory names in an alternative way.
// | mv-expand NewDir to typeof(string), PreviousDirs to typeof(string)
// | extend prev = split(PreviousDirs, '\\'), new = split(NewDir, '\\')
// | extend diff_new = set_difference(new, prev)
// | extend diff_count = array_length(diff_new)
// | where diff_count > 1
// | project-away prev, new, diff_count, diff
// get file profile info and filter based on global prevalence
| mv-expand NewSHA1 to typeof(string)
| invoke FileProfile(NewSHA1, 1000)
| where GlobalPrevalence < 200 or isempty(GlobalPrevalence)
Stages and Predicates
let FileNames, let Files and let Hashes are inlined into the numbered stages below.
Let binding: WinDevices
let WinDevices = materialize (
DeviceInfo
| where Timestamp > ago(8d)
| where OSPlatform startswith_cs "Windows"
| summarize make_set(DeviceId)
);
Stage 1: source
let WinDevices
Stage 2: source
let FileNames
Stage 3: source
let Files
Stage 4: source
let Hashes
Stage 5: source
DeviceImageLoadEvents
Stage 6: where
where Timestamp > ago(691200s)
Stage 7: where
where DeviceId =~ "WinDevices"
Stage 8: where
where isnotempty(InitiatingProcessFileName) and isnotempty(SHA1)
Stage 9: project
project DLLDir, FileName, Process, ProcessDir, SHA1
Stage 10: where
where (not (DLLDir startswith @"c:\windows") or DLLDir in~ (@"c:\windows", @"c:\windows\system32", @"c:\windows\syswow64", @"c:\windows\winsxs"))
Stage 11: where
where ((ProcessDir contains "AppData" and ProcessDir contains "Users") or ProcessDir in~ (@"c:\windows", @"c:\windows\system32", @"c:\windows\syswow64", @"c:\windows\winsxs") or ProcessDir contains "Program Files")
Stage 12: summarize
summarize by FileName, Process
Stage 13: where
where dcount_SHA1 > 1
Stage 14: join
join kind=rightsemi (DeviceImageLoadEvents) on FileName, Process
Stage 15: where
where FirstLoad > ago(86400s)
Stage 16: join
join (DeviceFileCertificateInfo) on SHA1
Stage 17: project-away
project-away SHA11
Stage 18: join
join kind=inner (DeviceImageLoadEvents) on FileName
Stage 19: project-rename
project-rename
Stage 20: extend
extend Suspicious
Suspicious =if
SHA1 =~ OtherSHA1'TRUE'else
'FALSE'Stage 21: summarize
summarize NewDir, NewSHA1, PreviousDirs, PreviousSHA1s by FileName
Stage 22: extend
extend diff
Stage 23: where
where diff !~ "[]"
Stage 24: sort
sort by FileName
Stage 25: mv-expand
mv-expand NewSHA1
Stage 26: invoke
invoke
Stage 27: where
where (isempty(GlobalPrevalence) or GlobalPrevalence < 200)
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DLLDir | in |
| field:"DLLDir" kind:in |
DLLDirectory | in |
| field:"DLLDirectory" kind:in |
DeviceId | in |
| field:"DeviceId" kind:in value:"WinDevices" |
GlobalPrevalence | is_null | field:"GlobalPrevalence" kind:is_null | |
GlobalPrevalence | lt |
| field:"GlobalPrevalence" kind:lt value:"200" |
InitiatingProcessFileName | is_not_null | field:"parent_process_name" kind:is_not_null | |
ProcessDir | in |
| field:"ProcessDir" kind:in |
ProcessDir | match |
| field:"ProcessDir" kind:match |
SHA1 | is_not_null | field:"sha1" kind:is_not_null | |
dcount_DeviceName | lt |
| field:"dcount_DeviceName" kind:lt value:"3" |
dcount_SHA1 | gt |
| field:"dcount_SHA1" kind:gt value:"1" |
diff | ne |
| field:"diff" kind:ne value:"[]" |
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
FileName | summarize |
NewDir | summarize |
NewSHA1 | summarize |
PreviousDirs | summarize |
PreviousSHA1s | summarize |
diff | extend |