its probably checking if the exception handler list has a changed entry like when someone adds their own exception handler to the process then it can detect it?
it also could be that they are checking if the ExceptionList ptr has been changed?
also interesting function i wonder how you found it/why it isnt virtualized
Its not virtualized because its an instrumentation callback. I wrote a blog which covered some aspects of the callback and how to locate it. You can read it here.
Every process on Windows has a section of memory that stores the KUSER_SHARED_DATA structure, almost always located at 0x7FFE0000. After replacing the inlined memory references with their corresponding structure fields, the code looks like this:
#define KUSER_SHARED_DATA (uint64_t)0x7FFE0000
const uint64_t tickCountMultiplier = *(uint64_t*)(KUSER_SHARED_DATA + 0x4);
const uint32_t tickCount = *(uint32_t*)(KUSER_SHARED_DATA + 0x320);
const uint64_t timeSinceBoot = (tickCount * tickCountMultiplier) >> 0x18;
Here, the time since the system booted is extracted and computed. Looking further, you can see timeSinceBoot being compared to the current exception list. If the values match, the exception list pointer is decoded and passed into a large jump table. That logic looks something like this:
const void* exceptionList = NtCurrentTeb()->NtTib.ExceptionList;
if (timeSinceBoot != (uint64_t)exceptionList)
{
const uint64_t value = (uint64_t)((int32_t)(exceptionList >> 0x20) - 1);
// The following code would look something like:
// switch (value)
// case 0: { ... }
}
At first, this looks strange since a constant value is being compared to a pointer, but this is intentional. Hyperion performs much of its initialization inside its instrumentation callback, and this is where most of that setup happens.
Normally, NtTib->ExceptionList points to the chain of exception handlers registered in the process’s address space. Hyperion, however, does not rely on this list. Instead, it maintains its own internal structure for registered handlers, hardcoding some of them as well. You can see evidence of this inside the KiUserExceptionDispatcher callback. Rather than using the exception list for its traditional purpose, Hyperion repurposes it to store different initialization states, which are then handled by the switch statement mentioned above.
At the end of the switch statement, you will find code like this:
NtCurrentTeb()->NtTib.ExceptionList = (uint64_t)timeSinceBoot | (exceptionList & 0xFFFFFFFF00000000);
Here, the exception list is reset to the time since system boot, signaling to future calls that initialization has completed.
could you explain why it cant be virtualized just because its an instrumentation callback?
The instrumentation callback is triggered on every transition from kernel mode to user mode. This means that every system call made within your process’s address space will return to the callback (if registered) before jumping to the original return address.
While virtualization is powerful for obfuscation, it has a severe impact on performance. It can slow down your application by nearly 2x, which is especially critical in game engines where performance is extremely important.
ooo makes sense and also what do you recommend to defeat virtualization?
do you think emulating is a good option?
That could work, I’ve seen a lot of new work surrounding more dynamic devitalization techniques. You can also try to match individual VM handlers using some IR like Triton.

