Friday, January 29, 2010 At 4:02PM
During a recent security review, we came across a .NET application that was encrypting query string data to thwart parameter based attacks. We had not been given access to the source code, but concluded this since each .aspx page was being passed a single Base64 encoded parameter which, when decoded, produced binary data with varying 16 byte blocks (likely AES considering it is the algorithm of choice for many .NET developers).
The Code
After doing some research (aka plugging the words “.NET”, “Query String” and “Encryption” into Google), we identified several references to a piece of code that had been written and published a few years back for encrypting query strings in .NET. The code we found even used the same parameter name as our application did to pass the encrypted query string data to each page, so we were fairly confident it was the code they were using.
Having written SPF, I am always interested to see how other applications implement cryptography since I know it is not always easy to do properly. In addition to the common problem of re-using the same IV for every encrypted query string, we noticed that the key was entirely derived from a static password embedded in the code (it was being derived using the .NET Framework PasswordDeriveBytes class directly from the literal string value “key”).
For reference, I’ve included the Decrypt method below:
private const string ENCRYPTION_KEY = "key"; public static string Decrypt(string inputText) { RijndaelManaged rijndaelCipher = new RijndaelManaged(); byte[] encryptedData = Convert.FromBase64String(inputText); byte[] salt = Encoding.ASCII.GetBytes(ENCRYPTION_KEY.Length.ToString()); PasswordDeriveBytes secretKey = new PasswordDeriveBytes(ENCRYPTION_KEY, salt); using (ICryptoTransform decryptor = rijndaelCipher.CreateDecryptor(secretKey.GetBytes(32), secretKey.GetBytes(16))) { using (MemoryStream memoryStream = new MemoryStream(encryptedData)) { using (CryptoStream cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read)) { byte[] plainText = new byte[encryptedData.Length]; int decryptedCount = cryptoStream.Read(plainText, 0, plainText.Length); return Encoding.Unicode.GetString(plainText, 0, decryptedCount); } } } }
Password based encryption schemes like this are common in many applications, since the key can easily be represented by a word or passphrase. The nice thing from an attacker’s perspective is that regardless of how large the real encryption key is, the feasibility of a brute force attack is largely dependent on the length and complexity of the password used to derive the key and not the key itself. So for this example, even though they are using 256-Bit AES encryption (generally considered secure), the password used to generate the key is easily brute forced since it is only 3 characters.
Given the code we found, the first and obvious test was to try decrypting our query string values with the same “key” string. Sadly that didn’t work. After trying several educated guesses at what we thought could be the password, I decided to clone the decryption logic into a .NET console utility and run a recursive alphanumeric brute force against the password. The approach was rather simple:
- Take one of our encrypted samples
- Loop through every alphanumeric character combination
- Using the identical logic shown above, derive the key and decrypt
The caveat here is that we really don’t know what value to expect when it decrypts, but chances are it should be just ASCII text (and hopefully a query string name/value pair). The good news is that most of the keys we generate will generate a CryptographicException, so we can rule out any key value that results in this exception. For safety’s sake I decided to convert the results of every successful decrypt to ASCII and save for further review if needed.
The Cloud
After running the utility for an hour or so I realized that a laptop Windows instance was not the optimal environment for running a brute force password crack (not to mention it rendered the machine pretty useless in the meantime). Having recently signed up for a test account on the Microsoft Azure cloud platform for some unrelated WCF testing, I thought this would be a great opportunity to test out the power of the Microsoft cloud. Even better, Azure is FREE to use until February 1, 2010.
The concept of using the cloud to crack passwords is not new. Last year, David Campbell wrote about how to use Amazon EC2 to crack a PGP passphrase. Having never really worked with the Azure platform (aside from registering for a test account), I first needed to figure out the best way to perform this task in the environment. Windows Azure has two main components, which both run on the Azure Fabric Controller (the hosting environment of Windows Azure):
- Compute – Provides the computation environment. Supports “Web Roles” (essentially web services and web applications) and “Worker Roles” (services that run in the background)
- Storage – Provides scalable storage (Blobs, Tables, Queue)
I decided to create and deploy a “Worker Role” to run the password cracking logic, and then log all output to a table in the storage layer. I’ll spare you the boring details of how to port a console utility to a Worker Role, but it’s fairly simple. The first run of the Worker Role was able to produce approximately 1,000,000 decryption attempts every 30 minutes, or about 555 tries/second. This was definitely faster than the speed I was getting on the laptop, but not exactly what I was hoping for from “the cloud”.
I did some research on how the Fabric Controller allocates resources to each application, and as it turns out there are 4 VM sizes available as shown below:
Compute Instance Size | CPU | Memory | Instance Storage | I/O Performance |
Small | 1.6 GHz | 1.75 GB | 225 GB | Moderate |
Medium | 2 x 1.6 GHz | 3.5 GB | 490 GB | High |
Large | 4 x 1.6 GHz | 7 GB | 1,000 GB | High |
Extra large | 8 x 1.6 GHz | 14 GB | 2,040 GB | High |
The size of the VM used by the Worker Role is controlled through the role properties that get defined when the role is configured in Visual Studio. By default, roles are set to use the “small” VM, but this is easily changed to another size. The task at hand is all about CPU, so I increased the VM to “Extra Large” and redeployed the worker role.
Expecting significant performance gains, I was disappointed to see that the newly deployed role was running at the same exact speed as before. The code was clearly not taking full advantage of all 8 cores, so a little more research led me to the Microsoft Task Parallel Library (TPL). TPL is part of the Parallel Extensions, a managed concurrency library developed by Microsoft for .NET that was specifically designed to make running parallel processes in a multi-core environment easy. Parallel Extensions are included by default as part of the .NET 4.0 Framework release. Unfortunately Azure does not currently support .NET 4.0, but luckily TPL is supported on .NET 3.5 through the Reactive Extensions for .NET (Rx).
Once you install Rx, you can reference the System.Threading.Tasks namespace which includes the Parallel class. Of specific interest for our purpose is the Parallel.For method. Essentially, this method executes a for loop in which iterations may run in parallel. Best of all, the job of spawning and terminating threads, as well as scaling the number of threads according to the number of available processors, is done automatically by the library.
As expected, this was the secret sauce I had been missing. Once re-implemented with a Parallel.For loop, the speed increased significantly to 7,500,000 decryption attempts every 30 minutes, or around 4,200 tries/second. That’s 1M tries every 4 minutes, meaning we can crack a 5 character alphanumeric (lowercase) password in about 4 hours, or the same 6 character equivalent in about 6 days. This is still significantly slower than the speed obtained by Campbell’s experiment, but then again he was using a distributed program designed specifically for fast password cracking (as opposed to the proof of concept code we are using here), not to mention I am also logging output to a database in the storage layer. At the time of writing, the password hasn’t cracked but the worker process has only been running for about 24 hours (so there’s still plenty of time). What remains to be seen is how fast this same code would run in the Amazon EC2 cloud, which may be a comparison worth doing.
The important takeaway here is not about the power of the cloud (since there’s nothing we can do to stop it), but rather about Password Based Encryption. Regardless of key length and choice of algorithm, the strength of your encryption always boils down to the weakest link…which in this case, is the choice of password.
Author: Brian Holyfield
©Aon plc 2023