This presentation details the technical analysis and bypass of Denuvo's anti-tamper technology, focusing on how it functions as a hardware identifier and the challenges involved in circumventing its protections.
Mind Map
点击展开
点击探索完整互动思维导图
Can you hear me? Okay. So, hi everyone
and welcome to my talk about reverse
First, a few words about me. Who am I?
My name is Maurice Hyman and I'm a cyber
security engineer working at Talis and I
used to mod Call of Duty games back in
the day. So, maybe you know me from mods
like Boy or XLabs. And you can also
follow me on Twitter if you want.
The agenda today will be first we'll try
to understand denovo what it is. Then
we'll try to analyze it and to find out
how it works. And then we'll try to to
patch it or to bypass it. And at the end
there will be a bit of performance
reasoning to have a look at the
performance of the game.
So first let's start by understanding Denovo.
Denovo.
Maybe some of you already know what
Denuvo is. For those of you who don't
know, it's an anti-temper solution
developed by IADO. And the main purpose
is essentially to protect games from
being copied. For example, Hogwarts
Legacy so that you can't pirate the
game. And it's essentially not really a
DRM itself, but rather it's a solution
that protects existing DRM solutions.
For example, you have the Steam DRM
layer, the Steam licensing system that
Hogwarts Legacy uses, and Denovo
prevents you from removing this Steam
licensing solution. So, you need to own
a license for that game. And in my
opinion, it's one of the strongest
protections there are to this date.
So, how does Denubo work?
you have the game and on the other side
you have the denovo server. At the
beginning, the game will try to generate
a hardware fingerprint which essentially
consists of your computer name, your
username and some other identifiers that
we'll see later. Once Denovo has done
that, it will generate a so-called steam ticket.
ticket.
A steam ticket which is essentially
proof of game ownership. So that
once these two things are generated,
they're sent to the Denovo server and
the server can then check that you
really own the game by validating this
Steam ticket. So it checks that you own
the game. Then will then probably
communicate to the Steam servers, check
that you own the game, and if you really
do, it will generate a denovo token that
specifically matches your fingerprint.
And at the end, the game is able to run
using this Denovo token. So that's
that's the the startup process of denovo.
denovo.
So you might ask yourself, what's this
fingerprint? And it's essentially a
collection of features that uniquely
identify your PC. So it could be your
computer name, your username, some other
identifiers that identify your CPU,
maybe some operating system identifiers.
There is a whole list of features that
Denua uses and it varies for each game.
So not every Denubo protected game uses
the same features to identify your PC,
The denovo token itself is an I guess
it's an encrypted or encoded XML file. I
try to decode it and it looks like this.
I hope you can read it. It starts with
some app ID. In my case, it was the
Steam app ID of Hogwarts Legacy and then
it contains some user ID of the user who
owns the game in case that's my Steam
ID. And at the end there's some encoded
game token data. I didn't try to look
what that is. It looks a bit like B 64,
but it's not. So, I don't know what that
is. But the important point is that this
data belongs to your fingerprint. So, it
specifically matches the fingerprint
that was generated from your PC.
And then once the Denubo server has sent
the token back, it is being stored on
disk. And that means that online
connectivity is only required for the
first launch or if your fingerprint
changes. So normally the denovo game
will not require any internet connection
Yeah. So what is this denovo token used
for? And essentially denovo has two
phases. The one I showed before with
this ping pong process is what I call
the startup phase. And essentially it's
the fingerprint collection process and
then this token generation.
And once you have this token, there's
the runtime phase when the game runs and
you play the game. And this runtime
phase only works with a valid token. And
the reason is that Denovo continuously
validates your PC during runtime. Don't
worry, it happens outside the render
loop. But the essence is that your PC is
being validated. And I don't know
exactly how it works because I didn't
reverse engineer that in detail. But I
assume that the game reads some of those
fingerprint features. For example, your
computer name and then it uses that to
encrypt data of the game. So data at
runtime is being encrypted using for
example your computer name. And then
this token contains the necessary
information to decrypt that piece of
data again. And that means that if the
token doesn't match, for example, your
computer name doesn't match what started
in the token, then the game will
probably crash.
So that means that your PC needs to
match this token.
And what makes the Anubu so strong is
that they have an individual protection
for each game.
First, these fingerprint features, they
seem to vary for each game, maybe even
each game version. So I checked
different Hogwarts legacy versions and
it was different. And the most important
point, I think, is that there is a super
strong integration into the game. So
it's very hard if you look at for
example the game binary it's very hard
to differentiate between original game
code and then the code that Denovo
inserted to protect it and the
integration is so strong that you can't
really distinguish between the two parts
and by doing that denovo has the option
to perform this runtime validation at
thousands of different places which
essentially makes a generic crack pretty
much impossible. So if you want to crack
a denovo game, you have to look at it
each game individually and design a
crack specifically for this specific
So now you might still wonder how is it
possible to bypass denuvo and in my
opinion there are two possibilities to
do that. You can try to remove denovo
from the game. So try to separate the
denovo protection from the game and just
remove it. In my opinion that's
completely insane. I think there are
people who did that but on older the
denuber versions but on the latest
version it's just it's just an insane
task because you can't separate the two
pieces from another.
And the other way to do it is you
essentially try to patch the
fingerprint. So if the game reads for
example your computer name you try to
patch that and feed a different value
for example the value of a PC that has a
valid Denovo token. And this is what I
did. So the goal is to replay the
fingerprint features of a different PC
and then hardcode the token that was
generated for this other PC and reuse it
on on my machine.
This is what I did.
And to do that I first need to find all
the features that Denuba uses to
identify this PC and at the end I need
to patch them. But first, let's try to
focus on this analysis part,
especially on how we can find those
fingerprint features. And once I looked
at the Nuvo, I also thought to myself,
how can I do that? How can I find out
what Denuvo does and how it identifies
your PC? And what I came up with is that
Denuvo somehow must communicate with
your operating system. It needs to
communicate with your hardware, with the
file system, anything to gather
information about your PC, to gather
identifiers. And I think there are only
three main ways of communication that a
Windows process can do. The first one is
API calls. So you can, for example, call
the Windows API and get the Windows username.
username.
You can read memory inside your process
that might contain information about
your PC or Denovo could use for example
special CPU instructions that reveals certain
certain
things about you about your CPU for
example there's the CPU instruct CPU ID
instruction or the could perform sys
calls or there are other ways to to
identify a PC using instructions
so we somehow need a way to analyze all
these three types of communication
And for that I have something that's
called Zorgan which is a Windows user
space emulator that I have developed and
maybe some of you remember I was already
here last year and I presented a Windows
user space emulator and meanwhile it got
a name so it's now called Zen and the
idea behind that is that we let the game
Hogwarts Legacy run inside this emulator
which has an emulated CPU emulated memor
memory and by
doing exactly that we have the option to
completely instrument the game. So we
can look at each instruction that's
being executed because as we emulate the
CPU we can also look at the instruction
that's being executed and as we emulate
the memory we can also analyze any kind
of memory interaction
and by using this emulation approach we
have super strong instrumentation
capabilities and what Zorgan does for me
for us is that it tries to log anything
that it considers suspicious. So it will
look at any instruction, any memory
access, any API call. It will analyze
anything and it will print it out in a
console for us to look at it. And I hope
that this works because I will try to do
a demo.
You can check it out on zogan.dev if you
want, but I hope it's readable.
And this is the website. You can also
find my talk from last year at the
bottom if you're interested on the
technical details on how it works. But I
also managed to compile this emulator to
web assembly. So it should run in the
browser. I hope
so. If we click on start, it should try
to load a file system that mimics
Windows. So it's essentially some kind
of mini Windows emulator inside your
browser. You will also see some Windows
DLS in there. And what we can do now, we
can add a file,
this Hogwarts legacy binary,
and we can just let it run. I hope
everything is readable.
Let me check the settings. That looks
good. So what what's happening now is
that inside my browser this Hogwarts
legacy Windows binary is being executed
within the emulator. And on the right
you can for example see the amount of
instructions that are executed. You can
see how much memory the process
consumes. Yeah, I guess that's readable
and the amount of active threads. And
after some time we should see some logs.
And the first thing we see is that
Hogwarts legacy or to be precise this
denovo startup sequence seems to be
executing virtual protect and we don't
care about that. We can see some import
redexes. Let's also skip about that. We
can see that it tries to access somehow
the command line. So there seems to be a
way to configure Denovo via your command
line but I don't know how. I didn't look
into that.
And once we skip over this, we can also
see that it tries to read environment
variables. That's maybe not super
readable, but it's also some Denuvo
debug stuff. So you can apparently
configure Denovo
to do some debugging, but we don't care
about that. And this is now where the
interesting part starts. So we can see
that Hogwarts Legacy executes a virtual
alloc function. And it does that by
allocating memory that is readable,
writable and executable. And what I
think is usually at least other denovo
protected games use that to like
outsource protected code into this
executable section and execute it from
there to make reverse engineering
harder. But it turns out that this is
not used in Hogwarts legacy.
So after that we can see that the
emulator prints that there was a import
read access and what this means is that
denovo reads from the import table of
the game. So the table that contains all
the imported API functions and denovo
tries to access them and I think what it
does here it takes for example this one
the API call selfread affinity mask. It
uses the address that is stored in this
import table and that in and then it
compares it to for example another
import from the same library and by
essentially subtracting these two
addresses you get the distance of those
two functions within for example kernel 32DL
32DL
and if you upgrade your PC
then chances are higher that kernel 32DL
also changes and that means that the
distance of those two functions funs
will also change because the DLA likely
got recompiled. So by measuring the
distance between two functions of the
same library, Denubo can essentially see
that your PC has not changed and they
seem to use that as a fingerprint
feature. I'm not sure if they're exactly
using the distance or something else,
but by accessing the import table,
Denovo starts to gain insight about your
PC and tries to form this fingerprint.
So this is the first thing that happens
and once we scroll down we can see the first
first
interesting thing that happens. We can
see that denovo executes a CPU ID
instruction. And for those of you who
don't know what it does, it's an
instruction that Intel provides to
deliver information about your process.
for example, special capabilities. If
there's AVX instruction support or
things like that, and using the leave,
that's what it's called, you can get the
the feature set that your CPU supports.
And Denovo seems to at least use this
returned value as part of the
fingerprint to identify your PC.
And now
it we can see a few lines below, it also
acts as a few other leaves. And I think
those three leaves contain the model
name of your CPU. So if you have an
Intel i7 CPU, then that's the string.
It's stored in there and Denua accesses
that and uses it as an identifier.
We can see also an access to K user
shared data. And for those of you who
don't know what that is, that's a
special memory region that each Windows
process has. And it contains data that
processes might need but they need it
without a big overhead. So for example,
if you want to measure the time between
two functions, then you can access the
system time through this K user shared
data memory region. So it contains
system time, processor count, memory
size that how much RAM you have. So it
contains all sorts of data that needs to
be accessed frequently
and specifically denov at this point
accesses this time zone bias which
probably contains the time zone you're
in. But I think this is not part of the
fingerprint. At least I tried to patch
that and it didn't change anything. So I
assume they use this to seed some random
number generator or something. So this
might not be relevant as an
identification feature.
A bit down below we can see like this
whole chunk essentially belongs
together. We can see that it accesses
the process environment block and then
it accesses NDLL
but we'll skip over that for now. I come
back to that later.
Here we can see that Denuvo accesses K
user shared data again and it reads for
example the product type you have. So
for example if you have Windows
professional or enterprise or something
like that. It also reads the amount of
physical pages you have. So essentially
the amount of RAM that your PC has. And
it also accesses the active processor
count and the build version. So all
these fields and memory accesses are
part of the fingerprint that Denovo generates.
generates. And
And
down below we can see the first
interesting API call that happens which
is called expand environment strings.
And that's an API provided by Windows to
essentially expand environment
variables. And we can see that denubo
tries to fetch your computer name using
this API call.
So this is definitely also part of this
hardware ID fingerprint that Denovo
collects. Then some more time zone
stuff. So I assume this is RNG again.
Then it executes the CPU ID instruction
again. So we can see that Denovo
performs the same actions multiple
times. So it might execute CPU ID
multiple times even though it already
got that information. And
And
down below we can now see again that
Denuba is reading from NDLL.
And what's happening there or rather in
this whole block is
is
I think
Denuba is not performing an integ
integrity check because I think there is
already research on denovo and there
were people who said that Denuba is
doing an integrity check on NTDL and
essentially verifying which Windows
version you have by doing that but I
think that's not what happens because if
we scroll Scroll down to this line. We
can see that Denovo executes a sys call
instruction to query information about
your system
and it obviously uses that as part of
your fingerprint because this returns
information that could be helpful to
identify your PC. But the important
thing is that to be able to issue a sys
call instruction, you need the sys call
ID. And the sys call ID can be obtained
by crawling all the exports from ndll
and then disassembling all the the
functions until you find the one with
the right sys call ID. And I think this
is what happens here. So, Denovo is
accessing NTDL and reading all the all
the function names and trying to find
the one that matches this sys call and
then it's disassembling all the code to
find the matching sys call ID to be able
to perform this inline sys call.
Denuvo also executes a RDTSC
instruction. I don't know what that's
used for and again I assume they
probably seed some internal RNG using
that but I might be wrong. Same goes for
this RNG seed version. I think this is
not used to identify your PC but it's
rather used to seat some RNG which makes sense.
sense.
Yeah. Again query system information sys
call. So we see it's doing the same
thing multiple times and we'll later see
why it does that.
But right here we can see that it
accesses the process environment block.
If you don't know what that is, that's a
block of memory that every Windows
process has and it contains information
about this process. So it contains the
environment variables. It contains the
command line and it also contains
process ID and everything that's
relevant and unique to this process.
And by doing that, Denuvo accesses
environment variables directly.
But I think in this case, this is also
not for the fingerprint because I tried
to patch that and it didn't change
anything. I think this is something that's
that's
either proceeding an RNG or Denovo has
something that I think it's called path
execution paths and the idea behind that
is that for example you have a function
in Hogwarts legacy that's responsible
for casting a spell and I think what
denovo does it takes this code of that
function and it duplicates it a few
times for example there are three three
variants of this function which all do
the same thing they cast a Well, but
Denovo protects them differently
and it can then at runtime decide which
one of them to execute. So it has
different execution paths and by by
doing that as a cracker you have to find
all these execution paths and patch all
of them and I think the access to those
environment variables here is used as
some kind of selection mechanism for
those execution paths. So depending on
how your environment variables are set
up for the process, it decides to
execute a different code path.
So not part of your fingerprint, I
think, but code path selection
down below. It also accesses the PEBB
and tries to get version information
about Windows and this is definitely
used to identify you. It also accesses
the number of processors. So similar to
what's stored in K user share data they
have quite a big redundancy within the
data they use
again here CPU ID
and here we can see another API call get
keyboard layout and I think again this
one is not relevant for the fingerprint
but rather is used for the scope path
selection stuff so I did not patch that
at the end again trying to read the
computer name using this expand
environment string.
It also accesses your system time and
I'm pretty sure that is part of this RNG
seeding process. So we can safely ignore
that. Here it reads four imports again.
So it accesses the import table again
and reads four functions from
ADV API and kernel 32DL
and those are definitely relevant. So
they're using the addresses stored in
the import table to identify your PC.
We can also see an API call get username
which obviously returns your username
and all this gray stuff is uninteresting.
uninteresting.
It fetches your Windows directory.
So where your Windows installation is
located on your drive. It also reads
information about your C drive. I don't
know exactly what this returns but
information about your hard drive. It
again fetches the computer name using a
different API.
So it has your your computer name
multiple times. Probably uses that to to
verify that you did not patch anything.
And at the end we can see an API call
called cryp acquire context.
And this essentially accesses Windows
crypto library. So this is the crypto
API that Windows provides. And I think
you can use that to as a fingerprint
feature. And the way it works is
probably that you can call this API and
tell it to encrypt some chunk of data.
And the key that's used to encrypt this
chunk of data is some key that's
specific to your PC. And by issuing this
API call, the Nuvo can get data that is encrypted
encrypted
in a manner that is unique to your PC and
and
essentially use that as a fingerprint feature.
feature.
And I think that's it because down below
we can see that it starts loading the
Steam API DL. And I obviously don't have
Steam inside this Windows environment in
the browser. So this will obviously fail.
fail. And
And
can you still hear me?
Okay, good.
And then at the end we will see some
it's still trying to load the Steam API
and we can see a message box at the end
that it's not able to load the Steam API
dl and then it just dies.
But we don't care because we got
everything that we're interested in.
However, at the end, the emulator prints
as an instruction summary. So, what this
is is the emulator counts the CPU ID uh
the the CPU instructions that were
executed and lists them in ascending
order. And we can for example see the
sys call instruction that it executed.
But we can also see for example an x get
pv instruction. This one behaves similar
to CPU ID. So it essentially I think
returns capabilities about your CPU. So
Denuvo also uses that as an identifier. And
And
I think
other Denuvo versions also make use of
special instructions that behave
differently on different CPUs. there
exists instructions that for example if
you execute them on an AMD CPU that
behaves in a slightly different way than
if you execute it on an Intel CPU or for
example if you executed on an old Intel
CPU it behaves differently. So there
there is a bunch of instructions that
behave in that way and the nuvo
definitely uses them in newer versions
but I think Hogwarts Legacy doesn't make
use of any of them in general if it does
they should be among the first 10 to 15
instructions but I don't think there are
any special instructions in there with
that behavior but I might be wrong and
that's about it. So I know that that was
a lot of input, but you don't have to
remember everything. But the emulator
essentially helped us to to analyze what
the Denovo binary does and how it
identifies your PC. And now that we know
the identifiers that Denovo uses, we can
try to to patch them and to to replay
these identifiers from a different PC.
And this is what we can do now.
We'll try to patch Denovo, but the way I
did it is just a proof of concept. So, I
was not interested in cracking the game.
I don't have the time for that. So, I
try to more or less create a proof of
concept to run the game on my PC.
And the way I patched API calls in
general. So for example this get
username or get get windows directory.
The way I patched them was by just
hooking them because it seems that
denovo does not have any kind of
integrity check on those API calls. So
the way it works you see that denovo
calls for example get username a and
what I did is I created a fake get
username a that returns the name of my
other PC and I essentially inserted a
jump instruction on the original. So it
always redirects to this fake one and then
then
that's fine because denovo does not
verify that and that way we can
essentially bypass all API calls at
least in this Hogwarts lexi version.
The next thing is import integrity and
patching that was also relatively easy.
I did that by allocating a trampoline at
a fixed memory location and then I
jumped to the original import.
So you have the game import table which
essentially is a list of all functions
from libraries and what I did was I
allocated a chunk of memory at this
exact address and it it's always this
address and I just write this address
into the game import table and here I
just in also create a jump instruction
that forwards to get username a so When
Denuvo reads the import table, it always
sees this address. No matter which
version of this DLL is installed on your
PC, it will always be this address. And
for the next import, it will then be the
next address in there. So all imports in
the game will always have the same
address. And that way the the feature
that DNU collects will also always match.
The next identifier is all the entries
in the process environment block.
to patch that also took the easy route.
I essentially unprotected the memory and
overwrote all the data with constant
values. So for example, if you the the
OS version that's stored in there, I
just wrote the OS version of my PC and
this can definitely have undesired side
effects. So, for example, if a library
within the process reads this Windows
version to determine if if it supports
some special API or not, then your PC
can obviously crash if you don't have
the right version in there. But I didn't
really care. It was just a proof of
concept. So, I overwrote everything I
could and that way I was able to patch
Patching K user shared data was pretty
hard and this is where it starts getting
super technical. So don't mind if it
gets really technical but the way I did
that is overwriting memory like for the
previous one did not work. I think
that's because the kernel prevents that.
And what I did instead I tried to find
every location in the game that accesses
this memory region. So I attached a
debugger and I did some sampling and I
played the game and to essentially I
tried to collect every memory access
every instruction that accesses K user
share data and this is obviously pretty
trash. So this I will definitely miss
places and with this path selection
thing that Denubo does I will definitely
not have explored all the code paths
that there exist. But again I I I didn't
care. It's just a proof of concept.
And using this sample data at runtime, I
then create a hook by disassembling the
original function instruction and then I
create a fake K user shared data and I
redirect this memory access there. So
it's a pretty bad approach, but for my
For the CPU ID instruction, I was way
too lazy to redo what I did for K user
shared data and I made use of a hypervisor.
hypervisor.
Maybe some of you are familiar with
that. A hypervisor is mostly a driver or
a standalone piece of software that is
used to virtualize operating systems. So
that's the core part of software that is
used to run virtual machines on your PC.
So for example, if you want to run Linux
on Windows or the other way around,
there is a hypervisor that manages all
that. And the way virtual machines work,
the way they are so fast is because most
instructions run on your real CPU except
for a few special instructions that
should not be executed on your real CPU.
There is a way for the hypervisor to
intercept those. And this works by
registering a call back at the CPU.
And if one of those instructions is
executed, the hypervisor is notified and
then can deal with it, for example, by
returning the values that it needs to.
And you don't need to run VMs using a
hypervisor. You can just make use of the
fact that you can register this callback
at the CPU. And this is exactly what I
did. So I just register this callback
which is called a VM exit handler. And
by doing that I'm able to intercept any
time the CPU executes a CPU ID
instruction. So if your game runs and it
executes a CPU ID instruction, my
hypervisor driver is being notified that
the game does that. And then I can check
where does this instruction come from.
So I check if it's Hogwarts legacy and
then I just patch the values that it
returns. And by doing that, I can also
patch this X get DB instruction by
disabling the feature bit in CPU ID that
tells Denuver that my CPU has that. And
I then essentially mimic the other PC.
Doing that is technically really
dangerous because you tell the game that
it has CPU capabilities that it really
does not have. And this can definitely
have undesired consequences if you run
it. But again, I didn't care. It was
just a proof of concept. I just wanted
to get the game run without owning it, so
so
For the inline SIS calls, I
was not able to patch them because it
turns out that Denuvo has some mini
integrity checks on them. So, you can't
just patch the memory where those
instructions are. They need to stay
intact. That means we're unable to hook them.
them.
And at this point I just made use of the
hypervisor again. And it turns out that
a hypervisor can hook CPU ID
instructions directly. But my my
hypervisor wasn't able to do that at
that time. So I made use of something
that's called EP hooking.
Unfortunately, I don't have the time to
explain in detail what that is. So if
you want to know more about EP hooking,
you can follow this link. But in short,
it allows to modify code in a way that
the user mode process is not able to
see. So you modify the the code the code
bytes and the user mode process still
thinks that the memory is completely
intact when reading it. But when
executing it, it executes the patched
one. So the hypervisor allows to do that
and by that I can patch the sys call instructions.