Skip to content

Intune Hack: Keep Critical App Running

I recently received a request to ensure that a critical app remains running on Intune-managed Windows devices, even if users attempt to close it. After testing several approaches, I found two reliable methods to solve this challenge and keep the app running 24/7. Here’s how I did it.

Table Of Contents

The Critical App – TeamViewer Host

The app in question was TeamViewer Host, a specialized version of the TeamViewer software that enables remote access to devices, even without requiring anyone to be present. It’s mainly used for remote support and maintenance tasks. The challenge and solutions presented are not limited to TeamViewer Host. They can easily be adapted for any application running on Windows.

TeamViewer is a third-party alternative to the Intune add-on Remote Help.

The TeamViewer Host application was made available to the endpoints through Microsoft Intune with policies assigned by the TeamViewer management portal. Getting the application out there should be easy with an Intune third-party application manager like Algiz, Patch My PC, Robopack, or Scappman. The challenge was, however, that users could exit the application from running, leaving it out of control.

Once the TeamViewer Host is stopped, the device cannot be reached remotely. Here lies my challenge: I need to keep this critical app running.

The Obvious TeamViewer Policy

Since the TeamViewer Host application has a policy from the TeamViewer management, it seems obvious to start looking for a solution there.

I can find the available policies in the TeamViewer management portal by navigating to Home – Design & Deploy – Policies. When editing the policy, I can add the setting “Disable TeamViewer shutdown” and set it to enabled and enforced. Enforced means a user can’t tamper with the configuration.

This setting removes the option to exit the application when right-clicking on the icon in the system tray.

However, the end user can still start Task Manager and end the TeamViewer process.

Finding a solution for this challenge caused me to spin out in this blog post.

Take 1: Check if Critical App Running; If Not, Start It

My first approach was to find a way of checking if the critical app was running and, if not, start it. Since I have more approaches coming, you might guess this take is not the best one – but let’s run through it. You might get an inspirational idea for other projects or maybe learn where not to go.

PowerShell Script To Check And Get Critical App Running

The central part of this solution was a PowerShell script that checked whether a critical app was running and then started it if not. This is how this script ended up looking.

$processName = "TeamViewer"
$teamViewerPath = "C:\Program Files (x86)\TeamViewer\TeamViewer.exe"

if (Test-Path $teamViewerPath) {
    $process = Get-Process -Name $processName -ErrorAction SilentlyContinue
    if ($process -eq $null) {
        # Log an event to the Application Log
        if (-not (Get-EventLog -LogName Application -Source "TeamViewerMonitor" -ErrorAction SilentlyContinue)) {
            New-EventLog -LogName Application -Source "TeamViewerMonitor"
        }
        Write-EventLog -LogName Application -Source "TeamViewerMonitor" -EventId 1001 -EntryType Information -Message "TeamViewer has stopped and is being restarted by TeamViewerMonitor from CloudWay."

        # Start TeamViewer
        Start-Process $teamViewerPath
    }
} else {
    # Log an event if TeamViewer is not installed
    if (-not (Get-EventLog -LogName Application -Source "TeamViewerMonitor" -ErrorAction SilentlyContinue)) {
        New-EventLog -LogName Application -Source "TeamViewerMonitor"
    }
    Write-EventLog -LogName Application -Source "TeamViewerMonitor" -EventId 1002 -EntryType Warning -Message "TeamViewer is not installed on this system."
}
PowerShell

The challenge with this script was triggering it. I ended up running it on a schedule using Windows Task Scheduler, but this introduced some new challenges that need to be discussed.

The Annoying Flashing PowerShell Window

When running the PowerShell script from a scheduled task, an annoying PowerShell window flashed briefly on each trigger. It does not respect the option to run with the Windows style hidden. This is unpleasant, especially if the time trigger had a short interval.

Of course, the time interval itself is one challenge, but I was focusing on the flashing windows at this time.

My friends MVP Sandy Zeng and MVP Nickolaj Andersen have shared a clever solution for the flashing windows challenge. They have created a tool, released through MSEndpointMGR, that addresses this challenge. The PSInvoker tool is available from GitHub.

Using the PSInvoker tool as an action in my scheduled task, my small PowerShell script ran silently on the device. The configuration for this action was like this.

The task would now start on schedule and silently kickstart the missing critical app running. No more annoying flashing windows!

Distribute The Schedule Task Using Intune

After tweaking the Scheduled Task to ensure TeamViewer Host started as the logged-on user, I found it interesting to pilot this solution by distributing the scheduled task using Intune. I created a PowerShell script to be used as an Intune platform script.

The script is available on my GitHub.

<#
    .SYNOPSIS
    This script adds a scheduled task which checks if TeamViewer is running. If TeamViewer is not running, the script starts the TeamViewer process and logs an event to the Application Log.

    .DESCRIPTION
    This script adds a scheduled task which checks if TeamViewer Host is running. If TeamViewer is not running, the script starts the TeamViewer process and logs an event to the Application Log.
    The script utilizes the PSInvoker tool from MSEndpointMgr to run the PowerShell script as a scheduled task without having av PowerShell window displaying for the end user.
    The scheduled task is set to run at logon as the current user to ensure TeamViewer Host is running as the user. 

    .NOTES
    Author:     Simon Skotheimsvik
    Filename:   TeamViewerMonitor-TimeTrigger.ps1
    Info:       https://skotheimsvik.no
    Reference:  https://github.com/MSEndpointMgr/PSInvoker/tree/master
    Versions:
            1.0.0 - 11.09.2024 - Initial Release, Simon Skotheimsvik
#>



#region Variables
$taskName = "TeamViewerMonitor"
$SimonDoesPath = "C:\Program Files\SimonDoes"
$xmlFilePath = "$SimonDoesPath\TeamViewerMonitor.xml"
$scriptPath = "$SimonDoesPath\CheckStartAndLogTeamViewer.ps1"
$zipFilePath = "$SimonDoesPath\PSInvoker.zip"
$installDate = Get-Date -Format "yyyy-MM-dd"
$Interval = 15  #Interval for scheduled task in minutes
#endregion

#region Download and Extract PSInvoker from MSEndpointMGR GitHub
# Define the URL and the destination path for the ZIP file
$zipUrl = "https://github.com/MSEndpointMgr/PSInvoker/releases/download/1.0.1/PSInvoker.zip"

# Download the ZIP file
Invoke-WebRequest -Uri $zipUrl -OutFile $zipFilePath

# Ensure the script directory exists
if (-not (Test-Path $SimonDoesPath)) {
    New-Item -Path $SimonDoesPath -ItemType Directory
}

# Extract the ZIP file to the script directory
Expand-Archive -Path $zipFilePath -DestinationPath $SimonDoesPath -Force

# Remove the ZIP file after extraction
Remove-Item -Path $zipFilePath
#endregion


#region Create PowerShell Script
# Create the PowerShell script to check and log TeamViewer status
$scriptContent = @'
$processName = "TeamViewer"
$teamViewerPath = "C:\Program Files (x86)\TeamViewer\TeamViewer.exe"

if (Test-Path $teamViewerPath) {
    $process = Get-Process -Name $processName -ErrorAction SilentlyContinue
    if ($process -eq $null) {
        # Log an event to the Application Log
        if (-not (Get-EventLog -LogName Application -Source "TeamViewerMonitor" -ErrorAction SilentlyContinue)) {
            New-EventLog -LogName Application -Source "TeamViewerMonitor"
        }
        Write-EventLog -LogName Application -Source "TeamViewerMonitor" -EventId 1001 -EntryType Information -Message "TeamViewer has stopped and is being restarted by TeamViewerMonitor from SimonDoes."

        # Start TeamViewer
        Start-Process $teamViewerPath
    }
} else {
    # Log an event if TeamViewer is not installed
    if (-not (Get-EventLog -LogName Application -Source "TeamViewerMonitor" -ErrorAction SilentlyContinue)) {
        New-EventLog -LogName Application -Source "TeamViewerMonitor"
    }
    Write-EventLog -LogName Application -Source "TeamViewerMonitor" -EventId 1002 -EntryType Warning -Message "TeamViewer is not installed on this system."
}
'@
#endregion

#region Ensure Script Directory Exists
# Ensure the script directory exists
if (-not (Test-Path "C:\Program Files\SimonDoes")) {
    New-Item -Path "C:\Program Files\SimonDoes" -ItemType Directory
}
#endregion

#region Write Script Content to File
# Write the script content to the file
Set-Content -Path $scriptPath -Value $scriptContent
#endregion

#region Create XML Content
# Create the XML content for the scheduled task
$xmlContent = @"
<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.3" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
  <RegistrationInfo>
    <Author>5337D3DB-D8D3-4\WDAGUtilityAccount</Author>
    <Description>This is a routine from SimonDoes to ensure TeamViewer host is running on the system.
Installed on $installDate by Intune.</Description>
    <URI>\TeamViewerMonitor</URI>
  </RegistrationInfo>
  <Triggers>
    <LogonTrigger>
      <Repetition>
        <Interval>PT$($Interval)M</Interval>
        <StopAtDurationEnd>false</StopAtDurationEnd>
      </Repetition>
      <Enabled>true</Enabled>
    </LogonTrigger>
  </Triggers>
  <Principals>
    <Principal id="Author">
      <GroupId>S-1-5-32-545</GroupId>
      <RunLevel>LeastPrivilege</RunLevel>
    </Principal>
  </Principals>
  <Settings>
    <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
    <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
    <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
    <AllowHardTerminate>true</AllowHardTerminate>
    <StartWhenAvailable>true</StartWhenAvailable>
    <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
    <IdleSettings>
      <StopOnIdleEnd>true</StopOnIdleEnd>
      <RestartOnIdle>false</RestartOnIdle>
    </IdleSettings>
    <AllowStartOnDemand>true</AllowStartOnDemand>
    <Enabled>true</Enabled>
    <Hidden>false</Hidden>
    <RunOnlyIfIdle>false</RunOnlyIfIdle>
    <DisallowStartOnRemoteAppSession>false</DisallowStartOnRemoteAppSession>
    <UseUnifiedSchedulingEngine>true</UseUnifiedSchedulingEngine>
    <WakeToRun>false</WakeToRun>
    <ExecutionTimeLimit>PT72H</ExecutionTimeLimit>
    <Priority>7</Priority>
  </Settings>
  <Actions Context="Author">
    <Exec>
      <Command>PSInvoker.exe</Command>
      <Arguments>CheckStartAndLogTeamViewer.ps1 --ExecutionPolicy Bypass</Arguments>
      <WorkingDirectory>$SimonDoesPath</WorkingDirectory>
    </Exec>
  </Actions>
</Task>
"@
#endregion

#region Write XML Content to File
# Write the XML content to the file
Set-Content -Path $xmlFilePath -Value $xmlContent
#endregion

#region Register Scheduled Task from XML
# Check if the scheduled task exists and delete it if it does
$taskExists = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue

if ($taskExists) {
    Unregister-ScheduledTask -TaskName $taskName -Confirm:$false
    Write-Output "Scheduled task '$taskName' deleted."
}

# Register the scheduled task from the XML file
Register-ScheduledTask -TaskName $taskName -Xml (Get-Content -Path $xmlFilePath -Raw)

Write-Output "Scheduled task '$taskName' created successfully from XML."
#endregion
PowerShell

The script starts with a region for variables where some adoption can be done. It first downloads the PSInvoker application from MSEndpointMGR and extracts it to a write-protected folder on the local computer.

Note: During the pilot, I downloaded directly from the GitHub repository, but the source for downloading must be better controlled in a production environment!

The next thing was to download the PowerShell script to the same folder. This will be used as the action in the scheduled task before the XML for the scheduled task was downloaded and used to set the solution up locally on the device.

The scheduled task is triggered when users log on and repeat for a defined set of minutes.

This was working, but I was not satisfied with the time interval. The solution would always have a time gap, potentially without the critical app running. The remedy would also run many times without any reason, with the critical app running already.

Take 2: Trigger On Process Termination Event

While disconnecting from the challenge for a couple of hours while feeding data to my Strava, my brain suddenly woke up to tell me to check the Event Log for an event on process termination. This would trigger just-in-time with minimal overhead or time gaps in the solution.

Intune Enable Detailed Audit Process Tracking

While the Event Log provides valuable information by default, it does not automatically capture the specific logs required for my intended use. To ensure these necessary logs are recorded, they must be explicitly enabled through policy settings. My natural go-to for this is the Intune Settings Catalog, and the following CSP describes the solution I ended up using: Audit Policy CSP | Microsoft Learn

I started by creating a new configuration profile following my naming standard.

I added the “Detailed Tracking Audit Process Termination” setting to the policy’s configuration blade, which is in the Auditing category.

This setting was then configured to track the success of process terminations.

An audit event will be generated each time a process ends. Success audits log successful terminations, while Failure audits log unsuccessful ones. If this policy is not configured, no audit events will be created when a process ends.

I am now targeting this policy to the group that distributed the critical app.

After some cloud minutes, this policy came down to my pilot device.

Verify Auditing of Process Terminations

Once the policy has synced to my pilot device, I can use the Event Viewer running as an Administrator (preferably using the LAPS Account). Running elevated, I can check the Security log for Event ID 4689.

Killing the TeamViewer process will give me a lovely event with Event ID 4689 and with information on the TeamViewer process!

This is the signal I am looking for!

Build My Scheduled Task To Keep Critical App Running

I can now build my new scheduled task from this audit event to keep the critical app running. Right-clicking on the event allows me to attach it to a task.

This leads me to a wizard for creating a basic task. First, I needed a name and description. I ran with the default, as this would be altered later.

The next part is information from the event.

Now, I will specify the action “to start a program”.

This is where I specify the program and the path for the application.

At the end of this wizard, I ensured that I ticked the box to open the Properties for this newly created task. I need to make some modifications.

First, when opening the Properties dialogue, I ensure the scheduled task runs as the logged-on user by clicking the “Change User or Group” button and selecting the local Users group.

Secondly, I navigate to the trigger to edit this.

I am changing this to a Custom XML trigger where I define the application in addition to the EventID.

The XML for the new event filter looks like this:

<QueryList>
  <Query Id="0" Path="Security">
    <Select Path="Security">*[System[Provider[@Name='Microsoft-Windows-Security-Auditing'] and (EventID=4689)]]and
    *[EventData[(Data='C:\Program Files (x86)\TeamViewer\TeamViewer.exe')]]</Select>
  </Query>
</QueryList>
XML

Closing out all the open dialogues, I received confirmation that the scheduled task has been created.

Looking into Scheduled Task as Administrator (using the LAPS account again), I can now see the newly created task.

It’s time to give the prototype a test run!

When the task is ended in Task Manager, my scheduled task is immediately triggered, kicking off the critical app! Sweet as candy!

PowerShell Script To Create The Scheduled Task

The new solution is working as expected, and it’s time to mass-produce it to more devices through Intune. I exported the pilot scheduled task to XML format to make it programmatically available.

This content was then implemented in a new version of my TeamViewer Monitor PowerShell script. This is available in my GitHub:

<#
    .SYNOPSIS
    This script adds a scheduled task which checks if TeamViewer has terminated and starts the process again.

    .DESCRIPTION
    This script adds a scheduled task which checks for Event ID 4689 in the Security log. If the event is triggered by TeamViewer.exe, the script starts the TeamViewer process again.
    The scheduled task is set to run as the current user to ensure TeamViewer Host is running as the user. 

    .NOTES
    Author:     Simon Skotheimsvik
    Filename:   TeamViewerMonitor-EventTrigger.ps1
    Info:       https://skotheimsvik.no
    Versions:
            1.0.0 - 11.09.2024 - Initial Release, Simon Skotheimsvik
            1.0.1 - 12.09.2024 - Changed to Event Trigger, Simon Skotheimsvik
#>

#region Variables
$taskName = "TeamViewerMonitor"
$tempPath = [System.IO.Path]::GetTempPath()
$xmlFilePath = "$tempPath" + "TeamViewerMonitor.xml"
$installDate = Get-Date -Format "yyyy-MM-dd"
#endregion

#region Create XML Content
# Create the XML content for the scheduled task
$xmlContent = @"
<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
  <RegistrationInfo>
    <Date>2024-09-11T23:45:36.3491732</Date>
    <Author>Simon Does: skotheimsvik.no</Author>
    <Description>This is a routine from SimonDoes to ensure TeamViewer Host is running on the system. `nInstalled on $installDate by Intune.</Description>
    <URI>\Event Viewer Tasks\Security_Microsoft-Windows-Security-Auditing_4689_$($taskName)</URI>
  </RegistrationInfo>
  <Triggers>
    <EventTrigger>
      <Enabled>true</Enabled>
      <Subscription><QueryList><Query Id="0" Path="Security"><Select Path="Security">*[System[Provider[@Name='Microsoft-Windows-Security-Auditing'] and (EventID=4689)]]and
    *[EventData[(Data='C:\Program Files (x86)\TeamViewer\TeamViewer.exe')]]</Select></Query></QueryList></Subscription>
    </EventTrigger>
  </Triggers>
  <Principals>
    <Principal id="Author">
      <GroupId>S-1-5-32-545</GroupId>
      <RunLevel>LeastPrivilege</RunLevel>
    </Principal>
  </Principals>
  <Settings>
    <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
    <DisallowStartIfOnBatteries>true</DisallowStartIfOnBatteries>
    <StopIfGoingOnBatteries>true</StopIfGoingOnBatteries>
    <AllowHardTerminate>true</AllowHardTerminate>
    <StartWhenAvailable>false</StartWhenAvailable>
    <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
    <IdleSettings>
      <StopOnIdleEnd>true</StopOnIdleEnd>
      <RestartOnIdle>false</RestartOnIdle>
    </IdleSettings>
    <AllowStartOnDemand>true</AllowStartOnDemand>
    <Enabled>true</Enabled>
    <Hidden>false</Hidden>
    <RunOnlyIfIdle>false</RunOnlyIfIdle>
    <WakeToRun>false</WakeToRun>
    <ExecutionTimeLimit>PT72H</ExecutionTimeLimit>
    <Priority>7</Priority>
  </Settings>
  <Actions Context="Author">
    <Exec>
      <Command>TeamViewer.exe</Command>
      <WorkingDirectory>C:\Program Files (x86)\TeamViewer\</WorkingDirectory>
    </Exec>
  </Actions>
</Task>
"@
#endregion

#region Write XML Content to File
# Write the XML content to the file
Set-Content -Path $xmlFilePath -Value $xmlContent
#endregion

#region Register Scheduled Task from XML
# Check if the scheduled task exists and delete it if it does
$taskExists = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue

if ($taskExists) {
  Unregister-ScheduledTask -TaskName $taskName -Confirm:$false
  Write-Output "Scheduled task '$taskName' deleted."
}

# Register the scheduled task from the XML file
Register-ScheduledTask -TaskName $taskName -Xml (Get-Content -Path $xmlFilePath -Raw)

Write-Output "Scheduled task '$taskName' created successfully from XML."
#endregion

#region Clean Up
# Remove the XML file after the task is registered
Remove-Item -Path $xmlFilePath -Force
Write-Output "Temporary XML file '$xmlFilePath' deleted."
#endregion
PowerShell

The script is shorter than the initial version, and the XML content has been easily modified to take content from the variables region in the script.

Intune Platform Script To Distribute The Solution

I am using an Intune Platform Script to distribute the PowerShell script to create the Scheduled Task.

I configure the new platform script following my naming conventions.

I will then upload my script and set the corresponding configuration.

Remember my earlier blog describing how to retrieve a platform script from Intune?
Simon does Intune Script Recovery Shortcut – Skip Graph Permissions! (skotheimsvik.no)

The platform script is now assigned to the group used for app distribution and the auditing configuration.

This way, the solution will follow the users getting the application assigned, ensuring I keep the critical app running.

critical app running

Add the users to the group, and Intune will ensure the app is installed and the devices are configured to keep the critical app running!

Wrapping Up The Critical App Running Challenge

Oh, you’re still around? Either you’re genuinely intrigued by keeping the critical app running, or you’re just here for the comedic value of my missteps along the route. Either way, I hope this blog gets those creative gears turning.

Please connect with me, comment or share it if you liked it.
Your feedback makes the effort worth it and keeps me going!

Published inAppAutomationEndpointIntunePowershellWindows

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *


The reCAPTCHA verification period has expired. Please reload the page.