Detect DLL Hijacking techniques from HijackLibs with Splunk
What is DLL Hijacking?
DLL Hijacking manipulates a trusted application into executing an unauthorized DLL. Antivirus and EDR solutions may not automatically detect this deceptive activity. Additionally, application whitelisting solutions like AppLocker might not prevent the rogue code from running. Numerous threat actors have been documented using this technique to meet their goals.
HijackLibs
HijackLibs project provides an curated list of DLL Hijacking candidates. A mapping between DLLs and vulnerable executables is kept and can be searched via this website. Additionally, further metadata such as resources provide more context, this can be invaluable for both defensive teams looking to detect hijacking techniques and red teamers seeking to identify exploitable DLLs.
HijackLibs example:
https://github.com/wietze/HijackLibs/blob/main/yml/3rd_party/python/python39.yml
---
Name: python39.dll
Author: Wietze Beukema
Created: 2022-09-26
Vendor: Python
ExpectedLocations:
- '%PROGRAMFILES%\Python39'
- '%LOCALAPPDATA%\Temp\%VERSION%'
- '%PROGRAMFILES%\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\VC\SecurityIssueAnalysis\python'
- '%USERPROFILE%\anaconda3'
VulnerableExecutables:
- Path: 'python39.exe'
Type: Sideloading
Resources:
- https://twitter.com/SBousseaden/status/1530595156055011330
Acknowledgements:
- Name: Samir
Twitter: '@sbousseaden'This yaml file indicate that the executable python39.exe is vulnerable to DLL Sideloading, python39.exeis expected to load python39.dll from one of these directories:
%PROGAMFILES%\Python39%LOCALAPPDATA%\Temp\%VERSION%%PROGRAMFILES%\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\VC\SecurityIssueAnalysis\python%USERPROFILE%\anaconda3
Any instance of python39.exe loading python39.dll from an unauthorized or unexpected location should be considered a potential exploitation target and warrants immediate investigation.
DLL Hijacking Detection with Splunk
We will focus on detecting Environment Variable-based DLL Hijacking and Side-loading techniques
Sigma rules are available for each vulnerable executable, but I won’t be creating hundreds of detection rules for this. That’s the beauty of Splunk: we can use lookup files. With just one detection rule, we’ll be able to detect all the techniques by using a lookup file that contains them all !
Lookup creation
First, we need to clone the GitHub repo that contains all the YAML files (466 YAML files as of October 1, 2023). Then we’ll use a Python script to retrieve all the information we need for detection, essentially converting the 466 YAML files into a single CSV file.
The python updated script and the csv file are available on Github: https://github.com/mthcht/awesome-lists/tree/main/Lists/Hijacklibs
import os
import csv
import yaml
import glob
# Prerequisite: the github project https://github.com/mthcht/HijackLibs must be cloned in the current directory
# If in your environment, some workstations have a different drive letter (other than C:) you may want to modify this script accordingly
# Initialize CSV file
csv_columns = ['file_name', 'expected_file_path', 'vulnerable_file_name', 'file_type', 'file_hash', 'link', 'hijacklib_link']
csv_file = "hijacklibs_list.csv"
def replace_variables(path):
path = path.replace("%LOCALAPPDATA%", 'C:\\Users\\*\\AppData\\Local')
path = path.replace("%PROGRAMFILES%", 'C:\\Program Files')
path = path.replace("%VERSION%", '*')
path = path.replace("%SYSTEM32%", 'C:\\Windows\\System32')
path = path.replace("%SYSWOW64%", 'C:\\Windows\\SysWOW64')
path = path.replace("%USERPROFILE%", 'C:\\Users\\*')
path = path.replace("%WINDIR%", 'C:\\Windows')
path = path.replace("%PROGRAMDATA%", 'C:\\ProgramData')
return path
with open(csv_file, 'w', newline='') as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=csv_columns)
writer.writeheader()
glob_pattern = os.path.join('.', 'HijackLibs-main', 'yml', '**', '*.yml').replace(os.sep, '/')
# Loop through each YAML file recursively in the 'yml' directory
for filename in glob.glob(glob_pattern, recursive=True):
print(f"Processing {filename}") # Debug print
with open(filename, 'r') as file:
data = yaml.safe_load(file)
print(f"Data: {data}") # Debug print
relative_path = os.path.relpath(filename, os.path.join('.', 'HijackLibs-main', 'yml'))
hijacklib_link = os.path.join('HijackLibs', 'yml', relative_path).replace(os.sep, '/')
replaced_paths = [replace_variables(loc) for loc in data.get('ExpectedLocations', [])]
final_paths = [path + ('*' if not path.endswith('*') else '') for path in replaced_paths]
# Build the row for CSV
csv_row = {
'file_name': data.get('Name', ''),
'expected_file_path': ";".join(final_paths),
'vulnerable_file_name': replace_variables(data.get('VulnerableExecutables', [])[0].get('Path', '')) if data.get('VulnerableExecutables', []) else '',
'file_type': data.get('VulnerableExecutables', [])[0].get('Type', '') if data.get('VulnerableExecutables', []) else '',
'file_hash': ";".join(data.get('VulnerableExecutables', [])[0].get('SHA256', [])) if data.get('VulnerableExecutables', []) else '',
'link': ";".join(data.get('Resources', [])),
'hijacklib_link': hijacklib_link
}
writer.writerow(csv_row)As you saw earlier, each location is indicated by a Windows environment variable. We can’t use those in our detection, so I’ve converted them using the replace_variables function in the script (I used the default drive letter "C:" in the paths, but some companies change it, so you may want to adjust that if it applies to you)
I've also added wildcards at the end of each expected path to match them for the Splunk detection rule.
Execute the script in the same directory as the cloned GitHub repo, and you’ll get the generated CSV lookup hijacklibs_list.csv with all the information we need. I recommend automating the generation and deployment of the lookup to your Splunk.
- Upload the lookup on your SIEM
- Create a definition lookup for our lookup (and remove the case sensitive match)
Splunk Detection
Simple Hunt for files in unexpected locations
`wineventlog` file_name=* file_path=*
| lookup hijacklibs_list file_name as file_name OUTPUT expected_file_path file_name as metadata_file_name file_hash as metadata_file_hash vulnerable_file_name as metadata_vulnerable_file_name file_type as metadata_file_type link as metadata_link hijacklib_link as metadata_hijacklib_link
| where isnotnull(expected_file_path)
| rex field=file_path "^(?<real_file_path>.*\\\\)([^\\\\]+)$"
| makemv delim=";" expected_file_path
| mvexpand expected_file_path
| search NOT
[| inputlookup hijacklibs_list
| eval expected_file_path=split(expected_file_path, ";")
| mvexpand expected_file_path
| table expected_file_path file_name
| rename expected_file_path as real_file_path]
| bucket _time as time span=1h
| stats
values(metadata_file_name)
values(metadata_file_hash)
values(metadata_vulnerable_file_name)
values(metadata_file_type)
values(metadata_link)
values(metadata_hijacklib_link)
earliest(_time) as firsttime
latest(_time) as lasttime
count by file_name real_file_path expected_file_path file_path time dest_nt_host
| rename values(*) as *
| fields - timeHunt for loaded image in unexpected location
Example with Sysmon EventID 7 (image loaded)
`wineventlog` signature_id=7 loaded_file=*
| lookup hijacklibs_list file_name as loaded_file OUTPUT expected_file_path file_name as metadata_file_name file_hash as metadata_file_hash vulnerable_file_name as metadata_vulnerable_file_name file_type as metadata_file_type link as metadata_link hijacklib_link as metadata_hijacklib_link
| where isnotnull(expected_file_path)
| rex field=ImageLoaded "^(?<real_file_path>.*\\\\)([^\\\\]+)$"
| makemv delim=";" expected_file_path
| mvexpand expected_file_path
| search NOT
[| inputlookup hijacklibs_list
| eval expected_file_path=split(expected_file_path, ";")
| mvexpand expected_file_path
| table expected_file_path file_name
| rename expected_file_path as real_file_path, file_name as loaded_file]
| bucket _time as time span=1h
| stats
values(metadata_file_name)
values(metadata_file_hash)
values(metadata_vulnerable_file_name)
values(metadata_file_type)
values(metadata_link)
values(metadata_hijacklib_link)
earliest(_time) as firsttime
latest(_time) as lasttime
count by expected_file_path real_file_path loaded_file ImageLoaded time dest_nt_host
| rename values(*) as *
| fields - timeFor these two searches, we look for file names in the logs that match those in our lookup. We then filter based on these matches and compare the full paths of the files found in the logs to the expected paths in our lookup. If a file path doesn’t match any of the expected paths, it will be flagged!
We assume that the fields file_path or ImageLoaded contain the full path, including the file name. This is usually the case with default Splunk parsing, which is why I had to create the real_file_path field for an accurate comparison with the expected paths. (but if you parsed it already in another field, directly use your parsed field)
Hunt for the presence of vulnerable executable hashes
`wineventlog` file_hash=*
| lookup hijacklibs_list file_hash as file_hash OUTPUT expected_file_path file_name as metadata_file_name file_hash as metadata_file_hash vulnerable_file_name as metadata_vulnerable_file_name file_type as metadata_file_type link as metadata_link hijacklib_link as metadata_hijacklib_link
| where isnotnull(file_hash)
| stats
values(metadata_file_name)
values(metadata_file_hash)
values(metadata_vulnerable_file_name)
values(metadata_file_type)
values(metadata_link)
values(metadata_hijacklib_link)
values(file_name)
values(file_path)
values(expected_file_path)
earliest(_time) as firsttime
latest(_time) as lasttime
count by _time dest_nt_host
| rename values(*) as *You can also combine this search with the unexpected DLL location to search for both the vulnerable executable hash and potential exploitation for a more accurate detection rule (Note that many entries in the lookup don’t have a hash specified, so you might miss out lots of matches.)
Example with the Sysmon EventID 7
`wineventlog` signature_id=7 loaded_file=*
| lookup hijacklibs_list file_name as loaded_file file_hash as file_hash OUTPUT expected_file_path file_name as metadata_file_name file_hash as metadata_file_hash vulnerable_file_name as metadata_vulnerable_file_name file_type as metadata_file_type link as metadata_link hijacklib_link as metadata_hijacklib_link
| where isnotnull(expected_file_path) AND isnotnull(metadata_file_hash)
| rex field=ImageLoaded "^(?<real_file_path>.*\\\\)([^\\\\]+)$"
| makemv delim=";" expected_file_path
| mvexpand expected_file_path
| search NOT
[| inputlookup hijacklibs_list
| eval expected_file_path=split(expected_file_path, ";")
| mvexpand expected_file_path
| table expected_file_path file_name
| rename expected_file_path as real_file_path, file_name as loaded_file]
| bucket _time as time span=1h
| stats
values(metadata_file_name)
values(metadata_file_hash)
values(metadata_vulnerable_file_name)
values(metadata_file_type)
values(metadata_link)
values(metadata_hijacklib_link)
earliest(_time) as firsttime
latest(_time) as lasttime
count by file_hash expected_file_path real_file_path loaded_file ImageLoaded time dest_nt_host
| rename values(*) as *
| fields - timeThat’s it, Happy Hunting !

