The following post describes the way I solved the challenge Exam Solutions from the NorthSec 2020 CTF.
A zip file containing two files was provided:
- SeverityHighProtector.zip
- SeverityHighProtector.exe
- ExamSolution.txt.protected
The challenge description also mentioned that we had to find the exam answers but since the teacher used very long passwords, it would be difficult to crack his password.
I first ran the command file on SeverityHighProtector.exe and found that it was a .NET binary:
Before decompiling it, I experimented with the binary and found that it was used to encrypt and decrypt files by using a user supplied password. After doing that, I opened the executable in dotPeek and quickly found the Protector class:
// Decompiled with JetBrains decompiler // Type: ConsoleApp1.Protector // Assembly: ConsoleApp1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; namespace ConsoleApp1 { internal class Protector { private Aes _aes; private string password; public Protector(string password) { this.password = password; this.CreateAes(); } private void CreateAes() { Rfc2898DeriveBytes rfc2898DeriveBytes = new Rfc2898DeriveBytes(this.password, new byte[8] { (byte) 42, (byte) 42, (byte) 42, (byte) 42, (byte) 7, (byte) 7, (byte) 7, (byte) 7 }); this._aes = Aes.Create(); this._aes.Mode = CipherMode.ECB; this._aes.Key = rfc2898DeriveBytes.GetBytes(16); this._aes.IV = rfc2898DeriveBytes.GetBytes(16); } private Stream GetStream(string filename) { return (Stream) File.OpenRead(filename); } private Stream GetOutStream(string filename) { return (Stream) File.OpenWrite(filename); } private byte[] GetPasswordHash(string password) { return SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(password)); } internal void Unprotect(string filename) { using (Stream stream = this.GetStream(filename)) { byte[] buffer1 = new byte[20]; stream.Read(buffer1, 0, 20); if (!((IEnumerable<byte>) buffer1).SequenceEqual<byte>((IEnumerable<byte>) this.GetPasswordHash(this.password))) this.GetPasswordHash(this.password))) { Console.WriteLine("Bad password !"); } else { using (Stream outStream = this.GetOutStream(filename + ".raw")) { using (ICryptoTransform decryptor = this._aes.CreateDecryptor()) { using (CryptoStream cryptoStream = new CryptoStream(outStream, decryptor, CryptoStreamMode.Write)) { int count = 1; while (count != 0) { byte[] buffer2 = new byte[1024]; count = stream.Read(buffer2, 0, 1024); cryptoStream.Write(buffer2, 0, count); } } } } } } } 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 encryptor = this._aes.CreateEncryptor()) { using (CryptoStream cryptoStream = new CryptoStream(outStream, encryptor, CryptoStreamMode.Write)) { int count = 1; while (count != 0) { byte[] buffer = new byte[1024]; count = stream.Read(buffer, 0, 1024); cryptoStream.Write(buffer, 0, count); } } } } } } } }
After reviewing the code, we find that the password that is used for encrypting the file is hashed using SHA1 and then put at the beginning of the encrypted file (line 97). Furthermore, the class Rfc2898DeriveBytes at line 27 is a pseudo-random number generator based on HMACSHA1 and we dig through the source code to find out how the key is initialized:
... internal void InitializeKey (byte[] key) { // When we change the key value, we'll need to update the initial values of the inner and outter // computation buffers. In the case of correct HMAC vs Whidbey HMAC, these buffers could get // generated to a different size than when we started. m_inner = null; m_outer = null; if (key.Length > BlockSizeValue) { KeyValue = m_hash1.ComputeHash(key); // No need to call Initialize, ComputeHash will do it for us } else { KeyValue = (byte[]) key.Clone(); } UpdateIOPadBuffers(); } ...
As you can see on line 9 and 10, when the key is longer than the block size, the key size is reduced by hashing it with the specified algorithm (SHA1 in this case). Finally, since the teacher is using very long passwords, we can assume that the key is longer than the block size. Therefore, we only have to feed the SHA1 of the password as the key to the HMAC function and we can decrypt the protected file!
To do that, I only patched the lines 27 to 37 of the Protector class with the following:
byte[] examHash = { 0xC2, 0xF3, 0xA6, 0x96, 0xB2, 0x1D, 0x5C, 0x49, 0xC4, 0xB8, 0x5F, 0x38, 0x39, 0x45, 0x0E, 0x6E, 0x5A, 0xF7, 0x88, 0x8A }; Rfc2898DeriveBytes rfc2898DeriveBytes = new Rfc2898DeriveBytes(examHash, new byte[8] { (byte) 42, (byte) 42, (byte) 42, (byte) 42, (byte) 7, (byte) 7, (byte) 7, (byte) 7 }, 1000);
After running the program, we are left with a decrypted file containing the exam answers:
All the answers for the exam are "B". Lulz ! FLAG-5349fda5a67a70adcd77
Thank you for reading this post!