1Password Secret Retrieval — Methodology and Implementation

Dwight Hohnstein
Posts By SpecterOps Team Members
13 min readAug 17, 2021

--

Background and Motivation

1Password is a password manager developed by AgileBits Inc., providing a place for users to store various passwords, software licenses, and other sensitive information in a virtual vaults secured with a PBKDF2 master password. Downloading a copy of the software and using it for awhile, I noticed that so long as 1Password remained unlocked, the passwords within it remained decrypted and readable in the UI. This was my initial motivation to dig into what’s happening under the hood.

One project that always has intrigued me is KeeThief, by Will and Lee Christensen. KeeThief leveraged the ClrMd debugging suite by Microsoft to walk the heap for .NET objects of interest and, since the 1Password client application is written in .NET, it felt like the perfect opportunity to get firsthand experience using it. Plus, who doesn’t like reading passwords?

A final note before proceeding — this post covers my methodology and thought process while attacking this problem. If you simply want to know what “worked,” I’d recommend skipping to the bottom and reading from “Attempt #4” onward.

1Password.exe

The 1Password.exe client is located in the user’s AppData folder and built in .NET. Throwing the executable in dnSpy and jumping to the Program’s main function, we see the main loop is short and sweet — it fixes up the DLL import path, imports 1Password.dll, and calls the exported function “run” from it.

That “run” function is part of a larger suite of “Native” function calls from the 1Password DLL, some of which are immediately interesting, such as “decrypt_with_vault_key.” Tracing that function back, we see that it’s called when the user goes to inspect certain elements in the UI as well as responsible for decrypting the contents of secrets when exporting them.

Code Paths Leading to the Decryption of Data

Using dnSpy again, I set a break point at the beginning of the WorkspaceItem.Decrypt function call and start up 1Password.exe. After entering our master password to the application we hit our breakpoint successfully.

Breakpoint at WorkspaceItem.Decrypt

This keys us into another interesting function export from 1Password.dll — get_item_data. This function is responsible for retrieving an item from the 1Password SQLite database where all the user’s secrets are stored. This function call returns metadata surrounding the entry, such as various identifiers, last updated date, if it’s a favorite or if it’s archived, etc. Most importantly, it retrieves two byte arrays named “Overview” and “Details.” Before decryption, each byte array contains data surrounding how that data was encrypted, such as the algorithm, the initialization vector, and more. This data is then passed to the 1Password.decrypt_with_vault_key function where the data within is decrypted and returned to the caller.

What the Overview Byte Array Contains before Decryption.

When the data from Overview and Details is decrypted, the former contains metadata on the secret, such as the URL it’s used at, what is it’s name or title, and any other miscellaneous information. The latter, Details, contains the username, the password, additional sections and notes, and even the password history of that secret.

Given all this in synthesis, we now have a plan of attack. We know that if the database is unlocked, we can enumerate over items within the database using 1Password.get_item_data, and by passing the Overview and Details JSON byte arrays to 1Password.decrypt_with_vault_key, we’d have plaintext secret material. Sounds easy enough, right?

Attempt #1: LoadLibrary(“1Password.dll”)

The first thing I tried was to simply load the 1Password DLL into my application and make the requisite function calls. In theory, it should’ve been simple enough — copy the PInvoke signatures the legitimate 1Password binary used to call the library exports, paste them into my application, and fix up the DLL search order path in order to load the DLL itself. Then I could call the same functions in the same order, using data retrieved from dnSpy debugging (such as the item ID, vault ID, etc.), and

I started my trivial, 15-lines-of-code program, in hopes it’d be as easy I had hoped.

Spoiler Alert: It Wasn’t That Easy

I was unable to track down exactly why I couldn’t load the DLL into my application, so I moved on to the next idea.

Attempt #2: Assembly.Load(“1Password”)

Well if I can’t load the DLL directly, what if I loaded the assembly using reflection, and got the methods and fields I needed reflectively? I set about updating the program to load the 1Password assembly, get handles to the functions within the native class using reflection, and invoking them directly to parse out the results.

How to Reflectively Load and Retrieve Values from 1Password

When I attempted to reflectively load the 1Password assembly however, I was greeted with another lovely error message. Analyzing this in retrospect, I probably could’ve predicted this since I couldn’t load the DLL dependency that 1Password.exe relies on, but oh well. You live and you learn.

My next thought was “you know what process already has 1Password.dll loaded into it? 1Password.exe.” So using the same code snippet above, I converted that assembly to shellcode using TheWover’s donut project and created a small shellcode runner to inject it into the 1Password process. (Note: At the time of testing I had to run this as an Administrator, but upon further research I was able to do this from a medium integrity process. More on this later.)

Whenever you’re injecting your application into another process, you’ll lose the niceties of Visual Studio’s debugging suite. You’ll need to use your caveman debugging tools and start spooling output to some sort of log file or retrieve output using DebugView and OutputDebugString.

Once the reflective loader was injected into the remote process and started off, I watched the log file in eager anticipation; however, I was greeted with the same error message in-process as I had seen out of process.

Assembly.Load Fails on 1Password.exe

Attempt #3: Walking the Heap

Alright 1Password, have it your way. Time to bring in the big guns: ClrMd. ClrMd is Microsoft’s suite of debugging utilities to triage running processes and crash dumps. If you’re familiar with Clarke’s Third Law, it describes precisely how I feel about ClrMd.

As mentioned in the introduction, KeeThief is a .NET application that leverages ClrMd to parse KeePass’s process heap for the location of KeePass databases, and extract the master password for those databases. Using it as a muse, I went about creating my first application using the debugging utilities.

From analyzing the 1Password binary, I know that the following classes and structs are interesting to me:

  • Opw.ImDecryptedItem — Stores Newtonsoft.Json.Linq.JObject representations of decrypted Overview and Details contents
  • Opw.ItemData — Stores decrypted Overview and Details JSON as Newtonsoft.Json.Linq.JObjects

ImDecryptedItem

ImDecryptedItem is a simple structure with two attributes, and those two attributes are the only ones we care about. So the initial thought was “let’s walk the heap for ImDecryptedItem objects and use reflection to load the byte arrays.” I’ll attach myself as a debugger, get those credential objects, and write a blog post. Nice and easy.

The Structure of Opw.ImDecryptedItem

Except.

It wasn’t that easy.

One thing that I should have realized before even attempting this is that the lifespan of the ImDecryptedItem is short — extremely short. It lives only within the lifespan of the function calling the WorkspaceItem.Decrypt function. That object lives and dies there, only to have its contents harvested and stored by some other object. As a result, when I walked the heap, there were no ImDecryptedItem objects.

Parsing Strings and Byte Arrays

Another somewhat naive approach is to parse the binary for strings and byte arrays. We know that the credential material is stored as a JSON string within the 1Password SQLite database. The raw bytes of that JSON string are returned to the caller via decrypt_with_vault_id, and that is parsed into a Newtonsoft.Json.Linq.JObject.

To look for these primitive types, I ended up using an extension library on top of CLRMD called DynaMD. DynaMD extends CLRMD in a few nice ways, but one of them being able to automatically parse out the primitive types you want from the heap in an iterator. Instead of walking each type, you can ask DynaMD to get you objects only of a specified type using the GetProxies<T> function, and act on those objects directly.

Reading Strings and Parsing Byte Arrays into JObjects

Using the GetProxies<T> iterator I enumerated both strings and byte arrays, trying to parse the latter into strings, and attempted to instantiate JObjects on the returned data. Unfortunately, this turned out to be a dead end and no credentials were stored as JSON strings or JSON byte arrays in the process heap. Password strings as a discrete unit, outside of a JSON encapsulation, were on the heap, but a string dump of the process heap isn’t exactly useful weaponization.

ItemData

The ItemData object is a more complex type that contains what’s called “non-blittable” fields. “What are blittable types?” you might ask, considering it’s a word I’d never encountered before. As it turns out, I’m pretty sure it’s a brand-new phrase that Microsoft invented, and it’s used to define types that have similar memory layouts in managed and unmanaged memory, meaning there’s no special handling that needs to be done by the interop marshaler. In essence, it means that “primitive types” are easily retrievable if you know the address of the object in memory, but everything else requires “work” (if possible at all). Primitive types are defined as follows:

You might notice that Newtonsoft.Json.Linq.JObject is not on this list, because nothing can ever be easy. Also not on this list: Dictionaries. JObjects are dictionaries of key-Newtonsoft.Json.Linq.JTokens essentially, and without the ability to manipulate dictionaries using CLRMD, JObjects are a bust.

You might be thinking to yourself, “If I have the address of an object in memory, and know that object’s size, couldn’t I copy that object from that process to my own and recast it as the object’s type?” Well, I tried that, and it failed for a variety of reasons. For one, I don’t know the memory layout of these complex types without further analysis. This is important to know because I’d need to start reading memory offsets from the objects address to start populating fields on duplicated object. Without being able to read those fields and, more importantly, the pointers within that object, when attempting to remarshal/retype that object will fail.

So recasting our gaze to JObjects, and knowing they store a collection of key-JToken pairs, we examine JTokens, and find those JTokens have JValues. JValues have two fields on them that make them extremely exciting for us. They contain:

  1. A _value field that stores an untyped object.
  2. A field that specifies the type of the object stored in the _value field.

This means that the _value field on JValues contain possibly blittable types, and as a result, we could read their values directly. Walking the heap and filtering JValue object types to just string types, we see that we’re actually able to pull some results back! We see that we are pulling some JValues with values of username, followed by the next JValue containing the username, and password, followed by the value of that password. Logically this makes sense. If you’re creating a new JObject filled with details of a secret, the associated JValues will most likely reside physically next to each other in memory.

Snippet to Parse JValues from the Heap
Dumping All String JValues from 1Password Heap

There’s just one problem with this approach though: 1Password only stores ItemData for items the user has recently retrieved through the UI. This is far from ideal, so we go on to the fourth and final attempt.

A Brief Aside about Proxy Credentials

1Password has the optional ability to connect to a network proxy in its configuration. This configuration is loaded at boot and saved on the Opw.ProxyCredentials. If you walk the heap, using the same reflection methods shown above, you can retrieve those proxy credentials from the requisite CLR objects. I’ve laid out all the bread crumbs, and retrieving those is left as an exercise to the reader.

The Object Containing Network Proxy Credentials

Attempt #4: Same Same, but Different

Attempt #4 should’ve been attempted directly after Attempt #1, but unfortunately, I failed to follow the tried and true K.I.S.S. principle.

Instead of loading the 1Password library into my current process, why not inject my code into 1Password.exe, and stick with the plan of invoking get_item_data and decrypt_with_vault_id directly. To get the requisite function pointers, I’d use GetProcAddress instead of reflectively loading the assembly, and write the results out to the log file so long as the function pointers resolved. To get the item IDs we want, I’ll just brute force 1000 entries and see what gets spit out.

Extracting Secrets from 1Password using a Medium Integrity Context

Injecting into 1Password

Up until this point in my testing, I had run all of my injection tests into 1Password as an administrator. Even though 1Password was running in a medium integrity context, I couldn’t acquire a process handle with the requisite access rights to allocate and write memory to it. Now that I had a working POC, I needed to weaponize the injection in such a way that I could perform it in a medium integrity instead of high.

With a little help from Lee Christensen, he pointed out that the default permissions for 1Password were rather restrictive as shown below.

Default DACL for 1Password.exe

1Password is launched in the same desktop session as the user though, and the user starts the process, so the user is the owner of that process. As a result, I could obtain a process handle with WRITE_DAC and QUERY_EXTENDED_INFORMATION on the process, and using those rights I could adjust the Discretionary Access Control List (DACL) of the process to receive a handle with PROCESS_ALL_ACCESS. Once the new DACL is set, I can get a new handle to 1Password with PROCESS_ALL_ACCESS, and successfully inject shellcode.

Full access to 1Password after DACL adjustment

Detection Guidance

Illegitimate password retrieval is somewhat challenging to detect, but not impossible. This technique requires:

Detecting Process Injection

The technique presented here relies on a handful of exported functions in 1Password DLL, and in order to load that DLL I inject directly into 1Password. Sysmon’s event ID (EID) 10 and Windows EID 4663 can detect process access and list the process accessing 1Password. Specifically, we’re looking for process access that at the bare minimum has PROCESS_VM_OPERATION and PROCESS_VM_WRITE, which is the minimum rights required to allocate new memory in a remote process. The method of execution here can vary from method to method, but for the POC I provide, additional access rights required are:

  • PROCESS_CREATE_THREAD
  • PROCESS_QUERY_INFORMATION
  • PROCESS_VM_READ

Another method not shown here is adding a new 1Password.exe.config file pointing to a DLL in the same folder as 1Password. That DLL could contain the same code in this POC to dump passwords on 1Password start. Setting a SACL on the folder itself and monitoring for that specific configuration file being added could add additional detection telemetry to this technique.

Brief Note: Not all processes accessing 1Password are inherently bad (e.g., Process Explorer), so it’s important to baseline what normal access looks like as compared to malicious access.

Detecting DACL Modifications

Windows provides a native event outside of Event Tracing for Windows (ETW), EID 4670, to monitor when security permissions on an object have changed. Specifically, you’ll be filtering for Object Types of “Process” and want to correlate the handle of this event to any 4663 events to see if someone is attempting to loosen the DACL on the 1Password process. This is required to inject into 1Password from a medium integrity context, and used in this POC.

Detecting Foreign AppDomains

Microsoft’s ETW provides several events surrounding CLRs being loaded and, in the case we care for, application domains being loaded. We only care about the latter as the 1Password binary itself is written in .NET, so hunting for CLR load events won’t help us. Instead, turn a keen eye to ETW EIDs 156 — 158. These events will log when an AppDomain is loaded and unloaded and, more importantly, the name of the application domain performing some action. If that AppDomain name isn’t 1Password, then someone else is having a very good time (hint: it’s not you or your organization).

Detecting 1Password SQLite Database Access

While I don’t implement this technique, if someone were to recover the master password to the 1Password database, they’d be able to decrypt the contents of the SQLite database manually without having to inject into 1Password like I do here. To ensure you’re catching edge cases, I’d create a System Access Control List (SACL) on the the SQLite database, located in “data” subfolder of the 1Password install location. In the same way that you might flag when non-Chrome processes access Google Chrome’s LoginData file, you’d monitor for processes attempting to access the SQLite database that aren’t 1Password.exe.

Conclusion

In the end, persistence paid off. If I had worked smarter instead of harder, I could’ve saved myself a hefty bit of time (read: days); however, I probably would not have become so intimately familiar with CLRMD and reflection if I hadn’t.

In this industry it’s easy to just document what worked, get the CVE, and release the POC. What we rarely see is the trials and tribulations of the researcher, their thought process, and how they tackle problems. Hopefully my approach will be illuminating to some.

That said, you shouldn’t get to the end of a lengthy blog post without some sort of Github link, so the 1Password Secret Retrieval Utility can be found below:

Addendum

The secrets in the 1Password vaults are stored in the 1Password SQLite database. I suspect they’re encrypted using a combination of BCrypt and DPAPI, in what I’m assuming is a somewhat similar fashion that Chromium browsers do. If one were to enumerate how that encryption mechanism worked and implement it themselves, then no process injection would be required, and instead you could just load the database and retrieve the secrets directly. This method works, and if it’s not broke don’t fix it, but if you want to go down the rabbit hole then by all means.

--

--