Background
In August 2015, a Turkish university student released a proof-of-concept ransomware, dubbed HiddenTear, to his GitHub repository. His justification at the time was to intentionally utilize poor security practices so that any bad actors that wished to copy his code would create ransomware that was fundamentally flawed. While this was done with good intentions, it opened the flood gates for low-skill authors to create their own variants. Eventually, the controversy created led the original author to take down the source code, however by that point the damage had been done: there were dozens of variants scattered around the web. One such variant, that this post will explore, is the NegozI infection.
We were able to obtain a sample of the infection for testing. Since it is written in C# decompiling using dotPeak was trivial. Overall, the code is very similar to HiddenTear with a few new "features" attempting to make it harder to crack. We were eventually able to get an efficient decryption method working. Example snippets of code, which may not work on their own, are presented in this blog and a complete working example is linked at the end of the post.
Application Process
The process starts by attempting to delete all ShadowCopies on the machine. Then, the program enumerates all drives looking for files with the following extensions:
string[] words = new string[38]
{
".txt",
".pdf",
".frm",
".msg",
".asmx",
".rpt",
".arw",
".qbo",
".qbw",
".sldprt",
".dwf",
".doc",
".adi",
".adt",
".docx",
".altr",
".xls",
".xlsx",
".ppt",
".pptx",
".odt",
".jpg",
".png",
".csv",
".sql",
"sln",
".php",
".asp",
".aspx",
".html",
".xml",
".psd",
".bat",
".js",
".css",
".sqlite",
".dwg",
".jpeg"
};
The rest of the logic simply takes all the found files and attempts to encrypt them using a Parallel ForEach loop. Interestingly, this feature was only added in .NET 4.0 so there are many machines where this particular variant would fail to run entirely.
Encryption
For the actual encryption the process is pretty straightforward.
For each file, there is a call to the following function using a new instance of .NET's Random class, specifying an integer between 30 and 50 which controls the length of the returned password.
public static string GetPass(int x)
{
string str = "";
Random random = new Random();
while (str.Length < x)
{
/* This is the range for ASCII
So converting from an int to a char will return a valid ASCII character
*/
char c = (char) random.Next(33, 125);
if (char.IsLetterOrDigit(c))
str += c;
}
return str;
}
var password = GetPass(new Random().Next(20, 50));
The resulting password is hashed using SHA256, which is then fed to the actual encryption function. The file is encrypted in memory with AES256.
private static byte[] Encrypt(byte[] bytesToBeEncrypted, byte[] passwordBytes)
{
byte[] salt = new byte[8]
{
(byte) 1,
(byte) 2,
(byte) 3,
(byte) 4,
(byte) 5,
(byte) 6,
(byte) 7,
(byte) 8
};
using (MemoryStream memoryStream = new MemoryStream())
{
using (RijndaelManaged rijndaelManaged = new RijndaelManaged())
{
rijndaelManaged.KeySize = 256;
rijndaelManaged.BlockSize = 128;
Rfc2898DeriveBytes rfc2898DeriveBytes = new Rfc2898DeriveBytes(passwordBytes, salt, 1000);
rijndaelManaged.Key = rfc2898DeriveBytes.GetBytes(rijndaelManaged.KeySize / 8);
rijndaelManaged.IV = rfc2898DeriveBytes.GetBytes(rijndaelManaged.BlockSize / 8);
rijndaelManaged.Mode = CipherMode.CBC;
using (CryptoStream cryptoStream = new CryptoStream((Stream) memoryStream, rijndaelManaged.CreateEncryptor(), CryptoStreamMode.Write))
{
cryptoStream.Write(bytesToBeEncrypted, 0, bytesToBeEncrypted.Length);
cryptoStream.Close();
}
return memoryStream.ToArray();
}
}
}
The resulting encrypted bytes are then written back over the original file, which is then moved to a .evil extension. A handy HTML page with decryption directions is written to each directory that had files encrypted as well, complete with a non-existent e-mail address and a very real BitCoin one. However, there is a problem. The encryption keys are generated and thrown away. Even if the ransom was paid there would be no way to give up the keys and decrypt the files.
The Flaws
The most obvious issue is the static salt, however that only does so much good if the password were truly a random 30-50 character one. Luckily, it isn't entirely random. Much like the original HiddenTear, the use of the .NET built in Random class is the real weakness as it is not cryptographically secure. Given a static input the output will always be the same. By default, the uptime of the system (in "ticks" or milliseconds) is used. This flaw is detailed in a post by the creator of HiddenTear. However, because of a few decisions by the author of this particular variant, there are some complicating factors.
First, there are actually 20 possible passwords for each "tick", depending on what length between 30 and 50 was generated. Second, the use of the parallel loop makes the time between when the password was generated and finally written to disk much more difficult to predict. Over a relatively slow network link we observed as much as half an hour of difference. Finally, the use of the Rfc2898DeriveBytes means that the passwords are hashed before being used in the encryption function. This greatly slows down any brute forcing attempt.
Decrypting
Since the Random function uses the system uptime as a seed, it is important to know how long the machine has been running (in ticks). Luckily, this information is logged to Event Log every day at noon with event ID 6013. Once the boot time is known, the initial possible seed can be calculated based on the file write time.
public static int getTicks(DateTime bootDate, string decryptFile)
{
var lastWrite = File.GetLastWriteTime(decryptFile);
var dateDiff = lastWrite - bootDate;
var currentTick = Convert.ToInt32(dateDiff.TotalMilliseconds);
// add 1000 millisecond to the start to account for lack of precision in last write
currentTick += 1000;
return currentTick;
}
This value can then be used in a modified version of the password generation function to return predictable results:
public static string GetPass(int x, int seed)
{
var str = "";
var random = new Random(seed);
while (str.Length < x)
{
var c = (char) random.Next(33, 125);
if (char.IsLetterOrDigit(c))
str += c;
}
return str;
}
With that, brute forcing can begin by trying each set of passwords per tick against the following function. It will simply return nothing if the decryption fails. In testing, we found that the AesCryptoServiceProvider class was faster than the class used in the original malware code. They're both using AES under the hood so there was no compatibility issue.
private static readonly byte[] saltBytes = {1, 2, 3, 4, 5, 6, 7, 8};
public static byte[] AES_UM_Decrypt(byte[] bytesToBeDecrypted, byte[] passwordBytes)
{
byte[] decryptedBytes = null;
using (var aes = new AesCryptoServiceProvider
{
KeySize = 256,
BlockSize = 128,
Mode = CipherMode.CBC
})
{
try
{
using (var ms = new MemoryStream())
{
var key = new Rfc2898DeriveBytes(passwordBytes, saltBytes, 1000);
aes.Key = key.GetBytes(keyBytes);
aes.IV = key.GetBytes(ivBytes);
using (var cs = new CryptoStream(ms, aes.CreateDecryptor(), CryptoStreamMode.Write))
{
cs.Write(bytesToBeDecrypted, 0, bytesToBeDecrypted.Length);
cs.FlushFinalBlock();
cs.Close();
}
decryptedBytes = ms.ToArray();
}
}
catch (Exception e)
{
}
return decryptedBytes;
}
}
We found that the function would sometimes return data as if it had successfully decrypted the file when it in fact had not. This was solved with a simple file header verification function.
Speeding Things Up
As mentioned above, the password and IV is run through a hashing algorithm significantly slowing down our brute force attempts. Because of the time-based nature of the password generation, however, the application would end up trying the same set of passwords against different files if they ended up being written around the same time. This means if we could cache already generated passwords then any future attempts that fell in that range would be significantly faster. We initially tried simply storing them in a file but this proved to be slow and difficult to do look ups against. We eventually moved to an application designed especially for caching: Redis. Since everything in Redis is stored in memory, fetching the already calculated keys and IV is extremely fast.
Since Redis supports storing raw bytes, we were able to store all possible passwords per seed value into a hash, which can then be easily looked up by the current tick being attempted. Better still, by adding the ability to add an offset when starting the decryption process we could distribute our decryption attempts across multiple machines all using the same Redis server. StackExchange's Redis library provided a simple way to interact with Redis.
// seed and directory of passwords for easy storage and indexing
public class CalculatedSeed
{
public int seed { get; set; }
public Dictionary<RedisValue, RedisValue> hashValues { get; set; }
public CalculatedSeed(int seedValue, Dictionary<RedisValue, RedisValue> dict)
{
seed = seedValue;
hashValues = dict;
}
public CalculatedSeed()
{
seed = 0;
hashValues = new Dictionary<RedisValue, RedisValue>();
}
}
public static byte[] genSha(string pass)
{
return SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(pass));
}
/* Takes input seed and generates passwords of lengths between 20-50 characters
* These passwords are used to generate a key and an initialization vector, which are each stored as bytes
* These values can be stored in Redis as byte arrays so that they only ever have to be calculated once
*/
public static HashEntry[] createPwdHashes(int seed)
{
var hashList = new List<HashEntry>();
foreach (int length in pwLengths)
{
// ransomeware actually used the SHA256 value of the password, not the actual generated password
var pass = genSha(GetPass(length, seed));
var passDeriveBytes = new Rfc2898DeriveBytes(pass, saltBytes, 1000);
var passBytes = passDeriveBytes.GetBytes(256 / 8);
var blockBytes = passDeriveBytes.GetBytes(128 / 8);
var combinedArray = new byte[passBytes.Length + blockBytes.Length];
Buffer.BlockCopy(passBytes, 0, combinedArray, 0, passBytes.Length);
Buffer.BlockCopy(blockBytes, 0, combinedArray, passBytes.Length, blockBytes.Length);
hashList.Add(new HashEntry(Convert.ToString(length), combinedArray));
}
return hashList.ToArray();
}
public static CalculatedSeed pullCache(int seed, ConnectionMultiplexer redis)
{
var db = redis.GetDatabase();
var pwdHash = new HashEntry[1];
var seedResult = new CalculatedSeed();
try
{
pwdHash = db.HashGetAll(Convert.ToString(seed));
seedResult = new CalculatedSeed(seed, pwdHash.ToDictionary());
}
catch (RedisConnectionException)
{
}
return seedResult;
}
// attempt to pull from redis cache
try
{
cacheHit = pullCache(seed, redis);
}
catch (Exception)
{
cacheHit = new CalculatedSeed();
}
// check if hash was pulled, otherwise generate seed results in place, then attempt to add to redis
if (cacheHit.hashValues.Count != 0)
{
seedResults.Add(cacheHit);
}
else
{
var pwdHashes = createPwdHashes(seed);
var hashDict = pwdHashes.ToDictionary();
try
{
db.HashSet(Convert.ToString(seed), pwdHashes);
}
catch (Exception)
{
Console.WriteLine("Warning: Unable to write to Redis");
}
seedResults.Add(new CalculatedSeed(seed, hashDict));
}
With a little scripting in our remote management utility, we were able to decrypt several hundred files in a few days by running the application in the background on workstations. As long as the application was run single threaded, performance impact was minimal.
Big thanks to Fabian Wosar and Michael Gillespie for their assistance pointing us towards the flaw in the password generation function and pointing us in the right direction with encryption.
The full working code can be found here.