Update download locationAnswered

Hi,

I’m new to this forum, so sorry if I duplicate an issue.

I want to update a program without using Administrative elevation, but I’m having trouble when it comes to downloading the files from the web server (the wyUpdate program opens fine without elevation), but after clicking the button to update, I get the elevation request.

Is there any way I can force a particular location? If not, I don’t mind granting the user account with access to the default download location (therefore not requiring Admin elevation), but I don’t know where this location is. I’m open to any ideas!

Any help would be appreciated!

Regards,

Martin.

Answer

If wyUpdate is in a folder that requires elevated permissions to install files, then it will ask for those permissions to install files.

Yes, that is what it’s doing, but the users don’t have these details (for security reasons) that’s why I’m trying to avoid this situation. If I know the download location, I can give the users read/write permission to that folder (via domain policies) and the problem is resolved.

I just need to know the physical location where it is trying to download or force a specific location where the user has these permissions.

Thanks,

Martin

ago

The download location doesn't matter. The installation location matters.

ago

I’m not too sure I understand. If the users (without Admin privileges) does not have read/write permission to the download location, would that not be the reason the wyUpdate is requesting elevation privileges?

The goal is that I need to update an application WITHOUT elevation privileges. The only reason elevation is being requested is that some process within wyUpdate is using something that the user does not have rights granted for. I’m supposing it is the download location, but I can’t be sure.

Your help would be appreciated.

ago

Wyatt,

Could you please clarify me last question? 
Thanks.

ago
Answer

I finally got around to looking into this issue, and discovered that there are a few problems:

1. Localized versiones of Windows use different names for common roles (Administrators in Spanish is Administradores) and these were no be picked up.

Once this was resolved (using SID's), I saw that inherited permissions were no being included for Administrators. This is ‘by design’ by Microsoft.

The Problem: When a user who is a member of the Administrators group runs the application without elevation (UAC filtered token), the Administrators group is completely removed from the current token's groups collection. So IsInRole() and checking identity.Groups both returned false for Administrators, even though the user could elevate.


The Solution: We added P/Invoke calls to GetTokenInformation(IntPtr, int, IntPtr, uint, out uint) with TokenLinkedToken to access the linked token (the full admin token that would be active after elevation). By checking if the Administrators SID exists in the linked token's groups, we can correctly determine that:
1.    The user is an Administrator (member of the group)
2.    They can elevate to get full admin rights
3.    They will have permissions to the folder after elevation
This allows HaveFolderPermissions() to correctly return true for folders that Administrators have access to, even when the app is running non-elevated, which prevents unnecessary elevation prompts when the user can actually write to the folder (or will be able to after a single elevation).

I can upload the affected file (frmMain.UserElevation.cs) if anyone is interested.

ago
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;
using Microsoft.Win32;
using wyUpdate.Common;

namespace wyUpdate
{
  public partial class frmMain
  {
    [DllImport("advapi32.dll", SetLastError = true)]
    private static extern bool GetTokenInformation(
      IntPtr TokenHandle,
      int TokenInformationClass,
      IntPtr TokenInformation,
      uint TokenInformationLength,
      out uint ReturnLength);

    [DllImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool CloseHandle(IntPtr hObject);

    private const int TokenLinkedToken = 19;

    //mjh
    private void StartSelfElevated()
    {
      ProcessStartInfo psi = new ProcessStartInfo
      {
        ErrorDialog = true,
        ErrorDialogParentHandle = Handle
      };
      if(SelfUpdateState == SelfUpdateState.WillUpdate)
      {
        //create the filename for the newly copied client
        psi.FileName = Path.Combine(tempDirectory, Path.GetFileName(VersionTools.SelfLocation));
        //copy self to the temp folder
        File.Copy(VersionTools.SelfLocation, psi.FileName, true);
      }
      else if(SelfUpdateState == SelfUpdateState.FullUpdate)
      {
        //launch the newly updated self
        psi.FileName = oldSelfLocation;
      }
      else if(isAutoUpdateMode)
      {
        psi.FileName = IsNewSelf ? newSelfLocation : oldSelfLocation;
        // oldSelfLocation is null when elevation is needed, but no self update is taking place
        if(string.IsNullOrEmpty(psi.FileName))
        {
          psi.FileName = VersionTools.SelfLocation;
        }
      }
      else
      {
        psi.FileName = VersionTools.SelfLocation;
      }
      if(needElevation)
      {
        // elevate to administrator
        psi.Verb = "runas";
        // check if UAC is enabled on the computer, if not throw an error
        bool canUAC = false;
        try
        {
          if(VistaTools.AtLeastVista())
          {
            using(RegistryKey key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System"))
            {
              // see if UAC is enabled
              if(key != null)
              {
                canUAC = (int)key.GetValue("EnableLUA", null) != 0;
              }
            }
          }
          else
          {
            // Assume XP / 2000 limited user can use the RunAs box
            // this may or may not be true.
            //TODO: make this more robust
            // see: http://www.windowsnetworking.com/kbase/WindowsTips/Windows2003/AdminTips/Admin/DisablingtheRunAsCommand.html
            canUAC = true;
          }
        }
        catch { }
        if(!canUAC)
        {
          //TODO: use a more specific error to tell the user
          // to re-enable UAC on their computer.
          error = clientLang.AdminError;
          ShowFrame(Frame.Error);
          return;
        }
      }
      try
      {
        string selfUpdatePath = Path.Combine(tempDirectory, "selfUpdate.sup");
        // write necessary info (base/temp dirs, new client files, etc.) to a file
        SaveSelfUpdateData(selfUpdatePath);
        psi.Arguments = $"-supdf:\"{selfUpdatePath}\"";
        if(IsNewSelf)
        {
          psi.Arguments += " /ns";
        }
        Process.Start(psi);
        Close();
      }
      catch(Exception ex)
      {
        // the process couldn't be started. This happens for 1 of 3 reasons:
        // 1. The user cancelled the UAC box
        // 2. The limited user tried to elevate to an Admin that has a blank password
        // 3. The limited user tries to elevate as a Guest account
        error = clientLang.AdminError;
        errorDetails = ex.Message;
        ShowFrame(Frame.Error);
      }
    }

    private bool NeedElevationToUpdate()
    {
      //mjh
      // if only updating local user files, no elevation is needed
      if(IsAdmin || OnlyUpdatingLocalUser())
      {
        return false;
      }
      // UAC Shield on next button for Windows Vista+
      if(VistaTools.AtLeastVista())
      {
        VistaTools.SetButtonShield(btnNext, true);
      }
      return true;
    }

    /// <summary>
    /// Checks if a SID is present in the current user's token, even if disabled (for UAC filtered tokens).
    /// Checks the linked token (full admin token) if available.
    /// </summary>
    private static bool IsSidInToken(SecurityIdentifier sid)
    {
      try
      {
        WindowsIdentity identity = WindowsIdentity.GetCurrent();
        
        // First check if actively in role (enabled groups)
        WindowsPrincipal principal = new WindowsPrincipal(identity);
        if(principal.IsInRole(sid))
        {
          return true;
        }

        // Check all groups in current token
        if(identity.Groups != null)
        {
          foreach(IdentityReference group in identity.Groups)
          {
            if(group.Value.Equals(sid.Value, StringComparison.OrdinalIgnoreCase))
            {
              return true;
            }
          }
        }

        // Also check the user SID itself (not just groups)
        if(identity.User != null && identity.User.Value.Equals(sid.Value, StringComparison.OrdinalIgnoreCase))
        {
          return true;
        }

        // If UAC is enabled and we have a linked token (full admin token), check that too
        if(VistaTools.AtLeastVista())
        {
          IntPtr hToken = identity.Token;
          IntPtr linkedToken = IntPtr.Zero;
          try
          {
            uint size = (uint)IntPtr.Size;
            IntPtr pLinkedToken = Marshal.AllocHGlobal(IntPtr.Size);
            try
            {
              if(GetTokenInformation(hToken, TokenLinkedToken, pLinkedToken, size, out size))
              {
                linkedToken = Marshal.ReadIntPtr(pLinkedToken);
                if(linkedToken != IntPtr.Zero)
                {
                  using(WindowsIdentity linkedIdentity = new WindowsIdentity(linkedToken))
                  {
                    // Check groups in the linked (elevated) token
                    if(linkedIdentity.Groups != null)
                    {
                      foreach(IdentityReference group in linkedIdentity.Groups)
                      {
                        if(group.Value.Equals(sid.Value, StringComparison.OrdinalIgnoreCase))
                        {
                          return true;
                        }
                      }
                    }
                  }
                }
              }
            }
            finally
            {
              Marshal.FreeHGlobal(pLinkedToken);
              if(linkedToken != IntPtr.Zero)
              {
                CloseHandle(linkedToken);
              }
            }
          }
          catch
          {
            // Ignore errors accessing linked token
          }
        }

        return false;
      }
      catch
      {
        return false;
      }
    }

    /// <summary>
    /// Checks if the current process has the necessary permissions to a folder. This allows "custom" elevation of
    /// limited users -- allowing them control over normally restricted folders.
    /// </summary>
    /// <param name="folder">The full path to the folder to check.</param>
    /// <returns>True if the user has permission, false if not.</returns>
    private static bool HaveFolderPermissions(string folder)
    {
      try
      {
        const FileSystemRights RightsNeeded = FileSystemRights.Traverse |
                                      FileSystemRights.DeleteSubdirectoriesAndFiles |
                                      FileSystemRights.ListDirectory | FileSystemRights.CreateFiles |
                                      FileSystemRights.CreateDirectories |
                                      FileSystemRights.Modify; //Read, ExecuteFile, Write, Delete
        FileSystemSecurity security = Directory.GetAccessControl(folder);
        AuthorizationRuleCollection rules = security.GetAccessRules(true, true, typeof(NTAccount));
        WindowsPrincipal currentuser = new WindowsPrincipal(WindowsIdentity.GetCurrent());
        FileSystemRights RightsHave = 0;
        FileSystemRights RightsDontHave = 0;
        foreach(FileSystemAccessRule rule in rules)
        {
          // First check to see if the current user is even in the role, if not, skip
          SecurityIdentifier sid;
          if(rule.IdentityReference.Value.StartsWith("S-1-"))
          {
            sid = new SecurityIdentifier(rule.IdentityReference.Value);
          }
          else
          {
            // Convert NTAccount to SID to avoid locale/string matching issues
            NTAccount ntAccount = new NTAccount(rule.IdentityReference.Value);
            try
            {
              sid = (SecurityIdentifier)ntAccount.Translate(typeof(SecurityIdentifier));
            }
            catch
            {
              // If translation fails, skip this rule
              continue;
            }
          }

          // Check if SID is in token (including disabled groups for UAC filtered tokens)
          if(!IsSidInToken(sid))
          {
            continue;
          }

          if(rule.AccessControlType == AccessControlType.Deny)
          {
            RightsDontHave |= (rule.FileSystemRights & FileSystemRights.FullControl);
          }
          else
          {
            RightsHave |= (rule.FileSystemRights & FileSystemRights.FullControl);
          }
        }
        // exclude "RightsDontHave"
        RightsHave &= ~RightsDontHave;
        //Note: We're "XOR"ing with RightsNeeded to eliminate permissions that
        //      "RightsHave" and "RightsNeeded" have in common. Then we're
        //      ANDing that result with RightsNeeded to get permissions in
        //      "RightsNeeded" that are missing from "RightsHave". The result
        //      should be 0 if the user has RightsNeeded over the folder (even
        //      if "RightsHave" has flags that aren't present in the 
        //      "RightsNeeded" -- which can happen because "RightsNeeded" isn't
        //      *every* possible flag).
        // Check if the user has full control over the folder.
        return ((RightsHave ^ RightsNeeded) & RightsNeeded) == 0;
      }
      catch
      {
        return false;
      }
    }

    //TODO: Expand this function to allow for artificially elevated limited users.
    //      For example, a limited user given the permission to write to HKLM
    //      (something that is normally forbidden).
    private bool OnlyUpdatingLocalUser()
    {
      // if installing
      //         - system folders
      //         - non-user registry
      //         - Windows Services
      //         - COM files
      // then return false
      // Also note how we're excluding the "BaseDir".
      // This is because the base directory may or may not be in the userprofile
      // directory (or otherwise writable by the user), thus it needs a separate check.
      // Ditto for the "client.wyc" file.
      if(((updateFrom.InstallingTo | InstallingTo.BaseDir) ^ InstallingTo.BaseDir) != 0)
      {
        return false;
      }
      string userProfileFolder = SystemFolders.GetUserProfile();
      // if the basedir isn't in the userprofile folder (C:\Users\UserName)
      // OR
      // if the folder isn't in the full control of the current user
      // THEN
      // we're not "only updating the local user
      if((updateFrom.InstallingTo & InstallingTo.BaseDir) != 0 && !(SystemFolders.IsDirInDir(userProfileFolder, baseDirectory) || HaveFolderPermissions(baseDirectory)))
      {
        return false;
      }
      // if the client data file isn't in the userprofile folder (or otherwise writable)
      // then bail out.
      if(!(SystemFolders.IsFileInDirectory(userProfileFolder, clientFileLoc) || HaveFolderPermissions(Path.GetDirectoryName(clientFileLoc))))
      {
        return false;
      }
      // when self-updating, if this client isn't in the userprofile folder
      if((SelfUpdateState == SelfUpdateState.WillUpdate || SelfUpdateState == SelfUpdateState.FullUpdate || SelfUpdateState == SelfUpdateState.Extracted)
        && !(SystemFolders.IsFileInDirectory(userProfileFolder, VersionTools.SelfLocation)
        || HaveFolderPermissions(Path.GetDirectoryName(VersionTools.SelfLocation))))
      {
        return false;
      }
      return true;
    }
  }
}
ago