After I encrypt memory with CryptProtectMemory, can I move it around?


A customer had a question about the the Crypt­Protect­Memory function. After using it to encrypt a memory block, can the memory block be moved to another location and decrypted there? Or does the memory block have to be decrypted at the same location it was encrypted?

The answer is that the memory does not need to be decrypted at the same memory address at which it was encrypted. The address of the memory block is not used as part of the encryption key. You can copy or move the memory around, and as long as you don't tamper with the bytes, and you perform the decryption within the scope you specified, then it will decrypt.

That the buffer can be moved around in memory is obvious if the scope was specified as CRYPT­PROTECT­MEMORY_CROSS_PROCESS or CRYPT­PROTECT­MEMORY_SAME_LOGON, because those scopes encompass more than one process, so the memory will naturally have a different address in each process. The non-obvious part is that it also holds true for CRYPT­PROTECT­MEMORY_SAME_PROCESS.

You can also decrypt the buffer multiple times. This is handy if you need to use the decrypted contents more than once, or if you want to hand out the encrypted contents to multiple clients, and leave each client to delay decrypting the data until immediately before they need it. (And then either re-encrypting or simply destroying the data after it is no longer needed in plaintext form.)

Today's Little Program demonstrates the ability to move encrypted data and to decrypt it more than once.

#include <windows.h> #include <wincrypt.h> #include <stdio.h> // horrors! mixing C and C++! union MessageBuffer { DWORD secret; char buffer[CRYPTPROTECTMEMORY_BLOCK_SIZE]; }; static_assert(sizeof(DWORD) <= CRYPTPROTECTMEMORY_BLOCK_SIZE, "Need a bigger buffer"); int __cdecl main(int, char **) { MessageBuffer message; // Generate a secret message into the buffer. message.secret = GetTickCount(); printf("Shhh... the secret message is %u\n", message.secret); // Now encrypt the buffer. CryptProtectMemory(message.buffer, sizeof(message.buffer), CRYPTPROTECTMEMORY_SAME_PROCESS); printf("You can't see it now: %u\n", message.secret); // Copy the buffer to a new location in memory. MessageBuffer copiedMessage; CopyMemory(copiedMessage.buffer, message.buffer, sizeof(copiedMessage.buffer)); // Decrypt the copy (at a different address). CryptUnprotectMemory(copiedMessage.buffer, sizeof(copiedMessage.buffer), CRYPTPROTECTMEMORY_SAME_PROCESS); printf("Was the secret message %u?\n", copiedMessage.secret); SecureZeroMemory(copiedMessage.buffer, sizeof(copiedMessage.buffer)); // Do it again! CopyMemory(copiedMessage.buffer, message.buffer, sizeof(copiedMessage.buffer)); // Just to show that the original buffer is not needed, // let's destroy it. SecureZeroMemory(message.buffer, sizeof(message.buffer)); // Decrypt the copy a second time. CryptUnprotectMemory(copiedMessage.buffer, sizeof(copiedMessage.buffer), CRYPTPROTECTMEMORY_SAME_PROCESS); printf("Was the secret message %u?\n", copiedMessage.secret); SecureZeroMemory(copiedMessage.buffer, sizeof(copiedMessage.buffer)); return 0; }

Bonus chatter: The enumeration values for the encryption scope are rather confusingly named and numbered. I would have called them

Old name Old value New name New value
CRYPT­PROTECT­MEMORY_SAME_PROCESS 0 CRYPT­PROTECT­MEMORY_SAME_PROCESS 0
CRYPT­PROTECT­MEMORY_SAME_LOGON 2 CRYPT­PROTECT­MEMORY_SAME_LOGON 1
CRYPT­PROTECT­MEMORY_CROSS_PROCESS 1 CRYPT­PROTECT­MEMORY_SAME_MACHINE 2

I would have changed the name of the last flag to CRYPT­PROTECT­MEMORY_SAME_MACHINE for two reasons. First, the old name CRYPT­PROTECT­MEMORY_CROSS_PROCESS implies that the memory must travel to another process; i.e., that if you encrypt with cross-process, then it must be decrypted by another process. Second, the flag name creates confusion when placed next to CRYPT­PROTECT­MEMORY_SAME_LOGON, because CRYPT­PROTECT­MEMORY_SAME_LOGON is also a cross-process scenario.

And I would have renumbered the values so that the entries are in a logical order: Higher numbers have larger scope than lower values.

Exercise: Propose a theory as to why the old names and values are the way they are.

Comments (23)
  1. Joshua says:

    Plausable solution: order of creation. NT wasn't multiuser until NT4 Terminal Server add-on.

    Exercise: dumbbell wave

  2. Kevin says:

    Exercise: CROSS_PROCESS is older than SAME_LOGON, and is more permissive due to backwards compatibility.

  3. NotThisMind says:

    Because the Cross Process was first designed to actually imply sharing memory between processes?

  4. Chris Crowther @ Work says:

    At a guess because CryptProtectMemory pre-dates multi-simultaneous users versions of Windows?

  5. Josh says:

    Because the enumerations themselves are encrypted?

  6. AndyCadley says:

    I'd assume because the predecessor, RtlEncryptMemory, specified the original versions and lacked SAME_LOGON originally?

  7. Gabe says:

    Windows NT has been multi-user since 1989 — in other words, multi-user security was designed in from Day One rather than added as an afterthought. Now it may not have had the concept of multiple window stations when it was first released in 1993, but it has always had different user contexts and impersonation.

    While NT 3.1 could not has supported multiple users running interactive Windows applications, it was certainly possible to have a file server that had multiple users logged in all in the same process (using impersonation to act as the logged in user) or, to have a telnet server that had different user processes running as different users. The CRYPT­PROTECT­MEMORY_SAME_LOGON option would have been useful even then, had the API existed.

  8. Myria says:

    I see two ways in which the design could have originated:

    Theory 1: dwFlags was originally a BOOL, named fAllowCrossProcess or something.  Programs passing TRUE for this parameter would be interpreted by the modern version as passing CRYPTPROTECTMEMORY_CROSS_PROCESS, while simultaneously extending the function to support the later additional feature CRYPTPROTECTMEMORY_SAME_LOGON.

    Theory 2 (IMO more likely): dwFlags was originally intended to be a flags parameter, but now doesn't really mean "flags".  Only one flag was defined at first, CRYPTPROTECTMEMORY_CROSS_PROCESS.  Eventually, the meaning of dwFlags morphed into meaning "scope" rather than "flags".  The absence of the former "flag" had to be given a name, hence CRYPTPROTECTMEMORY_SAME_PROCESS.  The same-machine scope was left as the former flag's name, CRYPTPROTECTMEMORY_CROSS_PROCESS.  The new functionality was added as CRYPTPROTECTMEMORY_SAME_LOGON.

  9. Billy O'Neal says:

    Because the people naming the flags were viewing the world through kernel-colored glasses?

  10. skSdnW says:

    @AndyCadley: The original function in ADVAPI32 goes back to Win2000.SP3 as far as I can tell so I don't see how the lack of multiple logons/runas/terminal server could be the reason. MSDN documents the RTL_ENCRYPT_OPTION_SAME_LOGON flag with no version notes and the Wine implementation is just a stub so somebody has to dig out a VM to really find out…

  11. Roman says:

    > #include <stdio.h> // horrors! mixing C and C++!

    I think if you include <assert.h>, it becomes valid C11.

    As for the exercise, I'm going to guess that originally only CRYPT­PROTECT­MEMORY_SAME_PROCESS and CRYPT­PROTECT­MEMORY_CROSS_PROCESS, so the common part was "PROCESS", and not "SAME". And when they added a new enumerator, it didn't fit the previous convention, but they didn't change the convention (or the numbers), because backwards compatibility.

  12. JamesJohnston says:

    I give up.  At first I thought that the function would exist since the early dates, and the newest flag added with NT 4.0 Terminal Server.

    Then I saw that the API itself didn't exist until Vista/Server 2003.  The reasoning of @Roman makes perfect sense, but normally I'd have expected this situation over a period of years, whereas it appears in this case it happened during the timespan of the Server 2003 beta period.  I guess they didn't decide to add SAME_LOGON until too late in the beta process to make any kind of breaking change.

    Also this seems to be very limited in security, just to try to limit the damage of a successful attack.  There are lots of opportunities to grab the memory before/after the encryption.  If the user can't do that, then they couldn't read it either if it was just sitting around unencrypted in RAM 100% of the time (see: airtight hatchway).  For example, here we have a race condition where the memory is unencrypted before the call to CryptProtectMemory.

  13. Adam Rosenfield says:

    Considering that CryptProtectMemory until Vista and Server 2003, I'm going to go out on a limb and say that this *didn't* pre-date NT4/multiuser Windows.

  14. Joshua says:

    Ref: Wine

    I suspect the "stub" technically meets the API contract of this API, which is why they got away with it, ignoring the fact it makes it useless.

  15. sense says:

    Is it true that SAME_LOGON can be broader than CROSS_PROCESS? For example a RPC server impersonating a remote client?

    Then the higher enumeration value makes more sense. (Normally I'd have gone with the creation-order theory though)

  16. AndyCadley says:

    @skSdnW: True, but since it was originally an undocumented function AFAIK I'd assume what happens went along the lines of Dev A creates function internally with options for Same Process/Cross Process, other Devs start using it. Dev B points out a scenario Dev A forgot, leading to an additional new option (Same Logon). Dev A decides it's not worth renaming the previous options, because it's a breaking change and the function isn't intended to be exposed publicly anyway. Then function get exposed publicly with weird names.

  17. Anonymous Coward says:

    @Joshua

    Probably because no Wine developer was bothered to implement it when there are much more important API calls. Priorities, and limited developer time…

  18. Neil says:

    @Joshua: also, it's not an emulator, so they only implement the bits that people actually need. (So for instance they might completely ignore the scope.)

  19. Kevin says:

    The target audience of Wine is not developers.  It's end users.  They don't care about security, correctness, etc.  I imagine a lot of the Wine API swallows calls like this one.

  20. foo says:

    Propose.. Maybe their best-fit enumeration values and names are what they are. Too bad for you

  21. Joshua says:

    @Kevin: In this case no matter. Unlike on Windows, crash dumps are off by default on Wine so you only get the security leak if you ask for it. Also, trying to ReadProcessMemory cross-user will fail no matter what as cross-user processes can't even see each other.

  22. Danny says:

    "After I encrypt memory with CryptProtectMemory, can I move it around?" Yes you can. But why bother anyway? Just move the data as it is, because Windows and cryptography is the same as NASA and Mars human expedition; a big company on a big subject with zero results so far.

  23. Gabe says:

    Joshua: The security leak isn't just in crash dumps — it's in any way that your memory can leak out. For example, the memory may get paged to disk or written to the hibernation file.

    Another way your memory can leak out is via uninitialized memory or buffer overread errors. This is how Heartbleed happened. You simply ask the SSL server for a random chunk of its memory and it sends it to you. If the keys are all encrypted except for the brief period of time when they're in use, getting a random chunk of OpenSSL's memory isn't too useful. If the keys are all sitting around in memory in plain text, a random chunk of memory can include things like private keys!

Comments are closed.

Skip to main content