From 53b74e576ce4018485aaaf951541bc48211a1477 Mon Sep 17 00:00:00 2001 From: Marcos Oviedo Date: Tue, 13 Dec 2022 18:04:49 -0300 Subject: [PATCH] Adding fallback mechanism to retrive UUID on Windows (#8993) * Adding fallback mechanism to retrive UUID on Windows * Fixing erroneous code comments * Addressing code review findings --- go.mod | 1 + go.sum | 2 + .../bug-8974-smbios-uuid-value-retrieval | 1 + orbit/cmd/orbit/orbit.go | 16 +- orbit/pkg/platform/platform.go | 8 + orbit/pkg/platform/platform_notwindows.go | 4 + orbit/pkg/platform/platform_windows.go | 144 ++++++++++++++++++ orbit/pkg/platform/platform_windows_test.go | 38 +++++ 8 files changed, 203 insertions(+), 11 deletions(-) create mode 100644 orbit/changes/bug-8974-smbios-uuid-value-retrieval create mode 100644 orbit/pkg/platform/platform_windows_test.go diff --git a/go.mod b/go.mod index 5a601aca90..e674f0ca4b 100644 --- a/go.mod +++ b/go.mod @@ -167,6 +167,7 @@ require ( github.com/dghubble/sling v1.3.0 // indirect github.com/dgraph-io/ristretto v0.1.0 // indirect github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect + github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e // indirect github.com/dimchansky/utfbom v1.1.1 // indirect github.com/docker/distribution v2.8.0+incompatible // indirect github.com/docker/go-connections v0.4.0 // indirect diff --git a/go.sum b/go.sum index 280af31d9b..27ad9f2307 100644 --- a/go.sum +++ b/go.sum @@ -364,6 +364,8 @@ github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUn github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q= +github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A= github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= diff --git a/orbit/changes/bug-8974-smbios-uuid-value-retrieval b/orbit/changes/bug-8974-smbios-uuid-value-retrieval new file mode 100644 index 0000000000..2f650dc8be --- /dev/null +++ b/orbit/changes/bug-8974-smbios-uuid-value-retrieval @@ -0,0 +1 @@ +* When WMI call fails on Windows, UUID can now be retrieved by reading the SMBIOS interface. diff --git a/orbit/cmd/orbit/orbit.go b/orbit/cmd/orbit/orbit.go index b5137d8184..8fdfe37be7 100644 --- a/orbit/cmd/orbit/orbit.go +++ b/orbit/cmd/orbit/orbit.go @@ -837,20 +837,14 @@ func (d *desktopRunner) interrupt(err error) { // shell out to osquery (on Linux and macOS) or to wmic (on Windows), and get the system uuid func getUUID(osqueryPath string) (string, error) { if runtime.GOOS == "windows" { - args := []string{"/C", "wmic csproduct get UUID"} - out, err := exec.Command("cmd", args...).Output() + uuidData, uuidSource, err := platform.GetSMBiosUUID() if err != nil { return "", err } - uuidOutputStr := string(out) - if len(uuidOutputStr) == 0 { - return "", errors.New("get UUID: output from wmi is empty") - } - outputByLines := strings.Split(strings.TrimRight(uuidOutputStr, "\n"), "\n") - if len(outputByLines) < 2 { - return "", errors.New("get UUID: unexpected output") - } - return strings.TrimSpace(outputByLines[1]), nil + + log.Debug().Msgf("UUID source was %s.", uuidSource) + + return uuidData, nil } type UuidOutput struct { UuidString string `json:"uuid"` diff --git a/orbit/pkg/platform/platform.go b/orbit/pkg/platform/platform.go index 9836063395..612a59271f 100644 --- a/orbit/pkg/platform/platform.go +++ b/orbit/pkg/platform/platform.go @@ -17,6 +17,14 @@ var ( ErrComChannelNotFound = errors.New("comm channel not found") ) +type UUIDSource string + +const ( + UUIDSourceInvalid = "UUID_Source_Invalid" + UUIDSourceWMI = "UUID_Source_WMI" + UUIDSourceHardware = "UUID_Source_Hardware" +) + // readPidFromFile reads a PID from a file func readPidFromFile(destDir string, destFile string) (int32, error) { // Defense programming - sanity checks on inputs diff --git a/orbit/pkg/platform/platform_notwindows.go b/orbit/pkg/platform/platform_notwindows.go index 289116496c..5f07123e8c 100644 --- a/orbit/pkg/platform/platform_notwindows.go +++ b/orbit/pkg/platform/platform_notwindows.go @@ -76,3 +76,7 @@ func GetProcessByName(name string) (*gopsutil_process.Process, error) { return foundProcess, nil } + +func GetSMBiosUUID() (string, UUIDSource, error) { + return "", UUIDSourceInvalid, errors.New("not implemented.") +} diff --git a/orbit/pkg/platform/platform_windows.go b/orbit/pkg/platform/platform_windows.go index ae1fe23685..757083d3ce 100644 --- a/orbit/pkg/platform/platform_windows.go +++ b/orbit/pkg/platform/platform_windows.go @@ -6,6 +6,7 @@ package platform import ( "errors" "fmt" + "os/exec" "strings" "syscall" "time" @@ -13,6 +14,7 @@ import ( "github.com/fleetdm/fleet/v4/orbit/pkg/constant" + "github.com/digitalocean/go-smbios/smbios" "github.com/hectane/go-acl" gopsutil_process "github.com/shirou/gopsutil/v3/process" "golang.org/x/sys/windows" @@ -176,3 +178,145 @@ func GetProcessByName(name string) (*gopsutil_process.Process, error) { return process, nil } + +// It obtains the BIOS UUID by calling "cmd.exe /c wmic csproduct get UUID" and parsing the results +func wmiGetSMBiosUUID() (string, error) { + args := []string{"/C", "wmic csproduct get UUID"} + out, err := exec.Command("cmd", args...).Output() + if err != nil { + return "", err + } + uuidOutputStr := string(out) + if len(uuidOutputStr) == 0 { + return "", errors.New("get UUID: output from wmi is empty") + } + outputByLines := strings.Split(strings.TrimRight(uuidOutputStr, "\n"), "\n") + if len(outputByLines) < 2 { + return "", errors.New("get UUID: unexpected output") + } + return strings.TrimSpace(outputByLines[1]), nil +} + +// It perform a UUID sanity check on a given byte array +func isValidUUID(uuidBytes []byte) (bool, error) { + // SMBIOS constants from spec here - https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.1.1.pdf + const uuidSize int = 0x10 // UUID size is calculated with field offset value (0xA) + node field length (6 bytes) + + // Sanity check on size + if len(uuidBytes) != uuidSize { + return false, errors.New("Invalid input UUID size") + } + + // UUID field sanity check for null values + // Logic is based on https://github.com/ContinuumLLC/godep-go-smbios/blob/ab7c733f1be8e55ed3e0587d1aa2d5883fe8801e/smbios/decoder.go#L135 + only0xFF, only0x00 := true, true + for i := 0; i < uuidSize && (only0x00 || only0xFF); i++ { + if uuidBytes[i] != 0x00 { + only0x00 = false + } + if uuidBytes[i] != 0xFF { + only0xFF = false + } + } + + if only0xFF { + return false, errors.New("UUID is not currently present in the system, but it can be set.") + } + if only0x00 { + return false, errors.New("UUID is not present in the system.") + } + + return true, nil +} + +// It obtains the BIOS UUID value by reading the SMBIOS "System Information" +// structure data on the OS SMBIOS interface. +// On Windows, the SMBIOS "System Information" data can be obtained by calling GetSystemFirmwareTable() +// https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-getsystemfirmwaretable +// Instead of just calling this native API, this function relies on Digital Ocean's go-smbios +// library. This package smbios provides detection and access to System Management BIOS (SMBIOS) and +// Desktop Management Interface (DMI) data and structures across: https://github.com/digitalocean/go-smbios +// This function should work as is on Linux thanks to the go-smbios interface abstraction. See the +// list of supported OSes on the go-smbios documentation. +// The windows go-smbios implementations calls to GetSystemFirmwareTable() +func hardwareGetSMBiosUUID() (string, error) { + // SMBIOS data in operating system-specific location + streamReader, smBIOSRawData, err := smbios.Stream() + if err != nil { + return "", fmt.Errorf("failed to open stream: %v", err) + } + + // Ensure that stream will be closed + defer streamReader.Close() + + // Decode SMBIOS structures from the stream. + decoder := smbios.NewDecoder(streamReader) + structSMBIOSdata, err := decoder.Decode() + if err != nil { + return "", fmt.Errorf("failed to decode BIOS structures: %v", err) + } + + // Determine SMBIOS version and table location from entry point + biosMajor, biosMinor, _ := smBIOSRawData.Version() + + // SMBIOS constants from spec here - https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.1.1.pdf + const systemInformationType uint8 = 0x01 // System Information indicator + const minBiosStructSize uint8 = 0x1b // Section 7.2 on the SMBIOS specification (0x1a min header size + null character) + const uuidOffset uint8 = 0x4 // UUID offset in System Information (Type 1) structure + const revMajorVersion int = 0x3 // SMBIOS revision that most of the current BIOS have - v3 specs were released in 2015 + const minLegacyMajorVersion int = 0x2 // Minimum SMBIOS Major rev that supports UUID little-endian encoding + const minLegacyMinorVersion int = 0x6 // Minimum SMBIOS Minor rev that supports UUID little-endian encoding + + // Walking the obtained SMBIOS data + for _, rawBiosStruct := range structSMBIOSdata { + if (rawBiosStruct.Header.Type == systemInformationType) && (rawBiosStruct.Header.Length >= minBiosStructSize) { + uuidBytes := rawBiosStruct.Formatted[uuidOffset:] + + // UUID sanity check + isValidUUID, err := isValidUUID(uuidBytes) + if err != nil { + return "", fmt.Errorf("%v", err) + } + + if !isValidUUID { + return "", errors.New("invalid UUID") + } + + // As of version 2.6 of the SMBIOS specification, the first 3 fields of the UUID are + // supposed to be encoded in little-endian (section 7.2.1) + var smBiosUUID string = "" + if (biosMajor >= revMajorVersion) || (biosMajor >= minLegacyMajorVersion && biosMinor >= minLegacyMinorVersion) { + smBiosUUID = fmt.Sprintf("%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X", + uuidBytes[3], uuidBytes[2], uuidBytes[1], uuidBytes[0], uuidBytes[5], uuidBytes[4], uuidBytes[7], uuidBytes[6], uuidBytes[8], uuidBytes[9], uuidBytes[10], uuidBytes[11], uuidBytes[12], uuidBytes[13], uuidBytes[14], uuidBytes[15]) + } else { + smBiosUUID = fmt.Sprintf("%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X", + uuidBytes[0], uuidBytes[1], uuidBytes[2], uuidBytes[3], uuidBytes[4], uuidBytes[5], uuidBytes[6], uuidBytes[7], uuidBytes[8], uuidBytes[9], uuidBytes[10], uuidBytes[11], uuidBytes[12], uuidBytes[13], uuidBytes[14], uuidBytes[15]) + } + + return smBiosUUID, nil + } + } + + return "", errors.New("UUID was not found") +} + +// It attempts to get SMBIOS UUID through WMI, and if this mechanism fails, it fallback into reading +// the actual SMBIOS hardware interface. +func GetSMBiosUUID() (string, UUIDSource, error) { + // It attempts first to get the UUID from WMI + uuid, err := wmiGetSMBiosUUID() + if err != nil { + + // If WMI fails, it fallback into reading the SMBIOS HW interface + uuid, err := hardwareGetSMBiosUUID() + if err != nil { + return "", "", fmt.Errorf("UUID could not be obtained through WMI and Hardware routes: %w", err) + } + + // UUID was obtained from reading the hardware SMBIOS UUID data + return uuid, UUIDSourceHardware, nil + } + + // UUID was obtained from calling WMI infrastructure + return uuid, UUIDSourceWMI, nil +} diff --git a/orbit/pkg/platform/platform_windows_test.go b/orbit/pkg/platform/platform_windows_test.go new file mode 100644 index 0000000000..138a51474f --- /dev/null +++ b/orbit/pkg/platform/platform_windows_test.go @@ -0,0 +1,38 @@ +//go:build windows +// +build windows + +package platform + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestUUIDNotPresent(t *testing.T) { + uuidBytes := []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} + _, err := isValidUUID(uuidBytes) + + assert.NotNil(t, err, "UUID not present: ") +} + +func TestUUIDNotSet(t *testing.T) { + uuidBytes := []byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF} + _, err := isValidUUID(uuidBytes) + + assert.NotNil(t, err, "UUID not set: ") +} + +func TestUUIDInvalidSize(t *testing.T) { + uuidBytes := []byte{0x11, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF} + _, err := isValidUUID(uuidBytes) + + assert.NotNil(t, err, "UUID validation size: ") +} + +func TestUUIDValid(t *testing.T) { + uuidBytes := []byte{0x11, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00} + _, err := isValidUUID(uuidBytes) + + assert.Nil(t, err, "UUID validation error: ") +}