From 1c7f94b74599f413c00e0e9779521453ebdba101 Mon Sep 17 00:00:00 2001 From: Marcos Oviedo Date: Mon, 6 Feb 2023 11:41:05 -0300 Subject: [PATCH] Adding support to user friendly command output (#9620) This relates to #9310 This PR adds a new column to the bridge mdm_table to provide a parsed and user-friendly output of the input command This will help in creating compliance queries There is also an improvement to UTF16 to go string conversion scenarios. This was required by scenarios on which output commands can vary in size I've also added support to restrict input commands to read-only commands (only the SyncML verb Get is supported) --- orbit/pkg/table/mdm/mdm_windows.go | 191 ++++++++++++++++++++++++++--- 1 file changed, 177 insertions(+), 14 deletions(-) diff --git a/orbit/pkg/table/mdm/mdm_windows.go b/orbit/pkg/table/mdm/mdm_windows.go index f4efd7a246..cf58bd2229 100644 --- a/orbit/pkg/table/mdm/mdm_windows.go +++ b/orbit/pkg/table/mdm/mdm_windows.go @@ -10,6 +10,7 @@ import ( "encoding/xml" "errors" "fmt" + "io" "os" "strings" "sync" @@ -28,9 +29,12 @@ var ( // Windows DLL and functions for runtime binding modmdmregistration = windows.NewLazySystemDLL("mdmregistration.dll") modmdmlocalmgmt = windows.NewLazySystemDLL("mdmlocalmanagement.dll") + modkernel32 = windows.NewLazySystemDLL("kernel32.dll") procIsDeviceRegisteredWithManagement = modmdmregistration.NewProc("IsDeviceRegisteredWithManagement") procSendSyncMLcommand = modmdmlocalmgmt.NewProc("ApplyLocalManagementSyncML") + proclstrlenW = modkernel32.NewProc("lstrlenW") + procRtlMoveMemory = modkernel32.NewProc("RtlMoveMemory") // Synchronization mutex mu sync.Mutex @@ -49,6 +53,50 @@ const ( mdmMutexName = "__MDM_LOCAL_MANAGEMENT_NAMED_MUTEX__" ) +// SyncML XML Parsing Types +type SyncMLHeader struct { + DTD string `xml:"VerDTD"` + Version string `xml:"VerProto"` + SessionID int `xml:"SessionID"` + MsgID int `xml:"MsgID"` + Target string `xml:"Target>LocURI"` + Source string `xml:"Source>LocURI"` + MaxMsgSize int `xml:"Meta>A:MaxMsgSize"` +} + +type SyncMLCommandMeta struct { + XMLinfo string `xml:"xmlns,attr"` + Type string `xml:"Type"` +} + +type SyncMLCommandItem struct { + Meta SyncMLCommandMeta `xml:"Meta"` + Source string `xml:"Source>LocURI"` + Data string `xml:"Data"` +} + +type SyncMLCommand struct { + XMLName xml.Name + CmdID int `xml:",omitempty"` + MsgRef string `xml:",omitempty"` + CmdRef string `xml:",omitempty"` + Cmd string `xml:",omitempty"` + Target string `xml:"Target>LocURI"` + Source string `xml:"Source>LocURI"` + Data string `xml:",omitempty"` + Item []SyncMLCommandItem `xml:",any"` +} + +type SyncMLBody struct { + Item []SyncMLCommand `xml:",any"` +} + +type SyncMLMessage struct { + XMLinfo string `xml:"xmlns,attr"` + Header SyncMLHeader `xml:"SyncHdr"` + Body SyncMLBody `xml:"SyncBody"` +} + // Columns is the schema of the table. func Columns() []table.ColumnDefinition { return []table.ColumnDefinition{ @@ -56,6 +104,7 @@ func Columns() []table.ColumnDefinition { table.TextColumn("enrolled_user"), table.TextColumn("mdm_command_input"), table.TextColumn("mdm_command_output"), + table.TextColumn("raw_mdm_command_output"), } } @@ -98,12 +147,19 @@ func Generate(ctx context.Context, queryContext table.QueryContext) ([]map[strin log.Debug().Msgf("mdm_bridge output command response:\n%s", outputCmd) + // grabbing the command parsed command output + minimalOutputCmd, err := getCmdResponseData(strings.TrimSpace(outputCmd)) + if err != nil { + return nil, fmt.Errorf("mdm command response parsing: %s ", err) + } + return []map[string]string{ { - "enrollment_status": deviceEnrollmentStatus, - "enrolled_user": enrollmentURI, - "mdm_command_input": inputCmd, - "mdm_command_output": outputCmd, + "enrollment_status": deviceEnrollmentStatus, + "enrolled_user": enrollmentURI, + "mdm_command_input": inputCmd, + "mdm_command_output": minimalOutputCmd, + "raw_mdm_command_output": outputCmd, }, }, nil } @@ -111,25 +167,91 @@ func Generate(ctx context.Context, queryContext table.QueryContext) ([]map[strin // returning table enrollment status + cmd output status if present return []map[string]string{ { - "enrollment_status": deviceEnrollmentStatus, - "enrolled_user": enrollmentURI, - "mdm_command_input": "", - "mdm_command_ouput": "", + "enrollment_status": deviceEnrollmentStatus, + "enrolled_user": enrollmentURI, + "mdm_command_input": "", + "mdm_command_ouput": "", + "raw_mdm_command_output": "", }, }, nil } -// builtin utf16tostring string expects a uint16 array but we have a pointer to a uint16 -// so we need to cast it after converting it to an unsafe pointer -// this is a common pattern though the buffer size varies -// see https://golang.org/pkg/unsafe/#Pointer for more details +// dummy charset reader just to satisfy the xml decoder requirements +func identReader(encoding string, input io.Reader) (io.Reader, error) { + return input, nil +} + +// getCommandResponseData returns the response data for a given command +func getCmdResponseData(outputCmd string) (string, error) { + var responseData string + + // creating a new SyncML message object + messageObject := new(SyncMLMessage) + + // parsing output SyncML message + d := xml.NewDecoder(bytes.NewReader([]byte(outputCmd))) + d.CharsetReader = identReader + + // decoding the XML message + if err := d.Decode(messageObject); err != nil { + return "", err + } + + // getting response data from output message + if len(messageObject.Body.Item) > 0 { + for _, element := range messageObject.Body.Item { + + // getting the results tag for the input commands + if element.XMLName.Local != "Results" { + continue + } + + // results will be appended in a comma separated list + if len(element.Item) > 0 { + + // extracting the data from the result + workStr := element.Item[0].Data + if len(workStr) == 0 { + workStr = "data_not_set" // default value for empty data + } + responseData += workStr + } + } + } + + return responseData, nil +} + +// builtin windows.UTF16ToString string expects a uint16 array but we have a uint16 pointer +// so we are allocating dynamic memory and moving data around before calling windows.UTF16ToString func localUTF16toString(ptr unsafe.Pointer) (string, error) { if ptr == nil { return "", errors.New("failed UTF16 conversion due to null pointer") } - uint16ptrarr := (*[maxBufSize]uint16)(ptr)[:] - return windows.UTF16ToString(uint16ptrarr), nil + // grabbing input string length + lenPtr, _, err := proclstrlenW.Call(uintptr(unsafe.Pointer(ptr))) + if err != windows.ERROR_SUCCESS { + return "", err + } + + // returning empty string if length is 0 + strBytesLen := int32(lenPtr) * 2 // Windows UNICODE uses 2 bytes per character + if strBytesLen == 0 { + return "", nil + } + + // allocating an uint16 array buffer + buf := make([]uint16, strBytesLen) + + // moving the data around + _, _, err = procRtlMoveMemory.Call((uintptr)(unsafe.Pointer(&buf[0])), (uintptr)(unsafe.Pointer(ptr)), uintptr(strBytesLen)) + if err != windows.ERROR_SUCCESS { + return "", err + } + + // and finally converting the uint16 array to a string + return windows.UTF16ToString(buf), nil } // getEnrollmentInfo returns the MDM enrollment status by calling into OS API IsDeviceRegisteredWithManagement() @@ -165,6 +287,42 @@ func getEnrollmentInfo() (uint32, string, error) { return isDeviceRegisteredWithMDM, uriData, nil } +// isReadOnlyCommandRequest returns true if the verbs used on input SyncML commads are only Get +func isReadOnlyCommandRequest(inputCmd string) (bool, error) { + if len(inputCmd) == 0 { + return false, errors.New("empty input command") + } + + // creating a new SyncMLBody message object + messageObject := new(SyncMLBody) + + // parsing output SyncML message + d := xml.NewDecoder(bytes.NewReader([]byte(inputCmd))) + d.CharsetReader = identReader + + // decoding the XML message + if err := d.Decode(messageObject); err != nil { + return false, err + } + + // sanity check on the input command structure + if len(messageObject.Item) == 0 { + return false, nil + } + + // checking if input SyncML commands are only Get + for _, element := range messageObject.Item { + + // checking if input SyncML verb is different that Get + commandVerb := strings.ToLower(element.XMLName.Local) + if commandVerb != "get" { + return false, fmt.Errorf("%s is a not supported SyncML command verb", commandVerb) + } + } + + return true, nil +} + // Borrowed from https://stackoverflow.com/questions/53476012/how-to-validate-a-xml func IsValidXML(s string) bool { return xml.Unmarshal([]byte(s), new(interface{})) == nil @@ -189,6 +347,11 @@ func isValidMDMcommand(inputCMD string) (bool, error) { return false, errors.New("input MDM command is not a valid XML") } + // checking if input MDM command is a read-only command + if validCmd, err := isReadOnlyCommandRequest(inputCMD); !validCmd { + return false, err + } + return true, nil }