Ransomware has been a growing issue for some time now. It has evolved into a big business, moving millions of dollars yearly from victims’ pockets into those of attackers.
The modus operandi of ransomware authors is to infect your machine through any vector (phishing, drive-by browser exploits, waterholing, etc.) and then proceed to encrypt your important files. After the encryption is performed, the key used is sent back to the attackers, who will then ask for a ransom in return for the key to get your files back.
Checkmarx, in collaboration with Check Point, was given a few ransomware malware samples to reverse engineer and audit for vulnerabilities. The samples given were all C# applications and the most interesting one we looked at was JobCrypter.
JobCrypter was a ransomware found in the wild in mid-2016 that asked for a 300 euro ransom after it infected your system and encrypted all your files. After you paid the ransom, the attackers would send you back the password, which you could then enter on the malware’s GUI to recover your files.
When we began, we tried to run the malware sample in an isolated environment, however nothing happened. So, we took the sample to ILSpy to analyze the code and scan it for vulnerabilities using Checkmarx CxSAST.
This allowed us to understand why the sample was failing: There was a ‘killswitch’ that would stop the ransomware from going into effect if the procedure of sending an email back to the attackers with the encryption password failed (which it did, with a ‘wrong credentials’ message from the email provider they were using). For testing purposes, we patched the original binary (using the ReflexIL plugin for ILSpy) to remove the killswitch and allow the malware to proceed with the infection process even if the email failed to send.
Going back to the code, one of the most critical findings reported by the CxSAST static analysis solution was the use of a cryptographically weak pseudo-random number generator.
For ransomware, this is a critical hit. A flaw in the password generation function creates a dramatic reduction in the number of possibilities the CreateRandomPassword() function can actually generate.
The malware authors call CreateRandomPassword with the length parameter hard-coded at 20, meaning they want the length of the resulting password to be 20 characters.
Since the character set used on the function is only digits, then one would expect the number of total possible combinations to be 10^20, because each of the 20 characters has 10 possible values (0-9).
This construction would be fine if there were no bias coming from the Random object that was used to select the characters in the “text” string. However, it is possible for the ransomware victim to calculate the seed value of the Random object used at the time of the encryption, thus being able to recreate the same password that was used by the virus to encrypt their files.
By default, the Random class seeds itself with a 32-bit integer called Environment.TickCount, which is the number of processor ‘ticks’ since boot. The seed is the only value needed in order to replicate the state of the random number generator.
You might have noticed that 32-bit is a lot smaller than 10^20. That realization is the first step towards recovering the key — you can brute-force all passwords returned by CreateRandomPassword(20) just by seeding the Random object with all values from 0 to 2,147,483,647. This takes approximately 18h using an off-the-shelf CPU. We wanted to do it even faster.
The next step is to further narrow the interval between the minimum possible Environment.TickCount value and the maximum.
At first we built a naive proof-of-concept that could crack the decryption password with only one caveat: the computer could not be infected later than 24h after boot. This helped us establish a maximum TickCount value lower than the maximum possible 32-bit value, thus lowering the time to crack to about 10 minutes.
Immediately after building the first proof-of-concept code, we figured out how to reduce the time to crack to under 10 seconds.
The fact that the decrypter tool would be running on the infected machine, allowed us to leverage crucial information about it, namely:
- When was the first file encrypted (FileCreated timestamp)
- When was the last boot time *before* the first file was encrypted
If we use these two timestamps, we can actually calculate a very close approximation of the TickCount value at the time when CreateRandomPassword(20) was called by simple taking the time difference between the first file’s encryption and the last boot time before that, and multiplying by 1000 (more or less the number of ticks in a second).
After we get this approximation, we use a standard offset of about 80.000 ticks where we look for the password. In under 10 seconds, the encryption key is found, as is shown in the video below.
The source code for the decryption tool will soon be available on Checkmarx’s GitHub.