fix(Extract-NTHashes): add HMAC-SHA256 to the AES export (breaking format bump to ELY2)
Protect-FileWithAES used AES-256-CBC with no integrity check. A wrong passphrase or corrupted/tampered ciphertext just decrypts to garbage (or an unhelpful padding exception) instead of being detected. Derives a second 32-byte key from the same PBKDF2 stream (the AES key and HMAC key are sequential, non-overlapping ranges of one Rfc2898DeriveBytes instance) and computes HMAC-SHA256 over magic+salt+iv+ciphertext (encrypt-then-MAC), appended as a trailer. Bumped the format magic from 'ELY1' to 'ELY2' so old and new files are distinguishable. This is a breaking change for whatever external tooling decrypts these exports (this repo only ever encrypts - decryption happens on a separate air-gapped machine per the README's FAQ) - documented the new layout and the break in the README. Verified with an isolated round-trip test (function extracted, dot-sourced, paired with a hand-written decrypt+HMAC-verify): correct passphrase round-trips cleanly, a wrong passphrase is rejected via HMAC mismatch, and a single flipped ciphertext byte is also rejected via HMAC mismatch, in all cases before any AES decryption is attempted. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+20
-23
@@ -7,7 +7,7 @@
|
|||||||
##################################################
|
##################################################
|
||||||
## Project: Elysium ##
|
## Project: Elysium ##
|
||||||
## File: Extract-NTHashes.ps1 ##
|
## File: Extract-NTHashes.ps1 ##
|
||||||
## Version: 2.4.5 ##
|
## Version: 2.4.6 ##
|
||||||
## Support: support@cqre.net ##
|
## Support: support@cqre.net ##
|
||||||
##################################################
|
##################################################
|
||||||
|
|
||||||
@@ -134,8 +134,15 @@ function Protect-FileWithAES {
|
|||||||
$salt = New-Object byte[] 16
|
$salt = New-Object byte[] 16
|
||||||
$rng.GetBytes($salt)
|
$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)
|
$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 = [System.Security.Cryptography.Aes]::Create()
|
||||||
$aes.KeySize = 256
|
$aes.KeySize = 256
|
||||||
@@ -143,34 +150,24 @@ function Protect-FileWithAES {
|
|||||||
$aes.Mode = [System.Security.Cryptography.CipherMode]::CBC
|
$aes.Mode = [System.Security.Cryptography.CipherMode]::CBC
|
||||||
$aes.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
|
$aes.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
|
||||||
$aes.GenerateIV()
|
$aes.GenerateIV()
|
||||||
|
|
||||||
$iv = $aes.IV
|
$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 {
|
try {
|
||||||
$magic = [System.Text.Encoding]::ASCII.GetBytes('ELY1')
|
$plainBytes = [System.IO.File]::ReadAllBytes($InputFile)
|
||||||
$outFileStream.Write($magic, 0, $magic.Length)
|
$cipherBytes = $encryptor.TransformFinalBlock($plainBytes, 0, $plainBytes.Length)
|
||||||
$outFileStream.Write($salt, 0, $salt.Length)
|
|
||||||
$outFileStream.Write($iv, 0, $iv.Length)
|
|
||||||
|
|
||||||
$cryptoStream = New-Object System.Security.Cryptography.CryptoStream($outFileStream, $encryptor, [System.Security.Cryptography.CryptoStreamMode]::Write)
|
$magic = [System.Text.Encoding]::ASCII.GetBytes('ELY2')
|
||||||
try {
|
$header = $magic + $salt + $iv
|
||||||
$buffer = New-Object Byte[] 8192
|
$mac = $hmac.ComputeHash($header + $cipherBytes)
|
||||||
while (($read = $fileStream.Read($buffer, 0, $buffer.Length)) -gt 0) {
|
|
||||||
$cryptoStream.Write($buffer, 0, $read)
|
[System.IO.File]::WriteAllBytes($OutputFile, ($header + $cipherBytes + $mac))
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
$cryptoStream.FlushFinalBlock()
|
$encryptor.Dispose(); $hmac.Dispose(); $aes.Dispose(); $rng.Dispose(); $kdf.Dispose()
|
||||||
$cryptoStream.Close()
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
$outFileStream.Close(); $fileStream.Close(); $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 {
|
function Get-FileChecksum {
|
||||||
|
|||||||
@@ -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).
|
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.
|
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
|
### Update Lithnet Password Protection store
|
||||||
Run script Elysium.ps1 as an administrator and choose option 5 (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.
|
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.
|
||||||
|
|||||||
Reference in New Issue
Block a user