Local MDM Command Execution

I found a handy module by Michael Niehaus some time ago called LocalMDM. I’ve since converted it to be usable as an ImmyBot module. This allows me to create a task that runs MDM commands just like Intune, without the need to be enrolled.

An unfortunate side-effect is the requirement to have Windows in embedded mode. The script will handle enabling it, however it means that there will be no Microsoft support if there are any issues. This is because embedded mode is technically only supported by IoT Core and IoT Enterprise editions of Windows. Though it has been tested and works on Pro, Education, and Enterprise as well.

Here is the module itself, called 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 {
        Invoke-ImmyCommand -Context System -ScriptBlock {
            $rc = [MDMLocal.Interface]::UnregisterDeviceWithLocalManagement()
            Write-Host "Unregisterd, rc = $rc"
        }
        Write-Progress "The device has been unregistered from LocalMDM. Policy enforcement is removed."
    }
}

Function Send-LocalMDMRequest {
    <#
        .SYNOPSIS
            Sends a SyncML request to the local MDM server.

        .DESCRIPTION
            This module leverages the Windows MDM local management APIs to directly
            process MDM policy requests (SyncML) on Windows devices. Windows Embedded mode
            will be enabled when this is run. IoT Core and IoT Enterprise are the only 
            operating systems with official support for Embedded mode. It can be enabled on 
            Windows 10/11 Pro, Education, and Enterprise SKUs, but is not officially supported.

        .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/2022/08/26/send-mdm-commands-without-an-mdm-service-using-powershell/
            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 {
        $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;
        }
    }
}
"@

        Write-Progress "Enabling Windows embedded mode..."
        $uuidBytes = Invoke-ImmyCommand -Context System -ScriptBlock { ([GUID](Get-WMIObject -Class win32_computersystemproduct).UUID).ToByteArray() }
        $hash = Invoke-ImmyCommand -Context System -ScriptBlock { 
            $hasher = [System.Security.Cryptography.HashAlgorithm]::Create('sha256') 
            $hash = $hasher.ComputeHash($($using:uuidBytes))
            return $hash
        }
        Get-WindowsRegistryValue -Path "HKLM:\SYSTEM\CurrentControlSet\Services\embeddedmode\Parameters" -Name "Flags" | RegistryShould-Be -Value $hash

        # THIS IS THE SAME AS LINES 136-142 WITHOUT UTILIZING IMMYBOT FUNCTIONS
        # Invoke-ImmyCommand -Context System -ScriptBlock { 
        #     $uuidBytes = ([GUID](Get-WMIObject -Class 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 {
        Write-Progress "Targetting $OmaUri"
        if ($Cmd -eq "Get"){
            Write-Progress "Running as a $Cmd command."
        } else {
            Write-Progress "Running as a $Cmd command with the following data: $Data"
        }
        Invoke-ImmyCommand -Context System -ScriptBlock {
            Add-Type -TypeDefinition $($using:source) -Language CSharp
            $global:localMDMCmdCounter = 0

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

            # Make sure we have a unique command ID
            [xml] $xml = $useSyncML
            $global:localMDMCmdCounter++
            $xml.SyncBody.FirstChild.CmdID = $global:localMDMCmdCounter.ToString()
            $cmdId, $locURI, $updatedSyncML = $global:localMDMCmdCounter, $xml.SyncBody.FirstChild.Item.Target.LocURI, $xml.OuterXml
            
            # 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) {
                Write-Host -ForegroundColor Yellow "$syncMLResultString"
                throw "Unexpected return code from MDM local management: $rc"
            }

            # Return the response details (Status of 200 is success)
            if ($($using:Raw)) {
                $syncMLResultString
            } else {
                [xml] $syncMLResult = $syncMLResultString
                $status = $syncMLResult.SyncML.SyncBody.Status[1]
                $Result = New-Object PSObject -Property ([ordered] @{
                    CmdId = $cmdId
                    Cmd = $status.Cmd
                    Status = $status.Data
                    OmaUri = $locURI
                    Data = $syncMLResult.SyncML.SyncBody.Results.Item.Data
                })
                Write-Host -Fore Green "The policy has been applied. Result:"
                return $Result
            }
        }
    }
}

Export-ModuleMember -Function Unregister-LocalMDM,Send-LocalMDMRequest

And the combined script for a task is:

param(
[Parameter(Position=0,Mandatory=$False,HelpMessage=@'
If set to $true, other parameters will be unused.
'@)]
[Boolean]$Unregister=$false,
[Parameter(Position=1,Mandatory=$True)]
[String]$OMAURI,
[Parameter(Position=2,Mandatory=$True)]
[ValidateSet('Add','Atomic','Delete','Exec','Replace','Result')]
[String]$SetCmd,
[Parameter(Position=3)]
[String]$DataValue
)

Import-Module LocalMDM

switch($method){ 
    test{
        $TestResult = Send-LocalMDMRequest -OMAURI $OMAURI -Cmd Get
        Write-Host -Fore DarkGreen $TestResult

        if ($null -eq $TestResult.Data -or $TestResult.Data -ne $DataValue -and $TestResult.Status -eq "200"){
            return $false
        }else{
            return $true
        }

    }
    get{
        return $TestResult.Data
    }
    set{
        if ($Unregister){
            Unregister-LocalMDM
            return
        }
        if ($null -ne $DataValue -and $SetCmd -eq "Add"){
            $SetCmd = "Replace"
        }
        Send-LocalMDMRequest -OMAURI $OMAURI -Cmd $SetCmd -Data $DataValue
    }
}

It’s a bit tricky, so there’s some things I’m still working on to smooth it out. It’s great for different types of remote wipes, though. For example, You could add a line in the task to remove the ppkg from C:\Recovery\Customizations and run a protected wipe. Alternatively, you can run a wipe and persist user data if needed. Wipes are a bit weird, so don’t expect the session to end in a compliant state. It’s difficult to determine if the wipe happened or not programmatically.

If you’re interested in this module and how it works, this is where I found it:

1 Like

An example of the parameters used for a remote wipe and persist user data:

Unregister - Leave default (false)

OMAURI - ./Vendor/MSFT/RemoteWipe/doWipePersistUserData
SetCmd - Exec
DataValue - 1

More about the RemoteWipe CSP