Normal view

There are new articles available, click to refresh the page.
Before yesterdaySliding Window

Windows Internals Notes

27 January 2024 at 02:40

I spent some time over the Christmas break least year learning the basics of Windows Internals and thought it was a good opportunity to use my naive reverse engineering skills to find answers to my own questions. This is not a blog but rather my own notes on Windows Internals. I’ll keep updating them and adding new notes as I learn more.

Windows Native API

As mentioned on Wikipedia, several native Windows API calls are implemented in ntoskernel.exe and can be accessed from user mode through ntdll.dll. The entry point of NTDLL is LdrInitializeThunk and native API calls are handled by the Kernel via the System Service Descriptor Table (SSDT).

The native API is used early in the Windows startup process when other components or APIs are not available yet. Therefore, a few Windows components, such as the Client/Server Runtime Subsystem (CSRSS), are implemented using the Native API.  The native API is also used by subroutines from Kernel32.DLL and others to implement Windows API on which most of the Windows components are created.

The native API contains several functions, including C Runtime Function that are needed for a very basic C Runtime execution, such as strlen(); however, it lacks some other common functions or procedures such as malloc(), printf(), and scanf(). The reason for that is that malloc() does not specify which heap to use for memory allocation, and printf() and scans() use a console which can be accessed only through Kernel32.

Native API Naming Convention

Most of the native APIs have a prefix such as Nt, Zw, and Rtl etc. All the native APIs that start with Nt or Zw are system calls which are declared in ntoskernl.exe and ntdll.dll and they are identical when called from NTDLL.

  • Nt or Zw: When called from user mode (NTDLL), they execute an interrupt into kernel mode and call the equivalent function in ntoskrnl.exe via the SSDT. The only difference is that the Zw APIs ensure kernel mode when called from ntoskernl.exe, while the Nt APIs don’t.[1] 
  • Rtl: This is the second largest group of NTDLL calls. It contains the (extended) C Run-Time Library, which includes many utility functions that can be used by native applications but don’t have a direct kernel support.
  • Csr: These are client-server functions that are used to communicate with the Win32 subsystem process, csrss.exe.
  • Dbg: These are debugging functions such as a software breakpoint.
  • Ki: These are upcalls from kernel mode for events like APC dispatching.
  • Ldr: These are loader functions for Portable Executable (PE) file which are responsible for handling and starting of new processes.
  • Tp: These are for ThreadPool handling.

Figuring Out Undocumented APIs

Some of these APIs that are part of Windows Driver Kit (WDK) are documented but Microsoft does not provide documentation for rest of the native APIs. So, how can we find the definition for these APIs? How do we know what arguments we need to provide?

As we discussed earlier, these native APIs are defined in ntdll.dll and ntoskernl.exe. Let’s open it in Ghidra and see if we can find the definition for one of the native APIs, NtQueryInformationProcess().

Let’s load NTDLL in Ghidra, analyse it, and check exported functions under the Symbol tree:

Well, the function signature doesn’t look good. It looks like this API call does not accept any parameters (void) ? Well, that can’t be true, because it needs to take at least a handle to a target process.

Now, let’s load ntoskernl.exe in Ghidra and check it there.

Okay, that’s little bit better. We now at least know that it takes five arguments. But what are those arguments? Can we figure it out? Well, at least for this native API, I found its definition on MSDN here. But what if it wasn’t there? Could we still figure out the number of parameters and their data types required to call this native API?

Let’s see if we can find a header file in the Includes directory in Windows Kits (WinDBG) installation directory.

As you can see, the grep command shows the NtQueryInformationProcess() is found in two files, but the definition is found only in winternl.h.

Alas, we found the function declaration! So, now we know that it takes 3 arguments (IN) and returns values in two of the structure members (OUT), ReturnLength and ProcessInformation.

Similarly, another native API, NtOpenProcess() is not defined in NtOSKernl.exe but its declaration can be found in the Windows driver header file ntddk.h.

Note that not all Native APIs have function declarations in user mode or kernel mode header files and if I am not wrong, people may have figured them out via live debug (dynamic analysis) and/or reverse engineering.

System Service Descriptor Table (SSDT)

Windows Kernel handles system calls via SSDT and before invoking a syscall, a SysCall number is placed into EAX register (RAX in case of 64 bit systems). It then checks the KiServiceTable from Kernel’s address space for this SysCall number.

For example, SysCall number for NtOpenProcess() is 0x26. we can check this table by launching WinDBG in local kernel debug mode and using the dd nt!KiServiceTable command.

It displays a list of offsets to actual system calls, such as NtOpenProcess(). We can check the syscall number for NtOpenProcess() by making the dd command display the 0x27th entry from the start of the table (It’s 0x26 but the table index starts at 0).

As you can see, adding L27 to the dd nt!KiServiceTable command displays the offsets for up to 0x26 SysCalls in this table. We can now check if offset 05f2190 resolves to NtOpenProcess(). We can ignore the last bit of this offset, 0. This bit indicates how many stack parameters need to be cleaned up post the SysCall.

The first four parameters are usually pushed to the registers and remaining are pushed to the stack. We have just one bit to represent number of parameters that can go onto the stack, we can represent maximum 0xf parameters.

So in case of NtOpenProcess, there are no parameters on the stack that need to be cleaned up. Now let’s unassembled the instructions at nt!KiServiceTable + 05f2190 to see if this address resolves to SysCall NtOpenProcess().

References / To Do

https://en.wikipedia.org/wiki/Windows_Native_API

https://web.archive.org/web/20110119043027/http://www.codeproject.com/KB/system/Win32.aspx

https://web.archive.org/web/20070403035347/http://support.microsoft.com/kb/187674

https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntqueryinformationprocess

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

https://techcommunity.microsoft.com/t5/windows-blog-archive/pushing-the-limits-of-windows-processes-and-threads/ba-p/723824

Windows Device Driver Documentation: https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/_kernel/

Process Info, PEB and TEB etc: https://www.codeproject.com/Articles/19685/Get-Process-Info-with-NtQueryInformationProcess

https://www.microsoftpressstore.com/articles/article.aspx?p=2233328

Offensive Windows Attack Techniques: https://attack.mitre.org/techniques/enterprise/

https://www.ired.team/miscellaneous-reversing-forensics/windows-kernel-internals/exploring-process-environment-block

https://github.com/FuzzySecurity/PowerShell-Suite/blob/master/Masquerade-PEB.ps1

https://s3cur3th1ssh1t.github.io/A-tale-of-EDR-bypass-methods/

Windows Drivers Reverse Engineering Methodology

Exploiting Inter-Process Communication through Shared Memory

14 May 2023 at 13:58

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

❌
❌