Monday, February 8, 2010

COM Elevation: Make Your Program UAC-Aware

Whether you're a student, hobbyist, or serious developer this tutorial will leave you with a great understanding of how to implement User Account Control elevations in your application. If you have ever used an operating system from Vista and above you are probrably familiar with that shield icon on Windows dialogs and even on some of the daily applications you may use. The tutorial will show you how applications can be UAC aware.
 

 
 


What is COM Elevation?
The simplest way to explain it would be executing  code that runs in the context of an administrator. If you need to perform a few tasks that require administrative rights then use COM elevation. It's not neccesary to manifest your applications if your application doesn't need administrative rights "all the time". 

The tutorial uses Visual Studio 2008 Professional and the .NET 3.5 framework.
You can find the complete project solution template at the end of this article.  You may use it as a building block for adding COM elevations to your applications.

1
Creating the project

On the Visual Studio 2008 IDE [menu] choose File > New > Project...
Choose from the [Add New Project] dialog Visual Basic > Windows > Windows Forms Application
Name the project: MyApplication

 
 
Windows-Form-Application
211829
 


Locate the solution explorer window and select MyApplication project.
Right click and choose from the [menu] Add > New Item...
Choose from the [Add New Item] dialog Common items > Code > Module
Name the module: Elevation

 
Elevation Module
211828
 


Locate the solution explorer window and select the Solution 'My Application'
Right click and choose from the [menu] Add > New > Project...
Choose from the [Add New Project] dialog Visual Basic > Windows > Class Library
Name the project: COMAdmin

 
COMAdmin
211830
 


Under the COMAdmin project class1.vb is created by default. Right click and choose Delete.
Locate the solution explorer window and select COMAdmin project.
Right click and choose from the [menu] Add > New Item...
Choose from the [Add New Item] dialog Common items > Code > COM Class
Name the COM Class: UACAdmin

 
UACAdmin
211831
 



Locate the solution explorer window and select COMAdmin project.
Right click and choose from the [menu] Add > New Item...
Choose from the [Add New Item] dialog Common items > Code > Module
Name the module: UACRegister

 
UACRegister
211834
 


Locate the solution explorer window and select COMAdmin project.
Right click and choose from the [menu] Add > New Item...
Choose from the [Add New Item] dialog Common items > General > Installer Class
Name the class: UACAdminInstaller

 
 
UACAdminInstaller
211835
 


  • From the COMAdmin project do the following:


Right click UACAdminInstaller and choose View Code
Add the following source code to UACAdminInstaller.vb
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
Imports System.ComponentModel
Imports System.Configuration.Install
Imports System.Runtime.InteropServices

Public Class uacAdminInstaller
Inherits Installer

Public Sub New()
MyBase.New()
InitializeComponent()
End Sub

_
Public Overloads Overrides Sub Install(ByVal stateSaver As System.Collections.IDictionary)
MyBase.Install(stateSaver)
Dim RegServices As New RegistrationServices
RegServices.RegisterAssembly(MyBase.GetType().Assembly, AssemblyRegistrationFlags.SetCodeBase)
End Sub

_
Public Overloads Overrides Sub Uninstall(ByVal savedState As System.Collections.IDictionary)
MyBase.Uninstall(savedState)
Dim RegServices As New RegistrationServices
RegServices.UnregisterAssembly(MyBase.GetType().Assembly)
End Sub

End Class


Right click UACRegister and choose View Code
Add the following source code to UACRegister.vb
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
83:
84:
85:
86:
87:
88:
89:
90:
Imports System.Runtime.InteropServices
Imports System.Reflection
Imports Microsoft.Win32


Namespace UserAccountControlRegister

Module uacRegister

Public Sub UacRegisterElevations(ByVal ComClassId As String, ByVal ResourceStringId As Integer)

Dim ComAppId As String = GetExecAssemblyGuid()
Dim ComLocation As String = GetExecAssemblyLocation()
Dim ComPermission() As Byte = GetAccessPermissions()

Dim RegElevation1 As RegistryKey = Registry.ClassesRoot.OpenSubKey("CLSID\{" & ComClassId & "}", True)
RegElevation1.SetValue(ElevationStrings.AppId, "{" & ComAppId & "}", RegistryValueKind.String)
RegElevation1.SetValue(ElevationStrings.LocalizedString, "@" & ComLocation & ",-" & ResourceStringId.ToString, RegistryValueKind.String)
RegElevation1.CreateSubKey(ElevationStrings.Elevation)
RegElevation1.Close()

Dim RegElevation2 As RegistryKey = Registry.ClassesRoot.OpenSubKey("CLSID\{" & ComClassId & "}\Elevation\", True)
RegElevation2.SetValue(ElevationStrings.Enabled, 1, RegistryValueKind.DWord)
RegElevation2.Close()

Dim RegElevation3 As RegistryKey = Registry.ClassesRoot.OpenSubKey("AppID\", True)
RegElevation3.CreateSubKey("{" & ComAppId & "}")
RegElevation3.Close()

Dim RegElevation4 As RegistryKey = Registry.ClassesRoot.OpenSubKey("AppID\{" & ComAppId & "}", True)
RegElevation4.SetValue(ElevationStrings.DllSurrogate, "", RegistryValueKind.String)
RegElevation4.SetValue(ElevationStrings.AccessPermission, ComPermission, RegistryValueKind.Binary)
RegElevation4.Close()

End Sub

Friend Function GetAccessPermissions() As Byte()
Dim pSd As IntPtr
Dim pSdLength As IntPtr
Dim pSdBytes() As Byte = Nothing
Dim lpszSDDL As String = "O:BAG:BAD:(A;;0x3;;;IU)(A;;0x3;;;SY)"
If UnSafeNativeMethods.ConvertStringSecurityDescriptorToSecurityDescriptor(lpszSDDL, UnSafeNativeMethods.SDDL_REVISION_1, pSd, pSdLength) Then
ReDim pSdBytes(pSdLength)
Marshal.Copy(pSd, pSdBytes, 0, pSdLength)
UnSafeNativeMethods.LocalFree(pSd)
End If
Return pSdBytes
End Function

Friend Function GetExecAssemblyGuid() As String
Dim id As [Assembly]
Dim Attributes As Object
id = [Assembly].GetExecutingAssembly
Attributes = id.GetCustomAttributes(GetType(GuidAttribute), False)
Return (DirectCast(Attributes(0), GuidAttribute).Value).ToString
End Function

Friend Function GetExecAssemblyLocation() As String
Return [Assembly].GetExecutingAssembly().Location.ToString
End Function

End Module


End Namespace


Friend Module ElevationStrings

Friend LocalizedString As String = "LocalizedString"
Friend DllSurrogate As String = "DllSurrogate"
Friend AccessPermission As String = "AccessPermission"
Friend Elevation As String = "Elevation"
Friend Enabled As String = "Enabled"
Friend AppId As String = "AppId"

End Module


Friend Module UnSafeNativeMethods
Friend Const SDDL_REVISION_1 = 1
_
Friend Function ConvertStringSecurityDescriptorToSecurityDescriptor(<[In]()> ByVal StringSecurityDescriptor As String, <[In]()> ByVal StringSDRevision As UInt32, ByRef SecurityDescriptor As IntPtr, ByRef SecurityDescriptorSize As IntPtr) As Boolean
End Function
_
Friend Function LocalFree(<[In]()> ByVal hMem As IntPtr) As IntPtr
End Function
End Module


Right click UACAdmin and choose View Code
Add the following source code to UACAdmin.vb
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
Imports System.Runtime.InteropServices
Imports COMAdmin.UserAccountControlRegister '// see [UACRegister.vb]

_
Public Class UACAdmin

#Region "COM GUIDs"
' These GUIDs provide the COM identity for this class
' and its COM interfaces. If you change them, existing
' clients will no longer be able to access the class.
Public Const ClassId As String = "805509c5-bd55-4e70-a9b5-cf8438214ebd"
Public Const InterfaceId As String = "62314a44-0740-4afc-9849-792d37371c91"
Public Const EventsId As String = "a38756ce-6a35-4d5f-b8ac-c545dfb80266"
#End Region

' A creatable COM class must have a Public Sub New()
' with no parameters, otherwise, the class will not be
' registered in the COM registry and cannot be created
' via CreateObject.
Public Sub New()
MyBase.New()
End Sub

Public Sub Admin_CreateRootFile(ByVal szPath As String, ByVal szLineToWrite As String)

' TODO:
' Administrative task small example
Dim fs As New IO.StreamWriter(szPath)
fs.WriteLine(szLineToWrite)
fs.Close()

End Sub

End Class



Public Class ComRegisterFunction

_
Friend Shared Sub RegisterFunction(ByVal t As Type)
' TODO:
' This registers the COM Class with elevation, only need to specify
' Class ID and Resource ID. The hard part is handled in UACRegister.vb
uacRegister.UacRegisterElevations(UACAdmin.ClassId, 100)

MsgBox("RegisterFunction:[OK]" & vbCrLf & "Remove this debug message.[UACAdmin.vb]", MsgBoxStyle.SystemModal)
End Sub

_
Friend Shared Sub UnregisterFunction(ByVal t As Type)

MsgBox("UnregisterFunction:[OK]" & vbCrLf & " Remove this debug message.[UACAdmin.vb]", MsgBoxStyle.SystemModal)
End Sub

End Class


Right click COMAdmin project and choose from the [menu] > Build


  • From the MyApplication project do the following:


Right click MyApplication project and choose Properties
Locate the References tab then click on the Add.. button
Locate COMAdmin and add it as a reference to the MyApplication project.

Right click Elevation and choose View Code
Add the following source code to Elevation.vb
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
83:
84:
85:
86:
87:
88:
89:
90:
91:
92:
93:
94:
95:
96:
Imports System
Imports System.Threading
Imports System.Security.Principal
Imports System.Security.Permissions
Imports System.Runtime.InteropServices


Namespace UserAccountControl

Module Elevation

Public Function CreateElevatedComObject(ByVal comclsid As Guid, ByVal comiid As Guid) As Object
' Executes a COM method with administrative rights.
Dim comClsidDirective As String = comclsid.ToString("B") ' use format directive {GUID}
Dim bo3 As New UnsafeNativeMethods.BIND_OPTS3
Dim comObj As Object = Nothing
Dim szmoniker As String = "Elevation:Administrator!new:" & comClsidDirective
bo3.cbStruct = Marshal.SizeOf(bo3)
bo3.hwnd = IntPtr.Zero
bo3.dwClassContext = UnsafeNativeMethods.CLSCTX_LOCAL_SERVER
Try
comObj = CoGetObject(szmoniker, bo3, comiid)
Catch
comObj = Nothing
End Try

Return comObj
End Function

Public Function CreateElevatedProcess(ByVal FileName As String, ByVal param As String) As Process
' Launches a processes using RunAs verb.
Dim psi As New ProcessStartInfo()

psi.UseShellExecute = True
psi.WorkingDirectory = Environment.CurrentDirectory
psi.FileName = FileName
psi.Verb = "runas"
psi.Arguments = param
psi.ErrorDialog = True

Return Process.Start(psi)
End Function

Public Sub Button_SetElevationRequiredState(ByVal this As IntPtr, ByVal bShow As Boolean)
' Adds shield icons to buttons/links or command links
UnsafeNativeMethods.SendMessage(this, BCM_SETSHIELD, IntPtr.Zero, New IntPtr(If(bShow, 1, 0)))
End Sub

Public Function IsAdminRole() As Boolean
Dim myPrincipal As WindowsPrincipal = CType(Thread.CurrentPrincipal, WindowsPrincipal)
Dim sid As New SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, Nothing)

Return myPrincipal.IsInRole(sid)
End Function

Public Function IsElevatedOs() As Boolean
Dim v As OperatingSystem
v = Environment.OSVersion
Return If(v.Version.Major >= 6, 1, 0)
End Function

End Module

End Namespace


Friend Module UnsafeNativeMethods

Friend Const CLSCTX_LOCAL_SERVER As UInteger = &H4
Friend Const BCM_SETSHIELD As UInteger = &H160C

' Windows Vista and above structure.
_
Friend Structure BIND_OPTS3
Dim cbStruct As UInteger
Dim grfFlags As UInteger
Dim grfMode As UInteger
Dim dwTickCountDeadline As UInteger
Dim dwTrackFlags As UInteger
Dim dwClassContext As UInteger
Dim locale As UInteger
Dim pServerInfo As Object
Dim hwnd As IntPtr
End Structure

_
Friend Function CoGetObject(ByVal pszName As String, <[In]()> ByRef pBindOptions As BIND_OPTS3, <[In](), MarshalAs(UnmanagedType.LPStruct)> ByVal riid As Guid) As Object
End Function

_
Friend Function SendMessage(ByVal hWnd As IntPtr, ByVal Msg As UInt32, ByVal wParam As IntPtr, ByVal lParam As IntPtr) As IntPtr
End Function

End Module



Double click Form1, from the toolbox add a new Button to the form.(Button1) by default.
Add the following source code to Form1
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
Imports MyApplication.UserAccountControl
Imports COMAdmin

' Visual Studio 2008 .NET 3.5
' TODO: You can start building your application from here
' with user account control elevations ready to be
' used with your new application.
' NOTE: Don't forget to add a reference to [COMAdmin.dll] in
' this application project.
' NOTE: You must have full edition to take advanatage of the
' setup installer project. The Express editions don't
' have visual studio installer templates.

Public Class Form1

Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

' TODO: Example that adds a shield icon to the button
' informing the user the action requires elevated
' user rights.
Button1.FlatStyle = FlatStyle.System
Elevation.Button_SetElevationRequiredState(Button1.Handle, True)

End Sub

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

Dim clsid As Guid = New Guid(COMAdmin.UACAdmin.ClassId)
Dim iid As Guid = New Guid(COMAdmin.UACAdmin.InterfaceId)

If Elevation.IsElevatedOs Then

Dim comRef As UACAdmin._UACAdmin = Nothing ' Interface
comRef = Elevation.CreateElevatedComObject(clsid, iid)
If IsNothing(comRef) <> True Then
' admin::task
comRef.Admin_CreateRootFile("c:\uac.txt", "working")

End If

Else

If Elevation.IsAdminRole Then
' admin::task
Dim comRef As New UACAdmin
comRef.Admin_CreateRootFile("c:\uac.txt", "working")
End If

End If

End Sub

End Class


When you have finished choose from the Visual Stuido IDE [menu] Build > Build Solution.



2
Creating and Embending Win32 resource into VB.NET

There is a few things you need to do before elevation can take place. The first is that the DLL needs to have a Win32 Resource string embended inside the DLL. This resource string is what will be displayed on the UAC prompt if it is enabled.
 
uacprompt
211860
 


Locate your projects directory under windows. 
example: C:\Documents and Settings\User\My Documents\Visual Studio 2008\Projects\MyApplication
Create a new folder named: Win32Resource

Create a new text file named: resource.h
Add the following:
1:
2:
#define IDS_ELEVATION 100


Create a new text file named:ElevationPrompt.rc
Add the following:
1:
2:
3:
4:
5:
6:
7:
#include "resource.h"

STRINGTABLE
BEGIN
IDS_ELEVATION "My Application"
END


Create a new file named: ElevationPrompt.bat
Add the following:
1:
2:
rc.exe ElevationPrompt.rc


Copy RC.EXE to the same location. You may also need to copy its dependancy RCDLL.DLL
Run ElevationPrompt.bat a new file should be created named ElevationPrompt.res
 
Win32ResourceFolderContents
211840
 


Copy the file ElevationPrompt.res file into the COMAdmin project folder located in the root of your project solution.
 
ElevationPromptResource
211841
 


Under the same location right click COMAdmin.vbproj and choose Open With > Notepad
Add the following line to the member:
1:
2:
ElevationPrompt.res


 
ComAdminWin32ResourceAppend
211845
 


Save the changes to ComAdmin.vbproj . The VB.NET IDE should detect the change when presented with the dialog just click 'Reload'

When you have finished choose from the Visual Studio IDE [menu] Build > Rebuild Solution.

The steps described for adding the line to the project file are used for VB.NET projects. The VB.NET IDE doesn't have a direct option for adding true Win32 resources to projects. You must follow the steps described to embend the string resource into the DLL for VB.NET.
Note: C# projects can directly add resource files under the Properties > Application tab section.



3
Creating a Setup Installation package that automatically installs and registers the COM class


Locate the solution explorer window and select Solution 'My Application'.
Right click and choose from the [menu] Add > New > Project...
Choose from the [Add New Project] dialog Other Project Types > Setup and Deployment > Setup Project
Name the setup project: MyApplicationDeployment
 
MyApplicationDeployment
211846
 



  • From the MyApplicationDeployment project do the following:


Locate the solution explorer window and select MyApplicationDeployment project.
Right click and choose from the [menu] Add > File...
Add the COMAdmin.dll file from the dialog.
Example: C:\Documents and Settings\User\My Documents\Visual Studio 2008\Projects\MyApplication\MyApplication\bin\Debug\COMAdmin.dll

Right click and choose from the [menu] Add > File...
Add the MyApplication.exe file from the dialog.
Example: C:\Documents and Settings\User\My Documents\Visual Studio 2008\Projects\MyApplication\MyApplication\bin\Debug\MyApplication.exe

Right click and choose from the [menu] View > Custom Actions...
Right click the Install folder and choose Add Custom Action...
When the dialog appears double click the Applications Folder option.
Select the COMAdmin.dll and choose OK.

Repeat the following steps above for the Uninstall folder.
Right click the UnInstall folder  and choose Add Custom Action...
When the dialog appears double click the Applications Folder option.
Select the COMAdmin.dll and choose OK.
 
 
CustomActions
211847
 

 
 
ProjectOverview
211849
 


Right click the MyApplicationDeployment project.
Choose from the [menu] Build.

Visual Studio should have created a setup (MSI) package.

You can download the solution project template here. Everything that has been discussed in the tutorial can be located in the project.

No comments:

Post a Comment