|
54 | 54 | * Alternate the character per level (`A/B/C/...`) if the parent directory starts rejecting duplicates. |
55 | 55 | * Keep a handle array so you can delete the chain cleanly after exploitation to avoid polluting the namespace. |
56 | 56 |
|
| 57 | +## Slowdown primitive #3 – Shadow directories, hash collisions & symlink reparses (minutes instead of microseconds) |
| 58 | + |
| 59 | +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: |
| 60 | + |
| 61 | +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`. |
| 62 | +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. |
| 63 | +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. |
| 64 | +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. |
| 65 | + |
| 66 | +```cpp |
| 67 | +ScopedHandle shadow = CreateDirectory(L"\\BaseNamedObjects\\A"); |
| 68 | +ScopedHandle target = CreateDirectoryEx(L"A", shadow.get(), shadow.get()); |
| 69 | +CreateCollidingEntries(shadow, 16000, dirs); |
| 70 | +CreateCollidingEntries(target, 16000, dirs); |
| 71 | +CreateSymlinkChain(shadow, LongSuffix(L"\\A", 16000), 63); |
| 72 | +printf("%f\n", RunTest(LongSuffix(L"\\A", 16000) + L"\\0", 1)); |
| 73 | +``` |
| 74 | +
|
| 75 | +*Why it matters*: A minutes-long slowdown turns one-shot race-based LPEs into deterministic exploits. |
| 76 | +
|
57 | 77 | ## Measuring your race window |
58 | 78 |
|
59 | 79 | 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`. |
@@ -94,7 +114,7 @@ The results feed directly into your race orchestration strategy (e.g., number of |
94 | 114 | ## Operational considerations |
95 | 115 |
|
96 | 116 | - **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. |
97 | | -- **One-shot bugs** – The expanded window (tens of microseconds) makes “single trigger” bugs realistic when paired with CPU affinity pinning or hypervisor-assisted preemption. |
| 117 | +- **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. |
98 | 118 | - **Side effects** – The slowdown only affects the malicious path, so overall system performance remains unaffected; defenders will rarely notice unless they monitor namespace growth. |
99 | 119 | - **Cleanup** – Keep handles to every directory/object you create so you can call `NtMakeTemporaryObject`/`NtClose` afterwards. Unbounded directory chains may persist across reboots otherwise. |
100 | 120 |
|
|
0 commit comments