Dealing With Script Kiddies - Cryptear.B Incident

16 January 2016

Two days ago, Securityweek reporter Eduard Kovacs contacted with me and asked if I heard about the Cryptear.B incident. Well I wasn't aware of that. I read the Trendmicro's post . It seems another group used my Hidden Tear code and caused some trouble again. In Trendmicro's article they claimed that the file recovery is not possible because the ransomware also encrypts the decryption key.

Another point which makes this attack unique is the fact that the generated key is lost within the valid file. The generated decryption key is saved inside a .txt file. Once dropped in the desktop folder, the malware starts encrypting files. Like other crypto-ransomware, this makes it very difficult to recover the files, even after the victim pays the ransom.

I sent them few mentions and told if they send me the malware maybe I can crack it because I had serious doubts about they didn't even try for it.

 

They didn't send me it. However, another security researcher Yonathan Klijnsma sent me the sample even if he is seriously against the Hidden Tear project. In this article he says:

“There is no educational purpose for releasing source code for a piece of ransomware,” Klijnsma told SecurityWeek. “Cryptographic implementations to secure files, sure, ransomware no. We have too much to deal with already, you really don't want to help anyone in that business.”

Initial Analyze

I already explained the security flaw in Hidden Tear in this article. You can check it if you don't know about it.

So I decompiled the ransomware to check the codes. It appeared to a fork of Hidden Tear Offline Edition . Offline edition was coded for the computers which has no internet access. You can read the details in this post

Let's check some piece of codes from the Cryptear.B

namespace hidden_tear_offline
{
	public class Form1 : Form
	{
	private string userName = Environment.UserName;

	private string computerName = Environment.MachineName.ToString();

	private string userDir = "C:\Users\";

	private string usbPassword = "text.txt";

	private string pcPassword = "\win.txt";

	private string backgroundImageUrl = "http://i.imgur.com/C8GwYiO.jpg";

	private IContainer components = null;

They didn't even change the project name. Let's check if they fixed the security flaw or not.

public string CreatePassword(int length)
{
	StringBuilder stringBuilder = new StringBuilder();
	Random random = new Random();
	while (0 < length--)
	{
		stringBuilder.Append("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890*!=&?&/"[random.Next("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890*!=&?&/".Length)]);
	}
	return stringBuilder.ToString();
}
 

They still using the .Net's Random Class which is seeded with Environment.TickCount (gets the number of milliseconds elapsed since the system started) . Which is reduces the surface of brute forcing and beyond that it’s easy to predict.

Let's check AES function.

	
public byte[] AES_Encrypt(byte[] bytesToBeEncrypted, byte[] passwordBytes)
{
byte[] result = null;
byte[] salt = new byte[]
{
	1,
	2,
	3,
	4,
	5,
	6,
	7,
	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(memoryStream, rijndaelManaged.CreateEncryptor(), CryptoStreamMode.Write))
		{
			cryptoStream.Write(bytesToBeEncrypted, 0, bytesToBeEncrypted.Length);
			cryptoStream.Close();
		}
		result = memoryStream.ToArray();
	}
}
return result;
}
 

They didn't change it a bit. It still using static salt for encryption and reuses the IV.

Understanding The Process

Cryptear.B works almost same with the Hidden Tear Offline. So let's talk about basic features of Hidden Tear Offline. It's designed for running inside a USB stick. When an attacker runs the ransomware inside a USB stick the basic workflow is:

  • ransomware clones itself to computer
  • creates a random encryption key
  • copies the key to usb stick and computer
  • waits for a specific time
  • encrypts files and drop a ransom note
  • removes key from computer.

So let's look at how Cryptear.B works. I recorded a video in my VM.

When you run the ransomware the basic workflow is:

  • It creates a random encryption key
  • Copies itself
  • Drops the encryption key to Desktop
  • Waits for a specific time
  • Changes the background
  • Encrypts the files

As you can see in the video, the encryption key is dropped to desktop before it starts to action. However most of people won't catch it.

The other silly mistake is when you run it for the second time, it gives an error and terminates before encryption. We can see the created key. However, this key is not same with the first key. So it won't help us. I just wanted to show how it's bad implemented.

And the Trendmicro was right. It doesn't send the key to a server and it just encrypts the key file. Even the ransom is paid, attackers can't recover the files. If it doesn't have a security flaw of course..

Recovering the Files

As I said before it still has the same security flaw with Hidden Tear. But it also have a flaw that Hidden Tear doesn't.

Remember that we need to have at least one plaintext version of an encrypted file in Hidden Tear's recovery process. Because the process was basically like this:

  • Decrypt the encrypted file with a key
  • Decrypted text == our plaintext?
  • If yes, we got the key
  • If no, try it with another key

In Cryptear.B we don't need to have a plaintext file. Remember that we have a encrypted text file in desktop which is includes the key. Plaintext version of the text file is in this format as you saw in the video:

"computername randomkey" | "dene-pc asjasblablabla*"

So the recover process will be:

  • Decrypt the encrypted text file with a key
  • Decrypted text == computer name + key?
  • If yes, we got the key
  • If no, try it with another key

Again, all we need to do is finding the seed. We can get it from timestamp of encrypted text file with File.GetLastWriteTime Method. Then we convert it to Environment.TickCount to get exact integer.

Let's check if we can find the encryption key first. Here is my Poc (note that "s1fGvX/XFh?QImK" string is generated by Cryptear.B):

  

namespace crypter_decrypter
{
class Program
{
static void Main(string[] args)
{

        string path = @"C:\Users\dene\Desktop\text.txt.lock";
        var timestamp = File.GetLastWriteTime(path) - DateTime.Now.AddMilliseconds(-Environment.TickCount);
        int ms = (int)timestamp.TotalMilliseconds;
        int count = 0;
        string password = "";
        int diff = 0;

        while (password != "s1fGvX/XFh?QImK")
        {
            password = CreatePassword(15, ms - diff);
            Console.WriteLine("Trying: " + password + " " + "Count: " + count);
            count++;
            diff++;
        }
        Console.WriteLine("Found: " + password + " " + count);
        Console.ReadLine();

    }

public static string CreatePassword(int length, int seed)
{
    const string valid = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890*!=&?&/";
    StringBuilder res = new StringBuilder();
    Random rnd = new Random(seed);
    while (0 < length--)
    {
        res.Append(valid[rnd.Next(valid.Length)]);
    }
    return res.ToString();
    }
    

And the result is:
Ekran Resmi 2016-01-16 15.06.58

Ok we got the key but there were to many tries. It was around 30 in Hidden Tear recovery

The reason is: It takes current system time as seed, generates a random key with it and waits for a long time. After then it encrypts the files. So when we get the file's last write timestamp, we have bigger time gap comparing to Hidden Tear. But still, it's not a huge gap.

Ok we know that our method is still working. So let's focus on a real scenario. We can recover the key file with this Poc. It works with the process which I described above:

namespace crypter_decrypter
{
class Program
{
static void Main(string[] args)
{
    string path = @"C:\Users\dene\Desktop\text.txt.lock";
    string draftdata = " ";
    var timestamp = File.GetLastWriteTime(path) - DateTime.Now.AddMilliseconds(-Environment.TickCount);
    int ms = (int)timestamp.TotalMilliseconds;
    int count = 0;
    string password = "";
    string inside = "";
    int diff = 0;
    while (true)
    {
        password = CreatePassword(15, ms - diff);
        Console.WriteLine("Trying: " + password + " " + "Count: " + count);
        byte[] bytesToBeDecrypted = File.ReadAllBytes(path);
        byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
        passwordBytes = SHA256.Create().ComputeHash(passwordBytes);
        byte[] bytesDecrypted = AES_Decrypt(bytesToBeDecrypted, passwordBytes);
        draftdata = System.Text.Encoding.UTF8.GetString(bytesDecrypted);
        inside = "DENE-PC-dene " + password;
        if (inside == draftdata)
        {
            break;
        }
        diff++;

    }

    Console.WriteLine("Found: " + password + " " + count);
    Console.ReadLine();

}

public static string CreatePassword(int length, int seed)
{
    const string valid = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890*!=&?&/";
    StringBuilder res = new StringBuilder();
    Random rnd = new Random(seed);
    while (0 < length--)
    {
        res.Append(valid[rnd.Next(valid.Length)]);
    }
    return res.ToString();
}

public static byte[] AES_Decrypt(byte[] bytesToBeDecrypted, byte[] passwordBytes)
{
    byte[] decryptedBytes = null;

    // Set your salt here, change it to meet your flavor:

    // The salt bytes must be at least 8 bytes.

    byte[] saltBytes = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 };

    using (MemoryStream ms = new MemoryStream())
    {
        using (RijndaelManaged AES = new RijndaelManaged())
        {

            AES.KeySize = 256;
            AES.BlockSize = 128;

            var key = new Rfc2898DeriveBytes(passwordBytes, saltBytes, 1000);
            AES.Key = key.GetBytes(AES.KeySize / 8);
            AES.IV = key.GetBytes(AES.BlockSize / 8);

            AES.Mode = CipherMode.CBC;

            using (var cs = new CryptoStream(ms, AES.CreateDecryptor(), CryptoStreamMode.Write))
            {
                cs.Write(bytesToBeDecrypted, 0, bytesToBeDecrypted.Length);
                cs.Close();
            }
            decryptedBytes = ms.ToArray();


        }
    }

    return decryptedBytes;
}
    }
    }

 

Conclusion

I don't know why these kiddies used offline edition. Maybe they thought that Hidden Tear is backdoored but offline edition wasn't. Anyway, it was more easy to deal with.

I think the Trendmicro guys didn't even try to crack this ransomware. Because it's so easy to do. Remember how Bitdefender lab defeated the Linux Encoder. It will be better if the Trendmicro uses the same approach on bad implemented ransomware incidents.