Ce billet décrit la façon dont j’ai résolu le défi Exam Solutions du CTF NorthSec 2020.
Un fichier zip contenant deux fichiers était fourni:
- SeverityHighProtector.zip
- SeverityHighProtector.exe
- ExamSolution.txt.protected
La description du défi mentionnait aussi qu’il fallait trouver les réponses de l’examen, mais que le professeur utilisait des mots de passe très longs, ce qui rendrait le cassage difficile.
J’ai d’abord lancé la commande file sur SeverityHighProtector.exe et constaté qu’il s’agissait d’un binaire .NET:![]()
Avant de le décompiler, j’ai expérimenté avec le binaire et découvert qu’il servait à chiffrer et à déchiffrer des fichiers à partir d’un mot de passe fourni par l’utilisateur. J’ai ensuite ouvert l’exécutable dans dotPeek et rapidement trouvé la classe Protector:
// 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);
}
}
}
}
}
}
}
}
En parcourant le code, on constate que le mot de passe utilisé pour chiffrer le fichier est haché avec SHA1, puis placé au début du fichier chiffré (ligne 97). De plus, la classe Rfc2898DeriveBytes à la ligne 27 est un générateur de nombres pseudo-aléatoires fondé sur HMACSHA1. Nous avons fouillé le code source pour voir comment la clé est initialisée:
...
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();
}
...
Comme on le voit aux lignes 9 et 10, lorsque la clé est plus longue que la taille de bloc, sa taille est réduite en la hachant avec l’algorithme spécifié, ici SHA1. Enfin, puisque le professeur utilise des mots de passe très longs, on peut supposer que la clé est plus longue que la taille de bloc. Il suffit donc de fournir le SHA1 du mot de passe comme clé à la fonction HMAC pour déchiffrer le fichier protégé!
Pour ce faire, j’ai simplement remplacé les lignes 27 à 37 de la classe Protector par ceci:
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);
Après l’exécution du programme, il nous reste un fichier déchiffré contenant les réponses de l’examen:
Toutes les réponses de l’examen sont « B ». Lulz!
FLAG-5349fda5a67a70adcd77
Merci d’avoir lu ce billet!
