DEP and NX Always Enforced for 64-bit Windows Processes

There is no any way to get an executable stack or heap (without VirtualProtect) in a 64-bit program on Windows 11. I went about trying to disable DEP and NX on an executable before I knew that. Spoiler, it does not work.

If you want to follow along you'll need Visual Studio or the Visual Studio Build Tools installed for the the developer command prompt.

The Background

Porting some (not affiliated) pwn.college shellcode exercises to Windows right now. These challenges take some shellcode, place it in a stack variable, and run it. I initially thought the linker option /NXCOMPAT:NO would give an executable stack. When it did not I hit it with the old VirtualProtect, made the memory EXECUTE_READWRITE, and moved on with my life.

Eventually I got curious whether I could get an executable stack and dove a little deeper. That also led to questions about an executable heap. It turns out that DEP is always enforced on 64-bit Windows no matter what.

The System

Some documentation talked about the system DEP policy, so I started there. You need to run Command Prompt as an Administrator then check with bcdedit.

C:\Windows\System32>bcdedit

Windows Boot Manager
--------------------
identifier              {bootmgr}
device                  partition=\Device\HarddiskVolume1
path                    \EFI\Microsoft\Boot\bootmgfw.efi
description             Windows Boot Manager
locale                  en-US
inherit                 {globalsettings}
default                 {current}
resumeobject            {9378e909-ec67-49fd-ab7b-46c2525300e3}
displayorder            {current}
toolsdisplayorder       {memdiag}
timeout                 30

Windows Boot Loader
-------------------
identifier              {current}
device                  partition=C:
path                    \Windows\system32\winload.efi
description             Windows 11
locale                  en-US
inherit                 {bootloadersettings}
recoverysequence        {9378e909-ec67-49fd-ab7b-46c2525300e3}
displaymessageoverride  Recovery
recoveryenabled         Yes
isolatedcontext         Yes
allowedinmemorysettings 0x15000075
osdevice                partition=C:
systemroot              \Windows
resumeobject            {9378e909-ec67-49fd-ab7b-46c2525300e3}
nx                      OptIn
bootmenupolicy          Standard
            

Note the nx OptIn line. Using bcdedit /set nx OptOut works. Due to Secure Boot through, I cannot set it to AlwaysOff. Secure Boot ruins all the fun.

C:\Windows\System32>bcdedit /set nx alwaysoff
An error has occurred setting the element data.
The value is protected by Secure Boot policy and cannot be modified or deleted.
            

Might as well switch Secure Boot off in BIOS and set NX to AlwaysOff. This requires a couple of reboots.

C:\Windows\System32>bcdedit /set nx alwaysoff
The operation completed successfully.
            

There is also the Exploit Protections menu. Since Windows messed around with Control Panel (perfect) and added Settings (awful), which is kind of like Control Panel Lite, who knows what this changes. Let's set DEP to off though.

I also added a program specific override.

The Realization

What clued me off that this might just be impossible is when I came across the API SetProcessDEPPolicy. The documentation reads: If the DEP system policy is OptIn or OptOut and DEP is enabled for the process, setting dwFlags to 0 disables DEP for the process. Let me tell you though, it does not.

Once I read those docs in more detail, I saw If this function is called on a 64-bit process, it fails with ERROR_NOT_SUPPORTED.

Once I read that I started to believe my efforts had been in vain (they had), so I went searching a little more. This support article states In 64-bit versions of Windows, hardware-enforced DEP is always enabled for 64-bit native programs. However, depending on your configuration, hardware-enforced DEP may be disabled for 32-bit programs. So it looks like all the DEP and NX options only apply to 32-bit processes.

Test Program

First, I would like to apologize. I just started this company and cannot afford syntax highlighting at this time.

This program queries a pointer for its memory protections and prints them out. It declares a stack pointer and a heap pointer and sets them up with a nop sled (0x90) and return (0xC3). Note, x86/64 opcodes, will not work on ARM. The program then prints out their memory protections.

Following that, we crash our program by casting our memory regions as function pointers and calling them. The commented out VirtualProtect calls are there so you can verify that these do run when the stack and heap regions are EXECUTE_READWRITE. Finally, there is a cute little print statement so we can see if the regions ran to completion.

#include <windows.h>
#include <stdio.h>

void PrintMemoryProtection(DWORD protect) {
    switch (protect) {
        case PAGE_EXECUTE:           printf("Execute\n"); break;
        case PAGE_EXECUTE_READ:      printf("Execute, Read\n"); break;
        case PAGE_EXECUTE_READWRITE: printf("Execute, Read, Write\n"); break;
        case PAGE_EXECUTE_WRITECOPY: printf("Execute, Write Copy\n"); break;
        case PAGE_NOACCESS:          printf("No Access\n"); break;
        case PAGE_READONLY:          printf("Read Only\n"); break;
        case PAGE_READWRITE:         printf("Read, Write\n"); break;
        case PAGE_WRITECOPY:         printf("Write Copy\n"); break;
        case PAGE_GUARD:             printf("Guard Page\n"); break;
        case PAGE_NOCACHE:           printf("No Cache\n"); break;
        case PAGE_WRITECOMBINE:      printf("Write Combine\n"); break;
        default:                     printf("Unknown protection\n"); break;
    }
}

int main() {
    // Not applicable, 32-bit only
    SetProcessDEPPolicy(0);
    
    MEMORY_BASIC_INFORMATION mbi;
    BYTE stack_ptr[0x100];
    PBYTE heap_ptr = (PBYTE)malloc(0x100);

    for (int i=0; i<0x100; i++) {
        stack_ptr[i] = 0x90; // nop
    }
    stack_ptr[0xFF] = 0xC3; // ret

    for (int i=0; i<0x100; i++) {
        heap_ptr[i] = 0x90; // nop
    }
    heap_ptr[0xFF] = 0xC3; // ret

    if (VirtualQuery(stack_ptr, &mbi, sizeof(mbi)) != 0) {
        printf("Memory Protection of %p (stack): ", stack_ptr);
        PrintMemoryProtection(mbi.Protect);
    } else {
        printf("Error getting memory protection\n");
    }

    if (VirtualQuery(heap_ptr, &mbi, sizeof(mbi)) != 0) {
        printf("Memory Protection of %p (heap):  ", heap_ptr);
        PrintMemoryProtection(mbi.Protect);
    } else {
        printf("Error getting memory protection\n");
    }

    DWORD oldProtect;
    // VirtualProtect(stack_ptr, 0x100, PAGE_EXECUTE_READWRITE, &oldProtect);
    ((void (*)())stack_ptr)();

    // VirtualProtect(heap_ptr, 0x100, PAGE_EXECUTE_READWRITE, &oldProtect);
    ((void (*)())heap_ptr)();

    printf("ExeCUTEd!\n");

    free(heap_ptr);

    return 0;
}
            

We will be compiling with cl test.c /link /NXCOMPAT:NO /DYNAMICBASE:NO /HIGHENTROPYVA:NO. Turning off ASLR does work for 64-bit processes.

The Results

First, if we compile a plain executable with cl test.c, then run dumpbin /HEADERS text.exe we see the following DLL Characteristics.

    8160 DLL characteristics
        High Entropy Virtual Addresses
        Dynamic base
        NX compatible
        Terminal Server Aware
            

Recompiling without NX compatibility and ASLR like so, cl test.c /link /NXCOMPAT:NO /DYNAMICBASE:NO /HIGHENTROPYVA:NO, we can see that the the compiler recognizes them.

    C:\Users\user\Documents\Development\pwncollege-win\shellcode-injection>cl test.c /link /NXCOMPAT:NO /DYNAMICBASE:NO /HIGHENTROPYVA:NO
    Microsoft (R) C/C++ Optimizing Compiler Version 19.36.32537 for x64
    Copyright (C) Microsoft Corporation.  All rights reserved.

    test.c
    Microsoft (R) Incremental Linker Version 14.36.32537.0
    Copyright (C) Microsoft Corporation.  All rights reserved.

    /out:test.exe
    /NXCOMPAT:NO
    /DYNAMICBASE:NO
    /HIGHENTROPYVA:NO
    test.obj
            

That is confirmed with another dumpbin /HEADERS test.exe

    8000 DLL characteristics
        Terminal Server Aware
            

The moment everyone has been waiting for! We run the program... and it crashes.

    C:\Users\user\Documents\Development\pwncollege-win\shellcode-injection>test.exe
    Memory Protection of 000000000014FDD0 (stack): Read, Write
    Memory Protection of 00000000004A4080 (heap):  Read, Write

    C:\Users\user\Documents\Development\pwncollege-win\shellcode-injection>echo %errorlevel%
    -1073741819
            

The memory is still only RW and we do not reach our post-call print statement. That error code is our dear friend STATUS_ACCESS_VIOLATION 0xc0000005. We can comment and uncomment the different run stack, run heap, and virtual protect combinations to validate we are not slowly losing our mind.

The End

Overall a negative result for what I was attempting, but a positive result for security. It took a decent amount of fiddling with settings before I found the doc that explained that this was just the way of the world now.

While it makes it impossible to develop an educational 64-bit stack smash on Windows, it is for the best. This makes our computers more secure and our bugs more valuable.