Original: This article is an independent of “Gargoyle, a decade later” by Josh Lospinoso, published on lospino.so on May 13, 2026.
All research, framing, the 2017 original Gargoyle proof of concept, the 2026 refresh, and the discussion of the broader sleep-obfuscation / temporal-memory-state family belong to the original author. The post contains no published code blocks; this rewrite reproduces the single editorial image at the original position and re-words the prose into our own English summary for blog length. For the full essay — including its careful claim-language and reading list — read the source.
Source: lospino.so/blog/gargoyle-a-decade-later · Repo: github.com/JLospinoso/gargoyle · Copyright: © 2026 Josh Lospinoso.

Executive Summary
In 2017 Josh Lospinoso published “gargoyle, a memory scanning evasion technique” — a small 32-bit Windows proof of concept that flipped a code region’s page protection between non-executable and executable on a timer, so that a point-in-time memory scanner asking “which private pages are executable right now?” could be honestly answered “none of them” while the program was dormant. The visible behaviour was deliberately boring: a periodic MessageBox. The interesting part was the state machine around the message box — a waitable timer, an APC delivery, a VirtualProtectEx protection transition, and an x86 stack-pivot path that re-entered the setup code so the loop could continue.
The 2026 retrospective — and the parallel refresh of the JLospinoso/gargoyle repository — reframes the whole thing. The original is preserved as the canonical Win32 proof. New x64, ARM64, and ARM64EC sibling demonstrations are explicitly labelled as siblings, not ports. An important SetWaitableTimer / SleepEx / APC validation correction is made plain: a wait returning is not the same fact as an APC completion routine being dispatched, and the new harness binds the claim to an alertable SleepEx(INFINITE, TRUE). The most interesting move, though, is editorial: Lospinoso swaps “memory scanning evasion” for “temporal memory state”, recasting Gargoyle from a hiding trick into a measurement problem — what state did the observer measure, at what time, at what layer, and what changed before and after. That recasting is what makes the post useful to defenders in 2026.
The original idea was time, not invisibility
Lospinoso’s strongest framing in the retrospective is this: memory state is temporal. A private page can be non-executable when a scanner looks at it and become executable later. A scanner that only asks “executable now?” can answer truthfully and still miss a state transition that happens before or after the snapshot. Gargoyle’s original contribution was to make that mismatch concrete in a 200-line Windows demo.
The author is explicit that “hiding” was the wrong word, even in 2017. The page never disappears. Whatever evidence the scanner gathers at scan time is still real evidence; it’s just bound to that moment. The defensive answer is to add timeline to memory state: allocation history, protection transitions, timer state, callback context, stack provenance, page-table evidence, CFG bitmap residue, endpoint telemetry. A point-in-time map is a slice; a state machine is the thing.
The compact Win32 proof
The original 32-bit Windows artefact fits in the head. Conceptually:
- Setup code displays a benign
MessageBox. - The setup region is then transitioned back to a non-executable state via
VirtualProtectEx. - A waitable timer is armed and a callback (APC) is queued for completion.
- The thread parks in an alertable wait. The timer due-time fires; the APC is dispatched.
- The APC body uses an x86 stack-pivot and a ROP gadget shape (the historical write-up used a gadget inside
mshtml.dll) to restore execute permission on the setup region and return into it, so the loop continues.
The original source files — main.cpp, setup.nasm, gadget.nasm — are still the source of truth for that canonical version in the refreshed repo. Lospinoso’s point in the retrospective is that the specific gadget never mattered. The point was that a small program could make memory protection a time-dependent property and force a scanner to be honest about what it had actually measured.
What aged well, and what didn’t
The idea aged better than the artefact. Temporal memory state shows up across the modern offensive toolbox: protection cycling, short work windows, sleep obfuscation, timer-driven re-entry, callback-driven execution, and stack-spoofing during sleep. Not all of that work descends from Gargoyle — some is adjacent, some is convergent — but it occupies the same problem space: security tools take snapshots; programs are state machines.
The artefact itself was narrower: x86, a historical stack-pivot / ROP shape, a 2017 Windows toolchain, live-and-visible validation. The title “memory scanning evasion” carried more swagger than the demo did. The 2026 refresh deliberately walks both of those back — the README now calls Gargoyle a “historical Windows research proof of concept for temporal memory-state evasion,” with the original Win32 implementation still central and the new architecture work labelled as sibling demonstrations rather than transparent ports.
From MessageBox to measurement
The MessageBox was always a pedagogical marker, not the mechanism. It said: the benign path ran here, just now. Nothing more. The retrospective’s most useful self-criticism is that a UI event is not a callback trace, a returning wait is not a proof of APC dispatch, and a page-protection observation in a memory viewer is not a complete history of every transition. The refresh attaches each demo path to a specific evidence statement instead of letting the visible marker speak for the whole state machine.
The 2026 refresh: making the old thing measurable
The refresh did not turn Gargoyle into a bigger runtime; it made it easier to build, teach, and audit. The build stays Windows-native (Visual Studio / MSBuild, NASM for x86/x64, ARMASM and COFF text extraction for ARM, with just recipes around repeatable checks). The interesting addition is the acceptance harness, which splits validation into named modes that each carry a smaller claim:
- Artifacts mode — expected files exist and PE machine metadata is compatible with the requested platform.
- Architecture mode — runtime identity facts recorded.
- Headless mode — benign local rounds where the native runtime supports it; ARM64 / ARM64EC variants report completed-round and callback-round counters.
- Live mode — the visible desktop MessageBox path for an interactive lab.
The Python acceptance package is typed, linted, tested and documented; the native checks include build coverage, MSVC analysis, and sanitizer-oriented validation. CI splits an ordinary Windows x64 gate from a hosted Windows-on-Arm smoke path — which is the change that turns ARM64 / ARM64EC support from aspirational prose into something with real automated evidence.
The SleepEx correction
The technical correction is small to state and easy to underestimate. Microsoft’s SetWaitableTimer documentation distinguishes the timer becoming signaled from the APC completion routine being called. When a completion routine is supplied, the APC is queued to the thread that armed the timer. That routine runs when the thread enters an alertable wait — documented by SleepEx.
The weak model the original demo invited was:
- Set a waitable timer.
- Wait on the timer handle.
- Observe that the wait returned.
- Infer that the completion routine ran.
The corrected model is:
- Set a waitable timer with a completion routine.
- Enter an alertable wait with
SleepEx(INFINITE, TRUE). - Let the timer due time queue the APC.
- Dispatch the completion routine while the thread is alertable.
- Interpret later re-entry evidence according to what the validation path actually observed.
Functionally similar, evidentially very different. A timer can become signaled, a UI marker can appear later, a page can have the expected protection at inspection time — but if the research claim is about a specific asynchronous path, the validation has to bind to that path, not to a wait that just happens to return.
Canonical, sibling, sibling, sibling
The architecture story in the refresh is deliberately asymmetric. The Win32 / x86 version is the canonical historical proof: main.cpp + setup.nasm + gadget.nasm, the stack trampoline, the system-DLL gadget search, the fallback gadget.pic. The other architectures are sibling demonstrations:
- x64 — separate
setup_x64.picandreentry_x64.picartifacts. The re-entry PIC remains executable while the setup PIC is parked, entersSleepEx(INFINITE, TRUE), and handles the restore shape around timer/APC re-entry. Not the old stack-pivot chain in 64-bit clothing. - ARM64 — ARMASM with COFF
.textextraction for PIC generation, ARM64 calling conventions, and CI-trackable completed/callback counters in the headless harness. - ARM64EC — uses the EC-code allocation path the demo needs, validates EC-specific behaviour, but explicitly does not demonstrate mixed x64 DLL interop and is not a general claim about every ARM64EC dynamic-code pattern.
The wording is deliberate: “ported everywhere” would have been wrong, because the x86 mechanism is not architecture-neutral. Calling each new architecture a sibling — with its own bounded claim — keeps the history of the work visible.
The decade around Gargoyle
The retrospective is careful about lineage. Three concentric circles, in the author’s framing:
- Declared relationship. WithSecure’s
dotnet-gargoylecalls itself a spiritual .NET equivalent; YouMayPasser describes itself as an x64 Gargoyle implementation; DeepSleep describes itself as a Gargoyle-like x64 variant. ShellcodeFluctuation is close but credits Gargoyle as the introduction to memory-protection flipping without claiming descent. - Family resemblance, not descent. Ekko, Cronos, FOLIAGE-related analysis, timer queues, context restoration, dormant-state changes, stack-spoofing — the broader sleep-obfuscation family. MDSec’s “How I Met Your Beacon” series places several of these in a page-protection / event-driven sleep-obfuscation neighbourhood; WithSecure’s timer-queue hunting work shows how quickly the defensive question becomes mechanism-specific.
- Defensive and academic descendants. Elastic’s Hunting In Memory taxonomy, F-Secure / Countercept’s WithSecureLabs Volatility plugin (timer-APC inspection, ROP-chain following,
VirtualProtectExargument analysis), PTE-aware memory-forensics research, and the Black Hat Asia 2023 work on transient implant-state footprints including CFG bitmap residue.
Lospinoso’s editorial discipline here is worth stealing: citation is not derivation, similarity is not lineage, defensive analysis is not an endorsement of misuse, and stronger anti-forensics are not “Gargoyle but better”.
What defenders can take from it now
- Treat memory state as a time series. “What protection does this page have?” is the wrong question on its own. “What sequence of events led here, and what evidence did they leave?” is the right one.
- Correlate ordinary mechanisms. Timers, APCs, alertable waits, protection changes, private memory — all individually fine. The signal is in the relationship: repeated private-region protection transitions correlated with timer/callback state and stack provenance.
- Use more than one memory abstraction. VADs, PTEs, PFNs, CFG bitmaps, thread stacks, timer objects, user-mode API traces, ETW, kernel telemetry, endpoint histories. When they disagree, the disagreement is often the finding.
- Build benign corpora. A safe, labelled, intentionally boring demo like the refreshed Gargoyle helps measure both misses and false positives. Real environments have JIT runtimes, AV products, anti-cheat, DRM, packers and instrumentation frameworks that already make memory look strange.
- Keep evidence language honest. A live
MessageBoxvalidates a visible action. A second one validates re-entry. ARM64/ARM64EC headless counters validate callback delivery more directly. None of those statements should inflate into a product-defeating claim.
Safe reproduction
The refreshed repo treats documentation as part of the proof of concept. Lospinoso explicitly recommends starting with the docs rather than the source: quickstart for the shortest safe build/validation path, lab setup for the Windows desktop assumptions, validation limitations for what the project does and does not establish (it does not claim product evasion, general stealth, durable footholds, or operator capability).
Concretely: artifacts mode and architecture mode for cheap automated checks; ARM64 / ARM64EC headless mode for the cleanest callback-delivery evidence available outside an interactive desktop; live mode (the visible MessageBox) for x86 / x64 on a real lab box. The right reproduction goal is modest: understand the state machine, run the validation path appropriate to the architecture, and compare the observed evidence to the claim being made.
Key Takeaways
- Gargoyle = temporal memory state. A code region spends most of its life as
PAGE_NOACCESS/non-executable, briefly becomes executable during a timer/APC-driven work window, then returns to dormant. - The 2017 demo’s structure — waitable timer + alertable wait + APC +
VirtualProtectExprotection transition + x86 stack-pivot ROP gadget inmshtml.dll— remains the canonical reference implementation in the refreshed repo. - The 2026 refresh adds sibling demonstrations for x64, ARM64 and ARM64EC, each with its own narrower validation language — not a single “ported everywhere” claim.
- The biggest technical correction is binding the validation to the
SetWaitableTimer+SleepEx(INFINITE, TRUE)alertable-APC dispatch path, not to a returning wait that could have been the APC. - The broader sleep-obfuscation family (Ekko, Cronos, FOLIAGE-related, timer-queue work, sleep-mask designs) lives in the same problem space but is convergent, not always descendant.
- The defensive landscape moved further than the offensive one. Memory-forensics tooling (Volatility plugins inspecting timer/APC and ROP chains, PTE-aware analysis, CFG-bitmap residue work) has caught up to the snapshot-vs-state-machine asymmetry that made the 2017 demo interesting.
- The healthy lesson is editorial: stop calling it “hiding,” call it “measurement.” The page never disappears; the question is which observer measured which state at which time.
Defensive Recommendations
- Instrument protection transitions over time. If your EDR records
NtProtectVirtualMemory/VirtualProtectExcalls, treat repeated RX → NA → RX cycles on the same private region as high-signal regardless of point-in-time scan results. - Correlate timer-APC state with private executable memory. The combination of a waitable timer with a completion routine, an alertable thread in
SleepEx, and a private region that periodically becomes executable is a small but specific pattern. Tools like the WithSecureLabs Volatility plugin already inspect this surface. - Verify callback identity, not just callback occurrence. The author’s own correction applies to defenders too: a thread waking up is not a proof of which routine ran. Stack provenance at the moment of wake is the better signal.
- Track CFG bitmap residue. A page that was executable earlier in process lifetime leaves traces — Black Hat Asia 2023’s “You Can Run But You Can’t Hide” work and PTE-aware forensics literature are good starting points.
- Multi-abstraction memory analysis. VADs, PTEs, PFN database, CFG bitmaps, thread stacks, timer objects, ETW: ask the same question of multiple subsystems and look for disagreement.
- Include benign corpora in your detector evaluation. Real environments have JIT, anti-cheat and DRM that flip page protections frequently. A detector trained only on dramatic artefacts will produce dramatic false positives.
- Stay close to vendor docs for timer / APC semantics. The
SetWaitableTimer+ completion-routine + alertable-wait flow is well-specified; deviation from that contract on the offensive side is itself a useful signal.
Conclusion
Lospinoso’s retrospective is unusually useful for being unusually disciplined about its own claims. The 2017 Gargoyle demo was always small; the 2026 refresh keeps it small, adds sibling architecture demonstrations, corrects an APC-validation semantic that the original blurred, and reframes the entire idea away from “memory scanning evasion” toward “temporal memory state.” The latter is the framing defenders should adopt regardless of whether they ever touch Gargoyle: a scanner’s snapshot answers a time-bound question; the program is a state machine that the defender needs to reason about over time. The original essay is worth reading in full for the claim-language alone — it’s a small clinic in how to revisit one’s own old security research without inflating it.
This article is an independent English-language rewrite of «Gargoyle, a decade later» by Josh Lospinoso, originally published on lospino.so on May 13, 2026. The original Gargoyle proof of concept is at github.com/JLospinoso/gargoyle. All research, code, and the carefully-worded claim language remain the work of the original author; please cite Josh Lospinoso when referencing this material.

