.NET 内存映射文件和任务计划程序,当 运行 来自任务时,客户端无法访问服务器创建的 MMF

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

我正在测试使用内存映射文件。我有一个写入命名 MMF 的服务器和一个从同名 MMF 读取并使用命名信号量防止同时访问的客户端。

当我运行客户端和服务器交互时,我可以从服务器->客户端传递数据。

当我 运行 来自计划任务的服务器并将其配置为仅 运行 当用户登录时(我是 运行 宁作为和已登录)和 运行 客户端交互,它也有效

当我从计划任务 运行 服务器并将其设置为 运行 时,无论用户是否登录或我实际需要的上下文 (SYSTEM),我的客户端都无法再看到MMF 已创建(即使我的 log/error 陷阱确认它是)。

我似乎遗漏了计划任务设置中的一些环境更改,这些更改阻止了我的 MMF 被读取。任何人都可以提供任何帮助吗?

编辑:示例代码来说明问题。

服务器:

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()

客户:

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()

编辑2:

基于查看一些似乎有类似问题的 C++ 和 C# 实现,我尝试修改 MMF 的安全性但无济于事。这是新代码,情况仍然存在:当我 运行 客户端和服务器在两个不同的进程中交互时,它们可以来回发送数据。当我 运行 计划任务中的服务器作为系统时,我无法读取根据我的调试日志成功创建的 MMF。当我 运行 PowerShell 正常或在提升的上下文中时,客户端收到的错误是:使用“1”参数调用“OpenExisting”的异常:“不存在给定名称的句柄。”

服务器:

$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()
}

客户:

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()

编辑 3:

因此,在我注意到对象管理器中的内存 'Section' 对象四处移动后,我经历了一系列迭代。我想看看这个位置是否看起来很重要,或者它是否在不同的情况下在对象上标记了不同的 ACL(它确实四处移动并以不同的 ACL 结束)。这是我已经完成的迭代矩阵,如果这有助于任何人帮助我解决此问题:

编辑4:

我也开始向 MMF 添加 Everyone, Full Control 并尝试迭代无济于事。

我开始关注流程完整性。虽然这似乎是一项富有成效的努力,但当服务器从 powershell_ise 交互地 运行 宁作为 SYSTEM 时(使用 psexec 生成 powershell_ise 作为 SYSTEM 的实例),我仍然可以拥有我的客户端(非提升)从 MMF 读取数据。但是,当 运行从计划任务中以 SYSTEM 身份运行时,进程仍然是 运行 'System' 完整性级别,但我仍然无法访问该文件。

所以...我明白了。够疯狂的,它是我所看到的所有不同事物的组合。当服务器不是从我的登录用户(通过 psexec)而是通过本地策略 > 启动脚本或计划任务 运行 作为系统执行时作为系统执行时:

  1. ...通过 Windows 完整性隔离
  2. 隔离
  3. ...它在 \BaseNamedObjects 内核命名空间而不是 \Sessions\BaseNamedObjects 中动态分配,就像作为交互式用户执行时那样(因此您需要更新您的名称定位)
  4. ...它被标记为一个新的且非常有限的 ACL
  5. 这发生在所有内核级对象上——所以我必须在我的 MemoryMappedFile 和我的 Semaphore 上考虑这些因素

因此针对上述几点进行了以下修改:

  1. 在运行时,服务器调整 SACL 以允许低完整性进程访问
  2. 当服务器只定位名称 'LinkMon' 时,客户端必须定位 'Global\LinkMon' 才能正确定位 MMF 内存部分的新位置
  3. 服务器使用构造函数创建了 MMF 和信号量,这些构造函数设置了明确的 ACL 以允许访问(最终是每个人,完全控制)——我还没有玩过,看看我能把它限制到什么程度仍然允许访问。我可能会尝试将其进一步限制为 Authenticated Users\Read 之类的内容,但我需要休息一下。

最后,这是允许服务器将数据写入 MMF 和标准用户 运行 的最终测试客户端和服务器(记住服务器 运行 作为计划任务中的系统)要从 MMF 读取的客户端:

服务器(大量内联注释):

## Credits for Standing on the Shoulders of Giants:  ##
## 
## 
## 
## https://blog.didierstevens.com/2010/09/07/integrity-levels-and-dll-injection/
## 
## 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. 

客户端(记录不多,但很多与服务器相同或相反):

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()