Create your own
Lesson illustration

Configuring Native x64 Desktop Test Targets Securely

Good to see the debugger workspace in place. In the previous lesson, you separated WinDbg’s symbol, source, and image paths so that a future crash can be tied to the exact executable, PDB, and source tree that produced it. Now you will create those artifacts deliberately.

This lesson builds two native x64 Win32 desktop targets from one small C++ source file:

  • NativeLabPlain: modern baseline protections, but no CFG/EH continuation metadata and no Segment Heap request.
  • NativeLabHardened: adds CFG, EH continuation metadata, CET compatibility marking, and an embedded Segment Heap manifest.

They are not “secure” and “insecure” labels. They are controlled experimental profiles. Later, you will inspect their PE metadata, observe their heaps at runtime, and use the same fixed builds during crash and exploitation labs.


1. Treat a target build as an experiment specification

For exploitation research, “Debug x64” is not a sufficient target description. A meaningful build record identifies at least:

  1. Compiler and architecture — which compiler generated the code and whether it is truly native x64.
  2. Compiler settings — which checks and metadata generation were requested.
  3. Linker settings — which PE security properties and load-configuration data were emitted.
  4. Manifest settings — process-facing metadata such as requested privilege level and heap selection.
  5. Runtime environment — OS build, process-mitigation policy, and debugger state.

The first four are determined at build time; the fifth is determined when Windows creates the process. This distinction prevents a frequent analytical mistake:

A compiler or linker switch can make a binary eligible for a defense, but it does not by itself prove that the operating system enforced that defense for a particular process.

For example:

  • /guard:cf instruments eligible indirect transfers and causes the linker to emit Control Flow Guard metadata.
  • /CETCOMPAT marks the image as compatible with CET; it does not by itself activate a shadow stack.
  • A <heapType>SegmentHeap</heapType> manifest entry requests Segment Heap for the process default heap; it does not mean every allocation in every loaded component necessarily follows one uniform allocator path.
  • /GS adds stack-cookie protection where the compiler judges it applicable; it is not a general memory-safety guarantee.

For this course, both profiles retain core baseline linker properties:

PropertyWhy retain it in both profiles
/DYNAMICBASEKeeps ASLR enabled.
/HIGHENTROPYVAAllows higher-entropy ASLR for a 64-bit process.
/NXCOMPATDeclares compatibility with DEP.
/GSRetains compiler stack-cookie instrumentation where applicable.
/DEBUGProduces PDB information needed for precise lab analysis.

The hardened profile then changes only the variables that later lessons need to inspect:

SettingPlainHardenedBuild phase
/guard:cfNoYesCompiler and linker
/guard:ehcontNoYesCompiler and linker
/CETCOMPATNoYesLinker
Segment Heap requestNoYesEmbedded manifest
Requested execution levelasInvokerasInvokerEmbedded manifest

Keep both applications at asInvoker. Administrator access is appropriate for provisioning the isolated VM and selected diagnostic tools, but a desktop target should normally run unelevated. That preserves the ordinary user-mode boundary you want to study rather than quietly granting the target privileges it does not need.


2. Make compiler, architecture, and output locations explicit

Use CMake presets rather than relying on whichever configuration happened to be selected in an IDE window. The preset becomes a version-controlled statement of the intended compiler, target architecture, generator, and configuration.

Create this project layout under the source directory established in the previous lesson:

C:\Lab\Source\NativeLab\
  CMakeLists.txt
  CMakePresets.json
  src\
    NativeLab.cpp
  manifests\
    NativeLabPlain.manifest
    NativeLabHardened.manifest

Open an x64 Native Tools Command Prompt for Visual Studio. That environment matters: it makes cl.exe, link.exe, the Windows SDK tools, and x64 libraries available. A 64-bit host OS alone does not guarantee that a command-line build is targeting x64.

Read the relevant portions of Microsoft’s CMake preset guidance before creating the preset. It explains both the role of a preset’s architecture/toolset strategy and Visual Studio’s supported Segment Heap automation. We will use a manually embedded manifest here because it makes the per-target setting visible in the project itself; the Visual Studio script is a useful alternative for a larger CMake project.

Configure and build with CMake Presets | Microsoft Learn

Read Microsoft Learn’s CMake preset guidance to understand how architecture, compiler selection, and the Visual Studio Segment Heap helper are represented in a reproducible configuration.

In “Select your target and host architecture when building with the Microsoft C++ Build Tools,” read the full discussion of architecture and strategy choices. In “Edit presets” under “Select your compilers,” read the compiler-selection examples through the generator discussion. Then read “Enable Segment Heap,” including the allowlist and exclusion behavior. Focus especially on architecture strategy and the Segment Heap purpose. In this lesson, do not combine the Visual Studio-provided Segment Heap script with the manual manifest below: choose one manifest-generation mechanism per target.

Create CMakePresets.json:

{
  "version": 6,
  "configurePresets": [
    {
      "name": "msvc-x64-debug",
      "displayName": "MSVC x64 Debug",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/out/build/${presetName}",
      "architecture": {
        "value": "x64",
        "strategy": "external"
      },
      "environment": {
        "CC": "cl",
        "CXX": "cl"
      },
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug",
        "CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
      }
    }
  ],
  "buildPresets": [
    {
      "name": "msvc-x64-debug",
      "configurePreset": "msvc-x64-debug"
    }
  ]
}

A few details deserve attention:

  • CC and CXX state that the intended compiler is MSVC’s cl.exe. They are resolved during the first configure, so delete the CMake build directory if you intentionally change compilers later.
  • The x64 Native Tools environment supplies the actual x64 compiler and libraries. With the Ninja generator, architecture.strategy: "external" means that environment—not CMake’s generator platform option—provides that architecture setup.
  • CMAKE_BUILD_TYPE is explicit because Ninja is a single-configuration generator.
  • compile_commands.json is not required by WinDbg, but it gives other analysis tools an exact record of compiler invocations.

Now add CMakeLists.txt:

cmake_minimum_required(VERSION 3.24)

project(NativeLab LANGUAGES CXX)

if(NOT MSVC)
  message(FATAL_ERROR "This laboratory project requires MSVC.")
endif()

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# Explicitly select the dynamic MSVC runtime: /MDd for Debug.
set(CMAKE_MSVC_RUNTIME_LIBRARY
    "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")

set(LAB_OUTPUT_DIR "C:/Lab/Targets/NativeLab/x64/Debug")

function(configure_native_lab_target target_name manifest_name)
  add_executable(${target_name} WIN32 src/NativeLab.cpp)

  target_compile_definitions(${target_name} PRIVATE
    UNICODE
    _UNICODE
    WIN32_LEAN_AND_MEAN
  )

  target_compile_options(${target_name} PRIVATE
    /W4
    /permissive-
    /EHsc
    /GS
    $<$<CONFIG:Debug>:/Od>
    $<$<CONFIG:Debug>:/Zi>
  )

  target_link_options(${target_name} PRIVATE
    /DEBUG
    /INCREMENTAL:NO
    /DYNAMICBASE
    /HIGHENTROPYVA
    /NXCOMPAT
    /MANIFEST:EMBED
    "/MANIFESTINPUT:${CMAKE_CURRENT_SOURCE_DIR}/manifests/${manifest_name}"
  )

  set_target_properties(${target_name} PROPERTIES
    RUNTIME_OUTPUT_DIRECTORY_DEBUG "${LAB_OUTPUT_DIR}"
    PDB_OUTPUT_DIRECTORY_DEBUG "${LAB_OUTPUT_DIR}"
    COMPILE_PDB_OUTPUT_DIRECTORY_DEBUG
      "${LAB_OUTPUT_DIR}/objpdb/${target_name}"
  )
endfunction()

configure_native_lab_target(
  NativeLabPlain
  NativeLabPlain.manifest
)

configure_native_lab_target(
  NativeLabHardened
  NativeLabHardened.manifest
)

target_compile_options(NativeLabHardened PRIVATE
  /guard:cf
  /guard:ehcont
)

target_link_options(NativeLabHardened PRIVATE
  /guard:cf
  /guard:ehcont
  /CETCOMPAT
)

This structure prevents accidental profile drift. Shared baseline options live in one function; only the hardened target receives the additional control-flow and compatibility settings.

Notice that /INCREMENTAL:NO is explicit even for a Debug profile. It is not a security mitigation. Its role here is operational: a nonincremental link produces a simpler, more consistent build artifact for debugger and binary-comparison work. It does not remove ASLR, does not make addresses fixed across launches, and does not eliminate the need to record the exact build.


3. Build a small desktop target that actually allocates memory

Add src\NativeLab.cpp:

#include <windows.h>

#include <cstdint>
#include <memory>

struct LabRecord {
    std::uint64_t marker;
    wchar_t label[64];
};

int WINAPI wWinMain(
    HINSTANCE,
    HINSTANCE,
    PWSTR,
    int)
{
    auto record = std::make_unique<LabRecord>();
    record->marker = 0x4C41424E41544956ULL; // "VITANBAL" as bytes

    auto* process_heap_buffer = static_cast<unsigned char*>(
        HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 0x180));

    if (process_heap_buffer == nullptr) {
        return 1;
    }

    process_heap_buffer[0] = 0x41;
    process_heap_buffer[0x17F] = 0x5A;

    MessageBoxW(
        nullptr,
        L"NativeLab allocation baseline completed.",
        L"NativeLab",
        MB_OK | MB_ICONINFORMATION);

    HeapFree(GetProcessHeap(), 0, process_heap_buffer);
    return 0;
}

This is intentionally unremarkable. It gives you two useful allocation forms without introducing a vulnerability:

  • std::make_unique<LabRecord>() creates a C++ object allocation.
  • HeapAlloc(GetProcessHeap(), ...) visibly invokes the process heap API with a known request size, bytes.

The later heap lessons will distinguish allocator metadata from the application bytes in these allocations and measure placement behavior under the hardened target’s Segment Heap request. For now, the target simply gives the build an observable, repeatable allocation workload.


4. Embed the manifest rather than leaving heap choice implicit

An application manifest is loader-visible metadata. It belongs in the executable rather than existing as an undocumented machine-local setting or debugger convenience.

The Microsoft documentation is particularly important on two points: Segment Heap is selected by a heapType application-manifest element on supported Windows versions, and the manifest should preferably be embedded in the executable resource.

Application manifests - Win32 apps - Microsoft Learn

Read Microsoft Learn’s application-manifest reference to connect the XML below to Windows loader behavior, embedding conventions, and privilege declarations.

Read the “File location” and “File name” sections first, then the complete “heapType” subsection and the “trustInfo” subsection. The key platform constraint is stated in the heapType description; Windows Server 2022 satisfies this OS-version requirement. In “trustInfo,” read the table of asInvoker, requireAdministrator, and highestAvailable values, with particular attention to execution-level intent.

Create manifests\NativeLabPlain.manifest:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly
    xmlns="urn:schemas-microsoft-com:asm.v1"
    manifestVersion="1.0">

  <assemblyIdentity
      version="1.0.0.0"
      processorArchitecture="amd64"
      name="Grasp.NativeLabPlain"
      type="win32" />

  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
    <security>
      <requestedPrivileges
          xmlns="urn:schemas-microsoft-com:asm.v3">
        <requestedExecutionLevel
            level="asInvoker"
            uiAccess="false" />
      </requestedPrivileges>
    </security>
  </trustInfo>
</assembly>

Then create manifests\NativeLabHardened.manifest:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly
    xmlns="urn:schemas-microsoft-com:asm.v1"
    manifestVersion="1.0"
    xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">

  <assemblyIdentity
      version="1.0.0.0"
      processorArchitecture="amd64"
      name="Grasp.NativeLabHardened"
      type="win32" />

  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
    <security>
      <requestedPrivileges
          xmlns="urn:schemas-microsoft-com:asm.v3">
        <requestedExecutionLevel
            level="asInvoker"
            uiAccess="false" />
      </requestedPrivileges>
    </security>
  </trustInfo>

  <asmv3:application>
    <asmv3:windowsSettings
        xmlns="http://schemas.microsoft.com/SMI/2020/WindowsSettings">
      <heapType>SegmentHeap</heapType>
    </asmv3:windowsSettings>
  </asmv3:application>
</assembly>

The hardened manifest’s heapType element is the relevant difference. The linker’s /MANIFEST:EMBED and /MANIFESTINPUT: options incorporate that file into the executable. A sidecar .manifest file may be useful during troubleshooting, but the embedded resource is the artifact you should preserve and inspect.

Visual Studio’s Manifest Tool property page with “Enable Segment Heap” set to “Yes.” This IDE setting produces the same category of embedded manifest request that the `NativeLabHardened.manifest` file expresses explicitly in the CMake project.

If you later build a conventional .vcxproj rather than a CMake project, the Visual Studio property shown above is a legitimate way to enable the setting. Do not enable it in both the project property page and an additional hand-authored manifest without first inspecting the final embedded manifest. The goal is one unambiguous configuration, not duplicated XML fragments.


5. Configure, build, and validate only what this lesson established

From the x64 Native Tools Command Prompt, run:

cd /d C:\Lab\Source\NativeLab
cmake --preset msvc-x64-debug
cmake --build --preset msvc-x64-debug

The expected outputs are:

C:\Lab\Targets\NativeLab\x64\Debug\NativeLabPlain.exe
C:\Lab\Targets\NativeLab\x64\Debug\NativeLabPlain.pdb
C:\Lab\Targets\NativeLab\x64\Debug\NativeLabHardened.exe
C:\Lab\Targets\NativeLab\x64\Debug\NativeLabHardened.pdb

First, confirm that the files exist and record hashes:

Get-ChildItem C:\Lab\Targets\NativeLab\x64\Debug

Get-FileHash `
  C:\Lab\Targets\NativeLab\x64\Debug\NativeLabPlain.exe,
  C:\Lab\Targets\NativeLab\x64\Debug\NativeLabHardened.exe `
  -Algorithm SHA256

Second, confirm that each file is a 64-bit PE. From the same developer command environment:

dumpbin /headers C:\Lab\Targets\NativeLab\x64\Debug\NativeLabPlain.exe
dumpbin /headers C:\Lab\Targets\NativeLab\x64\Debug\NativeLabHardened.exe

At this stage, check only these facts:

  • FILE HEADER VALUES identifies an AMD64 machine.
  • OPTIONAL HEADER VALUES lists Dynamic base, High Entropy VA, and NX compatible for both executables.
  • The hardened build reports the extra Guard/CET-related characteristics expected from its linker options.

Do not try to infer full exploitability from this output. The next lesson will make a disciplined comparison of PE security properties and distinguish header flags from load-configuration details.

Finally, extract the embedded manifests with the Windows SDK Manifest Tool:

mt.exe -inputresource:"C:\Lab\Targets\NativeLab\x64\Debug\NativeLabPlain.exe;#1" -out:Plain.extracted.manifest

mt.exe -inputresource:"C:\Lab\Targets\NativeLab\x64\Debug\NativeLabHardened.exe;#1" -out:Hardened.extracted.manifest

Open the extracted files in a text editor. The hardened version must contain:

<heapType>SegmentHeap</heapType>

The plain version must not. This is a direct build-artifact check, not an assumption based on a CMake file or an IDE checkbox.

Because the PDBs are now beside the executables in the directory added to WinDbg’s symbol path last lesson, perform a short debugger validation:

.reload /f NativeLabHardened.exe
lmvm NativeLabHardened
x NativeLabHardened!wWinMain

lmvm should identify matching private symbols, and the symbol lookup should resolve wWinMain. If it does not, stop and resolve that mismatch before proceeding to later crash-analysis work. A build that runs but lacks the matching PDB is not yet a complete lab target.

Add the following to your lab record:

Target source revision: <commit or archive identifier>
Compiler environment: MSVC cl.exe, x64 Native Tools environment
CMake preset: msvc-x64-debug
Build configuration: Debug
Plain EXE SHA256: <value>
Hardened EXE SHA256: <value>
Plain manifest: embedded, asInvoker, no heapType
Hardened manifest: embedded, asInvoker, heapType SegmentHeap
PDB validation: NativeLabPlain / NativeLabHardened private symbols loaded

Key takeaways

A modern exploit-development target is not defined merely by its C++ source. Its behavior depends on a specific chain: compiler instrumentation, linker-emitted PE metadata, embedded manifest settings, and runtime process policy.

You now have two native x64 desktop targets with fixed, inspectable differences:

  • Both preserve ASLR, DEP compatibility, stack cookies, debug symbols, and an unelevated asInvoker execution level.
  • NativeLabHardened additionally requests CFG and EH continuation instrumentation, CET compatibility, and Segment Heap.
  • The executable and matching PDB are placed in the controlled directories already configured in WinDbg.

Next, you will compare the two builds using linker output and PE-inspection tools, turning these declared build settings into concrete properties visible in the resulting binaries.

Can't find a good explanation? Sign up and we'll make it for you

Sign up