MA 2: IsDebuggerPresent Workaround
In our previous article we were trying to break IsDebuggerPresent so we can run the program even while using a debugger, and it was extremely easy to bypass.
Now, let’s think about this from the other angle, if we were trying to stop users from debugging our program, for whatever reason, we should definitely not use this because it’s easily bypassable, we should try something else.
This is essentially the game of cat and mouse that all game anti-cheat, and cheat developers, antivirus products and malware developers are continuously playing. Once a method is used to achieve a goal, a workaround is made. Over time this has created extremely complex systems that we know today, such as kernel level anti-cheat. The rabbit hole grows and grows.
Let’s play our own mini version of this cat and mouse.
Because IsDebuggerPresent is so obvious and commonplace, programs such as x64dbg have plugins you can download to automatically detect and disable basic anti debugger detection, also known as an “Anti-Anti-Debug library” (lol). You can find it at (https://github.com/x64dbg/ScyllaHide/releases). This works by intercepting (hooking) the call to IsDebuggerPresent itself, so when your program asks “am I being debugged?”, ScyllaHide’s replacement runs in place of the real function and just returns 0. The program never finds out a debugger is attached. ScyllaHide layers dozens of these hooks across all the common anti-debug APIs, so most off-the-shelf checks are defeated automatically the moment you load it.
So in our imaginary scenario, let’s pretend that we are trying to stop someone from debugging our program, and we know if we use IsDebuggerPresent, they will be able to bypass it. Our goal is to work out a way how to check if a debugger is present without ever calling IsDebuggerPresent. In reality this would not defeat ScyllaHide because it is much more advanced than this, but this process will help us understand the next move in the cat and mouse game. once defenders learn to hook a check, attackers move to a place hooks can’t reach. We’re about to take that exact step ourselves.
To start with, lets compile a simple c program that checks isDebuggerPresent, and nothing else, just to see what it looks like:
Code:

IDA:

The label __imp_IsDebuggerPresent comes from IDA reading your exe’s import table, which literally contains the string "IsDebuggerPresent" alongside "kernel32.dll". As long as the binary uses standard static imports (which is what #include <windows.h> + a normal call produces), IDA will resolve and label it consistently.
This call to rax which contains imp_IsDebuggerPresent, is just calling the premade function and returning either a 1 or 0, with test eax, eax then checking is this 0? (0 being no debugger present)
Our custom IsDebuggerPresent:
Code:

IDA:

Now let’s dig into the IDA disassembly of our custom version and see what’s actually happening.
The C code is small, just one line of dereferencing, but it compiles down to almost exactly what kernel32 does internally inside IsDebuggerPresent. Let’s break it down.
mov rax, gs:[60h]
This is the same instruction Windows uses inside the real IsDebuggerPresent function. The gs: segment register on x64 points at thread-local storage, and offset 0x60 inside that gives us the address of the PEB (Process Environment Block). The PEB is a struct Windows maintains for every running process, holding metadata like the loaded DLL list, command line, and (importantly for us) whether a debugger is attached.
movzx eax, byte ptr [rax+2]
With the PEB address in rax, this instruction reads the single byte at offset +2 inside the PEB, which is the BeingDebugged flag. movzx means “move with zero extend”, it reads just the one byte and pads the rest of eax with zeros, giving us either 0 (no debugger) or 1 (debugger attached). Thats it, done.
In the standard version we can see __imp_IsDebuggerPresent sitting in .idata, all wired up and ready for kernel32 to be called. In the custom version, there’s no such entry. We never asked kernel32 to do anything for us, so it was never added to the import table.
We can also use IDA’s text search (Alt+T) to look for the string “IsDebuggerPresent”. In the standard version it finds the import. In the custom version, nothing is found, as it was not used.
Same question, same answer, different visibility
The two programs ask the same question and get the same answer. From the operating system’s perspective they are identical. But from the reverser’s perspective they look very different:
| Check | Standard version | Custom version |
|---|---|---|
| Import for IsDebuggerPresent | Visible | Absent |
| String “IsDebuggerPresent” | Present in .idata | Absent |
| API call in disassembly | call cs:__imp_IsDebuggerPresent | None |
| ScyllaHide API hook fires | Yes | No |
| Actual PEB byte read | Yes, inside kernel32 | Yes, inline in main |
By moving the check inline, we’ve hidden it from imports, from string searches, and from API hooks. A reverser scanning quickly for “is this program checking for a debugger?” has a much harder time finding our custom version.
The limits of the trick
So, would our custom check actually defeat ScyllaHide? Sadly, no. ScyllaHide doesn’t only hook APIs, it also writes a 0 directly to the PEB BeingDebugged byte before our code even runs. Our inline check would still return 0 under ScyllaHide, even though there’s no API call for it to intercept.
What this exercise teaches us is the shape of the next move in the arms race. Once defenders learn to patch a specific byte, attackers either move to a different byte (NtGlobalFlag, heap flags, etc), or stop relying on Windows-maintained memory at all and use CPU instructions that defenders can’t easily fake.