daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

kernel-race-condition-object-manager-slowdown.md (22766B)


      1 ---
      2 title: "Kernel Race Condition Exploitation via Object Manager Slow Paths"
      3 section: "Windows"
      4 sectionSlug: "windows-hardening"
      5 sourcePath: "src/windows-hardening/windows-local-privilege-escalation/kernel-race-condition-object-manager-slowdown.md"
      6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/windows-hardening/windows-local-privilege-escalation/kernel-race-condition-object-manager-slowdown.md"
      7 sha: "188de82beb54e70956b2952367a0af91d26758b8"
      8 isIndex: false
      9 modified: true
     10 license: "CC-BY-NC-4.0"
     11 ---
     12 
     13 # Kernel Race Condition Exploitation via Object Manager Slow Paths
     14 
     15 ## Why stretching the race window matters
     16 
     17 Many Windows kernel LPEs follow the classic pattern `check_state(); NtOpenX("name"); privileged_action();`. On modern hardware a cold `NtOpenEvent`/`NtOpenSection` resolves a short name in ~2 µs, leaving almost no time to flip the checked state before the secure action happens. By deliberately forcing the Object Manager Namespace (OMNS) lookup in step 2 to take tens of microseconds, the attacker gains enough time to consistently win otherwise flaky races without needing thousands of attempts.<sup>[[1]](#references)</sup>
     18 
     19 ## Object Manager lookup internals in a nutshell
     20 
     21 * **OMNS structure** – Names such as `\BaseNamedObjects\Foo` are resolved directory-by-directory. Each component causes the kernel to find/open an *Object Directory* and compare Unicode strings. Symbolic links (e.g., drive letters) may be traversed en route.
     22 * **UNICODE_STRING limit** – OM paths are carried inside a `UNICODE_STRING` whose `Length` is a 16-bit value. The absolute limit is 65 535 bytes (32 767 UTF-16 codepoints). With prefixes like `\BaseNamedObjects\`, an attacker still controls ≈32 000 characters.
     23 * **Attacker prerequisites** – Any user can create objects underneath writable directories such as `\BaseNamedObjects`. When the vulnerable code uses a name inside, or follows a symbolic link that lands there, the attacker controls the lookup performance with no special privileges.<sup>[[1]](#references)</sup>
     24 
     25 ## Slowdown primitive #1 – Single maximal component
     26 
     27 The cost of resolving a component is roughly linear with its length because the kernel must perform a Unicode comparison against every entry in the parent directory. Creating an event with a 32 kB-long name immediately increases the `NtOpenEvent` latency from ~2 µs to ~35 µs on Windows 11 24H2 (Snapdragon X Elite testbed).
     28 
     29 ```cpp
     30 std::wstring path;
     31 while (path.size() <= 32000) {
     32     auto result = RunTest(L"\\BaseNamedObjects\\A" + path, 1000);
     33     printf("%zu,%f\n", path.size(), result);
     34     path += std::wstring(500, 'A');
     35 }
     36 ```
     37 
     38 *Practical notes*
     39 
     40 - You can hit the length limit using any named kernel object (events, sections, semaphores…).
     41 - Symbolic links or reparse points can point a short “victim” name to this giant component so the slowdown is applied transparently.
     42 - Because everything lives in user-writable namespaces, the payload works from a standard user integrity level.<sup>[[1]](#references)</sup>
     43 
     44 ## Slowdown primitive #2 – Deep recursive directories
     45 
     46 A more aggressive variant allocates a chain of thousands of directories (`\BaseNamedObjects\A\A\...\X`). Each hop triggers directory resolution logic (ACL checks, hash lookups, reference counting), so the per-level latency is higher than a single string compare. With ~16 000 levels (limited by the same `UNICODE_STRING` size), empirical timings surpass the 35 µs barrier achieved by long single components.
     47 
     48 ```cpp
     49 ScopedHandle base_dir = OpenDirectory(L"\\BaseNamedObjects");
     50 HANDLE last_dir = base_dir.get();
     51 std::vector<ScopedHandle> dirs;
     52 for (int i = 0; i < 16000; i++) {
     53     dirs.emplace_back(CreateDirectory(L"A", last_dir));
     54     last_dir = dirs.back().get();
     55     if ((i % 500) == 0) {
     56         auto result = RunTest(GetName(last_dir) + L"\\X", iterations);
     57         printf("%d,%f\n", i + 1, result);
     58     }
     59 }
     60 ```
     61 
     62 Tips:
     63 
     64 * Alternate the character per level (`A/B/C/...`) if the parent directory starts rejecting duplicates.
     65 * Keep a handle array so you can delete the chain cleanly after exploitation to avoid polluting the namespace.<sup>[[1]](#references)</sup>
     66 
     67 ## Slowdown primitive #3 – Shadow directories, hash collisions & symlink reparses (minutes instead of microseconds)
     68 
     69 Object directories support **shadow directories** (fallback lookups) and bucketed hash tables for entries. Abuse both plus the 64-component symbolic-link reparse limit to multiply slowdown without exceeding the `UNICODE_STRING` length:
     70 
     71 1. Create two directories under `\BaseNamedObjects`, e.g. `A` (shadow) and `A\A` (target). Create the second using the first as the shadow directory (`NtCreateDirectoryObjectEx`), so missing lookups in `A` fall through to `A\A`.
     72 2. Fill each directory with thousands of **colliding names** that land in the same hash bucket (e.g., varying trailing digits while keeping the same `RtlHashUnicodeString` value). Lookups now degrade to O(n) linear scans inside a single directory.
     73 3. Build a chain of ~63 **object manager symbolic links** that repeatedly reparse into the long `A\A\…` suffix, consuming the reparse budget. Each reparse restarts parsing from the top, multiplying the collision cost.
     74 4. Lookup of the final component (`...\\0`) now takes **minutes** on Windows 11 when 16 000 collisions are present per directory, providing a practically guaranteed race win for one-shot kernel LPEs.
     75 
     76 ```cpp
     77 ScopedHandle shadow = CreateDirectory(L"\\BaseNamedObjects\\A");
     78 ScopedHandle target = CreateDirectoryEx(L"A", shadow.get(), shadow.get());
     79 CreateCollidingEntries(shadow, 16000, dirs);
     80 CreateCollidingEntries(target, 16000, dirs);
     81 CreateSymlinkChain(shadow, LongSuffix(L"\\A", 16000), 63);
     82 printf("%f\n", RunTest(LongSuffix(L"\\A", 16000) + L"\\0", 1));
     83 ```
     84 
     85 *Why it matters*: A minutes-long slowdown turns one-shot race-based LPEs into deterministic exploits.<sup>[[1]](#references)</sup>
     86 
     87 ### 2025 retest notes & ready-made tooling
     88 
     89 - James Forshaw republished the technique with updated timings on Windows 11 24H2 (ARM64). Baseline opens remain ~2 µs; a 32 kB component raises this to ~35 µs, and shadow-dir + collision + 63-reparse chains still reach ~3 minutes, confirming the primitives survive current builds. Source code and perf harness are in the refreshed Project Zero post.<sup>[[1]](#references)</sup>
     90 - You can script setup using the public `symboliclink-testing-tools` bundle: `CreateObjectDirectory.exe` to spawn the shadow/target pair and `NativeSymlink.exe` in a loop to emit the 63-hop chain. This avoids hand-written `NtCreate*` wrappers and keeps ACLs consistent.<sup>[[2]](#references)</sup>
     91 
     92 ## Measuring your race window
     93 
     94 Embed a quick harness inside your exploit to measure how large the window becomes on the victim hardware. The snippet below opens the target object `iterations` times and returns the average per-open cost using `QueryPerformanceCounter`.<sup>[[1]](#references)</sup>
     95 
     96 ```cpp
     97 static double RunTest(const std::wstring name, int iterations,
     98         std::wstring create_name = L"", HANDLE root = nullptr) {
     99     if (create_name.empty()) {
    100         create_name = name;
    101     }
    102     ScopedHandle event_handle = CreateEvent(create_name, root);
    103     ObjectAttributes obja(name);
    104     std::vector<ScopedHandle> handles;
    105     Timer timer;
    106     for (int i = 0; i < iterations; ++i) {
    107         HANDLE open_handle;
    108         Check(NtOpenEvent(&open_handle, MAXIMUM_ALLOWED, &obja));
    109         handles.emplace_back(open_handle);
    110     }
    111     return timer.GetTime(iterations);
    112 }
    113 ```
    114 
    115 The results feed directly into your race orchestration strategy (e.g., number of worker threads needed, sleep intervals, how early you need to flip the shared state).
    116 
    117 ## Exploitation workflow
    118 
    119 1. **Locate the vulnerable open** – Trace the kernel path (via symbols, ETW, hypervisor tracing, or reversing) until you find an `NtOpen*`/`ObOpenObjectByName` call that walks an attacker-controlled name or a symbolic link in a user-writable directory.
    120 2. **Replace that name with a slow path**
    121    - Create the long component or directory chain under `\BaseNamedObjects` (or another writable OM root).
    122    - Create a symbolic link so that the name the kernel expects now resolves to the slow path. You can point the vulnerable driver’s directory lookup to your structure without touching the original target.
    123 3. **Trigger the race**
    124    - Thread A (victim) executes the vulnerable code and blocks inside the slow lookup.
    125    - Thread B (attacker) flips the guarded state (e.g., swaps a file handle, rewrites a symbolic link, toggles object security) while Thread A is occupied.
    126    - When Thread A resumes and performs the privileged action, it observes stale state and performs the attacker-controlled operation.
    127 4. **Clean up** – Delete the directory chain and symbolic links to avoid leaving suspicious artifacts or breaking legitimate IPC users.<sup>[[1]](#references)</sup>
    128 
    129 ## Applied chain: mutable Cloud Files placeholders + Object Manager path switching
    130 
    131 [ShieldBreak](https://github.com/MSNightmare/ShieldBreak), published as a bypass for RoguePlanet (CVE-2026-50656), demonstrates a broader exploitation pattern: make a privileged scanner classify one representation of a logical file, then change both its bytes and namespace resolution before remediation uses it. The PoC combines a Cloud Files hydration TOCTOU, an Object Manager shadow-directory fallback, CLFS-generated-name capture, and a local administrative-share link to turn Defender cleanup into a protected DLL write.<sup>[[3]](#references)[[4]](#references)</sup>
    132 
    133 ### 1. Substitute content through Cloud Files hydration
    134 
    135 Register an attacker-writable directory as a Cloud Files sync root, connect a `CF_CALLBACK_TYPE_FETCH_DATA` callback, and create a placeholder whose advertised size matches a deterministic detection trigger such as the EICAR ZIP. The first fetch returns the trigger and flips callback state; later fetches return the payload. After the scanner has classified the first representation, obtain the transfer key and restart hydration with payload-sized metadata, then force hydration to EOF.<sup>[[4]](#references)</sup>
    136 
    137 ```cpp
    138 CfRegisterSyncRoot(sync_root, &registration, &policies, flags);
    139 CfConnectSyncRoot(sync_root, callbacks, &state, connect_flags, &connection);
    140 CfCreatePlaceholders(sync_root, &placeholder, 1, 0, &created);
    141 // First FETCH_DATA => detection trigger; later FETCH_DATA => payload.
    142 CfGetTransferKey(placeholder_handle, &transfer_key);
    143 opInfo.Type = CF_OPERATION_TYPE_RESTART_HYDRATION;
    144 CfExecute(&opInfo, &restart_params);
    145 CfHydratePlaceholder(placeholder_handle, {0}, CF_EOF, 0, NULL);
    146 ```
    147 
    148 The security boundary fails if scan, verdict, and remediation refer only to a pathname or placeholder identity: neither guarantees that a later hydration returns the bytes that were inspected.<sup>[[4]](#references)</sup>
    149 
    150 ### 2. Switch an invariant path through a shadow-directory fallback
    151 
    152 Create a target Object Manager directory and a second directory with `NtCreateDirectoryObjectEx`, passing the target handle as its shadow/fallback directory. Put a same-named `WD_SCAN` entry in both resolution layers: the visible entry points to the normal working directory, while the fallback entry points to `\CLFS\??\<working-directory>`. Supply Defender only the invariant path below; deleting the visible link while the operation is active makes the same string fall through to the CLFS-backed entry.<sup>[[4]](#references)</sup>
    153 
    154 ```text
    155 \\.\globalroot\BaseNamedObjects\Restricted\WD_SHADOW_<GUID>\WD_SCAN\BERLIN
    156 ```
    157 
    158 This is distinct from using shadow directories only to slow lookup: the attacker changes the **meaning** of a previously accepted path without modifying its string.<sup>[[4]](#references)</sup>
    159 
    160 ### 3. Capture the generated name and install a filename-specific link
    161 
    162 Monitor the working directory with `ReadDirectoryChangesW`. On the first `FILE_ACTION_ADDED`, remove the visible `WD_SCAN` link to activate fallback lookup. Capture the second generated filename, open that CLFS-related file, and lock the range `0..MAXLONGLONG` with `LockFileEx`. While the privileged operation is stalled, replace `WD_SCAN` in the visible directory with a real Object Manager directory and create a child symbolic link named from the observed filename (the PoC strips its final four characters). Point it to the protected destination through local SMB:<sup>[[4]](#references)</sup>
    163 
    164 ```text
    165 \??\UNC\127.0.0.1\C$\Windows\System32\phoneinfo.dll
    166 ```
    167 
    168 The unprivileged process cannot write that destination itself, but Defender's SYSTEM context can traverse the loopback administrative share. Combining generated-name observation with a filename-specific Object Manager link avoids having to predict the remediation artifact in advance.<sup>[[4]](#references)</sup>
    169 
    170 ### 4. Stabilize the cleanup race and trigger a privileged loader
    171 
    172 Before scanning, the PoC stores a valid PE (`ntdll.dll`) in the placeholder's `:stream` NTFS alternate data stream. After redirection creates the protected base file, it opens `phoneinfo.dll:stream` with execute access and keeps a `PAGE_EXECUTE_READ | SEC_IMAGE` mapping alive while cleanup resumes; the live file/section objects constrain deletion or replacement during the final race. The restarted hydration now returns the payload DLL rather than EICAR, so the protected base file contains attacker-controlled code.<sup>[[4]](#references)</sup>
    173 
    174 A protected write is then converted to SYSTEM execution by placing a crafted `Report.wer` under `C:\ProgramData\Microsoft\Windows\WER\ReportQueue\...` and invoking `\Microsoft\Windows\Windows Error Reporting\QueueReporting` through the Task Scheduler COM API. In this chain, privileged WER processing loads the planted `C:\Windows\System32\phoneinfo.dll`; a named-pipe connection is used as the payload execution signal.<sup>[[4]](#references)</sup>
    175 
    176 ### Detection pivots
    177 
    178 Useful correlations are more specific than any single temporary filename and cover all namespace transitions in the chain:<sup>[[4]](#references)</sup>
    179 
    180 - A newly registered Cloud Files provider followed by EICAR detection and `CF_OPERATION_TYPE_RESTART_HYDRATION` on the same placeholder.
    181 - Object Manager paths containing `WD_TARGET_*`, `WD_SHADOW_*`, or `WD_SCAN`, especially a scan path below `\\.\globalroot\BaseNamedObjects\Restricted\`.
    182 - CLFS file creation followed by an exclusive whole-file lock and loopback access to `\\127.0.0.1\C$\Windows\System32\*.dll` from a privileged security process.
    183 - Creation of a System32 DLL together with an NTFS ADS, followed by `SEC_IMAGE` mapping of the stream.
    184 - An attacker-created WER queue entry followed by an unusual manual run of `\Microsoft\Windows\Windows Error Reporting\QueueReporting` and an image load of the planted DLL.
    185 
    186 ## Applied chain: oplock-gated mount-point switch against privileged remediation
    187 
    188 A reusable LPE pattern appears when a privileged scanner checks an attacker-controlled file and later remediates it by reopening the **pathname** rather than continuing through validated handles. FalconFlank is a public example targeting CrowdStrike Falcon's Office macro-removal workflow; the repository claims testing on Windows 11 25H2 and Windows Server 2025 with the relevant policy enabled, but publishes no CVE, affected-build range, vendor advisory, or patch status, so treat the product-specific claim as unverified and build-dependent.<sup>[[5]](#references)[[6]](#references)</sup>
    189 
    190 ### Race layout
    191 
    192 1. Build a writable tree whose final relative name is useful at the intended destination. The example uses `%TEMP%\\Flanker_{GUID}\\WindowsPowerShell\\v1.0\\bcrypt.dll`, but initially writes an OLE macro document—not a PE DLL—to `bcrypt.dll`. Content-based detection triggers the remediation while the attacker-controlled basename is preserved for the later side-load.<sup>[[5]](#references)</sup>
    193 2. Open the directories with broad sharing and `FILE_OPEN_REPARSE_POINT`, then request an asynchronous RH oplock on the trigger with `FSCTL_REQUEST_OPLOCK`, `OPLOCK_LEVEL_CACHE_READ | OPLOCK_LEVEL_CACHE_HANDLE`, and `REQUEST_OPLOCK_INPUT_FLAG_REQUEST`. Wait for the overlapped event and use its completion as the path-switch cue. An RH oplock-break notification is advisory rather than proof that every conflicting operation is blocked, so exploitability still depends on the victim's exact open/remediation sequence.<sup>[[5]](#references)[[7]](#references)</sup>
    194 3. After the break, remove the leaf directory with `FileDispositionInformationEx` (information class 64) using delete plus POSIX-semantics flags, close its handle, and apply an `IO_REPARSE_TAG_MOUNT_POINT` to the now-empty parent with `FSCTL_SET_REPARSE_POINT_EX`. The mount point redirects the unchanged suffix into a protected tree such as `\\SystemRoot\\System32\\WindowsPowerShell`; setting a reparse point fails if the directory is not empty, which explains the preceding deletion step.<sup>[[5]](#references)[[8]](#references)</sup>
    195 4. Resume the privileged workflow. If it resolves the string again without proving that the directory chain and final object are the ones previously inspected, the same logical pathname now reaches the attacker-selected protected directory. In the example, success is tested by reopening `C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\bcrypt.dll` read/write from the original process; this distinguishes the confused-deputy write primitive from the later code-execution stage.<sup>[[5]](#references)</sup>
    196 5. Replace the resulting file with the real DLL and activate a privileged loader. The PoC uses `CreateTransaction` + `CreateFileTransacted`, truncates the file, maps the DLL-sized replacement, copies the PE, and commits; TxF binds the file handle and subsequent handle-based operations to the transaction, but it is a post-race replacement mechanism rather than the source of the privilege boundary failure.<sup>[[5]](#references)[[9]](#references)</sup>
    197 6. Finally, run an existing privileged scheduled task whose executable probes the planted adjacent filename. FalconFlank invokes `\\Microsoft\\Windows\\Application Experience\\MareBackup`, waits for the DLL to connect to `\\??\\pipe\\FALCONFLANK`, and then deletes the planted file. Do not assume a particular resulting token solely from the task name—verify the launched process, module path, integrity level, and token on the tested build.<sup>[[5]](#references)</sup>
    198 
    199 The core audit question is therefore not “does the service validate the original input path?” but “does every privileged mutation remain bound to the same opened file and directory objects that were validated?” Holding handles across check and use, opening child objects relative to a trusted directory handle, rejecting unexpected reparse tags, and revalidating file identity before mutation close this class of pathname-substitution bug.<sup>[[1]](#references)[[8]](#references)</sup>
    200 
    201 ### Detection and PoC triage
    202 
    203 High-signal detection correlates the namespace transition with the privileged consumer: an OLE header under a DLL basename in a GUID-named temporary tree, an oplock break, POSIX-style removal of the leaf directory, creation of a mount point targeting a protected Windows directory, and creation or modification of the same basename below that destination. For the public example, add `C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\bcrypt.dll`, manual execution of `MareBackup`, and the `FALCONFLANK` named pipe as narrower pivots; none is sufficient alone.<sup>[[5]](#references)</sup>
    204 
    205 When reproducing the PoC, account for three reliability defects in the published source: it calls `FlushFileBuffers` with the embedded byte-array pointer rather than the file handle, tests a stale `HRESULT` after `GetFolder`, `GetTask`, and `Run`, and uses unbounded retry/wait loops for directory deletion, reparse creation, the oplock event, and pipe connection.<sup>[[5]](#references)</sup>
    206 
    207 ## Operational considerations
    208 
    209 - **Combine primitives** – You can use a long name *per level* in a directory chain for even higher latency until you exhaust the `UNICODE_STRING` size.
    210 - **One-shot bugs** – The expanded window (tens of microseconds to minutes) makes “single trigger” bugs realistic when paired with CPU affinity pinning or hypervisor-assisted preemption.
    211 - **Side effects** – The slowdown only affects the malicious path, so overall system performance remains unaffected; defenders will rarely notice unless they monitor namespace growth.
    212 - **Cleanup** – Keep handles to every directory/object you create so you can call `NtMakeTemporaryObject`/`NtClose` afterwards. Unbounded directory chains may persist across reboots otherwise.
    213 - **File-system races** – If the vulnerable path ultimately resolves through NTFS, you can stack an Oplock (e.g., `SetOpLock.exe` from the same toolkit) on the backing file while the OM slowdown runs, freezing the consumer for additional milliseconds without altering the OM graph.<sup>[[2]](#references)</sup>
    214 
    215 ## Defensive notes
    216 
    217 - Kernel code that relies on named objects should re-validate security-sensitive state *after* the open, or take a reference before the check (closing the TOCTOU gap).
    218 - Enforce upper bounds on OM path depth/length before dereferencing user-controlled names. Rejecting overly long names forces attackers back into the microsecond window.
    219 - Instrument object manager namespace growth (ETW `Microsoft-Windows-Kernel-Object`) to detect suspicious thousands-of-components chains under `\BaseNamedObjects`.
    220 
    221 ## References
    222 
    223 - [1] [Project Zero – Windows Exploitation Techniques: Winning Race Conditions with Path Lookups](https://projectzero.google/2025/12/windows-exploitation-techniques.html)
    224 - [2] [googleprojectzero/symboliclink-testing-tools](https://github.com/googleprojectzero/symboliclink-testing-tools)
    225 - [3] [MSNightmare/ShieldBreak](https://github.com/MSNightmare/ShieldBreak)
    226 - [4] [ShieldBreak.cpp (commit be016d8)](https://github.com/MSNightmare/ShieldBreak/blob/be016d8c18c8355a12753286c1ce9d5a48a0dab4/ShieldBreak.cpp)
    227 - [5] [FalconFlank.cpp (commit 702b574)](https://github.com/MSNightmare/FalconFlank/blob/702b57477a9f0a99ddabef56e7ebe6c1e99c2435/FalconFlank.cpp)
    228 - [6] [MSNightmare/FalconFlank](https://github.com/MSNightmare/FalconFlank)
    229 - [7] [Microsoft Learn - FSCTL_REQUEST_OPLOCK](https://learn.microsoft.com/en-us/windows/win32/api/winioctl/ni-winioctl-fsctl_request_oplock)
    230 - [8] [Microsoft Learn - FSCTL_SET_REPARSE_POINT_EX](https://learn.microsoft.com/en-us/windows-hardware/drivers/ifs/fsctl-set-reparse-point-ex)
    231 - [9] [Microsoft Learn - How to Use Transactional NTFS](https://learn.microsoft.com/en-us/windows/win32/fileio/how-to-use-transactional-ntfs)