Azov Ransomware: A Polymorphic Wiper Masquerading as Ransomware

December 14, 2022

The abundance of samples has allowed analysts to distinguish two different versions of Azov, one older and one slightly newer.
These two versions share most of their capabilities, but the newer version uses a different ransom note, as well as a different file extension for destroyed files (.azov).

Technical Analysis: Highlights

  • Manually crafted in assembly using FASM
  • Using anti-analysis and code obfuscation techniques
  • Multi-threaded intermittent overwriting (looping 666 bytes) of original data content
  • Polymorphic way of backdooring 64-bit ".exe" files across the compromised system
  • "logic bomb" set to detonate at a certain time. The sample analyzed below was set to detonate at 10-27-2022 10:14:30 AM UTC
  • No network activity and no data exfiltration
  • Using the SmokeLoader botnet and trojanized programs to spread
  • Effective, fast, and unrecoverable data wiper

Analysis of the Newer Azov Version

The focus is on the original sample of the newer Azov version (SHA256: 650f0d694c0928d88aeeed649cf629fc8a7bec604563bca716b1688227e0cc7e — as pointed out above, there is no major difference in functionality compared to the older version). This is a 64-bit portable executable file that has been assembled with FASM (flat assembler), with only 1 section .code (r+x), and without any imports.

Structure of the .code Section

The .code section has three parts, which are most easily seen by looking at its entropy:

  1. A very low-entropy part that appears to consist of plain strings used to construct the ransom note
  2. A high-entropy part containing the encrypted shellcode
  3. A plain code section implementing the unpacking routine

Unpacking Routine

The unpacking routine in the function AllocAndDecryptShellcode() is intentionally created to look more sophisticated than it is. But in reality, it is a simple seeded decryption algorithm using a combination of xor and rol, where key = 0x15C13.

The Wiping Routine

The wiping routine begins by creating a mutex (Local\\azov) to verify that two instances of the malware are not running concurrently.

If the mutex handle is successfully obtained, Azov creates persistence by trojanizing (similar to the backdooring routine) the 64-bit Windows system binary msiexec.exe or perfmon.exe and saving it as rdpclient.exe. A registry entry at SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run is created pointing to the newly created file.

The wiping procedure uses a trigger time - there is a loop where the analyzed sample checks system time, and if it is not equal to or larger than the trigger time, it sleeps 10s and loops again.

Once this logic bomb triggers, the wiper logic iterates over all machine directories and executes the wiping routine on each one, avoiding certain hard-coded system paths and file extensions.
Each file is wiped "intermittently", by which analysts mean a block of 666 bytes is overwritten with random noise, then an identically-sized block is left intact, then a block is overwritten again, and so on — until the hard limit of 4GB is reached, at which point all further data is left intact.

Once the wiping is finished, the new file extension .azov is added to the original filename.

Backdooring Routine

Before traversing the filesystem to search for files to be backdoored, a mutex named Local\\Kasimir_%c is created, with the %c replaced with the letter of the drive being processed.

The function TryToBackdoorExeFile() is responsible for backdooring 64-bit ".exe" files that meet certain conditions.

These specific conditions could be simplified as follows:

Pre-Processing Conditions

  • It is not a part of the exclude list of filesystem locations
  • The file extension is ".exe"
  • The file size is less than 20MB

Processing conditions:

  • The file is a 64-bit executable file
  • The PE section containing the Entry Point has enough space for the shellcode implant to be injected in the way of preserving the original Entry Point of PE (the shellcode start address will be placed at the address of the original Entry Point)
  • File size == PE size (PE size is manually calculated)

The processing conditions are all checked in the function TryToBackdoorExeFile().

Once the file meets all pre-processing and processing conditions, it is considered suitable for backdooring and pushed to function BackdoorExeFile().

Function BackdoorExeFile()

The function BackdoorExeFile() is responsible for the polymorphic backdooring of executable files.
It first obtains the address of the original code section (usually the .text section) and randomly modifies its content in several locations. Before injecting the main blob of shellcode into the modified code section, certain constant values are changed, and the whole shellcode is re-encrypted with the same encryption algorithm and key as used during the unpacking of the malware, described earlier.
After the backdoored file is written back to disk, three encoded data structures are appended to its end, which are effectively resources needed for the ransomware to function (for instance, an obfuscated form of the ransom note).

Despite the polymorphic backdooring, the encryption/decryption algorithm used during the unpacking and backdooring is consistent and can be used for Azov detection.

Anti-Analysis and Code Obfuscation Techniques

Preventing Software Breakpoints

Using routines that copy already decrypted and currently executing parts of shellcode to newly allocated memory and later transferring execution to it will sooner or later result in an exception if software breakpoints are set. In such situations, it is necessary to use hardware breakpoints.

Opaque Constants

Replacing constants with a code routine producing the same resulting constant's value. This can be repeatedly seen in routines responsible for calculating constant offsets rather than using them directly so that a direct call can be replaced with an indirect call.

Syntactic Confusion

Replacing an instruction with semantically equivalent instruction(s) that are not idiomatic or are outright bloat. One example of this is found in the routine responsible for parsing the export directory; another is the repeated replacement of a call with a direct or indirect jmp.

Dead (Junk) Code

Insertion of garbage bytes results in no meaningful instructions or even no instructions at all.

Opaque Predicates

A jz/jnz that appears to be a conditional jump but in practice always meets (or always does not meet) the condition, effectively functioning as an unconditional jump, confusing static analysis.

These two obfuscations can both be seen in the function FindGetProcAddress().