
Research disclosure: This article is an independent study of software architecture, defensive security, and anti-cheat integration. It is not an attempt to bypass Elytra, create cheats, interfere with WARDOGS, or publish operational details that would make abuse easier. I have intentionally left out sensitive protocol values, exact request formats, private identifiers, and details that would add little educational value while increasing misuse risk.
I also want to repeat something from my first article: I waited until the original closed playtest was over before starting the deeper binary work. I did not want research performed during an active test to become useful material for cheat development. Later work used legitimately available Early Access files and a kernel snapshot from my own machine.
Nothing here should be read as an official description from Bulkhead, Embark, or any other company involved. It is my interpretation of the evidence I collected from one Windows installation and one set of builds.
My first Elytra and WARDOGS article was written while some of the most important pieces were still missing.
At that point I could see the launcher, the Elytra service, the module system, the loader DLL, and the signed loader driver. I could also see references to another driver, but I did not yet have a useful view of its plaintext code or its runtime objects.
That changed later.
I eventually recovered the packaged Lighthouse module, compared the playtest and Early Access packages, traced the launcher protocol, followed the service's dynamic module loading, inspected the kernel objects created by Lighthouse, and analyzed a kernel snapshot taken while WARDOGS and Elytra were running.
The result is a much more complete picture. It is still not a complete description of every detection or every telemetry event, but the architecture is no longer a collection of disconnected clues.
This post is my attempt to put the whole story in one place without turning it into a manual for attacking the system.
The work stayed defensive and non-destructive.
I inspected files, manifests, signatures, imports, strings, registry entries, existing logs, disassembly, kernel objects, and an offline kernel snapshot. I also wrote small offline emulation scripts that interpreted copied instruction bytes. Those scripts did not load the drivers or execute their code natively.
I did not patch the game, inject code, hook functions, send crafted requests to the live anti-cheat, disable protections, or test a bypass. The original binaries were not modified.
The kernel snapshot deserves a specific mention because it changes the scope compared with the first article. It was captured from my own machine while the game and Elytra were running, then inspected offline. This gave me runtime state such as loaded module ranges, driver objects, device objects, file handles, and dispatch pointers without changing the running components.
The easiest way to understand Elytra is to stop thinking about it as one executable.
It is a modular chain:
1WARDOGS launcher 2 | 3 | starts and talks to 4 v 5Elytra Windows service 6 | 7 | loads signed modules dynamically 8 v 9Lighthouse bootstrap, loader, and heartbeat modules 10 | 11 | opens the Lighthouse device and coordinates modules 12 v 13Lighthouse user-mode module 14 | 15 | prepare and start kernel components 16 v 17Signed Elytra loader driver 18 | 19 | uses the normal Windows driver-loading path 20 v 21Lighthouse anti-cheat driver 22 | 23 | exposes a device interface and protected kernel logic 24 v 25Game session, local records, and backend-facing systems
There are several trust boundaries in that chain. The launcher does not do the kernel work itself. The service does not contain all detection logic. The signed loader driver and the main Lighthouse driver are separate components. The named pipe and the kernel device are also separate communication layers.
That distinction ended up answering many of the questions that were confusing in the beginning.
The flow begins in WardogsLauncher-Shipping.exe.
The launcher is not simply a splash screen that starts the game executable. It is the component that coordinates the Elytra session before handing control to the game.
The launcher checks the Elytra installation, starts Elytra.Service through the Windows Service Control Manager when necessary, connects to the service pipe, and performs a sequence of session operations.
The operations visible in the launcher include concepts equivalent to:
The launcher also passes process identity information that goes beyond a PID alone. The observed logic includes process creation time, which is a sensible defense against PID reuse.
Only after the Elytra side accepts this setup does the launcher proceed with the WARDOGS client.
This also explains an observation that was easy to reproduce: if the Elytra service is stopped while the game is running, the game closes almost immediately. If the service is stopped before launch, opening the game causes it to be started again.
I found the launcher side of that dependency and the session notification flow. I did not reduce the final shutdown decision to one single function, so I cannot say whether the last watchdog check lives in the protected game runtime, the service, or another session component. What is clear is that service availability is part of the game's trusted runtime state, not just an installation requirement.
The launcher and Elytra.Service communicate through a local named pipe using a JSON-RPC style protocol with framed messages.
This answered one of the early questions about whether the pipe existed only for control.exe.
It does not.
The normal WARDOGS launcher uses the service pipe for module and game-session operations. The pipe is part of the real launch integration.
The connection also includes a server identity check. The launcher queries the process behind the pipe and compares it with the service process it expects. That does not replace server-side authorization, but it is a useful protection against connecting to an unrelated local pipe endpoint.
What I did not establish is that the running WARDOGS client continues talking directly to this same pipe throughout gameplay. The launcher definitely uses it. The session can continue after an individual pipe connection goes away, which suggests that the service keeps session state independently or that other channels take over later.
service.exe and control.exe actually doservice.exe is the central user-mode coordinator.
It is a native 64-bit Rust application running as a privileged Windows service. Its responsibilities include module installation, package trust, dynamic module loading, session state, RPC handling, and coordination between the launcher and Elytra modules.
One of the more useful findings was its generic module loader. Rather than importing every module DLL by name, the service builds a module path at runtime, calls LoadLibraryW, resolves an exported factory named GetModuleInstance, and asks the returned object for an Elytra interface.
That is why the normal import table did not show elytraldrfs_shared.dll. The DLL is selected from module metadata and loaded dynamically.
control.exe appears to be a management and administrative client for the same service framework. It exposes useful command and interface descriptions, but I did not find it in the normal WARDOGS launch path. The launcher talks to the service directly, so the absence of a visible control.exe process during normal startup is not surprising.
Elytra distributes functionality in signed CAB packages with a manifest describing the module identity, revision, entry point, and dependencies.
The main packages I inspected were:
elytraldrfs_shared.dll and elytraldrfs_driver.sys;heartbeat.dll;lighthouse_bootstrap.dll, lighthouse_module.dll, driver.ini, and the protected driver payload.The Lighthouse manifest depends on both the loader and heartbeat modules. This is direct configuration evidence that the three pieces belong to the same runtime chain.
Comparing the closed playtest packages with the later Early Access cache also produced an interesting result. Several package revisions or signatures changed without meaningful executable-code changes, while the protected Lighthouse payload changed substantially. That is a useful reminder that package revision, signing metadata, and actual code changes are not the same thing.
The heartbeat module is a separate user-mode component loaded through the same module framework.
Its static code contains local named-pipe creation and communication capabilities, security descriptor handling, and cryptographic-library functionality. That supports a local coordination role.
It does not prove that the heartbeat module downloads the Lighthouse driver or directly uploads every anti-cheat event. In fact, the protected Lighthouse payload was already present locally inside its package, so a backend download is not required to explain where the driver bytes come from.
There may still be remote configuration, package updates, and server-side processing elsewhere. The point is simply that the package itself already contains the protected input.
Later analysis of lighthouse_bootstrap.dll filled in one of the largest remaining gaps between the Elytra service and the Lighthouse kernel driver.
The DLL exports the same GetModuleInstance factory expected by the service's dynamic module loader. It also contains the Elytra interface identifier previously observed in service.exe. Together, those two facts provide strong evidence that this is a service-loaded Elytra module rather than a DLL imported directly by the game.
The bootstrap discovers its own module directory and reads driver.ini. The configuration vocabulary includes a driver filename, a type field, and explicit compressed and encrypted modes. It also constructs paths to the protected driver material and to lighthouse_module.dll.
The bootstrap then performs the user-mode coordination work that had previously only been inferred:
lighthouse_module.dll through the Elytra module interface;elytra_.The strongest link is the stable Lighthouse identifier embedded in the bootstrap. It matches the identifier in the live kernel device name recovered from the snapshot. This connects the service-loaded bootstrap to the actual Lighthouse device object without relying on filename similarity alone.
I am deliberately not publishing the raw request values or wire layouts here. Static analysis confirms that multiple request classes exist, including a small initialization-style exchange, a path-bearing request, and a larger data-transfer operation. Their exact business meanings still require caller and response analysis, so labels such as handshake, registration, or telemetry remain interpretations rather than established protocol names.
This also gives lighthouse_module.dll a clearer place in the design. It is a small specialized component loaded by the bootstrap, while the bootstrap is the conventional orchestration layer that parses configuration, owns the kernel-device connection, and exposes the Elytra module interface.
elytraldrfs_shared.dll is the user-mode bridge between the modular service and the kernel loader.
The service loads it dynamically through GetModuleInstance. The DLL exposes the interface expected by the Lighthouse bootstrap and manages the loader driver's lifecycle.
Its code constructs the path to elytraldrfs_driver.sys, creates the required service registry entry, sets values such as the image path and start type, and calls the native Windows driver-loading API.
This corrected an early assumption of mine. The shared DLL is not loaded by the kernel driver. It is loaded by the user-mode Elytra service as a module. It then asks Windows to load the signed kernel loader.
The same interface also participates in accepting the protected Lighthouse file, mapping it as data, and forwarding the buffer and its protection flags into the loader path. The bootstrap provides the configuration and coordination around that operation.
elytraldrfs_driver.sys is the smaller kernel component. Its role is loading infrastructure, not the complete anti-cheat.
It creates a kernel device, exposes file-like and device-control operations, maintains memory-backed file content, and works with the companion DLL to make a prepared driver image available to Windows.
The loader imports and calls ZwLoadDriver. That is important because it rules out one of the more dramatic theories that appeared early in the research.
This is not evidence of a Driver Signature Enforcement bypass, and I did not find evidence that Lighthouse is manually mapped into ring 0 without Windows knowing about it. The architecture uses the normal Windows driver loader. The unusual part is how the driver image is prepared and presented before that call.
The loader itself has a section named .upx, but the bytes and execution style do not match a simple conclusion of standard UPX packing. The same bytes were present in the file and in the runtime snapshot, and the protected paths contain overlapping instructions, arithmetic branch construction, opaque predicates, and large saved-register frames.
My current description is selective control-flow and arithmetic obfuscation. I would not label it ordinary UPX, and I would not name a commercial virtualizer without better evidence.
The Lighthouse package contains a file named lighthouse_driver.sys, but the packaged bytes do not begin with the normal MZ header of a Windows PE file.
Its configuration explicitly marks it as encrypted and compressed, and its entropy is extremely high. This explains why opening the packaged file directly in a disassembler fails.
ZwLoadDriver does not decrypt or unpack arbitrary vendor formats. Windows expects a valid loadable driver image by the time the normal loading path reaches it.
The transformation therefore happens before or during the loader's memory-backed presentation stage. The Lighthouse bootstrap reads configuration that explicitly describes the protected representation, resolves the relevant module interfaces, and coordinates the driver setup. The loader chain prepares a valid image and then uses the ordinary Windows loading mechanism.
This is now supported more strongly than it was in the original analysis, but the exact division of decompression and decryption work is still unresolved. The configuration parser and transport are visible in the bootstrap; that does not, by itself, prove that the bootstrap contains the transformation algorithm.
This is a much better explanation than reflective kernel loading. It also matches what appeared in the kernel snapshot: Windows knew about the loaded Lighthouse image, its version resources, its driver object, its device objects, and its dispatch routines.
Once loaded, Lighthouse appears under a long randomized module filename and a randomized driver object name.
The random name is operational camouflage and namespace hygiene. It makes hard-coded names and simple filename matching less useful, but it does not hide the driver's identity from a serious kernel inspection.
The runtime image still exposed version resources identifying:
Lighthouse.sys;The driver also had a normal _DRIVER_OBJECT, registered dispatch routines, and two device objects. A stable symbolic link named \Device\EmbarkLighthouse pointed to a session-specific Lighthouse device name, which in turn led to the randomized driver object and its code.
That object chain was one of the clearest confirmations that Lighthouse was the main kernel component rather than a mysterious hidden allocation.
The Lighthouse driver registered handlers for opening, closing, cleanup, shutdown, and device-control requests. Unsupported major functions pointed to the normal Windows invalid-request handler.
Following the device-control entry was difficult because the function immediately enters heavily protected control flow. Instead of trying to read the entire giant listing by eye, I used offline concrete and symbolic execution over the copied memory image.
That produced a result I did not expect.
Across several valid synthetic IRPs, multiple IOCTL values, all four buffering methods, different input and output lengths, and both debugged and non-debugged caller states, the analyzed path did not read the IOCTL code or either request buffer.
It preserved the IRP, performed protected internal work, set the result to success with zero returned bytes, and completed the request.
At the time, this made the observed route look like a parameterless signal or guarded session trigger.
The bootstrap analysis changes the wider conclusion. It is now directly established that a legitimate Lighthouse user-mode component sends several distinct, buffered device-control requests, including one carrying path-related material and another capable of returning a much larger response. Therefore, the symbolic result describes only the specific protected route reached by those synthetic states. It cannot be generalized to the complete Lighthouse protocol.
Possible explanations include another device object, an earlier session gate, state-dependent dispatch, or a protected branch that the synthetic IRPs did not satisfy. Finding that gate is now a more useful research target than assuming the driver has no command processing.
The protected device-control path performs two particularly interesting checks.
First, it hashes a selected region of Lighthouse's own executable code using an XXH3-style 64-bit hash implementation. The target region contains real compiled code, not request data.
This is consistent with self-integrity measurement or with using the image state as input to protected control flow. I did not find a clean, simple comparison that would justify claiming a complete tamper policy, but the code measurement itself is real.
Second, Lighthouse obtains the current process and asks the Windows kernel whether that process is being debugged. In this Windows build, the check corresponds to the process debug-port state.
The result changes the protected route, but both routes eventually complete the synthetic request successfully. The longer route included a huge arithmetic state-transform function that made hundreds of writes, all of them confined to its own temporary stack frame. It did not write to the IRP, process object, driver globals, or telemetry file.
That was another useful correction. A scary-looking block of thousands of arithmetic instructions was not automatically a detector or enforcement routine. In this case it was part of the control-flow protection machinery.
So the fair conclusion is that a real anti-debug query and a real self-code measurement exist. The exact policy attached to them remains partially hidden.
Following the protected unload entry led to a smaller semantic helper. The driver submits an internal event before finishing its protected cleanup path.
That event eventually reaches a kernel writer using normal file APIs. In the snapshot, the shared kernel handle resolved to:
1C:\Windows\Lighthouse.bin
This file is not the Lighthouse driver payload. It is a normal cache-backed filesystem file opened for writing, with flush and close behavior visible in the driver.
The file contained high-entropy structured data with repeated aligned sequence values and many short records. There were no useful plaintext strings or common file signatures.
The best description I can support is an encrypted or cryptographically transformed local event journal. It appears suitable for lifecycle, integrity, or telemetry records that can be flushed and potentially consumed elsewhere.
I cannot identify every event type, prove that every record is uploaded, or describe its retention policy. One unload-related event identifier was observed, but publishing raw record formats or pretending to know the entire schema would go beyond the evidence and the purpose of this article.
The driver contains broad kernel functionality and protected logic consistent with process, memory, and system monitoring. During the wider analysis I found evidence of kernel API use, callback-related paths, process identity work, and virtual or physical memory utilities.
That does not mean every available routine is active in every game session.
I also do not have enough evidence to call Lighthouse or the Elytra loader a Windows file-system minifilter. The storage stack inspected for Lighthouse.bin contained the normal volume, encryption, I/O-rate, and snapshot drivers, not a visible Elytra filter. A filesystem-related device type in the loader is not enough to prove Filter Manager registration.
The same caution applies to native callback monitoring. Callback-related code and kernel imports are useful leads, but a complete list of active callbacks requires registration-state evidence for the exact running build.
The protected Lighthouse image contains instructions such as CPUID and RDTSC among large arithmetic and control-flow sequences.
Those instructions can be used for virtualization detection, timing checks, CPU feature selection, entropy, or ordinary performance measurement. Their presence alone does not tell us which purpose applies at each site.
In the paths I reduced, many apparently meaningful instructions turned out to be part of generated state transformations. I therefore would not claim that every CPUID or timestamp read is a hypervisor detector.
The reasonable conclusion is that Lighthouse has the building blocks for timing and environment inspection, while the exact enforcement behavior remains unproven.
This was one of the biggest questions throughout the research, and it is also where it is easiest to overstate findings.
The evidence supports these categories:
The evidence does not establish a complete process-list upload, screenshot capture pipeline, recent-files collection, connected-device inventory, or the exact contents of remote anti-cheat telemetry.
There are also code paths and capabilities related to process, kernel, and memory inspection, but capability is not the same thing as a confirmed event schema. A driver can import or implement a primitive without using it continuously or reporting its result to a backend.
WARDOGS diagnostics, Elytra integrity events, gameplay analytics, and server-side behavioral signals should be treated as separate pipelines until evidence connects them.
Several defensive choices stood out positively.
None of those features makes the system automatically secure. They do show that Elytra is designed as a layered trust system rather than one signature scanner attached to a game.
Several early conclusions can now be updated:
.upx name does not prove conventional UPX packing.ZwLoadDriver path.control.exe, is the normal client for game-session RPC.C:\Windows\Lighthouse.bin is a protected local event journal, not the driver image.These corrections are the main reason I wanted to write a second version instead of quietly editing a few paragraphs in the old post.
Even with the new evidence, some important questions remain open:
Those are useful directions for authorized defensive research. They are not gaps that should be filled by guessing.
The most interesting result is not one clever anti-debug trick or one heavily obfuscated function. It is the way the pieces fit together.
WARDOGS uses its launcher to create a trusted session with a privileged modular service. The service verifies and loads signed components. A dedicated Lighthouse bootstrap reads the packaged configuration, instantiates the user-mode Lighthouse module, and opens the Lighthouse kernel device. A separate user-mode loader module starts the signed kernel loader. That loader prepares the protected Lighthouse payload and hands it to the normal Windows driver-loading path. Lighthouse then creates recognizable kernel objects, exposes a controlled device interface, accepts multiple classes of buffered requests, protects important paths with heavy control-flow transformation, inspects caller state, and records protected local events.
The system is complicated, but it is not magic. Once each component is separated by role and trust boundary, the architecture becomes understandable.
That is the value of this kind of research for me. The goal is not to turn protections into a cheat-development checklist. It is to understand how real defensive systems are assembled, where assumptions can be tested, where evidence is still missing, and how future versions can become more secure, more observable, and more reliable.
No bypass was developed, no cheat was produced, and no exploitable vulnerability is being claimed here.
This remains a defensive research piece.