Recently published blog posts:
Go to the blog archive and browse all previous blog posts we have published so far.
Subscribe to the GovCERT.ch blog RSS feed to stay up to date and get notified about new blog posts.
Recently published whitepapers:
Subscribe to the whitepapers RSS feed to stay up to date and get notified about new whitepapers.
Report an incident: incidents[at]govcert{dot}chGeneral inquiries: outreach[at]govcert{dot}ch
The following email address can be considered as point of contact for FIRST members and other CERTs/CSIRTs:incidents[at]govcert{dot}ch
GovCERT.ch PGP-Key (preferred) Alternative GovCERT.ch PGP Key (for older versions of PGP without Curve25519 support) GovCERT.ch SMIME
Published on February 20, 2020 10:45 +0100 by GovCERT.ch (permalink) Last updated on February 25, 2020 15:00 +0100
Currently, we are observing HawkEye samples being distributed by large malspam waves. HawkEye1 is a keylogger which has been around quite a long time (since 2013) and has evolved since then and gained more functionality. There are several good blog posts about HawkEye in general 2 3. Recently we observed an interesting obfuscation method in a HawkEye binary 4, which we are going to describe in this blog post.
HawkEye is written in .NET and thus we can analyze it rather easily with the help of dnSpy 5.
Looking through the decompiled source code in dnSpy we find the method gate in the class Sinister.
gate
Sinister
The Base326 encoded strings are concatenated, then reversed and decoded. The output of the decoded string is a DLL written in .NET as well. We can easily decode the string using rev and base32 on a Linux system or with the help of similar tools.
rev
base32
After decoding, the DLL is loaded and the property iraq is set via reflection 7. This value is a concatenated string consisting of the following nine static strings.
iraq
Side note: The concatenated string used here is encoded with non-latin letters (maybe Farsi, any feedback most welcome). Trying to concatenate the strings in a text editor did not really work, however, using dotnetfiddle.net revealed the correct string. Update: According to our colleagues at FedPol/BKP the string is actually Urdu. Thanks for the hint, most appreciated.
)
After opening the DLL in dnspy, we can examine the set_iraq method. The only thing this method does is to pass the argument to the method ArgueMents.e1.
dnspy
set_iraq
ArgueMents.e1
The method e1 loads and starts another executable file (lines 34–36). In order to analyze this executable, we need to know how it is loaded into the rawAssembly array and how we can extract it.
e1
rawAssembly
Line 30 instantiates a Bitmap object with the value that was passed to the set_iraq method. Looking at the code of the method i10 we see, that the image is loaded from the ResourceManager of the main binary. The bitmap is a PNG file, located in the resource section of the main executable.
Bitmap
i10
ResourceManager
Next, the Bitmap is passed to the method e5. This method reads the width and the height of the PNG. It then loops over the height (inner loop) and the width (outer loop) and stores the red, green and blue value of every pixel into a array and returns it. The alpha values as well as all pixels that are r,g,b,a = (0,0,0,0) are omitted.
e5
r,g,b,a = (0,0,0,0)
The resulting array is then passed to the decryption method e9.
e9
In this method, a new array is created which is 16 bytes smaller than the array containing the color values. Then, the image array is copied to the new array (the first 16 bytes are omitted). The first 16 bytes are in fact the key needed to decrypt the rest of the data using XOR. There is a for loop, which will iterate over every value in the smaller array and XORs the value with the corresponding value of the key.
XOR
for
The decoded data is a PE file (exe), which is loaded into the memory. Finally the EntryPoint of the File is called.
EntryPoint
As the key is stored in the Image itself, we can write a small python script to decrypt this and similar images.
#!/usr/bin/python3 from PIL import Image import argparse KEY_LENGTH = 16 def get_color_values(file_name): arr = bytearray() im = Image.open(file_name) w, h = im.size for i in range(w): for j in range(h): r, g, b, t = im.getpixel((i, j)) # ignore zero values if (r, g, b, t) != (0, 0, 0, 0): arr.extend([r, g, b]) return arr def process(input_file, output_file): arr = get_color_values(input_file) key = arr[:KEY_LENGTH] data = arr[KEY_LENGTH:] for i in range(len(data)): data[i] ^= key[i % KEY_LENGTH] with open(output_file, "wb") as o: o.write(data) if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("input_file") parser.add_argument("output_file") args = parser.parse_args() process(args.input_file, args.output_file)
In this case, the resulting PE file is once again heavily obfuscated, the first stage is obfuscated with Babel Obfuscator 8. The final payload after several obfuscation rounds is HawkEye.
The obfuscation technique using a PNG file to store a PE file is neither new, nor very advanced. However we found it to be noteworthy nevertheless as we do not see it often. It could be an interesting way to bypass anti virus products, because the PE file is loaded directly into the memory, however in this case, the resulting HawkEye binary stores a copy of itself on the disk after the infection and therefore may be detected by AV products.
The detection rate of VirusTotal shows, that the “smuggling” technique is actually working. The initial malicious file 4 is detected by only 17 AV engines (two weeks after the first upload). The decrypted PE file9 was detected by 34 engines right after upload.
Malpedia page of HawkEye ↩︎
New HawkEye Reborn Variant Emerges Following Ownership Change ↩︎
HawkEye Keylogger – Reborn v8: An in-depth campaign analysis ↩︎
HawkEye sample analysed in this blog post ↩︎
dnSpy ↩︎
Base32 ↩︎
Reflection in .NET ↩︎
babelfor.NET ↩︎
Decrypted HawkEye Sample ↩︎
Back to top