“Severity High Protector” was a challenge in Northsec’s 2020 online CTF. Teams were given a zip file which contained two files:

  1. ExamSolution.txt.protected - an encrypted file
  2. SeverityHighProtector.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!