From f3ddc49729267e7ab40fbfc59f295ed0eec74832 Mon Sep 17 00:00:00 2001 From: Jakob Friedl <71284620+jakobfriedl@users.noreply.github.com> Date: Tue, 4 Nov 2025 13:53:54 +0100 Subject: [PATCH] Improved Windows version fingerprinting and fixed console window not being focused on double-click. --- src/agent/protocol/registration.nim | 166 ++++++++++++++-------------- src/client/main.nim | 4 + src/client/views/sessions.nim | 6 +- 3 files changed, 92 insertions(+), 84 deletions(-) diff --git a/src/agent/protocol/registration.nim b/src/agent/protocol/registration.nim index c9db43b..73575ca 100644 --- a/src/agent/protocol/registration.nim +++ b/src/agent/protocol/registration.nim @@ -1,4 +1,4 @@ -import winim, os, net, strutils, registry, zippy +import winim, os, net, strutils, registry, zippy, strformat import ../../common/[types, serialize, sequence, crypto, utils] import ../../modules/manager @@ -69,90 +69,94 @@ 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 fingerprinting -type - ProductType = enum - UNKNOWN = 0 - WORKSTATION = 1 - DC = 2 - SERVER = 3 - # API Structs -type OSVersionInfoExW {.importc: protect("OSVERSIONINFOEXW"), header: protect("").} = 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 +type + OSVersionInfoExW {.importc: protect("OSVERSIONINFOEXW"), header: protect("").} = 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 -proc getWindowsVersion(info: OSVersionInfoExW, productType: ProductType): string = - let - major = info.dwMajorVersion - minor = info.dwMinorVersion - build = info.dwBuildNumber - spMajor = info.wServicePackMajor + # Windows Version fingerprinting + ProductType {.size: sizeof(uint8).} = enum + UNKNOWN = "Unknown" + WORKSTATION = "Workstation" + DC = "Domain Controller" + SERVER = "Server" + + WindowsVersion = object + major: DWORD + minor: DWORD + buildMin: DWORD # Minimum build number (0 = any) + buildMax: DWORD # Maximum build number (0 = any) + productType: ProductType + name: string + +const VERSIONS = [ + # Windows 11 / Server 2022+ + # WindowsVersion(major: 10, minor: 0, buildMin: 22631, buildMax: 0, productType: WORKSTATION, name: protect("Windows 11 23H2")), + # WindowsVersion(major: 10, minor: 0, buildMin: 22621, buildMax: 22630, productType: WORKSTATION, name: protect("Windows 11 22H2")), + WindowsVersion(major: 10, minor: 0, buildMin: 22000, buildMax: 0, productType: WORKSTATION, name: protect("Windows 11")), + WindowsVersion(major: 10, minor: 0, buildMin: 26100, buildMax: 0, productType: SERVER, name: protect("Windows Server 2025")), + WindowsVersion(major: 10, minor: 0, buildMin: 20348, buildMax: 26099, productType: SERVER, name: protect("Windows Server 2022")), + + # Windows 10 / Server 2016-2019 + WindowsVersion(major: 10, minor: 0, buildMin: 19041, buildMax: 19045, productType: WORKSTATION, name: protect("Windows 10 2004/20H2/21H1/21H2/22H2")), + WindowsVersion(major: 10, minor: 0, buildMin: 17763, buildMax: 19040, productType: WORKSTATION, name: protect("Windows 10 1809+")), + WindowsVersion(major: 10, minor: 0, buildMin: 10240, buildMax: 17762, productType: WORKSTATION, name: protect("Windows 10")), + WindowsVersion(major: 10, minor: 0, buildMin: 17763, buildMax: 17763, productType: SERVER, name: protect("Windows Server 2019")), + WindowsVersion(major: 10, minor: 0, buildMin: 14393, buildMax: 14393, productType: SERVER, name: protect("Windows Server 2016")), + WindowsVersion(major: 10, minor: 0, buildMin: 0, buildMax: 0, productType: SERVER, name: protect("Windows Server (Unknown Build)")), + + # Windows 8.x / Server 2012 + WindowsVersion(major: 6, minor: 3, buildMin: 0, buildMax: 0, productType: WORKSTATION, name: protect("Windows 8.1")), + WindowsVersion(major: 6, minor: 3, buildMin: 0, buildMax: 0, productType: SERVER, name: protect("Windows Server 2012 R2")), + WindowsVersion(major: 6, minor: 2, buildMin: 0, buildMax: 0, productType: WORKSTATION, name: protect("Windows 8")), + WindowsVersion(major: 6, minor: 2, buildMin: 0, buildMax: 0, productType: SERVER, name: protect("Windows Server 2012")), + + # Windows 7 / Server 2008 R2 + WindowsVersion(major: 6, minor: 1, buildMin: 0, buildMax: 0, productType: WORKSTATION, name: protect("Windows 7")), + WindowsVersion(major: 6, minor: 1, buildMin: 0, buildMax: 0, productType: SERVER, name: protect("Windows Server 2008 R2")), + + # Windows Vista / Server 2008 + WindowsVersion(major: 6, minor: 0, buildMin: 0, buildMax: 0, productType: WORKSTATION, name: protect("Windows Vista")), + WindowsVersion(major: 6, minor: 0, buildMin: 0, buildMax: 0, productType: SERVER, name: protect("Windows Server 2008")), + + # Windows XP / Server 2003 + WindowsVersion(major: 5, minor: 2, buildMin: 0, buildMax: 0, productType: WORKSTATION, name: protect("Windows XP x64 Edition")), + WindowsVersion(major: 5, minor: 2, buildMin: 0, buildMax: 0, productType: SERVER, name: protect("Windows Server 2003")), + WindowsVersion(major: 5, minor: 1, buildMin: 0, buildMax: 0, productType: WORKSTATION, name: protect("Windows XP")), +] + +proc matchesVersion(version: WindowsVersion, info: OSVersionInfoExW, productType: ProductType): bool = + if info.dwMajorVersion != version.major or info.dwMinorVersion != version.minor: + return false + if productType != version.productType: + return false + if version.buildMin > 0 and info.dwBuildNumber < version.buildMin: + return false + if version.buildMax > 0 and info.dwBuildNumber > version.buildMax: + return false + return true + +proc getWindowsVersion(info: OSVersionInfoExW, productType: ProductType): string = + for version in VERSIONS: + if version.matchesVersion(info, if productType == DC: SERVER else: productType): # Process domain controllers as servers, otherwise they show up as unknown + if productType == DC: + return version.name & protect(" (Domain Controller)") + else: + return version.name - if major == 10 and minor == 0: - if productType == WORKSTATION: - if build >= 22000: - return protect("Windows 11") - else: - return protect("Windows 10") + # Unknown windows version, return as much information as possible + return fmt"Windows {$int(info.dwMajorVersion)}.{$int(info.dwMinorVersion)} {$productType} (Build: {$int(info.dwBuildNumber)})" - else: - case build: - of 20348: - return protect("Windows Server 2022") - of 17763: - return protect("Windows Server 2019") - of 14393: - return protect("Windows Server 2016") - else: - return protect("Windows Server 10.x (Build: ") & $build & protect(")") - - elif major == 6: - case minor: - of 3: - if productType == WORKSTATION: - return protect("Windows 8.1") - else: - return protect("Windows Server 2012 R2") - of 2: - if productType == WORKSTATION: - return protect("Windows 8") - else: - return protect("Windows Server 2012") - of 1: - if productType == WORKSTATION: - return protect("Windows 7") - else: - return protect("Windows Server 2008 R2") - of 0: - if productType == WORKSTATION: - return protect("Windows Vista") - else: - return protect("Windows Server 2008") - else: - discard - - elif major == 5: - if minor == 2: - if productType == WORKSTATION: - return protect("Windows XP x64 Edition") - else: - return protect("Windows Server 2003") - elif minor == 1: - return protect("Windows XP") - else: - discard - - return protect("Unknown Windows Version") proc getProductType(): ProductType = # The product key is retrieved from the registry diff --git a/src/client/main.nim b/src/client/main.nim index f116e67..c387283 100644 --- a/src/client/main.nim +++ b/src/client/main.nim @@ -201,6 +201,10 @@ proc main(ip: string = "localhost", port: int = 37573) = console.draw(connection) newConsoleTable[agentId] = console + if sessionsTable.focusedConsole.len() > 0: + igSetWindowFocus_Str(sessionsTable.focusedConsole.cstring) + sessionsTable.focusedConsole = "" + # Update the consoles table with only those sessions that have not been closed yet # This is done to ensure that closed console windows can be opened again consoles = newConsoleTable diff --git a/src/client/views/sessions.nim b/src/client/views/sessions.nim index dae6d65..3dfcb80 100644 --- a/src/client/views/sessions.nim +++ b/src/client/views/sessions.nim @@ -15,6 +15,7 @@ type agentImpersonation*: Table[string, string] selection: ptr ImGuiSelectionBasicStorage consoles: ptr Table[string, ConsoleComponent] + focusedConsole*: string proc SessionsTable*(title: string, consoles: ptr Table[string, ConsoleComponent]): SessionsTableComponent = result = new SessionsTableComponent @@ -23,6 +24,7 @@ proc SessionsTable*(title: string, consoles: ptr Table[string, ConsoleComponent] result.agentActivity = initTable[string, int64]() result.selection = ImGuiSelectionBasicStorage_ImGuiSelectionBasicStorage() result.consoles = consoles + result.focusedConsole = "" proc cmp(x, y: UIAgent): int = return cmp(x.firstCheckin, y.firstCheckin) @@ -39,9 +41,7 @@ proc interact(component: SessionsTableComponent) = if not component.consoles[].hasKey(agent.agentId): component.consoles[][agent.agentId] = Console(agent) - # Focus the existing console window - else: - igSetWindowFocus_Str(fmt"[{agent.agentId}] {agent.username}@{agent.hostname}".cstring) + component.focusedConsole = fmt"[{agent.agentId}] {agent.username}@{agent.hostname}" component.selection.ImGuiSelectionBasicStorage_Clear()