Packers are often used to hide the functionality of executable files from unwanted observers. This can be done for legitimate purposes, such as protecting your intellectual property, but also for malicious purposes; for example, hiding malicious content from security products.
ReversingLabs TitaniumCore is a fully automated static analysis engine with inspection capabilities that must overcome these obfuscation attempts in order to uncover the component elements, typically binary files. To improve visibility in our products, we continuously improve unpacking and broaden our support for new packer formats- some solely dedicated to hiding malware.
Legitimate-purpose packers are usually advertised on company websites, and are not difficult to find. On the other hand, packers used for malicious purposes are advertised in various hacking forums and websites usually not so open to the public. Those malicious packers are mostly advertised as truly, fully undetectable, and guarantee a period of this undetectability to the buyer. Their price varies from a few dollars to several thousand. During an investigation, we came across a packer named NTCrypt.
Crypter advertised as available on a purchasing website
NTCrypt was being sold with a $950 monthly subscription, and we wanted to see what its authors offer for this price. One way to discover that is similar to playing poker - pay the price to see the cards. Since gambling is not really our game, we wondered if there was another, more reliable way to find out what NTCrypt does.
Our Titanium Platform is used by many customers for getting insight into files entering their business networks. While analyzing those files, Titanium Platform extracts metadata from different file formats, including Microsoft PE files. This haystack of files in the cloud can be searched based on that extracted metadata from Titanium Platform’s A1000, our cybersecurity threat analysis solution.
Not a lot of information is available about NTCrypt on its advertising page. In fact, the only thing we found was the product name, so we gave that a try. The A1000 Advanced Search allows us to find files even without precisely knowing the exact term we are searching for. This can be done by expanding the part of the term we know with wildcard characters. Searching metadata extracted from files for pe-product-name:NTCrypt* returned a few results.
Processing those files with our platform gave us detailed insight into their content. Most of them were quite similar native binaries, compiled in the range Tue Dec 06 11:20:04 2016 +/- 20 minutes. They all had the same “Rich header” and the same “imphash”. One interesting thing was the comment in the “Version info” structure which gave us a clue about the packer's real purpose (if there were ever any doubts).
Version info structure
The next step was to execute those files and try to see what options they offer in the packing process. Unfortunately, nothing really happened after starting the files. No user interface was displayed, no error message shown, no login screen presented. Nothing happened. It was a dead end - or was it?
Among our previous search results was also one PE/.Net Exe file. Its “Version info” structure was the same as for the other files. The main difference was the PE type - the earlier file versions were native PE files, but this one was a PE/.NET file. While browsing the website where the packer was sold, we encountered a few videos demonstrating the usage of the packer. One of the videos unveiled the contents of the directory where NTCrypt is run from.
Contents of the NTCrypt folder visible in the advertising video
The collected file had the same icon as the file visible in the packer's advertising video. Executing it returned a few errors about missing components (probably other files visible inside the directory shown in the video), but skipping through them actually got us to the user interface - the same one we saw in the video.
Packer builder user interface
So we found ourselves a packer. Now we only needed to figure out how it works.
When reversing packers, we usually have files generated with it, or the packer executable itself which we then use to generate the files we are going to reverse. Let's try to encrypt a file with the program we just found. We give the program the path to the file we want to pack, leave all other settings on their default values, and try to create the packed executable. It's not successful; instead we get an error about a missing stub.
Missing stub error
What is a stub? A stub is a skeleton program that knows how to unpack the files created with the target packer. During the packing process, the packer “patches” the stub with configuration values so the stub could know where in the packed file it can find the data it needs to unpack. Stubs are usually distributed together with the packer, but we don’t have them, since we found the packer executable in our cloud platform.
Another potential problem for us is that packers sold for malicious purposes often get distributed with “unique” stubs for different buyers, so there could be many different stubs in the wild. But we will solve those problems later; for now, let’s give the packer a dummy stub just to bypass the error. The packer doesn’t complain about the missing stub anymore - the new problem is the missing “CryptEngine.dll”.
Missing CryptEngine.dll error
We saw that file in the advertising video, and also clicked through a warning related to it during the packer's startup. Now we have obviously reached the point where we have to supply it to the packer if we want the packer to do anything. The DLL name tells us that this is probably the main component of the packer. The only things we know about this file are its name and size visible in the video. Unfortunately for us, A1000 search queries based on different variations of pe-product-name and pe-original-name in combination with the file size didn’t return any results.
Let’s try to find more information by examining the packer executable we had already found. Looking at the files extracted from this PE/.Net executable reveals that there is a DLL embedded in it.
DLL embedded in executable’s resources
Examining metadata extracted with Titanium platform shows us export names, including the original file name which confirms our assumption that this could be the missing “CryptEngine.dll”.
After putting the extracted DLL into the NTCrypt folder, renaming it to its original name, and starting the packer, we no longer get the “missing CryptEngine.dll” warning. Instead, we are now stuck in a login loop with all of the user interface controls disabled. A workaround for this problem is to temporarily rename “CryptEngine.dll” to something else, start the packer, and when it fully loads, rename “CryptEngine.dll” back to its original name. However, after this, trying to pack our program fails again (now with an unknown crash), but manages to create the output file we specified.
The output file and the stub have the same file size. Analyzing them with a hex editor indicates some differences. It looks like the packer crashed after configuring the stub. That is great - now we can see what the packer is modifying in the stub. Processing both files with Titanium platform shows us that the packer changes the file's checksum, timestamp, and debug directory entry fields in the PE header. It also modifies some data in the “.data” section.
Our earlier hypothesis was that “CryptEngine.dll” is the core component of the packer and does all the packing logic. Since our packing attempt failed with an unspecified error, the best thing to do now would be to reverse-engineer “CryptEngine.dll” to see how it works and what it does.
Disassembling it with IDA gives us a detailed insight into the packing process. Previously, our static analysis indicated that this DLL exports only one function named “CryptFile”. Looking at this function in IDA shows that it receives 10 arguments including FilePath, StubPath, OutputPath, and SectionName. It looks like we are dealing with quite a smart function.
CryptFile function declaration
The packing algorithm could be roughly described as follows:
read packing target into memory
if compression enabled:
compress packing target with LZ compression
encrypt packing target with Blowfish algorithm
configure the stub:
generate “random” timestamp
generate XOR encryption key from timestamp and entry point
encrypt packing configuration with generated key
write XOR encryption key to “.data” section
Write encrypted packing configuration to “.data” section
write stub to output file
evade ASLR if configured
generate random section name if configured
AddSection to output file
calculate new sizeOfInitializedData and write it to output file
generate new value for debug data directory field in the PE header
write that value to file
There are a few interesting things in that algorithm that we’re going to use to hunt down packed samples in our cloud. The first specific thing is the way XOR encryption key for configuration encryption is generated. The low word of the entry point becomes the high word of the key, and the low word of the generated timestamp becomes the low word of the key.
XOR key generation
The second significant thing is the position in the “.data” section where the generated XOR encryption key is stored. It is exactly at the beginning of the second half of the section's raw data. Immediately after the XOR key follows the configuration encrypted with a simple algorithm, using this XOR key among other arithmetic operations. The data stored in this configuration includes random seed used in the Blowfish algorithm, and other options that can be configured during the packing process (like compression or startup name).
The third notable thing is the AddSection function included from another DLL called - you would never have guessed it - “AddSection.dll”. It takes four parameters: OutputFilePath, OutputFileBuffer, OutputFileSize, and SectionName. We can take a pretty good guess that this function adds a section named SectionName to the file located at OutputFilePath. The section's content is filled with data from OutputFileBuffer, which is OutputFileSize bytes large. If we wanted to make our packer work, we could implement “AddSection.dll” on our own, but that is not really necessary since we already know everything “CryptEngine.dll” does.
AddSection call from CryptEngine.dll
Now that we know how the packer works, the only thing left unknown are the stubs. Remember those versions of NTCrypt mentioned earlier in this text - the ones that seemed not to do anything when executed? That looks very similar to the behavior of an unconfigured stub. Reversing one of those stubs in a debugger confirmed our assumptions. A few parts of the stub are encrypted with a simple XOR-NOT algorithm. This is the first layer of encryption, and it changes from stub to stub. It is used to encrypt the second layer Blowfish decryption we found in CryptEngine.dll. Setting a breakpoint after the part of code which decrypts the first layer of encryption reveals that encrypted data contains default S-box values of the Blowfish algorithm.
Decryption routine and decrypted default S-box values
The same default S-box values are also present in the CryptEngine.dll.
Default S-box values in CryptEngine.dll
We are now quite confident that these are the actual stubs, and that we can reverse them in detail, as well as create signatures used for identification. The only problem is that we have only 7 stub samples at the moment. As mentioned earlier, custom packers very often provide “unique” samples per buyer. It would be nice if we had more samples, as that would allow us to detect variations between stub versions and adapt our signatures so they could match a broader set of samples. The A1000 threat analysis platform offers us a way to create YARA rules that apply to all files that are being processed by our Titanium Platform. Its retrohunt feature even allows us to match those YARA rules against all samples that have been processed by the Titanium Platform in the last 90 days. So let’s go and hunt those stubs down.
uint16(0) == 0x5A4D and
(uint32(uint32(0x3C) + 4 + 20 + 16) & 0x0FFFF) != 0 or
(pe.timestamp & 0x0FFFF) != 0
for any i in (0..pe.number_of_sections - 1):
pe.sections[i].name == ".data" and
(pe.sections[i].raw_data_size >> 1)
( (uint32(uint32(0x3C) + 4 + 20 + 16) & 0x0FFFF) << 0x10) |
(pe.timestamp & 0x0FFFF)
Yara rule for detecting NTCrypt packed samples
We created our YARA rule based on functionalities implemented in CryptEngine.dll, because that part of the packer is most likely to stay unchanged between versions. Most often, packer creators only generate new stubs or add new controls to the packer user interface, but the main packing logic implemented in a special DLL is very likely going to remain the same between smaller version changes. After we created the new YARA rule, we launched a retrohunt and it matched around 400 new files.
Yara retrohunt results
After analyzing those files, we discovered two new stub versions that significantly differed from the initial samples found earlier. We modified our signatures so they could match those stubs. Also, our YARA rule is continuously finding new stubs that use the same logic implemented in CryptEngine.dll. This approach offers our customers a great way to automatically discover new threats when they appear in the future.
Win32.Trojan.Konus sample AV detection rate
Titanium Platform analysis also showed that NTCrypt-packed samples often have a low AV-detection rate. Although most of the AV solutions do eventually detect them during run-time, it is often enough for malware to slip by email and web gateway protections, and cause damage to an inadequately protected system.
Win32.Trojan.Remcos sample AV detection rate
The analysis also revealed that NTCrypt was used in Remcos and DarkRat campaigns. Especially interesting was the DarkRat packed sample. While the unpacked sample is detected as Win32.Trojan.Darkrat, the original NTCrypt-packed sample has a different detection - Win32.Trojan.Fuu. This illustrates how packing can hide the true nature of a threat.
Darkrat sample AV detection
Packed Darkrat sample AV detection rate
In this blog we’ve demonstrated how our Titanium Platform can assist with threat hunting and malware analysis on a real-world example, the malicious packer NTCrypt. We’ve presented the features and functionalities of some of our analytic tools. Combine those with a large collection of metadata, and you can really accelerate analysis and quickly find that needle in a haystack.
NTCrypt builder (SHA256)
NTCrypt Stubs (SHA256)
NTCrypt packed Win32.Trojan.Konus sample (SHA256)
NTCrypt packed Win32.Trojan.Remcos sample (SHA256)
NTCrypt packed Win32.Trojan.Darkrat sample (detected as Win32.Trojan.Fuu) (SHA256)
Unpacked Win32.Trojan.Darkrat sample (SHA256)