Reworking HidGuardian


Hellas,

I’m a bit late on this fairly breaking news – unbelievable, I know – but here we go. You might have already had the honor to play around with my bastard-child device “firewall” HidGuardian. If you haven’t; check it out! In a month or so. When the new version is ready. Hopefully. Gah 😅

Now, seriously, what’s the bloody deal here you might ask. Good question! But first I have to explain a bit where we come from and why this project is such a mess.

I originally started working on HidGuardian (which shall be referred to as HG for short from now on) to combat one simple problem: doubled input when using DS4Windows (currently actively maintained fork) with a game supporting both native DualShock 4 and Xbox 360 controllers. Because since DS4Windows emulates an X360 pad after manipulating the controller inputs according to the users desires you basically end up with two different input devices presented on the system, both sending state changes nonetheless. An option to hide the physical DS4 from other processes than DS4Windows got added but as far as I recall this feature was faulty and unreliable on Windows 10 due to some design changes of the operating system.

To combat this issue I spun up a small PoC project about two years ago showing that one basically only needs to intercept and fail the CreateFile API calls within the game process when it tries to open and therefore read directly from the DS4. It was working just fine. But relying on API hooking isn’t the smartest idea. Granted, it’s powerful and may work 99% of the time but there are too much pitfalls, especially false Anti-Virus flagging and possibly triggering Anti-Cheat mechanisms in multi-player games which is considered unhealthy for the innocent player.

So what to do? Well, if you need more power over user-land shenanigans you need to descend into the pits of hell kernel-mode where those handy and powerful filter driver reside. Being no stranger to kernel drivers I attempted to solve the challenge in a more elegant way than hooking into processes. Why not just sit above device in question and handle blocking open requests from there? This provides a low-level common gateway where all processes have to pass through transparently plus while running with kernel privileges we get to have the last word. Period.

Once in kernel-land we get the advantage that all requests targeted at the desired device have to go through out filter driver first. The user-mode call  CreateFile will invoke the WDF callback EVT_WDF_DEVICE_FILE_CREATE within the driver, having to pass the filter before the function driver of the device receives it. At this stage we can examine the request and basically make the decision if we’ll forward (a.k.a. allow) or complete it with an error state (a.k.a. deny). One major limitation (or at least a bigger challenge) is the fact, that you don’t get much information along with the request other than the PID (Process ID) the callback was invoked under. But that was enough to work with for a first PoC. Hence HG version 1 was backed rather rushed but provided proof that I was on the right track.

Now simply denying every request and therefore hiding the device from practically all processes is fun but not especially useful. To gain more control over the devices data flow at least some processes still need access (like feeders/sinks for ViGEm, or remapping Software like InputMapper, DS4Windows, x360ce etc.) so a mechanism for whitelisting was required. Therefore I – once again in a rush – exposed the ability to tell the driver which PIDs should get the privilege of access via a privileged user-mode service and a simple library which would communicate with the service so that the application referencing the library wouldn’t depend on elevated privileges. This worked surprisingly well. Until you think about it for approximately two seconds. First trouble which surfaced was orphaned PIDs in the registry. If the process utilizing my library crashed or decided to ignore calling the clean-up function, the service wouldn’t remove the PID and the driver would allow access to any process which would randomly match the stale PID later on. Less than ideal. Plus depending on the to-be-whitelisted process to call an external dependency isn’t always possible either. The solution? Redesign-Time! Yay! 😆

Meet HidGuardian v2

Or rather, don’t. It was a failure. Back to the drawing board! 😞

Meet V3!

Now here we go! A lot has changed on both design and code-base to deliver a more use-case-oriented solution for – well – everybody and not just developers. The most noteworthy change involves the evolution of the user-mode service (tenderly named HidCerberus) which communicates with the filter driver via inverted calls and gets asked for every request the driver receives. Information like Process, Hardware and Instance IDs of the device in question gets passed up to user-land where the service can then use standard Windows API to reverse-lookup the Process ID to name, path and other properties utilized as a base for decision-making. A simple plug-in architecture can then offload the decision making process and tie it to literally any form of 3rd party code (rules from a YAML/JSON file, a Webhook or DLL call, Named Pipe, the current weather in Timbuktu etc.) and passes the results back to the driver, which will then forward the queued request to the target or complete it with a failure, thus denying access. Doesn’t that sound like a step up? It’s almost like I listened to the community feedback for once 🤓

What’s next?

Now hold your horses, I know this all sounds awesome and you probably wanna get your fingers on it and throw it in production but let me tell you that although the driver is in a stable condition, there’s still an awful lot to do on the user-mode side of things and *roll the drums* documentation. At the time of writing I’m also hopping around other projects related to my good ol’ fella the DualShock controller and trying to improve other things around my ecosystem like documentation, website, the beloved Discord server, documentation and last but not least: life! 😮

That’s it?

Yep. I can’t really promise much other than: stick around and watch for news 🙃

Cya!

Liked it? Take a second to support nefarius on Patreon!