LocalMDM.psm1

# -----------------------------------------------------------------------------
# Local MDM Module
# Author: Michael Niehaus
# Description:
# This module leverages the Windows MDM local management APIs to directly
# process MDM policy requests (SyncML) on Windows devices. Provided as-is
# with no support. See https://oofhours.com for related information.
# -----------------------------------------------------------------------------

# -----------------------------------------------------------------------------
# One-time initialization
# -----------------------------------------------------------------------------

#Requires -Version 5.1
#Requires -RunAsAdministrator

# Define the native methods
$source = @"
using System;
using System.Runtime.InteropServices;
 
namespace MDMLocal
{
    public class Interface
    {
        [DllImport("mdmlocalmanagement.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        internal static extern uint RegisterDeviceWithLocalManagement(out uint alreadyRegistered);
 
        [DllImport("mdmlocalmanagement.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern uint UnregisterDeviceWithLocalManagement();
 
        [DllImport("mdmlocalmanagement.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        internal static extern uint ApplyLocalManagementSyncML(string syncMLRequest, out IntPtr syncMLResult);
 
        [DllImport("kernel32.dll")]
        internal static extern uint LocalFree(IntPtr hMem);
 
        public static uint Apply(string syncML, out string syncMLResult)
        {
            uint rc;
            uint alreadyRegistered;
            IntPtr resultPtr;
 
            rc = RegisterDeviceWithLocalManagement(out alreadyRegistered);
 
            rc = ApplyLocalManagementSyncML(syncML, out resultPtr);
            syncMLResult = "";
            if (resultPtr != null)
            {
                syncMLResult = Marshal.PtrToStringUni(resultPtr);
                LocalFree(resultPtr);
            }
            return rc;
        }
    }
}
"@

Add-Type -TypeDefinition $source -Language CSharp
$global:localMDMCmdCounter = 0

function updateCmdId
{
    param([string]$syncML)

    # Convert to XML
    [xml] $xml = $syncML

    # Set the incremented CmdID value
    $global:localMDMCmdCounter++
    $xml.SyncBody.FirstChild.CmdID = $global:localMDMCmdCounter.ToString()

    # Return back the XML string
    return $global:localMDMCmdCounter, $xml.SyncBody.FirstChild.Item.Target.LocURI, $xml.OuterXml
}

# -----------------------------------------------------------------------------
# Send-LocalMDMRequest
# -----------------------------------------------------------------------------

function Send-LocalMDMRequest
{
<#
.SYNOPSIS
    Sends a SyncML request to the local MDM server.
 
.DESCRIPTION
    Sends a SyncML request to the local MDM server. This must be run with admin rights in a 64-bit PowerShell process started with the
    "-MTA" switch.
     
.PARAMETER SyncML
    Specifies the explicit SyncML XML string that should be sent to the local MDM service.
 
.PARAMETER OmaUri
    Specifies the OMA-URI path that should be used to construct a SyncML request.
 
.PARAMETER Cmd
    Specifies the MDM command that should be used to construct a SyncML request. Valid values are "Get", "Add", "Atomic", "Delete", "Exec", "Replace", and "Result". The default is "Get".
 
.PARAMETER Format
    Specifies the format of the data value to be included in the SyncML request. The default value is "int".
 
.PARAMETER Type
    Specifies the type of the data value to be included in the SyncML request. The default value is "text/plain".
 
.PARAMETER Data
    Specifies the data to be included in the SyncML request. This is optional for some requests (e.g. "Get").
 
.PARAMETER Raw
    Specifies that the result should be returned as a raw string (exactly as returned by the local MDM service) rather than as a PowerShell object.
 
.EXAMPLE
    Send-LocalMDMRequest -OmaUri "./DevDetail/Ext/Microsoft/ProcessorArchitecture"
 
.EXAMPLE
    Send-LocalMDMRequest -OmaUri "./DevDetail/Ext/Microsoft/ProcessorArchitecture"
 
.OUTPUTS
    The result of the SyncML request. If -Raw is specified, this will be an XML string. Otherwise, it will be a PowerShell object.
 
.LINK
    https://oofhours.com/
    https://github.com/ms-iot/iot-core-azure-dm-client/blob/master/src/SystemConfigurator/CSPs/MdmProvision.cpp
    https://docs.microsoft.com/en-us/windows/iot-core/develop-your-app/embeddedmode
 
#>


    [cmdletbinding()]  
    Param(
        [Parameter(ParameterSetName='Raw', Mandatory = $true)]
        [String]$SyncML,
        [Parameter(ParameterSetName='Assisted', Mandatory = $true)]
        [String]$OmaUri,
        [Parameter(ParameterSetName='Assisted', Mandatory = $false)]
        [ValidateSet("Get", "Add", "Atomic", "Delete", "Exec", "Replace", "Result")]
        [String]$Cmd = "Get",
        [Parameter(ParameterSetName='Assisted', Mandatory = $false)]
        [String]$Format = "int",
        [Parameter(ParameterSetName='Assisted', Mandatory = $false)]
        [String]$Type = "text/plain",
        [Parameter(ParameterSetName='Assisted', Mandatory = $false)]
        [String]$Data = "",
        [Parameter()]
        [Switch]$Raw = $false
    )

    BEGIN {
        # Enable embedded mode
        $uuidBytes = ([GUID](gwmi win32_computersystemproduct).UUID).ToByteArray()
        $hasher = [System.Security.Cryptography.HashAlgorithm]::Create('sha256')
        $hash = $hasher.ComputeHash($uuidBytes)
        Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\embeddedmode\Parameters" -Name "Flags" -Value $hash
    }

    PROCESS {

        # Depending on the parameter set, build or use SyncML
        if ($PSCmdlet.ParameterSetName -eq "Raw") {
            $useSyncML = $SyncML            
        } else {
            $useSyncML = @"
<SyncBody>
    <$Cmd>
        <CmdID>1</CmdID>
        <Item>
            <Target>
                <LocURI>$OmaUri</LocURI>
            </Target>
            <Meta>
                <Format xmlns="syncml:metinf">$Format</Format>
                <Type xmlns="syncml:metinf">$Type</Type>
            </Meta>
            <Data>$Data</Data>
        </Item>
    </$Cmd>
</SyncBody>
"@

        }
        # Make sure we have a unique command ID
        $cmdId, $locURI, $updatedSyncML = updateCmdId($useSyncML)

        # Make a request and check for fatal errors
        $syncMLResultString = ""
        $rc = [MDMLocal.Interface]::Apply($updatedSyncML, [ref]$syncMLResultString)
        if ($rc -eq 2147549446) {
            throw "MDM local management requires running powershell.exe with -MTA."
        } elseif ($rc -eq 2147746132) {
            throw "MDM local management requires a 64-bit process."
        } elseif ($syncMLResultString -like "Error*") {
            throw $syncMLResultString
        } elseif ($rc -ne 0) {
            throw "Unexpected return code from MDM local management: $rc"
        }

        # Return the response details (Status of 200 is success)
        if ($raw) {
            $syncMLResultString
        } else {
            [xml] $syncMLResult = $syncMLResultString
            $status = $syncMLResult.SyncML.SyncBody.Status[1]
            New-Object PSObject -Property ([ordered] @{
                CmdId = $cmdId
                Cmd = $status.Cmd
                Status = $status.Data
                OmaUri = $locURI
                Data = $syncMLResult.SyncML.SyncBody.Results.Item.Data
            })
        }
    }
}

# -----------------------------------------------------------------------------
# Unregister-LocalMDM
# -----------------------------------------------------------------------------

function Unregister-LocalMDM
{
<#
.SYNOPSIS
    Unregisters the local MDM server.
 
.DESCRIPTION
    Unregisters the local MDM server. This will in some cases revert any policies configured via the local MDM server back to their default values.
 
.EXAMPLE
    Unregister-LocalMDM
 
.OUTPUTS
    A message confirming the unregister operation.
 
#>


[cmdletbinding()]  
Param(
)
    PROCESS {
        $rc = [MDMLocal.Interface]::UnregisterDeviceWithLocalManagement()
        Write-Host "Unregisterd, rc = $rc"
    }
}