We generally try to keep off of disk as much as possible on engagements- there’s less to clean up and fewer chances of being caught. However, occasionally we have a need to store data on disk on a target system, and we want to try to do this in a secure way in case any incident responders start to catch on. Examples would be reboot-persistent keyloggers, something that monitors locations for specific files and clones/exfiltrates them, and the pilfering of KeePass files (see the Example: KeePass + EncryptedStore section below).
If we have to write a file to disk, we want to do it in a way that prevents the recovery of the data as best we can and uses only built-in tools to do so. This post will detail one of our solutions to this problem. The code detailed in this post is live on GitHub.
An Encrypted Store Design
We have a few specific design requirements for our encrypted datastore. We want something with:
- reasonably strong crypto – we chose AES with cipher block chaining (CBC) and a randomized IV, as well as RSA + AES to use a public key to encrypt a random AES key per encrypted unit (more on this below)
- doesn’t leave the password with the file (in order to prevent easy recovery)
- accepts multiple files, and doesn’t require decrypting/re-encrypting the entire store on each addition to it
- accepts arbitrary data like keystrokes as well as files
- ‘platform independent’ decryption on a variety of platforms with a variety of languages
The storage format we came up with is ‘packetized’, with discrete units of a specific format appended to a single file. This way the store can be appended to easily without constant encryption/decryption. The store format is as follows:
[4 bytes representing size of next block to decrypt] [0] (indicating straight AES) [16 byte IV] [AES-CBC encrypted file block] [compressed stream] [260 characters/bytes indicating original path] [file contents] ... [4 bytes representing size of next block to decrypt] [1] (indicating RSA+AES) [128 bytes random AES key encrypted with the the RSA public key] [16 byte IV] [AES-CBC encrypted file block] [compressed stream] [260 characters/bytes indicating original path] [file contents] ...
To encrypt a file for ENCSTORE.bin:
- Read raw file contents
- Pad original full file PATH to 260 Bytes
- Compress [PATH + file] using IO.Compression.DeflateStream
- If using RSA+AES, generate a random AES key and encrypt using the RSA public key
- Generate random 16 Byte IV
- Encrypt compressed stream with AES-CBC using the predefined key and generated IV
- Calculate length of encrypted block + IV
- Append 4 Byte representation of length to ENCSTORE.bin
- Append 0 byte if straight AES used, 1 if RSA+AES used
- Optionally append 128 bytes of RSA encrypted random AES key if RSA+AES scheme used
- Append IV to ENCSTORE.bin
- Append encrypted file to ENCSTORE.bin
Decryption happens in reverse:
- While there is more data to decrypt:
- Read first 4 bytes of ENCSTORE.bin and calculate length value X
- Read next X bytes of encrypted file
- Read the first byte of the encrypted block to see if AES or RSA decryption is specified
- If RSA-AES is specified (byte == 1):
- Read the next 128 bytes of encrypted block and decrypt the random AES key using the RSA private key
- Read the next 16 bytes of block and extract the IV
- Read remaining block and decrypt AES-CBC compressed stream using specified key and extracted IV
- Decompress [PATH + file]
- Split path by \ and create nested folder structure to mirror original path
- Write original file\data to mirrored path
To implement the integration of arbitrary data into the same container format, a ‘data tag’ string (like ‘keylog’) is used in lieu of the file path, and the arbitrarily passed data is used instead of extracting the file contents.
The AES/RSA “packets” are also stackable and any number of both types of packets can be appended to the same write location.
EncryptedStore.ps1
The PowerShell code to do this is currently on GitHub. The EncryptedStore.ps1 script is PowerShell version 2.0 compatible, and uses [System.Security.Cryptography.AesCryptoServiceProvider] for the AES implementation, [System.Security.Cryptography.RSACryptoServiceProvider] for the RSA implementation, and [System.IO.Compression.DeflateStream] for the compression implementation.
If you want to use RSA encryption for the store, you first need to generate an RSA public/private key pair with $Key = New-RSAKeyPair. Be sure to save the key object if you want to be able to decrypt any of your data!
Write-EncryptedStore will create an encrypted store and accepts data/file paths on the pipeline. There’s also a 1 gigabyte default storage limit which can be modified with -StoreSizeLimit 100MB. It requires a -StorePath and -Key, which is MD5 hashed for an AES password if not 32 characters. If the key string is of the format ‘^<RSAKeyValue><Modulus>.*</Modulus><Exponent>.*</Exponent></RSAKeyValue>$‘, the public key format generated by New-RSAKeyPair, then the RSA+AES scheme is used instead of straight AES. SecureStrings are also usable with the -SecureKey parameter.
Here’s how to store off a set of target files into C:\Temp\debug.bin:
'secret.txt','secret2.txt' | Write-EncryptedStore -StorePath C:\Temp\debug.bin -Key 'Password123!'
If you have arbitrary data to store, the function also takes a -DataTag X argument to pretag the saved data with something like “keylog”. Here’s an example:
"keystrokes" | Write-EncryptedStore -StorePath C:\Temp\debug.bin -Key 'Password123!' -DataTag 'keylog'
Since the atomic storage unit is indifferent to tagged data or files, you can store both in the same container. Write-EncryptedStore actually wraps the more generic Out-EncryptedStore function, which takes the types of input specified above and outputs the set of encrypted bytes containing the encrypted data. Out-EncryptedStore also has a -Base64Encode flag that will return everything as a base64-encoded string. This can be useful in some situations for transport (like in a RAT).
-StorePath defaults to $Env:Temp\debug.bin if a value is not specified. It also accepts \\UNC\file.bin paths, registry paths (“HKLM:\SOFTWARE\\something\key\valuename”), and WMI namespaces (“ROOT\Software\namespace:ClassName”) for additional storage options. A remote computer is specifiable for all three storage options with -ComputerName <Computer> with a separate -Credential <X> being specifiable as well. All of these options are present with Read-EncryptedStore as well (described below).
Here are all the local/remote storage options available:
$RSA = New-RSAKeyPair # local tests $ComputerName = 'localhost' $StorePath = 'C:\Temp\temp.bin' Write-Host "`n[$ComputerName] AES Storepath : $StorePath" ".\secret.txt" | Write-EncryptedStore -StorePath $StorePath -Key 'Password123!' Read-EncryptedStore -StorePath $StorePath -Key 'Password123!' -List Get-EncryptedStoreData -StorePath $StorePath | Remove-EncryptedStore Start-Sleep -Seconds 1 Write-Host "`n[$ComputerName] RSA Storepath : $StorePath" ".\secret.txt" | Write-EncryptedStore -StorePath $StorePath -Key $RSA.Pub Read-EncryptedStore -StorePath $StorePath -Key $RSA.Priv -List Get-EncryptedStoreData -StorePath $StorePath | Remove-EncryptedStore Start-Sleep -Seconds 1 $StorePath = 'HKLM:\SOFTWARE\Microsoft\SystemCertificates\DomainCertificate' Write-Host "`n[$ComputerName] AES Storepath : $StorePath" ".\secret.txt" | Write-EncryptedStore -StorePath $StorePath -Key 'Password123!' Read-EncryptedStore -StorePath $StorePath -Key 'Password123!' -List Get-EncryptedStoreData -StorePath $StorePath | Remove-EncryptedStore Start-Sleep -Seconds 1 Write-Host "`n[$ComputerName] RSA Storepath : $StorePath" ".\secret.txt" | Write-EncryptedStore -StorePath $StorePath -Key $RSA.Pub Read-EncryptedStore -StorePath $StorePath -Key $RSA.Priv -List Get-EncryptedStoreData -StorePath $StorePath | Remove-EncryptedStore Start-Sleep -Seconds 1 $StorePath = 'ROOT\Software:WindowsUpdate' Write-Host "`n[$ComputerName] AES Storepath : $StorePath" ".\secret.txt" | Write-EncryptedStore -StorePath $StorePath -Key 'Password123!' Read-EncryptedStore -StorePath $StorePath -Key 'Password123!' -List Get-EncryptedStoreData -StorePath $StorePath | Remove-EncryptedStore Start-Sleep -Seconds 1 $StorePath = 'ROOT\Software:WindowsUpdate' Write-Host "`n[$ComputerName] RSA Storepath : $StorePath" ".\secret.txt" | Write-EncryptedStore -StorePath $StorePath -Key $RSA.Pub Read-EncryptedStore -StorePath $StorePath -Key $RSA.Priv -List Get-EncryptedStoreData -StorePath $StorePath | Remove-EncryptedStore Start-Sleep -Seconds 1 # remote tests $ComputerName = 'PRIMARY.testlab.local' $Credential = Get-Credential 'TESTLAB\administrator' $StorePath = 'C:\Temp\temp2.bin' Write-Host "`n[$ComputerName] AES Storepath : $StorePath" ".\secret.txt" | Write-EncryptedStore -ComputerName $ComputerName -Credential $Credential -StorePath $StorePath -Key 'Password123!' Read-EncryptedStore -ComputerName $ComputerName -Credential $Credential -StorePath $StorePath -Key 'Password123!' -List Get-EncryptedStoreData -ComputerName $ComputerName -Credential $Credential -StorePath $StorePath | Remove-EncryptedStore -ComputerName $ComputerName -Credential $Credential Start-Sleep -Seconds 1 Write-Host "`n[$ComputerName] RSA Storepath : $StorePath" ".\secret.txt" | Write-EncryptedStore -ComputerName $ComputerName -Credential $Credential -StorePath $StorePath -Key $RSA.Pub Read-EncryptedStore -ComputerName $ComputerName -Credential $Credential -StorePath $StorePath -Key $RSA.Priv -List Get-EncryptedStoreData -ComputerName $ComputerName -Credential $Credential -StorePath $StorePath | Remove-EncryptedStore -ComputerName $ComputerName -Credential $Credential Start-Sleep -Seconds 1 $StorePath = 'HKLM:\SOFTWARE\Microsoft\SystemCertificates\DomainCert' Write-Host "`n[$ComputerName] AES Storepath : $StorePath" ".\secret.txt" | Write-EncryptedStore -ComputerName $ComputerName -Credential $Credential -StorePath $StorePath -Key 'Password123!' ".\u2.txt" | Write-EncryptedStore -ComputerName $ComputerName -Credential $Credential -StorePath $StorePath -Key 'Password123!' Read-EncryptedStore -ComputerName $ComputerName -Credential $Credential -StorePath $StorePath -Key 'Password123!' -List Get-EncryptedStoreData -ComputerName $ComputerName -Credential $Credential -StorePath $StorePath | Remove-EncryptedStore -ComputerName $ComputerName -Credential $Credential Start-Sleep -Seconds 1 Write-Host "`n[$ComputerName] RSA Storepath : $StorePath" ".\secret.txt" | Write-EncryptedStore -ComputerName $ComputerName -Credential $Credential -StorePath $StorePath -Key $RSA.Pub ".\u2.txt" | Write-EncryptedStore -ComputerName $ComputerName -Credential $Credential -StorePath $StorePath -Key $RSA.Pub Read-EncryptedStore -ComputerName $ComputerName -Credential $Credential -StorePath $StorePath -Key $RSA.Priv -List Get-EncryptedStoreData -ComputerName $ComputerName -Credential $Credential -StorePath $StorePath | Remove-EncryptedStore -ComputerName $ComputerName -Credential $Credential Start-Sleep -Seconds 1 $StorePath = 'ROOT\Software:WindowsUpdate2' Write-Host "`n[$ComputerName] AES Storepath : $StorePath" ".\secret.txt" | Write-EncryptedStore -ComputerName $ComputerName -Credential $Credential -StorePath $StorePath -Key 'Password123!' Read-EncryptedStore -ComputerName $ComputerName -Credential $Credential -StorePath $StorePath -Key 'Password123!' -List Get-EncryptedStoreData -ComputerName $ComputerName -Credential $Credential -StorePath $StorePath | Remove-EncryptedStore -ComputerName $ComputerName -Credential $Credential Start-Sleep -Seconds 1 Write-Host "`n[$ComputerName] RSA Storepath : $StorePath" ".\secret.txt" | Write-EncryptedStore -ComputerName $ComputerName -Credential $Credential -StorePath $StorePath -Key $RSA.Pub Read-EncryptedStore -ComputerName $ComputerName -Credential $Credential -StorePath $StorePath -Key $RSA.Priv -List Get-EncryptedStoreData -ComputerName $ComputerName -Credential $Credential -StorePath $StorePath | Remove-EncryptedStore -ComputerName $ComputerName -Credential $Credential Start-Sleep -Seconds 1
Read-EncryptedStore will recover the data from a specified encrypted store. It also requires -StorePath and -Key/-SecureKey. If you want to just list the files in a store, use the -List parameter:
Read-EncryptedStore -StorePath C:\Temp\debug.bin -Key 'Password123!' -List
If you want to extract the data leave off the -List parameter, and Read-EncryptedStore will extract out all the data to the local folder, cloning the original paths. If there’s a filename conflict, additional files are appended with a counter:
Read-EncryptedStore -StorePath C:\Temp\debug.bin -Key 'Password123!'
As shown in the test examples, the Get-EncryptedStoreData and Remove-EncryptedStore functions can be used to retrieve/remove encrypted store data, again from all three storage options, local or remote.
EncryptedStore.py
There’s also a Python version of Read-EncryptedStore. It uses pycrypto for the crypto implementations and zlib for the decompression implementation. It currently only supports AES containers.
To list the files for a given store:
./EncryptedStore.py –store debug.bin –key ‘password’ –list
To extract the files:
./EncryptedStore.py –store debug.bin –key ‘password’
Example Use Case: KeePass + EncryptedStore
A while back I released a post detailing how to operationally “attack” KeePass databases. As a follow up I wrote a script that searches for any KeePass.ini (version 1.X) or KeePass.config.xml (version 2.X) configuration files in C:\Users\, C:\Program Files\, and C:\Program Files (x86)\. This script is also included with EncryptedStore. Any found configurations are parsed and a custom PSObject is output with relevant information detailing database/keyfile locations, as well as information like the 2.X SecureDesktop setting, and whether a Windows user account was used to create the composite master key. In the situation where a user account is used as a mixin, user name/SID/domain information is output along with user master key locations:
This made a great candidate to pair with the EncryptedStore approach. Write-EncryptedStore and Out-EncryptedStore can take the output from Find-KeePassconfig on the pipeline and encrypt all found KeePass files into a single datastore:
Hopefully others find this of use. The storage format is simple enough that other language implementations should be possible as well if anyone has any interest.
WoW! Excellent article, as always!
take a look at excubits low level kernel windows services – with it you can protect your keepass files so that only the keepass binary from specific location would be able to access them, and also protect the keepass memory space, so all the existing attacks are useless!