How to deal with AES encryption in malware analysis

How to deal with AES encryption in malware analysis

In malware investigation, there are basically two types of analysis: static and dynamic. Simply put, static analysis comprises all ways of studying a software without running it, while its dynamic counterpart is based on observing the program while it is running. It is in the interest of malware authors to make both forms of analysis difficult, thus making detection more difficult.

In this newsletter we will focus on AES encryption in malware analysis. That is, how it is employed to make the static study of malicious software more difficult and how to deal with this type of technique.

Sample chosen

This bulletin is based on a sample of the January 2022 HawkEye Keylogger (MD5:019A689DCC5128D85718BD043197B311). It is a .NET executable that employs several obfuscation techniques, including the use of AES encryption to hide important strings related to its
operation.

Being a .NET, its static analysis involves C# code. We understand that this characteristic makes it easier to understand the concepts we will present, since it is simpler to read than assembly, the language involved in malware analysis in other languages.

Finally, this sample is publicly available on the Internet. This allows interested parties to reproduce all the analysis steps outlined here on their own machines.

Identification

The employment of AES encryption will not always be obvious when analyzing the encrypted content itself. Consider the example below, one of the main functions of Hawkeye Keylogger:

Figure 1: Function with string obfuscation

At first glance, the strings declared in the above function are encoded with base64 (a crucial clue to reach this conclusion is the presence of "==" at the end of the text). Our first impulse is to decode this content and hopefully find out what the author of the malware has decided to hide from us. Let's take the string gnfyANw8 as an example:

gnfyANw8 = "EcCYDcH+PxTAszyHU/StRg=="

After decoding, we get the following result:

.À.
Áþ?.À³<.Sô.F

Ok, we are out of luck. The result is unreadable, which makes no sense for a string. If we continue looking at the code after these variable declarations, we will find a clue that their de-fuzzing does not only involve base64. Each of them is passed individually as a parameter to the same function. This behavior is demonstrated in the following code snippet:

Path.Combine(this.njZ5r2Fw(this.bfuPrPbT), this.njZ5r2Fw(this.gnfyANw8)) 

The image below shows the content of this function in its entirety.

Figure 2: Function responsible for decrypting variables

AES and Rjindael

The acronym AES refers to anencryption standard(Advanced Encryption Standard) established by NIST. This standard comprises three members of the Rjindael cipher family, characterized by the bit-size of the key used. Specifically, Rjindael ciphers that use 128-bit, 192-bit and 256-bit keys are part of AES.

AES falls into the group of symmetric encryption, which means that the same key is used to encrypt and decrypt a content. Asymmetric encryption (e.g. RSA) requires a private key to reverse; for this reason it is often used in ransomwarebut is rarely employed for obfuscation.

Cryptography is a very complex field and it is not the intention of this newsletter to explain the step-by-step operation of the AES standard. We will only explain the concepts needed to identify its presence in functions (like the one shown in Figure 2) and decrypt elements without having to execute malware (i.e., we will keep the whole approach focused on static analysis). The first step is to associate mentions of Rjindael with AES.

Next, the code in question begins to perform operations involving arrays. The relevant portions are highlighted below:

byte[] array = new byte[32];
byte[] sourceArray =
md5CryptoServiceProvider.ComputeHash(Encoding.ASCII.GetBytes(s));
Array.Copy(sourceArray, 0, array, 0, 16);
Array.Copy(sourceArray, 0, array, 15, 16);
rijndaelManaged.Key = array;
rijndaelManaged.Mode = CipherMode.ECB;

The first point of attention is the size of the array created: 32 bytes, or 256 bits. At the end of the highlighted snippet, we see that this array will be used as the key for the Rjindael cipher - that is, it is AES-256 encryption. Having this key, we can do the work of this function without actually executing it, frustrating the malware author's effort to have certain strings only decrypted during execution. We then need to manually obtain the array named array.

Creating an AES key

The steps of creating an AES key vary little between different applications. In short, an array of 8, 24, or 32 bytes (AES-128, 192, and 256, respectively) is always expected to result.

In the code we analyze this array is created with the following instructions:

byte[] array = new byte[32];
byte[] sourceArray =
md5CryptoServiceProvider.ComputeHash(Encoding.ASCII.GetBytes(s));
Array.Copy(sourceArray, 0, array, 0, 16);
Array.Copy(sourceArray, 0, array, 15, 16);

We have already talked about creating an empty array of 32 bytes in size to receive the key. Next we see a method to compute the hash of the variable s using the MD5 algorithm. This variable was declared at the beginning of the function we analyzed and has the string hlNCwqLIdBYdaloFZajsnThCVnhYFN. And why generate an MD5 hash? Because regardless of the input, this hash always outputs 16 bytes. This hash is stored in sourceArray.

Next we see two lines employing the Array.Copy method. This method is used in the presented code with the following entries:

Copy(Array, Int32, Array, Int32, Int32)

Translating: the function will copy elements from an array(sourceArray) starting at position Int32(0) to another array(array) starting at position Int32(0). This copy will be of Int32(16) elements. Remembering the characteristics of the two arrays, we understand that 16 bytes will be copied from the array containing the MD5 hash (i.e., all bytes will be copied) to the empty 32-byte array. So we have 16 bytes filled and another
16 bytes blank array.

The next line uses the same method to copy all the bytes from sourceArray again, this time to array position 15. This means overwriting the last byte of the previous operation with the first byte of sourceArray and filling the rest. We now have 31 bytes filled and one byte zeroed. To make it easier to understand, we will now demonstrate the operations described in the last two paragraphs.

sourceArray = [6E 13 31 95 5E AE 65 A4 13 8D 96 26 FC E2 CC E5] (MD5 hash of variable s)
array = [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]
(empty 32-byte array)

First copy operation:

array = [6E 13 31 95 5E AE 65 A4 13 8D 96 26 FC E2 CC E5 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00]

Second copy operation:

array = [6E 13 31 95 5E AE 65 A4 13 8D 96 26 FC E2 CC 6E 13 31 95 5E AE 65 A4 13 8D 96 26 FC E2 CC E5 00]

Here is our 32-byte key.

Deciphering variables

In possession of the key, all that remains is for us to understand how the inputs to the function that undoes the encryption are handled. This is demonstrated in the last three lines of the function:

ICryptoTransform cryptoTransform = rijndaelManaged.CreateDecryptor();
byte[] array2 = Convert.FromBase64String(input);
result = Encoding.ASCII.GetString(cryptoTransform.TransformFinalBlock(array2,
0, array2.Length));

The strings are decoded from base64 and stored in hexadecimal in a new array, array2. This array will be decrypted with the cryptoTransform.TransformFinalBlock method, converted to ASCII and stored in the variable result. Using this information together with the key we calculated in the previous item, we have created a recipe in CyberChef to undo the variable encryption employed by Hawkeye Keylogger. It is available at this link. Let's see what each variable in item 3 contains after running the decryption function:

gnfyANw8 = " JavaUpdater.exe
bfuPrPbT = "C:\Users\Ebecco\AppData\Local\Temp"
hXkfmZ = "Microsoft\Windows NT\CurrentVersion\Windows Software"
QaP8HK = "Load".

This allows us to identify the file with full path to where the malware will be copied ("C:/Users\Ebecco\AppData\Local\Temp\JavaUpdater.exe") and a new key (Load) that will be created in the registry ("Microsoft\Windows NT\CurrentVersion\Windows Software").

We can also search for other functions that call the njZ5r2Fw function to find more encrypted strings in the code. The image below gives an example:

Figure 3: Example of an encrypted string in another function

After using our CyberChef recipe we found out that "iqfiKzfW5rUJpUrUzDCUhA==" stands for "img". This is an image present in the malware's resources, which will be used in steganography to obtain a new executable that exists only in memory. But this is a subject for a future newsletter.

Figure 4: Image that will be turned into an executable during malware execution

Conclusion

Encryption is an effective method for protecting content, whether malignant or benign. Being able to identify its presence and reverse it is an important tool in a malware analyst's arsenal and greatly extends the reach of a static analysis.

As already mentioned throughout this report, the code that implements an encryption algorithm is usually reused, not written from scratch for each implementation. Thus, the concepts presented here do not only apply to the sample chosen for analysis; they can be employed in the analysis of other malware that also uses AES encryption. This is also true for the recipe we provide from CyberChef - as long as you have the key, other aspects of it (such as the key) can be adapted to undo the encryption used in other samples.

If you are comfortable programming in C#, you can also leverage the function present in the malware code to work in your favor. For this purpose we provide a generic function to reverse the encryption, courtesy of Saskia Hiltemann on GitHub.

using System;
using System.Security.Cryptography;
using System.Text;

public class Program
{
     public static void Main()
     {


               string pass = "[STRING CRIPTOGRAFADA]";
               string target= "[STRING DA CHAVE]";
               byte[] keyarray = new byte[32];

               // Base64 decrypt target string
               byte[] target2 = Convert.FromBase64String(target);

               // Set our encryption/decryption key
               MD5CryptoServiceProvider mD5CryptoServiceProvider = new 
MD5CryptoServiceProvider();
               byte[] sourceArray = 
mD5CryptoServiceProvider.ComputeHash(Encoding.ASCII.GetBytes(pass));
               Array.Copy(sourceArray, 0, keyarray, 0, 16);
               Array.Copy(sourceArray, 0, keyarray, 15, 16);

               // Set up Rijndael Decrypt
               RijndaelManaged rijndaelManaged = new RijndaelManaged();
               rijndaelManaged.Key = keyarray;
               rijndaelManaged.Mode = CipherMode.ECB;
               ICryptoTransform cryptoTransformDecrypt = rijndaelManaged.CreateDecryptor();

               // Do it
               byte[] result = cryptoTransformDecrypt.TransformFinalBlock(target2,0, 
target2.Length);
               System.Text.Encoding encoding = new System.Text.ASCIIEncoding();

               // print result
               Console.WriteLine(encoding.GetString(result));

     }
}

References

  1. https://en.wikipedia.org/wiki/Advanced_Encryption_Standard
  2. https://github.com/shiltemann/
  3. VX-UNDERGROUND
  4. https://docs.microsoft.com/en-us/dotnet/api/?view=net-6.0

By Alexandre Siviero

One Reply to "How to deal with AES encryption in malware analysis"

  1. Alexandre,
    Excellent article, congratulations! 👋

Leave a Comment

Your e-mail address will not be published. Required fields are marked with *