In the field, you need tools that do exactly what they say without the fluff. Safe Harbor is one of those tools - a BOF to help operators quickly identify processes that are convenient for covert operations. It serves two purposes: one, to locate "safe" processes during post-exploitation, and two, as a straightforward example of using Windows APIs in BOFs for red team tooling, in case you are new to this. There is really nothing fancy in terms of WinAPI wizardry, so I thought talking about this BOF was a good way to also help people who have not tried writing them before, understand how they are made.
The Dual Purpose
Practical Use in the Field
Safe Harbor helps you find processes that blend in, making them good candidates for injection or further post-exploitation actions. It targets processes with trusted modules like wininet.dll
or winhttp.dll
, and scans for RWX memory regions, indicators that can signal an ideal injection point. Additionally, it verifies digital signatures, so you know you’re working with processes that fit the bill in terms of being less scrutinized by EDRs.
A Hands-On Windows API Example
Beyond its operational value, this BOF is a minimalistic example of using key Windows APIs. It shows how to:
- Enumerate processes with
EnumProcesses
- Open process handles using
OpenProcess
- Retrieve module info via
EnumProcessModules
andGetModuleFileNameExA
- Scan memory with
VirtualQueryEx
- Verify signatures using
WinVerifyTrust
The code pretty much gets straight to the point. It’s a starting point if you’re building your own tools or just want to see practical Windows API use in action.
From Gimme Shelter to Safe Harbor
The concept behind Safe Harbor draws heavy inspiration from the techniques outlined in Gimme Shelter. The idea is simple: find processes that are likely to be ignored by defensive systems. By targeting processes that are signed or use common DLLs, you’re less likely to trigger alarms. It’s about minimizing your footprint and keeping your operations off the radar. Landing beacons in "safe havens" can help stay around for longer without being detected. Ultimately this should push defensive teams to re-calibrate their detection threshold by challenging assumptions.
High-Level Overview
Here’s a quick look at some core snippets:
Process Enumeration & Handle Acquisition
// Allocate memory for process IDs and enumerate processes
DWORD* processes = (DWORD*)MyAlloc(sizeof(DWORD) * 1024);
if (!EnumProcesses(processes, sizeof(DWORD) * 1024, &cbNeeded)) {
// Handle error...
}
DWORD processCount = cbNeeded / sizeof(DWORD);
Here, we're calling EnumProcesses
and storing the process array inside processes
. Later, we'll be iterating through these processes and picking out useful information about them like whether they are running from signed binaries, loaded module information, etc...
Module Inspection & Memory Scanning
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid);
if (hProcess) {
HMODULE* modules = (HMODULE*)MyAlloc(sizeof(HMODULE) * 1024);
EnumProcessModules(hProcess, modules, sizeof(HMODULE) * 1024, &cbModulesNeeded);
// Scan memory for RWX segments
MEMORY_BASIC_INFORMATION mbi;
DWORD_PTR addr = 0;
while (VirtualQueryEx(hProcess, (LPCVOID)addr, &mbi, sizeof(mbi))) {
if (mbi.State == MEM_COMMIT && mbi.Type == MEM_PRIVATE && mbi.Protect == PAGE_EXECUTE_READWRITE) {
// Process this RWX region
}
addr += mbi.RegionSize;
}
MyFree(modules);
CloseHandle(hProcess);
}
This snippet illustrates how to pull module info and scan for existing RWX memory in running processes - key indicators for potential injection sites. By leveraging existing RWX memory regions, we can avoid calling VirtualAlloc
or similar APIs ourselves, therefore skipping one common phase of process injection routines. Reducing the telemetry we are generating, generally will give us a better chance at staying undetected.
Instead of aggressively scanning all memory, it uses VirtualQueryEx
to only check committed, private regions with RWX permissions - focusing on the areas that matter most for injection opportunities while attempting to minimise telemetry generated.
.NET and HTTPs Helper Libraries
Many C2 agents will rely on specific DLLs being loaded into processes that they then leverage for connectivity and post-exploitation activity. Three interesting ones we want to be aware of as attackers are:
- mscoree.dll / clr.dll
- wininet.dll
- winhttp.dll
Both of the win*.dll libraries are generally useful for agents that communicate over HTTP(s) traffic with their C2 teamserver. mscoree.dll
and clr.dll
are two libraries that are always loaded into processes running .NET assemblies. This is useful for when operators need to run .NET post-exploitation tooling in-memory. For example, when leveraging inline-execute-assembly
or similar. You could still run post-exploitation .NET tooling in non .NET processes, but you do have to keep in mind that it will require you to load the CLR into the process manually. This itself could raise alerts if that is abnormal activity for that process.
The following code shows how these are identified in a "bruteforcey" way using basic WinAPI calls.
if (modules) {
DWORD cbModulesNeeded2;
if (EnumProcessModules(hProcess, modules, sizeof(HMODULE) * 1024, &cbModulesNeeded2)) {
DWORD moduleCount = cbModulesNeeded2 / sizeof(HMODULE);
char* modulePath = (char*)MyAlloc(MAX_PATH);
if (modulePath) {
for (DWORD j = 0; j < moduleCount; j++) {
if (GetModuleFileNameExA(hProcess, modules[j], modulePath, MAX_PATH)) {
if (!foundWininet && custom_strstr(modulePath, "wininet.dll")) {
foundWininet = true;
}
if (!foundWinhttp && custom_strstr(modulePath, "winhttp.dll")) {
foundWinhttp = true;
}
if (!isDotNet && (custom_strstr(modulePath, "mscoree.dll") || custom_strstr(modulePath, "clr.dll"))) {
isDotNet = true;
}
if (foundWininet && foundWinhttp && isDotNet) {
break;
}
}
}
MyFree(modulePath);
}
}
MyFree(modules);
}
Signed Processes
Processes running from signed binaries are always a good target too. Generally processes with signed binaries are less scrutinized by EDRs as they essentially have a trust stamp due to them being signed with a code signing certificate before being distributed. Malicious activity is not expected to come from these processes during common daily usage. This makes them worth exploring as well.
The way this BOF identifies these primarily relies on the WinVerifyTrust
API as shown below.
HRESULT CheckFileSignature(const char* filePath) {
GUID WVTPolicyGUID = WINTRUST_ACTION_GENERIC_VERIFY_V2;
WINTRUST_FILE_INFO FileData;
WINTRUST_DATA WinTrustData;
WCHAR* wFilePath = my_alloc_wide(filePath);
if (!wFilePath) return E_OUTOFMEMORY;
my_memset((char*)&FileData, 0, sizeof(FileData));
FileData.cbStruct = sizeof(WINTRUST_FILE_INFO);
FileData.pcwszFilePath = wFilePath;
my_memset((char*)&WinTrustData, 0, sizeof(WinTrustData));
WinTrustData.cbStruct = sizeof(WinTrustData);
WinTrustData.dwUIChoice = WTD_UI_NONE;
WinTrustData.fdwRevocationChecks = WTD_REVOKE_NONE;
WinTrustData.dwUnionChoice = WTD_CHOICE_FILE;
WinTrustData.pFile = &FileData;
WinTrustData.dwStateAction = WTD_STATEACTION_IGNORE;
WinTrustData.dwProvFlags = WTD_SAFER_FLAG | WTD_CACHE_ONLY_URL_RETRIEVAL;
HRESULT hr = WinVerifyTrust((HWND)INVALID_HANDLE_VALUE, &WVTPolicyGUID, &WinTrustData);
MyFree(wFilePath);
return hr;
}
Potential Improvements
No tool is perfect. Here’s how I'd improve this:
- Targeted Enumeration: Let operators pass a specific PID to narrow the scope instead of scanning every process.
- Handle Optimization: Reduce the number of handles requested to lower telemetry and reduce detection risk. From my personal research, the only optimisations I found in this area required higher privileges and that was too much of a tradeoff for my use case.
- Expanded DLL Checks: Incorporate additional DLLs in the search criteria to cover a broader range of “safe” processes.
- InlineWhispers3: Use indirect syscalls to call WinAPIs avoiding EDR hooks
Overall, Safe Harbor gives you a clear picture of which processes are “safer” to mess with when trying to stay under the radar. You could use safe harbor as is, or build on the example to tailor it to your operational needs - as mentioned, there is quite a lot of room for OPSEC improvements here. Stay sharp and if you make any improvements to it and want to share them I'm all ears!
SafeHarbor BOF Github link: https://github.com/ibaiC/SafeHarbor-BOF