Deep dive Microsoft Intune Management Extension – PowerShell Scripts

Microsoft made a big step forward in the Modern Management field. Limitations like custom configurations or even Win32 App installs can be addressed now. Microsoft developed an EMS agent (aka SideCar) and released it as a new Intune feature called Intune Management Extension. This agent is able to manage and execute PowerShell scripts on Windows 10 devices and it does this quite well.

IntuneManagementExtensionMSIProperties

We can simply upload our own PowerShell scripts for device configuration:

PowerShellScriptsIntune

How do we get the agent installed on the devices?

The ability to deploy basic MSI packages via MDM OMA-DM channel, is provided by Microsoft since the beginning of Windows 10. This is achieved by using the EnterpriseDesktopAppManagement CSP.

Microsoft is using this mechanism to deploy the agent to Windows 10 devices. Beginning with Windows 10 Version 1607 we have support of the Intune Management Extension now.

The workflow is basically like this. If a PowerShell script is assigned to a user or device and the agent is not installed, it will be pushed down automatically to the device via EnterpriseDesktopAppManagement CSP by Intune. This can be verified and traced in the “Advanced Diagnostics Report” of the MDM management.

Open Settings > Accounts > Access work or school > Connected to TenantName’s Azure AD > Info > scroll down to the bottom and click “Create report”

AdvancedDiagnosticsReportNow you get an report about all MDM configurations including the desktop app installations. Here we can see the EnterpriseDesktopAppManagement msi install of the EMSAgent with the ProductCode: {25212568-E605-43D5-9AA2-7AE8DB2C3D09}

EMSAgentInstallReport

./device/Vendor/MSFT/EnterpriseDesktopAppManagement/MSI/%7B25212568-E605-43D5-9AA2-7AE8DB2C3D09%7D

The instruction can be translated into more human readable form with ProductCode and curly brackets, as the yellow marked resource uses URL encoding and here %7B = { and %7D = }

./device/Vendor/MSFT/EnterpriseDesktopAppManagement/MSI/{25212568-E605-43D5-9AA2-7AE8DB2C3D09}

If the device encounters any error during installation of the agent, we can troubleshoot this with the eventlog:

Start event viewer > Applications and Services Logs > Microsoft > Windows > DeviceManagement-Enterprise-Diagnostics-Provider > Admin

An example where the agent installation went wrong with an error event id 1924 looks like this:

Event ID 1924, Error

EnterpriseDesktopAppManagement CSP: An application install has failed. 
Examine the MSI log (C:\Windows\system32\config\systemprofile\AppData\
Local\mdm\{25212568-E605-43D5-9AA2-7AE8DB2C3D09}.log) for more details. 
Install command: ("C:\Windows\system32\msiexec.exe" /quiet /l*v 
C:\Windows\system32\config\systemprofile\AppData\Local\mdm\
{25212568-E605-43D5-9AA2-7AE8DB2C3D09}.log /qn /i "C:\Windows\system32\
config\systemprofile\AppData\Local\mdm\
{BD19E4D8-D6C9-48B2-A53C-579D03B29FE9}.msi" ), 
MSI ProductCode: {25212568-E605-43D5-9AA2-7AE8DB2C3D09}, 
User SID: (S-0-0-00-0000000000-0000000000-000000000-000), 
Result: (User cancelled installation.).

Here we can see the original MSI install command including logfile path for further analysis:

C:\Windows\system32\config\systemprofile\AppData\Local\mdm\ {25212568-E605-43D5-9AA2-7AE8DB2C3D09}.log

The agent will get installed in the 32-bit Program Files path:

C:\Program Files (x86)\Microsoft Intune Managment Extension

IntuneManagementExtension

How does the agent gets it’s policy?

The agent will start to figure out the service endpoint via Service Discovery. A common approach in Microservices architecture in modern cloud platforms. After getting the Intune Service URL (https://somemssubdomain.manage.microsoft.com/SideCar/StatelessSideCarGatewayService) the agent can start to communicate and will receive its assigned policies.

All this can be traced in the logfiles here:

C:\ProgramData\Microsoft\IntuneManagementExtension\Logs

IntuneManagementExtensionLogs

The agent will start to download and execute the assigned PowerShell script here:

C:\Program Files (x86)\Microsoft Intune Managment Extension\Policies\Scripts\UserGUID_ScriptGUID.ps1

and the results are written here:

C:\Program Files (x86)\Microsoft Intune Managment Extension\Policies\Results\UserGUID_ScriptGUID.output|.error|.timeout

After successful execution the script and results are cleaned up and nothing is left on the device. Corresponding registry entries for the assigned scripts and execution results are here:

HKLM\SOFTWARE\Microsoft\IntuneManagementExtension\Policies\UserGUID\ScriptGUID

IntuneManagementExtensionRegistry

How often does the agent check for new policies?

The default is every 60 minutes.

What are the PowerShell script execution options?

We can execute scripts in system context or user context. Optional we can enforce a signature check.

ScriptSettings

The current file size limit is 10KB for ASCII scripts and 5KB for unicode scripts. The current file size limit is max. 200KB.

How do I enforce next agent check for policies?

Simply restart the Windows Service “Microsoft Intune Management Extension”.

IntuneManagementExtensionWindowsService

Is there a particular order in which multiple scripts are executed?

No, they are executed in random order.

What can be done with the PowerShell script execution?

Some examples and potentially pitfalls with the usage of the Intune Management Extension are listed below:

Install Win32 application

Install Win32 applications can be done easily by using a web request to get sources for the installation and finally execution of the sources. This technique is used a lot in the Chocolatey space. A simple example with 7-zip is shown below and can be modified to your needs:

# Author: Oliver Kieselbach
# Date: 11/28/2017
# Description: Download executable or msi and execute to install.

# The script is provided "AS IS" with no warranties.
$url = "http://www.7-zip.org/a/7z1701-x64.msi"
$filePath = "c:\windows\temp\7z1701-x64.msi"
$ProgressPreference = 0
Invoke-WebRequest $url -OutFile $filePath -UseBasicParsing
& $filePath /quiet

To use Chocolatey you should follow the install instructions from the Chocolatey website:

iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))

After Chocolatey Framework installation you can use choco.exe install Package in additional PS scripts to install Chocolatey software. An excellent post from Peter van der Woude regarding Chocolatey and Intune Management Extension can be found here: Combining the powers of the Intune Management Extension and Chocolatey

 

Write registry keys in x64 hive and not WOW6432Node

As the agent is an 32-bit agent every PowerShell script execution will be in the 32-bit agent process. If we write a registry key on a x64 device from a 32-bit process it will be redirected to the WOW6432Node in the registry. This is often not the desired behavior. To solve this we can restart the script as a 64-bit process for script execution. An example to write a registry key to the x64 hive with this technique is shown below:

# Author: Oliver Kieselbach
# Date: 11/28/2017
# Description: Write to registry and ensure execution from x64 process environment.

# The script is provided "AS IS" with no warranties.

Param([switch]$Is64Bit = $false)

Function Restart-As64BitProcess
{
 If ([System.Environment]::Is64BitProcess) { return }
 $Invocation = $($MyInvocation.PSCommandPath)
 if ($Invocation -eq $null) { return }
 $sysNativePath = $psHome.ToLower().Replace("syswow64", "sysnative")
 Start-Process "$sysNativePath\powershell.exe" -ArgumentList "-ex bypass -file `"$Invocation`" -Is64Bit" -WindowStyle Hidden -Wait
}

Function New-RegistryKey
{
 Param([Parameter(Mandatory = $true)]
 [string]$Key,
 [Parameter(Mandatory = $true)]
 [string]$Name,
 [Parameter(Mandatory = $true)]
 [string]$Value,
 [ValidateSet("String", "ExpandString", "Binary", "DWord", "Multistring", "QWord", ignorecase=$true)]
 [string]$Type = "String")
 try
 {
 $subkeys = $Key.split("\")

foreach ($subkey in $subkeys)
 {
 $currentkey += ($subkey + '\')
 if (!(Test-Path $currentkey))
 {
 New-Item -Type String -Path $currentkey | Out-Null
 }
 }

Set-ItemProperty -Path $currentkey -Name $Name -Value $Value -Type $Type -ErrorAction SilentlyContinue
 }
 catch [system.exception]
 {
 $message = "{0} threw an exception: `n{0}" -f $MyInvocation.MyCommand, $_.Exception.ToString()
 Write-Host $message
 }
}

if (!$Is64Bit) { Restart-As64BitProcess }
else
{
 # Enable Potentially Unwanted Application protection
 New-RegistryKey -Key "hklm:\SOFTWARE\Microsoft\Windows Defender" -Name "PUAProtection" -Value "1" -Type DWord
}

 

 

Execute in user context and show dialog

To show a simple example to execute scripts in user context we can use the following script to present a Message Box to the user:

 Add-Type -AssemblyName PresentationFramework
 [System.Windows.MessageBox]::Show('Hello World from Sid-Car!')

HelloSideCar

 

I hope this explains the technical implementation of the Microsoft Intune Management Extension and the extension addresses some road blockers you might have in the past with Intune regarding advanced configurations.

12 thoughts on “Deep dive Microsoft Intune Management Extension – PowerShell Scripts”

  1. I created a PS1 script with only the command , new-localuser -name Peter , in the log file i see ‘new-localuser’ is not recognized.
    Second PS1 script with only , get-proces , runs fine.
    Any idea’s why the first is failing ?
    Script is set to run in SYSTEM context.

    1. Yes, I think you need to run it from 64-bit process. Try to run “Get-Command Get-LocalUser” from a x86 PowerShell and you will see Get-LocalUser is not recognized as cmdlet. Running from x64 will succeed. To accomplish New-LocalUser execution, try my script example “writing to x64 registry hive”. This will re-launch the process in x64 and the cmdlet should succeed.

  2. Is this correct script for creating a new local user?

    Param([switch]$Is64Bit = $false)

    Function Restart-As64BitProcess
    {
    If ([System.Environment]::Is64BitProcess) { return }
    $Invocation = $($MyInvocation.PSCommandPath)
    if ($Invocation -eq $null) { return }
    $sysNativePath = $psHome.ToLower().Replace(“syswow64”, “sysnative”)
    Start-Process “$sysNativePath\powershell.exe” -ArgumentList “-ex bypass -file `”$Invocation`” -Is64Bit” -WindowStyle Hidden -Wait
    }

    Function New-LocalUser
    {
    New-LocalUser “Oliver”
    }

    1. First of all: I do not recommend transferring secure data like passwords via this approach! But I know it’s done out there. The GPOs are often used for local user management tasks too (that approach uses a simple xml file with the password base64 encoded -> this is not secure!). Please consider something that was build for that purpose. Admin account management should be done via LAPS or other third party. I know there is no equivalent solution like LAPS for a cloud only environment right now. But the approach below is not secure and should therefore used within a test environment only!

      The modified script below will work but be aware that this should not be used in production! The script content will be logged to the IntuneManagementExtension.log file. There you will find the clear text password as defined in the script. As an example I have copied the particular log file entry below the script to show that:

      # Author: Oliver Kieselbach
      # Date: 01/03/2018
      # Description: Create a local user account.
      
      # REMARK: DO NOT USE IN PRODUCTION, Password will not be unique and be visible in clear text in log file!
       
      # The script is provided "AS IS" with no warranties.
      
      Param([switch]$Is64Bit = $false)
      
      Function Restart-As64BitProcess
      {
          If ([System.Environment]::Is64BitProcess) { return }
          $Invocation = $($MyInvocation.PSCommandPath)
          if ($Invocation -eq $null) { return }
          $sysNativePath = $psHome.ToLower().Replace("syswow64", "sysnative")
          Start-Process "$sysNativePath\powershell.exe" -ArgumentList "-ex bypass -file `"$Invocation`" -Is64Bit" -WindowStyle Hidden -Wait
      }
      
      if (!$Is64Bit) { Restart-As64BitProcess }
      else
      {
          Start-Transcript -Path "$env:temp\CreateLocalUser.log"
      
          $password = ConvertTo-SecureString "SecretP@ssw0rd!" -AsPlainText -Force
          New-LocalUser "John" -AccountNeverExpires:$true -FullName "John Doe" -Password $password
      
          Stop-Transcript
      }
      

      ########################

      <![LOG[Processing policy with id = fc7efe54-fa66-43cd-8a4d-dd1c4cbde00c for user 73d664e4-0886-4a73-b745-c694da45ddb4]LOG]!><time="11:57:28.6288432" date="1-3-2018" component="IntuneManagementExtension" context="" type="1" thread="5" file="">
      <![LOG[Policy body = # Author: Oliver Kieselbach
      # Date: 01/03/2018
      # Description: Create a local user account.
      
      # REMARK: DO NOT USE IN PRODUCTION, Password will not be unique and be visible in cleartext in logfiles!
       
      # The script is provided "AS IS" with no warranties.
      
      Param([switch]$Is64Bit = $false)
      
      Function Restart-As64BitProcess
      {
          If ([System.Environment]::Is64BitProcess) { return }
          $Invocation = $($MyInvocation.PSCommandPath)
          if ($Invocation -eq $null) { return }
          $sysNativePath = $psHome.ToLower().Replace("syswow64", "sysnative")
          Start-Process "$sysNativePath\powershell.exe" -ArgumentList "-ex bypass -file `"$Invocation`" -Is64Bit" -WindowStyle Hidden -Wait
      }
      
      if (!$Is64Bit) { Restart-As64BitProcess }
      else
      {
          Start-Transcript -Path "$env:temp\CreateLocalUser.log"
      
          $password = ConvertTo-SecureString "SecretP@ssw0rd!" -AsPlainText -Force
          New-LocalUser "John" -AccountNeverExpires:$true -FullName "John Doe" -Password $password
      
          Stop-Transcript
      }, hash = ewM7Neczxjcg/Y6ukJQcO8yAke1iFEuYE3R70ixU4ZQ=]LOG]!><time="11:57:28.7538445" date="1-3-2018" component="IntuneManagementExtension" context="" type="1" thread="5" file="">
      
      1. This version does even use error codes. But for now these error codes are not reflected in the UI in Intune. As long as the script could be executed through the agent the Intune UI will show success at the moment.

        # Author: Oliver Kieselbach
        # Date: 01/03/2018
        # Description: Create a local user account.
        
        # REMARK: DO NOT USE IN PRODUCTION, Password will not be unique and be visible in cleartext in logfiles!
         
        # The script is provided "AS IS" with no warranties.
        
        Param([switch]$Is64Bit = $false)
        
        $exitCode = 0
        
        Function Restart-As64BitProcess
        {
            If ([System.Environment]::Is64BitProcess) { return }
            $Invocation = $($MyInvocation.PSCommandPath)
            if ($Invocation -eq $null) { return }
            $sysNativePath = $psHome.ToLower().Replace("syswow64", "sysnative")
            $(Start-Process "$sysNativePath\powershell.exe" -ArgumentList "-ex bypass -file `"$Invocation`" -Is64Bit" -WindowStyle Hidden -Wait -PassThru).ExitCode
        }
        
        if (!$Is64Bit) { $exitCode = Restart-As64BitProcess }
        else
        {
            Start-Transcript -Path "$env:temp\CreateLocalUser.log"
        
            $password = ConvertTo-SecureString "SecretP@ssw0rd!" -AsPlainText -Force
            New-LocalUser "John" -AccountNeverExpires:$true -FullName "John Doe" -Password $password -ErrorVariable errLocalUser -ErrorAction SilentlyContinue
            if ($errLocalUser)
            {
                $exitCode = -1
            }
        
            Stop-Transcript
        }
        
        exit $exitCode
        
  3. Oliver, thanks a lot. This helps me a lot! Also usable for future script now i know where to place the PowerShell commandlets in the script.

    1. I’m happy to help. But remember the technique used will not report errors thrown by the x64 script part. The script error code seen by Intune is always the x86 part and this should always be successful. The initial script is started as x86 and the one with the logic will spawn another process in x64 environment. if I find time I will see if I can enhance the script to catch the error code from the x64 part.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s