Exploiting Inter-Process Communication through Shared Memory

Update (June 20 2023): This blog is based on the vulnerability I discovered/reported in SAP SQLAnywhere 17.0 (CVE-2023-33990) back in January 2023. SAP patched it in July 2023; however, their product security response team declined to credit me for this disclosure because I reported the vulnerability through their customer support channel, which was the standard procedure at Veritas.

Sometimes during our Penetration tests, we come across Windows applications or programs that use shared memory for Inter-Process Communication (IPC). It’s either one of its own services or some third-party component, application, or service it interacts with.

There are two ways processes can communicate with each other, Shared Memory and Message Passing, and these are known as IPC models. In this blog post, we will look at a Shared Memory model that uses Memory Mapped File (MMF) which is also known as Section Object.

We will write a short C program to intentionally create an insecure shared memory region using memory-mapped file and then write another C program to abuse or exploit this insecure shared memory to read/dump data from it. The sourced and binaries are available on my Github repo.

What is Inter-Process Communication?

When a process communicates with another process running on the same system, it’s called Inter-Process Communication (IPC). There are several use cases to for IPC and some of them include:

  1. A process may need to share some data or resources with other processes, for example, environment data from system.
  2. Divide the task in several subtasks and spawn new processes for each subtask to improve performance.
  3. Integrating with third-party modules, components or services.

Moreover, there are several benefits to implementing IPC using shared memory over normal file or network I/Os and yes, it comes with some drawbacks too. Please refer to the links in the References section for more information about pro’s and con’s of using shared memory.

Let’s understand how Process A can communicate with Process B running on the same system using shared memory with the help of following diagram:

Reference: The Neso Academy YouTube Channel

If Process A wants to communicate with Process B, it first needs to create a shared memory region in its own address space and set appropriate permissions on it. The Process B then needs to attach to this shared memory region and read data from or write data to this region depending on what it was designed to do.

How to create a shared memory region then?

In this blog post, we will look at creating a shared memory region using memory-mapped files (MMF). A memory-mapped file is a region in (virtual) memory where contents of a file are loaded and the mapping between this file and memory space is maintained.This enables an application, including multiple processes, to modify the file by reading and writing directly to the memory instead of performing file I/O which is cumbersome.

There are two types of memory-mapped files:

  • Persisted memory-mapped files – In this type, we specify a file on the disk to be used for shared memory. The contents of this file are then loaded into memory and when all processes finish working on it, the data is then written or saved to this file on the disk. This is useful when dealing with large files, especially on 32-bit systems where we cannot have more than 4 GB shared memory.
  • Non-persisted memory-mapped files – In this type, we instruct the program to use the system Page file instead of using any specific file on the disk. The data, however, is lost when the processes finish working on it. These files are suitable for creating shared memory for inter-process communications (IPC).

We will go with the second option, non-persisted memory-mapped file in which we will use the system Page file for creating a shared memory region and we will use one of the commonly used Windows API called CreateFileMapping().

Process A – Creating a shared memory (memory-mapped file backed by or using the system Page file)

This is the process that would be creating a new shared memory region in its own address space and here’s the excerpts from the code.

HANDLE hFileMapping;
PWSTR SharedMemData;
LPVOID lpFileMap = NULL; // Initialise to NULL to avoid uninitialised pointer variable error.
int fMapSize = 4 << 10; // 4 KB 
const wchar_t* fMapName = L"Global\\SharedMemTest";

hFileMapping = CreateFileMapping(INVALID_HANDLE_VALUE,  &sa,  PAGE_READWRITE,  0,  fMapSize,  fMapName);
//error handling here

The function CreateFileMapping() takes six arguments, however the second and sixth arguments are optional. Let’s try to understand it with the help of following representation:

Here, we’re passing INVALID_HANDLE_VALUE to create a shared memory region/page using the system Page file. The second argument &sa is a pointer to the SECURITY_ATTRIBUTES structure. We will briefly discuss it later in this blog post. The third argument sets Read/Write permissions on this memory region.

The fifth parameter fMapSize specifies the size for the memory region, which is 4 KB in this case and the last parameter specifies the name for this memory region, which is “Global\SharedMemTest” in this case. The prefix Global allows this memory region to be accessible from other Logon sessions, so any other user logged into the machine can read from ProcessA’s memory though it was started by another user, say Administrator.

If the CreateFileMapping() function succeeds, it returns a handle to the newly created file mapping object. This handle is then stored in the variable of type HANDLE, hFileMapping.

ProcessA – Map the file mapping object into the address space

After that, we need to map this file mapping object into ProcessA’s address space and we do that using the MapViewofFile() function.

lpFileMap = MapViewOfFile(hFileMapping,   FILE_MAP_ALL_ACCESS,  0,  0,   fMapSize);
//error handling here

This function takes 5 parameters. The first one is a handle to the file mapping object, which in our case is hFileMapping. The second argument FILE_MAP_ALL_ACCESS set the read/write access on this file mapping object, so that ProcessA can write into it.

The third and fourth parameters are dwFileOffsetHigh and dwFileOffsetLow respectively. We’ve set them both to 0 so that can map this file mapping object from the beginning. The last parameter, fMapSize specifies the maximum size for this file mapping object. In our case, it’s 4 KB ( int fMapSize = 4 << 10 ).

If successful, this function returns the starting address of the mapped view which is then stored in variable lpFileMap.

ProcessA – Writing into the shared memory

So far, we created a shared memory region and mapped it into the ProcessA’s address space, we can now start writing data into it using either the memcpy() function or wcsspy_s() function. Here’s the code:

wcscpy_s((PWSTR)lpFileMap, sizeof(secretData), secretData);
SharedMemData = lpFileMap;

printf("The following connection string was written to the shared memory %ls successfully:\n\n%s", fMapName, SharedMemData);

So, we pass the starting address of the mapped view (lpFileMap) and secretData along with its size. The secretData string should now be in the shared memory and we should be able to print it back:

ProcessA – Setting Permission on the shared memory

While creating a file mapping object using CreateFileMapping() function, the second argument, &sa we passed to it was a pointer to SECURITY_ATTRIBUTES structure (lpFileMappingAttributes).

If this attribute is set to NULL, the handle cannot be inherited and the file mapping object gets a default security descriptor. The access control lists (ACL) in the default security descriptor for a file mapping object come from the primary or impersonation token of the creator.

However, there are uses cases where we need to change the permissions on file mapping objects and this is where things usually go wrong. We either end up setting incorrect permissions or no permissions at all. Let’s understand it with the help of following diagram:

As you can see, the second parameter to CreateFileMapping() is a pointer to SECURITY_ATTRIBUTES structure called sa. One of the members of this structure, lpSecurityDescriptor holds a pointer another structure sd which is a SECURITY_DESCRIPTOR.

This security descriptor structure contains information about an object that we want to secure, in this case, file mapping object SharedMemTest. We can set Security identifiers (SIDs) as well as Discretionary Access Control List  (DACL) to specify which users or groups will have access to this object.

As you can see from the code snippet above, we’re giving members of the domain or local users group full access to this file mapping object.So, any user or process should be able to read and write into this shared memory region as long as they are part of at least domain/local users group.

We can use Process Explorer to check permissions on this object.

As you can see, ProcessA created a named file mapping object (shared memory) “SharedMemTest” and since this machine is not part of a domain yet, it granted full access to the members of the local Users group. Any low privileged local user should be able to read from this shared memory region as well as write to it.

In some cases, you may see no permissions assigned at all. In such cases, a warning will be displayed –

“No permissions have been assigned to this object.

This is a potential security risk because anyone who can access this object can take ownership of it. The object’s owner should assign permissions as soon as possible.”

So to summarise, we created a shared memory region called SharedMemTest using CreateFileMapping() function, set permissions on it such that any user who’s part of the Users group should be able to read from and write to it, and then wrote a secretData string into it.

SharedMemReader – Reading data from insecure shared memory region

Now, we need to see how any process or user with low privileges can read from or write to ProcessA’s shared memory SharedMemTest. This process should be able to attach to the shared memory “SharedMemTest” created by ProcessA. For that we need to use the same function MapViewOfFile() that we saw earlier. Here’s the code snippet from SharedMemReader.C

LPCSTR lpName;
HANDLE hFileMapping = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, lpName);

// Store the starting address of the mapped view into lpMapStartAddress
LPVOID lpMapStartAddress = MapViewOfFile(hFileMapping, FILE_MAP_READ, 0, 0, 0);

printf("\n[+]Dumping data from shared memory:\n");
printf("%s\n", (LPCSTR)lpMapStartAddress); // Typecast lpMapStartAddress to LPCSTR

Let’s login to the machine with admin account, launch ProcessA.exe and then we will SSH into this machine using a low privileged account called “lowpriv”, and launch SharedMemReader.exe to see if we can read the secretData string from ProcessA’s shared memory, “SharedMemTest”.

What’s the impact?

Depending on what permissions the victim process assigns to the shared memory object, it may be possible any low privileged user or a process to read data from it, write malicious data into process’s memory and/or crash it (DoS).

In some cases, it may be possible for an attacker or attacking process exploit this issue for privilege escalation.

How to test?

As we saw earlier, we can use Process Explorer to manually check if the target process or application fails to secure named or unnamed objects properly, and then use the SharedMemReader.exe that we wrote, to see if we can read from the process memory.

Please note that we need to launch Process Explorer with administrative rights so that it can query the target process for all the section objects it created. Otherwise, we wouldn’t see any handles for the target process.

Once we find named or unnamed section object(s), we can then try reading from or writing into them. For DoS, we can analyse the crash in WinDBG, x64dbg, x86dbg or Immunity Debugger.

Source and Binaries

https://github.com/SlidingWindow/ipc-shared-memory

References

https://www.x86matthew.com/view_post?id=shared_mem_utils

https://learn.microsoft.com/en-us/previous-versions/windows/desktop/legacy/aa379560(v=vs.85)

https://learn.microsoft.com/en-gb/windows/win32/api/winnt/ns-winnt-security_descriptor?redirectedfrom=MSDN

https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createfilemappinga

https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-openfilemappinga

https://learn.microsoft.com/en-us/windows/win32/secauthz/access-control-lists

https://learn.microsoft.com/en-us/windows/win32/secauthz/security-identifiers

https://learn.microsoft.com/en-us/windows/win32/secauthz/well-known-sids

Leave a comment