“Severity High Protector” was a challenge in Northsec’s 2020 online CTF. Teams were given a zip file which contained two files:
ExamSolution.txt.protected
- an encrypted fileSeverityHighProtector.exe
- a binary with options to “Protect” or “Unprotect” a given file with a password.The binary was .NET-based and it was not obfuscated, so I loaded it into dnSpy
to get the full source code.
After skimming the code, it was obvious the most important pieces were in the Protector
class, and specifically three places:
First, CreateAes
:
private void CreateAes()
{
Rfc2898DeriveBytes rfc2898DeriveBytes = new Rfc2898DeriveBytes(this.password, new byte[]
{
42,
42,
42,
42,
7,
7,
7,
7
});
this._aes = Aes.Create();
this._aes.Mode = CipherMode.ECB;
this._aes.Key = rfc2898DeriveBytes.GetBytes(16);
this._aes.IV = rfc2898DeriveBytes.GetBytes(16);
}
The interesting part of this function is it’s use of the Rfc2898DeriveBytes
class. This class uses PBKDF2
to derive keys, using HMACSHA1
.
Next, Protect
. This function encrypted a file using the derived keys from CreateAes
. Interestingly, it also prepended the plaintext SHA
hash to the encrypted file.
internal void Protect(string filename)
{
using (Stream stream = this.GetStream(filename))
{
using (Stream outStream = this.GetOutStream(filename + ".protected"))
{
outStream.Write(this.GetPasswordHash(this.password), 0, 20);
using (ICryptoTransform cryptoTransform = this._aes.CreateEncryptor())
{
using (CryptoStream cryptoStream = new CryptoStream(outStream, cryptoTransform, CryptoStreamMode.Write))
{
int count = 1;
while (count != 0)
{
byte[] buffer = new byte[1024];
count = stream.Read(buffer, 0, 1024);
cryptoStream.Write(buffer, 0, count);
}
}
}
}
}
}
Finally, Unprotect
. This checked if the password passed via STDIN
matched the hash of the password stored in the encrypted file. Then it performed a simple decryption.
internal void Unprotect(string filename)
{
using (Stream stream = this.GetStream(filename))
{
byte[] array = new byte[20];
stream.Read(array, 0, 20);
if (!array.SequenceEqual(this.GetPasswordHash(this.password)))
{
Console.WriteLine("Bad password !");
}
else
{
using (Stream outStream = this.GetOutStream(filename + ".raw"))
{
using (ICryptoTransform cryptoTransform = this._aes.CreateDecryptor())
{
using (CryptoStream cryptoStream = new CryptoStream(outStream, cryptoTransform, CryptoStreamMode.Write))
{
int count = 1;
while (count != 0)
{
byte[] buffer = new byte[1024];
count = stream.Read(buffer, 0, 1024);
cryptoStream.Write(buffer, 0, count);
}
}
}
}
}
}
}
The first, obvious approach was cracking the SHA
hash. I started hashcat
against it while I did more research. The fact that we had a SHA1
hash, and the key derivation formula was based on SHA
seemed like a potential avenue, and I was quickly able to find this post describing the following:
PBKDF2_HMAC_SHA1(password) == PBKDF2_HMAC_SHA1(HEX_TO_STRING(SHA1(password)))
if the chosen_password is larger than 64 bytes.
From there, I took the decompilation from dnSpy and created my own solution in Visual Studio which copied the Unprotect
and CreateAes
functions. Luckily, Rfc2898DeriveBytes
had a defintion which accepted two byte arrays, plus an interation count, so I simply updated CreateAes
to be:
private void CreateAes()
{
Rfc2898DeriveBytes rfc2898DeriveBytes = new Rfc2898DeriveBytes(sha1_bytes, // sha1 hash expressed in raw byte form
new byte[] // original salt
{
42,
42,
42,
42,
7,
7,
7,
7
}, 1000); // Default interation count is 1000, and required in this overload
this._aes = Aes.Create();
this._aes.Mode = CipherMode.ECB;
this._aes.Key = rfc2898DeriveBytes.GetBytes(16);
this._aes.IV = rfc2898DeriveBytes.GetBytes(16);
}
I deleted the password hash check from Unprotect
, compiled, ran, and got the flag!