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
This commit is contained in:
Marcos Oviedo 2022-12-13 18:04:49 -03:00 committed by GitHub
parent b6f12a1109
commit 53b74e576c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 203 additions and 11 deletions

1
go.mod
View file

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

2
go.sum
View file

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

View file

@ -0,0 +1 @@
* When WMI call fails on Windows, UUID can now be retrieved by reading the SMBIOS interface.

View file

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

View file

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

View file

@ -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.")
}

View file

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

View file

@ -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: ")
}