简体   繁体   中英

.NET Memory Mapped Files and Task Scheduler, client can't access server-created MMF when running from Task

I'm testing out working with Memory Mapped Files. I have a server that writes to a named MMF and a client that reads from the same-named MMF and using a named Semaphore for simultaneous access prevention.

When I run both the client and the server interactively, I can pass data from server -> client.

When I run the server from a Scheduled Task and configure it to only run when the user's logged in (and I'm the user who it's running as and is logged in), and run the client interactively, it also works

When I run the server from a Scheduled Task and set it to run whether or not the user's logged in OR my actual desired context (SYSTEM), my client no longer can see that the MMF is created (even though my log/error trapping confirms that it is).

I seem to be missing some environment change in the Scheduled Task settings that's preventing my MMF from being read. Can anyone provide any assistance?

Edit: Sample code to illustrate the problem.

Server:

try {
    $mmf = [System.IO.MemoryMappedFiles.MemoryMappedFile]::CreateOrOpen('LinkMon', 10MB)

    ## Create a mutex that's used to temporaily block access to the Memory Mapped File
    $MmfSemaphore = [System.Threading.Semaphore]::new(1, 1, 'LinkMonSemaphore')
} catch {
    $_
}


while (1) {

    if ([datetime]::Now.Second % 2 -eq 0) {
        $s = Get-Process | Select -First 10
    } else {
        $s = Get-Service | Select -First 10
    }

    $j = $s | ConvertTo-Json
    $b = [System.Text.Encoding]::Ascii.GetBytes($j)

    try {
        $MmfSemaphore.WaitOne()
        $MmfStream = $mmf.CreateViewStream()
        $MmfBw = [System.IO.BinaryWriter]::new($MmfStream)

        ## [System.Text.Encoding]::Ascii.GetString(([System.Text.Encoding]::Ascii.GetBytes((("$($b.Length)").PadLeft(10, '0')))))

        $MmfBw.Write(([System.Text.Encoding]::Ascii.GetBytes((("$($b.Length)").PadLeft(10, '0')))))
        $MmfBw.Write($b)
        $MmfStream.Position = 0  

        $MmfSemaphore.Release()
    } catch {
        $_
    }

    Start-Sleep -Seconds 1
}


$MmfSemaphore.Dispose()
$MmfBw.Dispose()
$MmfStream.Dispose()
$mmf.Dispose()

Client:

try {
    $mmf = [System.IO.MemoryMappedFiles.MemoryMappedFile]::OpenExisting('LinkMon')

    $MmfSemaphore = [System.Threading.Semaphore]::OpenExisting('LinkMonSemaphore')
} catch {
    $_
}

while (1) {
    try {
        $MmfSemaphore.WaitOne()

        $MmfStream = $mmf.CreateViewStream()
        $MmfBr = [System.IO.BinaryReader]::new($MmfStream)

        $DataLength = ([int]([System.Text.Encoding]::ASCII.GetString($MmfBr.ReadBytes(10))))
        $Payload = $MmfBr.ReadBytes($DataLength)
        $MmfStream.Position = 0  


        $MmfSemaphore.Release()

        $o = [System.Text.Encoding]::Ascii.GetString($Payload) | ConvertFrom-Json
    } catch {
        $_
    }

    $o | ft
    Start-Sleep -Milliseconds 500
}

$MmfSemaphore.Dispose()
$MmfBr.Dispose()
$MmfStream.Dispose()
$mmf.Dispose()

Edit2:

Based on looking at some C++ and C# implementations that appeared to have similar problems, I tried modifying the security of the MMF but to no avail. Here is the new code and the situation still stands: When I run the client and server interactively in two different processes, they can send data back and forth. When I run the server in a Scheduled Task as System, I cannot read the MMF that according to my debug log, was successfully created. The error the client receives when I run PowerShell normally or in elevated context is: Exception calling "OpenExisting" with "1" argument(s): "No handle of the given name exists."

Server:

$code = @"
using System.IO.MemoryMappedFiles;
using System.Security.AccessControl;
using System.Security.Principal;

namespace mmf
{
    public static class Security
    {
        public static MemoryMappedFileSecurity GetMmfSec() 
        {
            var security = new MemoryMappedFileSecurity();
            security.AddAccessRule(new System.Security.AccessControl.AccessRule<MemoryMappedFileRights>(new SecurityIdentifier(WellKnownSidType.WorldSid, null), MemoryMappedFileRights.FullControl, AccessControlType.Allow));
            return security;
        }
    }
}
"@
Add-Type -Language CSharp $code

$acl = [mmf.Security]::GetMmfSec()

## Create the initial Memory Mapped File and semaphore to synchronize writes to it.
try {
    $mmf = [System.IO.MemoryMappedFiles.MemoryMappedFile]::CreateOrOpen('Global\Restricted\LinkMon', 10MB, [System.IO.MemoryMappedFiles.MemoryMappedFileAccess]::ReadWrite, [System.IO.MemoryMappedFiles.MemoryMappedFileOptions]::None, $acl, [System.IO.HandleInheritability]::Inheritable)
    $sd = $mmf.GetAccessControl()

    $sd | ConvertTo-Json | Out-File C:\temp\server_access.txt

    ## Create a sempahore that's used to temporaily block access to the Memory Mapped File
    $MmfSemaphore = [System.Threading.Semaphore]::new(1, 1, 'LinkMonSemaphore')
} catch {
    $_
}


while (1) {

    if ([datetime]::Now.Second % 2 -eq 0) {
        $s = Get-Process | Select -First 10
    } else {
        $s = Get-Service | Select -First 10
    }
    $s | ft

    $j = $s | ConvertTo-Json
    $b = [System.Text.Encoding]::Ascii.GetBytes($j)

    try {
        $MmfSemaphore.WaitOne()
        $MmfStream = $mmf.CreateViewStream()
        $MmfBw = [System.IO.BinaryWriter]::new($MmfStream)

        ## [System.Text.Encoding]::Ascii.GetString(([System.Text.Encoding]::Ascii.GetBytes((("$($b.Length)").PadLeft(10, '0')))))

        $MmfBw.Write(([System.Text.Encoding]::Ascii.GetBytes((("$($b.Length)").PadLeft(10, '0')))))
        $MmfBw.Write($b)
        $MmfStream.Position = 0  #Reset the position of the pointer back to the top of the file so we're overwriting the old info

        $MmfSemaphore.Release()
    } catch {
        $_
    }

    Start-Sleep -Milliseconds 500
}

trap {
    $MmfSemaphore.Dispose()
    $MmfBw.Dispose()
    $MmfStream.Dispose()
    $mmf.Dispose()
}

Client:

try {
    $mmf = [System.IO.MemoryMappedFiles.MemoryMappedFile]::OpenExisting('Global\Restricted\LinkMon', [System.IO.MemoryMappedFiles.MemoryMappedFileRights]::ReadWrite, [System.IO.HandleInheritability]::Inheritable)

    $MmfSemaphore = [System.Threading.Semaphore]::OpenExisting('LinkMonSemaphore')
} catch {
    $_
    break
}

while (1) {
    try {
        $MmfSemaphore.WaitOne()

        $MmfStream = $mmf.CreateViewStream()
        $MmfBr = [System.IO.BinaryReader]::new($MmfStream)

        $DataLength = ([int]([System.Text.Encoding]::ASCII.GetString($MmfBr.ReadBytes(10))))
        $Payload = $MmfBr.ReadBytes($DataLength)
        $MmfStream.Position = 0  


        $MmfSemaphore.Release()

        $o = [System.Text.Encoding]::Ascii.GetString($Payload) | ConvertFrom-Json
    } catch {
        $_
    }

    $o | ft
    Start-Sleep -Milliseconds 500
}

$MmfSemaphore.Dispose()
$MmfBr.Dispose()
$MmfStream.Dispose()
$mmf.Dispose()

EDIT3:

So I went through a bunch of iterations after I noticed that the memory 'Section' object in the Object Manager was moving around. I wanted to see if the location appeared to matter or if it stamped a different ACL on the object in different cases (and it did move around and end up with different ACLs). Here is the matrix of iterations I've gone though if this helps anyone help me troubleshoot this:

在此处输入图像描述

EDIT4:

I also started adding Everyone, Full Control to the MMF and tried the iterations to no avail.

I started looking at Process Integrity. And while that seemed like a fruitful endeavor, when the server is running as SYSTEM from powershell_ise interactively (use psexec to spawn an instance of powershell_ise as SYSTEM), I can still have my client (non-elevated) read data from the MMF. However, when running as SYSTEM from a Scheduled Task, the process is still running with 'System' Integrity level but I still can't access the file.

So... I got it. Crazy enough, it was a combination of all the different things I was looking at. When the server is executed as System not from my logged on user (via psexec) but via either Local Policy > Startup Script or Scheduled Task running as System:

  1. ...it gets isolated with Windows Integrity isolations
  2. ...it gets dynamically allocated in the \BaseNamedObjects Kernel namespace instead of the \Sessions\1\BaseNamedObjects as it does when executed as an interactive user (so you need to update your name targeting)
  3. ...it gets stamped with a new and very limited ACL
  4. This happens to ALL kernel level objects -- so I had to account for these considerations on my MemoryMappedFile AND my Semaphore

So the following modifications were made to address the aforementioned points:

  1. At runtime, the server adjusts the SACL to allow access to by Low Integrity Processes
  2. While the server targets, just the name, 'LinkMon', the client had to target, 'Global\LinkMon' to properly locate the new location of the MMF memory section
  3. The server created the MMF and Semaphore with Constructors that set explicit ACLs to allow for access (ultimately Everyone, Full Control) -- I haven't played seeing how limited I can make this to still allow access. I'll probably try limiting this further to something like Authenticated Users\Read, but I need a break.

And finally, here are the final test client and server (remember server running as System from a Scheduled Task) that allows the server to write data to the MMF and a standard user running client to read from the MMF:

Server (heavy inline comments):

## Credits for Standing on the Shoulders of Giants:  ##
## https://stackoverflow.com/questions/41369232/createmutex-fails-after-impersonation
## https://stackoverflow.com/questions/3282365/opening-a-named-pipe-in-low-integrity-level/14424623#14424623
## https://stackoverflow.com/questions/34887587/gaining-access-to-a-memorymappedfile-from-low-integrity-process
## https://blog.didierstevens.com/2010/09/07/integrity-levels-and-dll-injection/
## https://stackoverflow.com/questions/9912534/how-to-create-a-new-process-with-a-lower-integrity-level-il
## https://docs.microsoft.com/en-us/windows/win32/sysinfo/kernel-objects
## https://docs.microsoft.com/en-us/windows/win32/termserv/kernel-object-namespaces
## https://www.tiraniddo.dev/2019/02/a-brief-history-of-basenamedobjects-on.html


Add-Type -Language CSharp @"
using System;
using System.Runtime.InteropServices;
using System.ComponentModel;

namespace Native
{
    public static class NativeMethods
    {
        public const string LOW_INTEGRITY_SSL_SACL = "S:(ML;;NW;;;LW)";

        public static int ERROR_SUCCESS = 0x0;

        public const int LABEL_SECURITY_INFORMATION = 0x00000010;

        public enum SE_OBJECT_TYPE
        {
            SE_UNKNOWN_OBJECT_TYPE = 0,
            SE_FILE_OBJECT,
            SE_SERVICE,
            SE_PRINTER,
            SE_REGISTRY_KEY,
            SE_LMSHARE,
            SE_KERNEL_OBJECT,
            SE_WINDOW_OBJECT,
            SE_DS_OBJECT,
            SE_DS_OBJECT_ALL,
            SE_PROVIDER_DEFINED_OBJECT,
            SE_WMIGUID_OBJECT,
            SE_REGISTRY_WOW64_32KEY
        }



        [DllImport("advapi32.dll", EntryPoint = "ConvertStringSecurityDescriptorToSecurityDescriptorW")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern Boolean ConvertStringSecurityDescriptorToSecurityDescriptor(
            [MarshalAs(UnmanagedType.LPWStr)] String strSecurityDescriptor,
            UInt32 sDRevision,
            ref IntPtr securityDescriptor,
            ref UInt32 securityDescriptorSize);

        [DllImport("kernel32.dll", EntryPoint = "LocalFree")]
        public static extern UInt32 LocalFree(IntPtr hMem);

        [DllImport("Advapi32.dll", EntryPoint = "SetSecurityInfo")]
        public static extern int SetSecurityInfo(SafeHandle hFileMappingObject,
                                                    SE_OBJECT_TYPE objectType,
                                                    Int32 securityInfo,
                                                    IntPtr psidOwner,
                                                    IntPtr psidGroup,
                                                    IntPtr pDacl,
                                                    IntPtr pSacl);
        [DllImport("advapi32.dll", EntryPoint = "GetSecurityDescriptorSacl")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern Boolean GetSecurityDescriptorSacl(
            IntPtr pSecurityDescriptor,
            out IntPtr lpbSaclPresent,
            out IntPtr pSacl,
            out IntPtr lpbSaclDefaulted);
    }

    public class InterProcessSecurity
    {

        public static void SetLowIntegrityLevel(SafeHandle hObject)
        {
            IntPtr pSD = IntPtr.Zero;
            IntPtr pSacl;
            IntPtr lpbSaclPresent;
            IntPtr lpbSaclDefaulted;
            uint securityDescriptorSize = 0;

            if (NativeMethods.ConvertStringSecurityDescriptorToSecurityDescriptor(NativeMethods.LOW_INTEGRITY_SSL_SACL, 1, ref pSD, ref securityDescriptorSize))
            {
                if (NativeMethods.GetSecurityDescriptorSacl(pSD, out lpbSaclPresent, out pSacl, out lpbSaclDefaulted))
                {
                    var err = NativeMethods.SetSecurityInfo(hObject,
                                                  NativeMethods.SE_OBJECT_TYPE.SE_KERNEL_OBJECT,
                                                  NativeMethods.LABEL_SECURITY_INFORMATION,
                                                  IntPtr.Zero,
                                                  IntPtr.Zero,
                                                  IntPtr.Zero,
                                                  pSacl);
                    if (err != NativeMethods.ERROR_SUCCESS)
                    {
                        throw new Win32Exception(err);
                    }
                }
                NativeMethods.LocalFree(pSD);
            }
        }
    }
}
"@



Add-Type -Language CSharp @"
using System.IO.MemoryMappedFiles;
using System.Security.AccessControl;
using System.Security.Principal;

namespace ipc
{
    public static class Security
    {
        public static MemoryMappedFileSecurity GetMmfSec() 
        {
            var security = new MemoryMappedFileSecurity();
            security.AddAccessRule(new System.Security.AccessControl.AccessRule<MemoryMappedFileRights>(new SecurityIdentifier(WellKnownSidType.WorldSid, null), MemoryMappedFileRights.FullControl, AccessControlType.Allow));
            security.AddAccessRule(new System.Security.AccessControl.AccessRule<MemoryMappedFileRights>(new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null), MemoryMappedFileRights.FullControl, AccessControlType.Allow));
            security.AddAccessRule(new System.Security.AccessControl.AccessRule<MemoryMappedFileRights>(new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null), MemoryMappedFileRights.FullControl, AccessControlType.Allow));
            return security;
        }

        public static SemaphoreSecurity GetSemaphoreSec()
        {
            var security = new SemaphoreSecurity();
            security.AddAccessRule(new System.Security.AccessControl.SemaphoreAccessRule(new SecurityIdentifier(WellKnownSidType.WorldSid, null), SemaphoreRights.FullControl, AccessControlType.Allow));
            security.AddAccessRule(new System.Security.AccessControl.SemaphoreAccessRule(new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null), SemaphoreRights.FullControl, AccessControlType.Allow));
            security.AddAccessRule(new System.Security.AccessControl.SemaphoreAccessRule(new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null), SemaphoreRights.FullControl, AccessControlType.Allow));
            return security;
        }
    }
}
"@

## Get the ACLs to set in the constructors for the MMF and Semaphore to pass to the constructor on kernel level object creation
$mmfacl = [ipc.Security]::GetMmfSec()
$semaphacl = [ipc.Security]::GetSemaphoreSec()

$semaphCreated = $false

## Create the initial Memory Mapped File and semaphore to synchronize writes to the MMF--much of this only matters when the interactive user isn't executing the server (e.g. from Service, Scheduled Task, Startup Script, etc.)
try {
    $mmf = [System.IO.MemoryMappedFiles.MemoryMappedFile]::CreateOrOpen('LinkMon', 10MB, [System.IO.MemoryMappedFiles.MemoryMappedFileAccess]::ReadWrite, [System.IO.MemoryMappedFiles.MemoryMappedFileOptions]::None, $mmfacl, [System.IO.HandleInheritability]::Inheritable)

    ## Set the server process integrity level to low
    [Native.InterProcessSecurity]::SetLowIntegrityLevel($mmf.SafeMemoryMappedFileHandle)

    ## Create a sempahore that's used to temporaily block access to the Memory Mapped File
    $MmfSemaphore = [System.Threading.Semaphore]::new(1, 1, 'LinkMonSemaphore', [ref]$semaphCreated, $semaphacl)
} catch {
    $_
    break
}

## Infinite loop to constantly send data to the MMF, test MMF contention via the Semaphore between the client and server, and see passing data between them as well
while (1) {

    ## Alternate every two seconds to send random data to the MMF to test catching it with the client
    if ([datetime]::Now.Second % 2 -eq 0) {
        $s = Get-Process | Select -First 10
    } else {
        $s = Get-Service | Select -First 10
    }
    $s | ft

    ## Convert the object to text (serialize) and get the bytes to send to the MMF -- Despite it's name, it's a buffer more-so than a file
    $j = $s | ConvertTo-Json
    $b = [System.Text.Encoding]::Ascii.GetBytes($j)

    ## Get a kernel lock via the Semaphore, and write our binary data to the MMF
    try {
        $MmfSemaphore.WaitOne()
        $MmfStream = $mmf.CreateViewStream()
        $MmfBw = [System.IO.BinaryWriter]::new($MmfStream)

        ## Write the length of the data in the first 10 bytes so the client knows how far to read into the file and then write the data
        ## Otherwise, you'll be reading the entire size of the statically assinged MMF each time
        $MmfBw.Write(([System.Text.Encoding]::Ascii.GetBytes((("$($b.Length)").PadLeft(10, '0')))))
        $MmfBw.Write($b)
        $MmfStream.Position = 0  #Reset the position of the pointer back to the top of the file so we're overwriting the old info

        ## Release the kernel level lock
        
    } catch {
        $_
    } finally {
        $MmfSemaphore.Release()
    }

    Start-Sleep -Milliseconds 500
}


$MmfSemaphore.Dispose()
$MmfBw.Dispose()
$MmfStream.Dispose()
$mmf.Dispose()


## TODO:
## One of the significant problems I've run into with the Semaphore and PowerShell if that if you close PowerShell (either the client or server) while the lock is acquired, the lock remains.
## Since there is no 'using' construct analogous to that in Csharp that auto-implements IDisposable, all Semaphore locks should have their releases in a Try/Finally block. 

Client (not so heavily documented but much is the same or in reverse of the server):

try {
    ## Notice the targeted name here is not the same:  the server targets, 'LinkMon', but since it's running as System from a Scheduled Task, it's created in the Global BaseNamedObject Kernel Namespace so it must target that new location
    ## HUGE thanks to the Sysinternals team--I used WinObj.exe a lot to see the different conditions throughout all of this
    $mmf = [System.IO.MemoryMappedFiles.MemoryMappedFile]::OpenExisting('Global\LinkMon')

    $MmfSemaphore = [System.Threading.Semaphore]::OpenExisting('Global\LinkMonSemaphore')
} catch {
    $_
    break
}

while (1) {
    try {
        $MmfSemaphore.WaitOne()

        $MmfStream = $mmf.CreateViewStream()
        $MmfBr = [System.IO.BinaryReader]::new($MmfStream)

        $DataLength = ([int]([System.Text.Encoding]::ASCII.GetString($MmfBr.ReadBytes(10))))
        $Payload = $MmfBr.ReadBytes($DataLength)
        $MmfStream.Position = 0  

        $o = [System.Text.Encoding]::Ascii.GetString($Payload)
    } catch {
        $_
    } finally {
        $MmfSemaphore.Release()
    }

    $o | ft
    Start-Sleep -Milliseconds 500
}

$MmfSemaphore.Dispose()
$MmfBr.Dispose()
$MmfStream.Dispose()
$mmf.Dispose()

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM