[Solved] Creating named pipes with non-default permissions

Just starting out? Need help? Post your questions and find answers here.
mikejs
Enthusiast
Enthusiast
Posts: 175
Joined: Thu Oct 21, 2010 9:46 pm

[Solved] Creating named pipes with non-default permissions

Post by mikejs »

Hi all,

I'm currently working on a project that involves multiple components, one of which is a windows service, and some of which run as the user.

To communicate between these, I've been assuming I could use named pipes and had done quite a bit of testing, proof of concept stuff.

Now that I get to the point where I actually have the service running, I run into a problem that my components running in user space can't talk to it - they get an access denied when trying to connect to the pipe. This is clearly a permissions problem somewhere - if I run the client side with elevation (i.e. run as administrator), it all starts working again.

Up to now, I've been creating the named pipe like this:

Code: Select all

handle=CreateNamedPipe_("\\.\pipe\MyPipeName", 
                        #PIPE_ACCESS_DUPLEX | #FILE_FLAG_FIRST_PIPE_INSTANCE, 
                        #PIPE_TYPE_MESSAGE | #PIPE_READMODE_MESSAGE | #PIPE_REJECT_REMOTE_CLIENTS,
                        1, 
                        65536, 
                        65536, 
                        0, 
                        #Null)
The relevant bit is that last #Null in the place of a pointer to a SECURITY_ATTRIBUTES structure.

According to the docs (https://docs.microsoft.com/en-us/window ... namedpipea):
If lpSecurityAttributes is NULL, the named pipe gets a default security descriptor and the handle cannot be inherited. The ACLs in the default security descriptor for a named pipe grant full control to the LocalSystem account, administrators, and the creator owner. They also grant read access to members of the Everyone group and the anonymous account.
That's clearly not enough, and for non-elevated programs to access it, I need to grant more rights to the Everyone group. When the client connects to the pipe, it uses CallNamedPipe_, which requires GENERIC_READ and GENERIC_WRITE.

So, I need to construct one of these somehow, but the API for doing so looks impenetrable to me. It looks many layers deep where I have to have a SECURITY_ATTRIBUTES structure that includes pointers to other kinds of structure which in turn include pointers to more structures.

Does anyone happen to have reasonably straightforward existing PB code for assembling one of these structures? It's an area of the Windows API I've managed to steer clear of up to now...

The permissions themselves do not need to be complicated - I just need to allow any process to connect to the pipe and send/receive messages.
Last edited by mikejs on Mon Sep 13, 2021 11:35 am, edited 1 time in total.
firace
Addict
Addict
Posts: 946
Joined: Wed Nov 09, 2011 8:58 am

Re: Creating named pipes with non-default permissions

Post by firace »

Does this work for you:

Code: Select all


sa\nLength =SizeOf(SECURITY_ATTRIBUTES) 
sa\bInheritHandle = 0
sa\lpSecurityDescriptor = 0


handle=CreateNamedPipe_("\\.\pipe\MyPipeName", 
                        #PIPE_ACCESS_DUPLEX | #FILE_FLAG_FIRST_PIPE_INSTANCE, 
                        #PIPE_TYPE_MESSAGE | #PIPE_READMODE_MESSAGE | #PIPE_REJECT_REMOTE_CLIENTS,
                        1, 
                        65536, 
                        65536, 
                        0, 
                        @sa)
mikejs
Enthusiast
Enthusiast
Posts: 175
Joined: Thu Oct 21, 2010 9:46 pm

Re: Creating named pipes with non-default permissions

Post by mikejs »

Hi,

That's part of it, but the key thing is that default permissions aren't enough - I need to provide a real security descriptor with explicit permissions in it.

Having played around with this for a while, I've got something that seems to work, so posting it here in case it helps anyone else. This thread was very useful in getting started:

viewtopic.php?f=12&t=54341&p=410919

It covers the process of initialising a security descriptor and dacl, although the dacl it creates is purposefully empty and I need to put things in it.

As I understand it, the way this works is:
  • To create a named pipe with custom permissions, I have to provide a SECURITY_ATTRIBUTES structure.
  • SECURITY_ATTRIBUTES contains a pointer to a SECURITY_DESCRIPTOR
  • SECURITY_DESCRIPTOR contains a pointer to a DACL
  • DACL is a kind of ACL
  • ACL is a compound structure comprising an ACL header and then one or more ACEs which can be of various types.
  • For what I want to do, an ACCESS_ALLOWED_ACE is the relevant type.
All the structure definitions seem to be known to PB out of the box. Likewise, most of the functions I need to call and the constants I need to use are also known by default - one exception was the CreateWellKnownSID call and a couple of the constants for that, which I was able to get from MS docs. This means we don't need to get too involved with how the ACL is structured.

Code: Select all

Prototype.i p_CreateWellKnownSID(sidtype.l,
                                 *domainsid,
                                 *psid,
                                 cbsid.l)
Global.i lib_advapi32
Global CreateWellKnownSID.p_CreateWellKnownSID

; Constants we don't get for free
; From here: https://docs.microsoft.com/en-us/windows/win32/api/winnt/ne-winnt-well_known_sid_type
#WinWorldSid=1
#WinLocalSystemSid=22

Procedure.i LoadADVAPI32()
  Protected out.i, func$, func.i
  out=#True
  lib_advapi32=OpenLibrary(#PB_Any,"C:\Windows\System32\ADVAPI32.DLL")
  If lib_advapi32
    func$="CreateWellKnownSid"
    func=GetFunction(lib_advapi32, func$)
    If func
      CreateWellKnownSID.p_CreateWellKnownSID=func
    Else
      Debug("failed to get function from advapi32 library: "+func$)
      out=#False
    EndIf
  Else
    Debug("Failed to open advapi32 library.")
    out=#False
  EndIf
  ProcedureReturn out
EndProcedure

Procedure CreateMyNamedPipe()
  Protected *sa.SECURITY_ATTRIBUTES
  Protected *sd.SECURITY_DESCRIPTOR
  Protected *dacl.ACL
  Protected *sid
  Protected.l rightsmask
  Protected.l sidsize
  Protected.i handle, result
  
  ; Create a security attributes structure
  *sa=AllocateMemory(SizeOf(SECURITY_ATTRIBUTES))
  ; Set the length
  *sa\nLength=SizeOf(SECURITY_ATTRIBUTES)
  
  ; Create a security descriptor
  *sd=AllocateMemory(#SECURITY_DESCRIPTOR_MIN_LENGTH)
  
  ; Store the descriptor pointer on the sa structure
  *sa\lpSecurityDescriptor=*sd
  
  ; Init the security descriptor
  result=InitializeSecurityDescriptor_(*sd, #SECURITY_DESCRIPTOR_REVISION)
  Debug("InitializeSecurityDescriptor result: "+Str(result))
  
  ; Create and init the dacl. Size unknown - 4096 is a guess and seems plenty
  *dacl=AllocateMemory(4096)
  result=InitializeAcl_(*dacl, 4096, #ACL_REVISION2)
  Debug("InitializeACL result: "+Str(result))
  
  ; Permissions for System - all access.
  *sid=AllocateMemory(4096)
  sidsize=4096
  ; WinLocalSystemSid seems to be the same as NT AUTHORITY\SYSTEM
  result=CreateWellKnownSID(#WinLocalSystemSid, #Null, *sid, @sidsize)
  Debug("CreateWellKnownSID result: "+Str(result))
  ; the above call has updated sidsize with the size it actually used. Need to reset to use again below
  sidsize=4096
  
  ; Add the permssion to the dacl
  result=AddAccessAllowedAce_(*dacl, #ACL_REVISION2, #FILE_ALL_ACCESS, *sid)
  Debug("AddAccessAllowedAce result: "+Str(result))
  
  ; Permissions for Everyone group.

  ; Rights mask needed for everyone access.
  rightsmask=#FILE_LIST_DIRECTORY |
             #FILE_READ_ATTRIBUTES |
             #FILE_READ_DATA |
             #FILE_READ_EA |
             #SYNCHRONIZE |
             #READ_CONTROL |
             #FILE_WRITE_DATA |
             #FILE_WRITE_ATTRIBUTES |
             #FILE_WRITE_EA

  FillMemory(*sid, 4096)
  ; WinWorldSid is Everyone group.
  result=CreateWellKnownSID(#WinWorldSid, #Null, *sid, @sidsize)
  Debug("CreateWellKnownSID result: "+Str(result))
  result=AddAccessAllowedAce_(*dacl, #ACL_REVISION2, rightsmask, *sid)
  Debug("AddAccessAllowedAce result: "+Str(result))
  
  result=SetSecurityDescriptorDacl_(*sd, #True, *dacl, #False)
  Debug("SetSecurityDescriptorDacl result: "+Str(result))
  
  ; Can now create the pipe
  handle=CreateNamedPipe_("\\.\pipe\MyPipeName", 
                          #PIPE_ACCESS_DUPLEX | #FILE_FLAG_FIRST_PIPE_INSTANCE, 
                          #PIPE_TYPE_MESSAGE | #PIPE_READMODE_MESSAGE | #PIPE_REJECT_REMOTE_CLIENTS,
                          1, 
                          65536, 
                          65536, 
                          0, 
                          *sa)
                          
  ; Can now free things
  FreeMemory(*dacl)
  FreeMemory(*sd)
  FreeMemory(*sa)

  ; ... rest of code.

 EndProcedure
 
 CreateMyNamedPipe()
 
The above is hacked around a bit from the code I'm actually using, and doesn't do any real error detection (if anything goes wrong it carries on regardless). But I think it covers everything that needs doing.

The other key to solving this was the AccessChk utility from SysInternals (https://docs.microsoft.com/en-us/sysint ... /accesschk). That allowed me to look at what the default permissions were on a named pipe. That let me see that SYSTEM was being given FILE_ALL_ACCESS, and the above permissions for Everyone are the default set plus the WRITE versions of all the READ permissions it already had.
Post Reply