Hunting for unsigned drivers, like Printer Nightmare
By Gianni Castaldi
It has been busy times, and I have not written much lately.
So, I have had some time to think about new detections. And while there are enough blog posts and news articles about Printer Nightmare, I decided to do a small one. This blog post intends to be reusable, so we will not go in-depth about how point and print works, but we will create a hunting query to detect this attack.
The query will consist of 4 steps:
- Detect all driver loads by the print spooler
- Get the distinct SHA1 hashes and use the FileProfile function
- Join the driver loads with the FileProfile data
- Filter out data we do not want to see
Detect all driver loads by the print spooler
Microsoft Defender for Endpoint does a great job in recording all the activity on a system, and when you have a use case, you can also easily onboard all the activity to Azure Sentinel, so this query can be run at both places. In this case, we will be looking at the table: DeviceImageLoadEvents. That table contains all the loaded DLLs, and we want to combine them with loading from the print spooler spoolsv.exe
let DriverLoads = DeviceImageLoadEvents | where InitiatingProcessFileName == "spoolsv.exe"; DriverLoads
Get the distinct SHA1 hashes and use the FileProfile function
Now that we have all the driver loads the next step is getting the SHA1 hashes, to verify enrich them with the FileProfile function.
let DistinctFiles = DriverLoads | distinct SHA1 | invoke FileProfile(); DistinctFiles
The FileProfile function returns the following data:
Join the driver loads with the FileProfile data
The next step would be to enrich the DriverLoads with the FileProfile data. To do this we will perform an inner join on the SHA1 hash, as we have learned in the Kusto Gym, we will use the inner join because 1 result from the FileProfile data can be matched to multiple lines in the DriverLoads data.
DriverLoads | join kind=inner DistinctFiles on SHA1
Filter out data we do not want to see
The FileProfile function returns a lot of useful information, so now it is time to tune the information so it will not generate a lot of false positives. I’ve tested the query against several Microsoft Defender for Endpoint tenants and found that the following filters gave zero false positives:
| where SignatureState != "SignedValid" | where GlobalPrevalence < 40
But we could also use the filter ThreatName to see if there are any known threats
| where isnotempty(ThreatName)
Another great way is to use the GlobalFirstSeen, because that works with the Microsoft Cloud dates and not the NTFS file dates.
| where GlobalFirstSeen > ago(30d)
To wrap this blog post up. we collected the print spooler service DLL loads, compared them against the data of Microsoft Defender for Endpoint, and filtered out what we do not want to see. This way we were able to find the Printer Nightmare attack. During tests, I was also able to find all new (Global first seen of 30 days) DLLs from 5k devices, loaded in the last 24 hours without a timeout. We can have a lot of fun with this query, during hunts or in detections. And like I wrote earlier we can also import the table into Azure Sentinel to have greater retention.
I have posted the complete query on GitHub so you can find it easily after reading the blog.
Thanks for reading and if you have any questions or ideas for a blog post let me know.