Chip Security Testing 
Binary Security Analysis 
Resources 
Blog
Contact us
Back to all articles
Binary Analysis

"Hack Trick" write-up: DG'hAck 2022

10 min read
Edit by Mathieu Favréaux • Nov 28, 2022
Share

This challenge was a fun dive into a malware-like sample & kernel structures. Here is how we solved it:

Challenge presentation

The challenge's presentation is the following:

One of our employees recently tried to install a pirated game and as a result compromised his/her system.

The antivirus had been previously deactivated, and the executable turned out to be a malware.

We retrieved the executable file and now need your expertise in order to understand what the malware does.

Original in French:

Une de nos employées a récemment compromis son système en essayant d'installer un jeu cracké.

Son anti-virus avait été désactivé et l'exécutable s'est révélé être un malware.

Nous avons récupéré l'exécutable et avons désormais besoin de votre expertise pour comprendre ce que ce malware fait.

When starting the binary in a virtualized environment, we are greeted with the following error message:

first-rec-screenshot.png

Has the malware already deployed? Or is it really an error message? Let's try to solve this challenge using a dynamic-first approach with REVEN & timeless analysis.

VM uptime

A quick record shows the following call tree, prior to the end of the program:

First record call tree

We can recognize two things from there:

First, the executable is (as expected) packed & obfuscated. This is made obvious by the various calls to VirtualProtect, LoadLibraryA. Also, if we take a look at the memory history of the code in location +0x1070, we see it has been extracted by the program.

We can ignore all that and jump straight to the 3 calls at the bottom that catch our attention:

  • GetTickCount
  • CreateFileW
  • GetTickCount

At first glance, we could assume the two calls of GetTickCount are used to measure the time spent in the CreateFile call.

However, when looking at the condition of the 2nd call to GetTickCount, it is obvious that its return value is used as is:

First record Second Tick Count

The program is simply checking whether the system's uptime is superior to 1 hour and if not, starts loading our error message at +0x1554. But then what about the first call to GetTickCount? Tainting that one's return value shows is copied over to r14 but apparently never used, making this call a possible red herring.

Defeating this first anti-analysis measure is simple:

  • Either wait for the uptime to be high enough,
  • Or force the system uptime.

We choose the 2nd path as the quickest. In the above image we see the address to the tick counter memory location at 0x7ffe0320. This value is updated by regular interrupts. Since the recording environment is a VM running in emulation, we can use GDB to connect to QEMU's gdbserver and edit the value ourselves:

(gdb) p/x (unsigned long)*(0x7ffe0320) $3 = 0xc725 (gdb) set {unsigned long}(0x7ffe0320)=0x36ee90 (gdb) p/x (unsigned long)*(0x7ffe0320) $4 = 0x36ee90 (gdb) c

Fun fact: after updating the value to force the uptime from a few minutes to an hour, the screen saver kicks in immediately and turns the screen black.

The file access

With this first obstacle bypassed, we are greeted with a different error message:

> game_crack_vs.exe [!] Failed with error: 2

We record a 2nd trace to see what is going on. This time we find that the failing check is against the return value to CreateFile, so let's take a closer look to this call:

CreateFileW called at #3844876 call qword ptr ds:[rip+0x11e8c] lpFileName = \\.\Htsysm72FB dwDesiredAccess = 0xc0000000 [...] -> 0xffffffff

The program is trying to open a file named \\.\Htsysm72FB with R/W permissions. Of course, this file does not exist on the system, so the call fails & the program stops.

As a first approach, let's try to change the file name being open. Again, we'll use QEMU's GDB server to patch the memory.

(gdb) b *0x7ffcef994270 Breakpoint 4 at 0x7ffcef994270 (gdb) c Continuing. Breakpoint 4, 0x00007ffcef994270 in ?? () (gdb) set {char}$rcx = 'C' (gdb) set {char}($rcx+2) = ':' (gdb) set {char}($rcx+4) = '\\' [...] (gdb) set {char}($rcx+20) = '\0' (gdb) c Continuing.

Note: we place the breakpoint on CreateFileW at 0x7ffcef994270 in the system library because this address does not change between traces as we do not restart the VM between records, but restore a same live snapshot.

Note: if you know a simpler way to write wide char strings to memory, I'll gladly take it.

We get a different error message this time and the call tree gives us an interesting clue:

Wrong file clue

The program is trying to call DeviceIoControl against our file: this means it's not expecting a regular file, but a driver. In retrospect I guess the filename made this obvious already.

DeviceIoControl called at #23226158 call qword ptr ds:[rip+0x11d3c] hDevice = 0x1ac dwIoControlCode = 0xaa013044 lpInBuffer = 0xfc268ffae0 nInBufferSize = 0x8 lpOutBuffer = 0xfc268ffad8 nOutBufferSize = 0x4 -> 0x0

The IOCTL number is 0xaa013044. A quick Internet search shows our program is trying to communicate with a driver named Capcom.sys. This driver normally gets installed along with a game, and is very trivially used to execute code in the kernel, which the crackme is trying to do here.

Installing the driver

Hence, our next step is to install the driver in our VM:

Install driver

And with that done, we record the execution of our binary again. And now our VM crashes:

Driver crash

Again, the trace points us to the bug: we clearly see the capcom.sys driver executes a payload at 0x26c1dd20008:

Trace of driver crash

And that payload's address is indeed coming straight from our call to DeviceIO:

DeviceIoControl called at #4255203 call qword ptr ds:[rip+0x11d3c] hDevice = 0x1c4 dwIoControlCode = 0xaa013044 lpInBuffer = 0xafe6f3fca0 nInBufferSize = 0x8 [...] -> ?
[0xafe6f3fca0] == 0x26c1dd20008

However, this payload does not seem to work as expected.

Debugging the payload

Let's extract our payload and take a closer look with Binary Ninja:

Payload disassembly

This reveals it is accessing the KPROCESS structure, at an offset of +0x2e0 and +0x2e8. However, this offset is outside of 1903's KPROCESS structure, and this will point into the container of KPROCESS: EPROCESS. In this structure, it is obvious that +0x2e8 is not a pointer, so our offset are wrong here.

However, there is a double linked list nearby: ActiveProcessLinks. It would make sense for the payload to parse the active processes and do stuff. Is there a version of this structure where +0x2e8 points to this list? As a matter of fact, there is: 1809's EPROCESS has this list right where we want it. So the code is doing the following:

void* gsbase struct KPROCESS* myself = *(*(gsbase + 0x188) + 0xb8) struct KPROCESS* proc = myself do // Look for SYSTEM process proc = proc->ActiveProcessLinks.Flink proc = proc - 0x2e8 while (proc->UniqueProcessId != 4) int64_t rcx_2 rcx_2 = (*(proc + 0x348)) rcx_2.b = rcx_2.b & 0xf0 *(my_proc + 0x348) = rcx_2

But what about +0x348? This points to the back link of SessionProcessLinks, which doesn't make much sense, especially since our trace shows they are NULL pointers in the target SYSTEM process. Again, looking around the versions of this structure, we find that in Windows 8's EPROCESS, +0x348 points to Token (while ActiveProcessLinks is still at +0x2e8), which makes much more sense - we're trying to steal the SYSTEM token. Hence, we can now decompile our payload as:

void* gsbase struct KPROCESS* myself = *(*(gsbase + 0x188) + 0xb8) struct KPROCESS* proc = myself do // Look for SYSTEM process proc = proc->ActiveProcessLinks.Flink proc = proc - 0x2e8 while (proc->UniqueProcessId != 4) struct _EX_FAST_REF rcx_2 rcx_2.Value = proc->Token.Value rcx_2.Value.b = rcx_2.Value.b & 0xf0 myself->Token = rcx_2

Note: the token's lower 4 bytes which are set to 0 are the RefCnt.

From there, we have two options:

  • Either use the right Windows version,
  • Or make the payload work on our version

Patching the payload should be as easy as patching a few bytes, so let's try that. We want to turn this:

488b9be8020000 mov rbx, qword [rbx+0x2e8] 4881ebe8020000 sub rbx, 0x2e8 488b8be0020000 mov rcx, qword [rbx+0x2e0] ;[...] 488b8b48030000 mov rcx, qword [rbx+0x348] ;[...] 48898848030000 mov qword [rax+0x348], rcx

into this:

488b9bf0020000 mov rbx, qword [rbx+0x2f0] 4881ebf0020000 sub rbx, 0x2f0 488b8be8020000 mov rcx, qword [rbx+0x2e8] ;[...] 488b8b60030000 mov rcx, qword [rbx+0x360] ;[...] 48898860030000 mov qword [rax+0x360], rcx

We can patch it at one of two stages:

  • Either patch the packed binary itself
  • Or patch the payload right before execution

We first tried to patch the binary itself because it was more fun, half expecting it to trip a checksum somewhere.

Patching the binary

There are five bytes to patch, and for each a similar method is used. We'll use the value e0 as example:

  1. Taint the instruction's byte backward to see where it's coming from: Taint to follow patch
  2. We find the origin is written from an instruction's immediate, which the taint does not follow:
    mov dword ptr ss:[rbp-0x80], 0x2e08b
  3. We taint this immediate again, and we find the moment it gets unpacked, giving us the origin's location within the executable itself.
    mov dl, byte ptr ds:[rsi]

Surprisingly, once patched this way the binary still seems to work. Moreover, the system now does not crash (hurray) and we see the following calls:

Patched calltree

Some calls to access the registry stand out here. And after a closer look, we see that they write our flag to the registry key \\HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run\Flag.

RegSetValueExW called at #6398375 call qword ptr ds:[rip+0x11af6] hKey = 0x1cc lpValueName = Flag Reserved = 0x0 dwType = 0x1 lpData = DG\x04ACK{6\x01Fe9npGl\x01\x01xSOfkSQ7H\x01 cbData = 0x38 -> 0x0

Wait... The flag is obviously corrupted. Is this due to us patching the binary? It probably is.

Patching the payload instead

Let's fall back to patching the in-memory payload instead, and keep the binary pristine. Again, we simply use GDB against the VM to break at 0xfffff8000dc40573, where the driver is about to execute the payload, and patch the few bytes.

This time, the call to RegSetValueExW looks right:

RegSetValueExW called at #19879296 call qword ptr ds:[rip+0x11af6] hKey = 0x1cc lpValueName = Flag Reserved = 0x0 dwType = 0x1 lpData = DGHACK{6Qve9nFGlpFxSOfkS57} cbData = 0x38 -> 0x0

Woohoo!

Could we have seen the flag in memory prior to this call? Well, not really: the memory access history shows that it is deobfuscated right before the call to RegCreateKeyExW(note that in the screenshot below, the grey characters are a predictive display - they are not yet written in memory but about to be):

flag-deobf.png

In conclusion, solving the issues step by step, making a new recording each time, in order to force the challenge to run until the end turned out to be an effective approach.

Share

Categories

All articles
(108)
Binary Analysis
(57)
Chip Security
(43)
Corporate News
(18)
Expert Review
(6)
Time Travel Analysis
(13)

you might also be interested in

Chip Security

The backup superhero of Post-Quantum Cryptography

8 min read
Edit by Jad Zahreddine • Oct 24, 2025
CopyRights eShard 2026.
All rights reserved
Privacy policy | Legal Notice
CHIP SECURITY
esDynamicExpertise ModulesInfraestructureLab Equipments