diff --git a/Extract-NTHashes.ps1 b/Extract-NTHashes.ps1 index e51870d..c1c8a7a 100644 --- a/Extract-NTHashes.ps1 +++ b/Extract-NTHashes.ps1 @@ -7,7 +7,7 @@ ################################################## ## Project: Elysium ## ## File: Extract-NTHashes.ps1 ## -## Version: 2.4.5 ## +## Version: 2.4.6 ## ## Support: support@cqre.net ## ################################################## @@ -134,8 +134,15 @@ function Protect-FileWithAES { $salt = New-Object byte[] 16 $rng.GetBytes($salt) + # Derive two independent keys from one PBKDF2 byte stream: the first 32 bytes for AES-256, the + # next 32 for HMAC-SHA256 (Rfc2898DeriveBytes.GetBytes returns a continuous stream across + # calls on the same instance, so these two ranges never overlap). CBC alone gives no integrity + # check - tampered or corrupted ciphertext just decrypts to garbage (or throws an unhelpful + # padding exception) instead of being detected. Encrypt-then-MAC over magic+salt+iv+ciphertext + # (format 'ELY2') catches both. Older 'ELY1' files this script produced have no MAC. $kdf = New-Object System.Security.Cryptography.Rfc2898DeriveBytes($Passphrase, $salt, 100000, [System.Security.Cryptography.HashAlgorithmName]::SHA256) - $key = $kdf.GetBytes(32) + $aesKey = $kdf.GetBytes(32) + $hmacKey = $kdf.GetBytes(32) $aes = [System.Security.Cryptography.Aes]::Create() $aes.KeySize = 256 @@ -143,34 +150,24 @@ function Protect-FileWithAES { $aes.Mode = [System.Security.Cryptography.CipherMode]::CBC $aes.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7 $aes.GenerateIV() - $iv = $aes.IV - $encryptor = $aes.CreateEncryptor($key, $iv) - - $fileStream = [System.IO.File]::Open($InputFile, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read) - $outFileStream = [System.IO.File]::Create($OutputFile) + $encryptor = $aes.CreateEncryptor($aesKey, $iv) + $hmac = [System.Security.Cryptography.HMACSHA256]::new($hmacKey) try { - $magic = [System.Text.Encoding]::ASCII.GetBytes('ELY1') - $outFileStream.Write($magic, 0, $magic.Length) - $outFileStream.Write($salt, 0, $salt.Length) - $outFileStream.Write($iv, 0, $iv.Length) + $plainBytes = [System.IO.File]::ReadAllBytes($InputFile) + $cipherBytes = $encryptor.TransformFinalBlock($plainBytes, 0, $plainBytes.Length) - $cryptoStream = New-Object System.Security.Cryptography.CryptoStream($outFileStream, $encryptor, [System.Security.Cryptography.CryptoStreamMode]::Write) - try { - $buffer = New-Object Byte[] 8192 - while (($read = $fileStream.Read($buffer, 0, $buffer.Length)) -gt 0) { - $cryptoStream.Write($buffer, 0, $read) - } - } finally { - $cryptoStream.FlushFinalBlock() - $cryptoStream.Close() - } + $magic = [System.Text.Encoding]::ASCII.GetBytes('ELY2') + $header = $magic + $salt + $iv + $mac = $hmac.ComputeHash($header + $cipherBytes) + + [System.IO.File]::WriteAllBytes($OutputFile, ($header + $cipherBytes + $mac)) } finally { - $outFileStream.Close(); $fileStream.Close(); $aes.Dispose(); $rng.Dispose(); $kdf.Dispose() + $encryptor.Dispose(); $hmac.Dispose(); $aes.Dispose(); $rng.Dispose(); $kdf.Dispose() } - Write-Host "File has been encrypted (PBKDF2+AES-256-CBC): $OutputFile" + Write-Host "File has been encrypted (PBKDF2+AES-256-CBC+HMAC-SHA256): $OutputFile" } function Get-FileChecksum { diff --git a/README.md b/README.md index 693f187..298907f 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,8 @@ If you want to know the script was executed without collecting telemetry, set a Run script Elysium.ps1 as an administrator and choose option 3 (Extract and Send Hashes). Domains are listed in configuration order, after which the script prompts for the replication-capable account password. With valid credentials, it extracts current NTLM hashes (no history) for active accounts, compresses the results, encrypts them with the configured passphrase, and uploads the payload to the configured storage (Azure Blob or S3-compatible). A checksum-verified round-trip download confirms the upload before local artifacts are removed. +**Encrypted export format (v2.4.5+, magic `ELY2`):** `4-byte magic 'ELY2' | 16-byte PBKDF2 salt | 16-byte AES IV | AES-256-CBC ciphertext | 32-byte HMAC-SHA256`. Both keys are derived from the configured passphrase via one PBKDF2-SHA256 (100,000 iterations) byte stream: the first 32 bytes are the AES key, the next 32 are the HMAC key. The HMAC covers `magic | salt | iv | ciphertext` (encrypt-then-MAC) so a decrypt tool must verify it *before* decrypting - CBC alone doesn't detect a wrong passphrase or corrupted/tampered ciphertext, it just produces garbage or an unhelpful padding exception. This is a breaking change from the older `ELY1` format (no HMAC trailer) produced before v2.4.5; any external decryption tooling on the air-gapped cracking machine needs updating to match. + ### Update Lithnet Password Protection store Run script Elysium.ps1 as an administrator and choose option 5 (Update Lithnet Password Protection Store). Configure the target folder via `LithnetStorePath` in `ElysiumSettings.txt` (the location created with `Open-Store`). The script automatically imports the `khdb.txt` file unless you override/add additional NTLM hash lists in `LithnetHashSources` (comma or semicolon separated). You can also populate plaintext password lists (`LithnetPlaintextSources`) and banned-word files (`LithnetBannedWordSources`), or enable `LithnetSyncHibp=true` to seed the store directly from the Have I Been Pwned API (using `Sync-HashesFromHibp`). Behind the scenes the helper loads the `LithnetPasswordProtection` module, opens the store, runs [`Import-CompromisedPasswordHashes`](https://docs.lithnet.io/password-protection/advanced-help/powershell-reference/import-compromisedpasswordhashes)/`Import-CompromisedPasswords`/`Import-BannedWords` for each configured file, and then closes the store.