|
. NET System Management Services Authors: Golomshtok A. Published year: 2005 Pages: 52/79 |
The designers of the System.Management namespace took a somewhat simplistic approach to enveloping the security- related capabilities of WMI. They primarily focused on exposing the functionality for managing the security settings of client processes. As a result, the System.Management security model is easy to use, but it definitely lacks support for low-level manipulation of the security settings.
Throughout all of the code examples, shown earlier in this book, I consistently and consciously neglected most of the issues related to WMI security, crudely assuming that these examples are being executed by a user with administrative privileges. Moreover, when I described the process of connecting to WMI, I barely scratched the surface and chose to rely on the ability of System.Management types to automatically establish a connection on an as-needed basis. Thus, all of the code examples used something similar to the following boilerplate code to bind to a WMI class or object:
ManagementObject mo = new ManagementObject(@"\.\root\CIMV2:Win32_Process=100"); Console.WriteLine(mo["__CLASS"]); ...
This code looks deceivingly simple and you may not realize what is really going on behind the scenes.
Before the newly constructed object can be used, a valid connection to WMI must be established and the instance of the ManagementObject type must be bound to the underlying WMI object. However, the code does not seem to be doing either of these things—instead, it simply instantiates the object and prints out its properties. Such simplicity and ease of use is afforded by the ability of the System.Management ManagementObject type to initialize itself and establish the appropriate connections in a lazy fashion. In fact, none of the type constructors issue any COM API calls, and therefore, they do not even attempt to contact WMI. Instead, when an instance of a type is first accessed, it undergoes the initialization stage. The initialization code typically checks whether the instance is already connected to WMI, and if necessary, it invokes the initialization sequence. As part of the initialization, the object of type ManagementScope is created and then its initialization code is invoked. The ManagementScope 's InitializeGuts method, briefly discussed in Chapter 2, obtains a pointer to IWbemLocator object and calls its ConnectServer method to retrieve the IWbemServices interface pointer. The IWbemLocator::ConnectServer method has the following signature:
HRESULT IWbemLocator::ConnectServer( const BSTR strNetworkResource, const BSTR strUser, const BSTR strPassword, const BSTR strLocale, LONG lSecurityFlags, const BSTR strAuthority, IWbemContext* pCtx, IWbemServices** ppNamespace );
where
strNetworkResource : A pointer to a valid BSTR that contains a WMI path to the target object.
strUser : A pointer to a valid BSTR that contains the name of the security principal to be used to establish the connection to WMI. If this parameter is set to NULL , ConnectServer will use the name of the currently logged on user. When using the user name from a domain other than the current domain, the name should be prefixed with the domain name and a double backslash separator—i.e., <domain_name>\<user_name> .
strPassword : A pointer to a valid BSTR that contains a password to be used to establish the connection to WMI. If this parameter is set to NULL , ConnectServer will use the password of the currently logged on user. An empty string is considered to be a valid password of zero length.
strLocale : A pointer to a valid BSTR that specifies the locale (i.e., MS_xxx ) to be used to retrieve the information from WMI. If this parameter is set to NULL , the current locale is used.
lSecurityFlags : Reserved for future use. Must be set to zero.
strAuthority : A pointer to a valid BSTR that contains a designation of the authentication method to be used for establishing the connection, as well as the name of the domain in which to obtain the user information for authentication. If this parameter begins with string "Kerberos:" , Kerberos authentication is used and the remainder of the string must contain a valid Kerberos principal name. If the parameter begins with "NTLMDOMAIN:" , then Windows NT Challenge/Response authentication protocol (also known as Windows NT LAN Manager or NTLM protocol) is used and the remainder of the string must contain a valid NTLM domain name. If the parameter is set to NULL , ConnectServer uses NTLM authentication and the NTLM domain of the currently logged on user. Note that the name of the domain cannot be specified simultaneously in two places (i.e., strUser and strAuthority parameters); doing so will cause ConnectServer to fail.
pCtx : A pointer to the IWbemContext object used to provide additional information to certain WMI providers. This parameter is typically set to NULL .
ppNamespace : An output parameter used to receive the pointer to a valid IWbemServices object.
As you can deduce from the method signature, it is possible to control many aspects of the connection establishment process, such as the name of the connecting user and domain as well as the authentication method. Unfortunately, the lazy initialization sequence of the ManagementObject type constructs a default instance of the ManagementScope type so that the IWbemLocator::ConnectServer method is called with most of its parameters set to NULL values. Obviously, this causes WMI to use the NTLM authentication along with the name and domain of the currently logged on user. While adequate for some of the management scenarios, this may result in access- denied errors when the current user is not granted full administrative access to the target WMI resources.
The good news is that you can override the default behavior by explicitly constructing an instance of the ManagementScope type and associating it with the ManagementObject . Take a look at the following code snippet:
ManagementScope ms = new ManagementScope(); ms.Path = new ManagementPath(@"\BCK_OFFICE\root\CIMV2"); ms.Options.Username = "Administrator"; ms.Options.Password = "password"; ManagementObject mo = new ManagementObject(@" Win32_Process=100"); mo.Scope = ms; Console.WriteLine(mo["__CLASS"]);
Here, the Path property of the newly created ManagementScope object is assigned to the instance of the ManagementPath type, which specifies the remote WMI namespace to connect to. Then, the Username and Password properties of the ConnectionOptions object, pointed to by the Options property of the ManagementScope object, are initialized to the name of the user and the password to be used for establishing a connection. Finally, the new instance of the ManagementObject type is created and associated with the ManagementScope object by setting its Scope property. When the ManagementObject instance is accessed by the WriteLine method, rather than constructing a default ManagementScope object, the initialization code of the ManagementObject type uses the ManagementScope instance associated with the object. Interestingly, in this case, all information that pertains to the WMI connection—not only the user name and the password, but also the namespace path—is always extracted from the associated ManagementScope object. Thus, the following code is incorrect and will generate an error:
ManagementScope ms = new ManagementScope(); ms.Options.Username = "Administrator"; ms.Options.Password = "password"; ManagementObject mo = new ManagementObject( @"\BCK_OFFICE\root\CIMV2:Win32_Process=100"); mo.Scope = ms; Console.WriteLine(mo["__CLASS"]);
Here, rather than setting the Path property of the ManagementScope object, we supply a full namespace and an object path to the constructor of the ManagementObject type. Therefore, the resulting ManagementScope object contains a default namespace path \.\root\CIMV2 . Upon setting the Scope property of the ManagementObject instance, the namespace path of ManagementObject essentially is overridden by the namespace path of the associated ManagementScope object. Asa result, rather than binding to the remote namespace \BCK_OFFICE\root\CIMV2 , the initialization code of the ManagementObject type binds to the local \.\root\CIMV2 namespace. For security reasons, it is not allowed to change the user credentials when it connects to a local namespace; therefore, the preceding code throws a ManagementException , complaining that user credentials cannot be used for local connections.
The coding pattern just discussed is not the only way to create a ManagementScope object and associate it with an appropriate System.Management type. One of the constructors of the ManagementObject type, for instance, accepts the instance of ManagementScope as a parameter:
ManagementScope ms = new ManagementScope(); ms.Path = new ManagementPath(@"\BCK_OFFICE\root\CIMV2"); ms.Options.Username = "Administrator"; ms.Options.Password = "password"; ManagementPath mp = new ManagementPath("Win32_Process=100"); ManagementObject mo = new ManagementObject(ms, mp, null); Console.WriteLine(mo["__CLASS"]);
Here, the ManagementScope object is the first parameter of the ManagementObject type constructor. The second parameter is an object of type ManagementPath , which points to an appropriate WMI Win32_Process object to bind to. Finally, the last parameter is an instance of ObjectGetOptions type, which, for the purposes of this discussion, may simply be ignored, hence it is set to null . Note that this code behaves similarly to the code snippet, shown earlier—in other words, the namespace path of the ManagementScope instance overrides the namespace path of the ManagementPath object. Thus, if you forget to initialize the Path property of the ManagementScope object, the code will generate an error.
Since the default ManagementScope object is constructed during the initialization of the ManagementObject unless the ManagementScope parameter is supplied to the type constructor, it is entirely possible to use the default object rather than constructing a brand new one:
ManagementObject mo = new ManagementObject( @"\BCK_OFFICE\root\CIMV2:Win32_Process=100"); mo.Scope.Options.Username = "Administrator"; mo.Scope.Options.Password = "password"; Console.WriteLine(mo["__CLASS"]);
This code is certainly more concise and, perhaps, a bit more efficient than my first example because it alleviates the need for allocating a new instance of the ManagementScope type.
Finally, you do not need to rely on the initialization code of the ManagementObject type to implicitly establish a connection to WMI. With the ManagementScope , you can request a connection explicitly:
ManagementScope ms = new ManagementScope(); ms.Path = new ManagementPath(@"\BCK_OFFICE\root\CIMV2"); ms.Options.Username = "Administrator"; ms.Options.Password = "password"; ms.Connect(); ManagementObject mo = new ManagementObject(@" Win32_Process=100"); mo.Scope = ms; Console.WriteLine(mo["__CLASS"]);
As I already mentioned, the initialization sequence of the ManagementObject type first checks if there is a valid WMI connection and then attempts to use it if such a connection exists. Here, since the ManagementScope object is already connected to WMI by the time the ManagementObject initialization code is invoked, the entire connection establishment process is bypassed and the ManagementScope 's connection is used instead. The apparent benefit of such an approach is that you can reuse a single connected ManagementScope object with multiple instances of the ManagementObject , thus achieving a marginal performance gain.
The ManagementObject type is not the only System.Management type designed to work in conjunction with ManagementScope . In fact, all System.Management types that represent WMI entities, such as ManagementClass , ManagementEventWatcher , and ManagementObjectSearcher , offer the same functionality. Thus, each of these types is equipped with a constructor that takes the ManagementScope object as a parameter and the Scope property, which exposes the associated instance of the ManagementScope type.
At the beginning of this chapter, I mentioned that in order to establish the proper security settings for a WMI session, the client process should call several security API functions such as CoInitializeSecurity and CoSetProxyBlanket . Fortunately, it is not something that a developer of the management applications has to do manually. Here again, the initialization logic of the ManagementScope type kindly takes care of setting the appropriate authentication and impersonation levels for the process. A developer however, is afforded the luxury of controlling both the authentication and the impersonation settings through the instance of the ConnectionOptions type, associated with the ManagementScope object. This type, briefly mentioned above, has the following properties:
Authentication : Gets or sets the COM authentication level to be used for the WMI connection
Impersonation : Gets or sets the COM impersonation level to be used for the WMI connection
Authority : Gets or sets the authority value to be used for the WMI connection
Locale : Gets or sets the locale value to be used for the WMI connection
Username : Gets or sets the name of the security principal to be used for theWMI connection
Password : Gets or sets the password to be used for the WMI connection
EnablePrivileges : Gets or sets a Boolean flag that indicates whether the privileges are to be enabled for the WMI connection
As you can see, all of these properties, with the exception of the EnablePrivileges , which is discussed later in this chapter, map directly to the parameters of the IWbemLocator::ConnectServer method. However, these properties are also used by the private Secure method of the ManagementScope type, which, during the initialization phase, sets the appropriate authentication and impersonation levels for the process and "blesses" the retrieved IWbemServices interface pointer.
The Username , Password , Locale , and Authority properties of the ConnectionOptions type are governed by the same rules as the respective parameters of the IWbemLocator::ConnectServer method. The Impersonation and Authentication properties, however, are of enumeration types ImpersonationLevel and AuthenticationLevel respectively, which means that you must use a member of the appropriate enumeration when initializing these properties. It should come as no surprise that the members of these enumeration types map one-to-one to the respective COM authentication and impersonation level constants listed in Tables 8-2 and 8-3. The members of the AuthenticationLevel enumeration type are shown in Table 8-4.
|
MEMBER |
COM CONSTANT |
DESCRIPTION |
|---|---|---|
|
Default |
RPC_C_AUTH_LEVEL_DEFAULT |
Under Windows NT 4.0, this value defaults to RPC_C_AUTH_LEVEL_CONNECT . Under Windows 2000 and later, this value instructs COM to select an appropriate authentication level using its normal security blanket negotiation algorithm. |
|
None |
RPC_C_AUTH_LEVEL_NONE |
Instructs COM not to perform any authentication. |
|
Connect |
RPC_C_AUTH_LEVEL_CONNECT |
Causes COM to authenticate the credentials of the client only when the client establishes a session with the server. |
|
Call |
RPC_C_AUTH_LEVEL_CALL |
Instructs COM to authenticate the client at the beginning of each RPC when the server receives the request. |
|
Packet |
RPC_C_AUTH_LEVEL_PKT |
Instructs COM to ensure that the data is received from the authenticated client. |
|
PacketIntegrity |
RPC_C_AUTH_LEVEL_PKT_INTEGRITY |
Causes COM to ensure the integrity of the data received from the client. |
|
PacketPrivacy |
RPC_C_AUTH_LEVEL_PKT_PRIVACY |
Instructs COM to perform all types of authentication referred to in this table and encrypt all RPC arguments. |
|
Unchanged |
The authentication level remains unchanged. |
The members of the ImpersonationLevel enumeration type are shown in Table 8-5.
|
MEMBER |
COM CONSTANT |
DESCRIPTION |
|---|---|---|
|
Default |
RPC_C_IMP_LEVEL_DEFAULT |
This value can be used with Windows 2000 and later. It instructs COM to select an appropriate impersonation level using its normal security blanket negotiation algorithm. |
|
Anonymous |
RPC_C_IMT_LEVEL_ANONYMOUS |
The client remains anonymous to the server. The impersonation is possible, but since the server's impersonation token will not contain any client security information, the server will not be able to perform any tasks under the security context of the client. |
|
Identity |
RPC_C_IMP_LEVEL_IDENTITY |
The server's impersonation token will include the client's identity, which implies that the server will be able to impersonate the client for ACL checking. However, the server will not be able to access the system objects on behalf of the client. |
|
Impersonate |
RPC_C_IMP_LEVEL_IMPERSONATE |
The server may impersonate the client's security context, but only while it is accessing the resources on the local machine on behalf of the client. In other words, the impersonation token cannot be used across the machines' boundaries. |
|
Delegate |
RPC_C_IMP_LEVEL_DELEGATE |
The server may impersonate the client's security context while accessing the resources on local or remote machines on behalf of the client. The impersonation token can be passed across the machines' boundaries. This impersonation level is available under Windows 2000 and later. |
By constructing an instance of the ConnectionOptions type and associating it with the management scope, you may effectively control the security settings for a WMI client process:
ConnectionOptions co = new ConnectionOptions(); co.Username = "Administrator"; co.Password = "password"; co.Authentication = AuthenticationLevel.PacketPrivacy; co.Impersonation = ImpersonationLevel.Impersonate; ManagementScope ms = new ManagementScope(@"\BCK_OFFICE\root\CIMV2", co); ManagementObject mo = new ManagementObject(@" Win32_Process=100"); mo.Scope = ms; Console.WriteLine(mo["__CLASS"]);
Unless the ConnectionOptions object is explicitly associated with an instance of the ManagementScope , the latter constructs a default ConnectionOptions instance during its initialization. Therefore, you can use the default object to control the security settings of the process:
ManagementScope ms = new ManagementScope(); ms.Path = new ManagementPath(@"\BCK_OFFICE\root\CIMV2"); ms.Options.Username = "Administrator"; ms.Options.Password = "password"; ms.Options.Authentication = AuthenticationLevel.PacketPrivacy; ms.Options.Impersonation = ImpersonationLevel.Impersonate; ManagementObject mo = new ManagementObject(@" Win32_Process=100"); mo.Scope = ms; Console.WriteLine(mo["__CLASS"]);
Finally, the ConnectionOptions type is equipped with a convenient constructor method that allows you to set all of the object properties at once. This constructor has the following signature:
public ConnectionOptions ( string locale, string username, string password, string authority, ImpersonationLevel impersonation, AuthenticationLevel authentication, bool enablePrivileges, ManagementNamedValueCollection context, TimeSpan timeout );
where
locale : A string that specifies the locale (i.e., MS_xxx ) to be used for the WMI connection
username : A string that specifies the name of the user to be used for the WMI connection
password : A string that specifies the password to be used for the WMI connection
authority : A string that specifies the authority to be used for the WMI connection
impersonation : A member of the ImpersonationLevel enumeration that indicates the impersonation level to be used for the WMI connection.
authentication : A member of the AuthenticationLevel enumeration that indicates the authentication level to be used for the WMI connection
enablePrivileges : A Boolean flag that indicates whether the privileges are to be enabled for the WMI connection
context : A ManagementNamedValueCollection that contains providerspecific context values
timeout : A TimeSpan object that sets the timeout value for the connection operation
Using this constructor, the earlier code may be changed as follows :
ConnectionOptions co = new ConnectionOptions( null, "Administrator", "password", null, ImpersonationLevel.Impersonate, AuthenticationLevel.PacketPrivacy, false, null, new TimeSpan()); ManagementScope ms = new ManagementScope(@"\BCK_OFFICE\root\CIMV2", co); ManagementObject mo = new ManagementObject(@" Win32_Process=100"); mo.Scope = ms; Console.WriteLine(mo["__CLASS"]);
Although using this constructor may save you a bit of typing, it certainly does not promote code readability and lacks in clarity when compared to setting the properties of the ConnectionOptions object explicitly.
Some operations that can be carried out through WMI may require the user to hold certain system privileges. For instance, the ability to reboot a computer is dependent on SeShutdownPrivilege or SeRemoteShutdownPrivilege , which must not only be granted to the user, but must also be enabled for the process that is attempting the reboot. The privileges can be granted using the local security policy editor, shown in Figure 8-2.
Figure 8-2:
Granting privileges
Once the privileges are granted, they have to be explicitly enabled by the process attempting the privileged operation. Thus, in order to reboot a remote computer you may write code similar to the following:
ManagementClass mc = new ManagementClass( @"\BCK_OFFICE\root\CIMV2:Win32_OperatingSystem"); foreach(ManagementObject mo in mc.GetInstances()) { mo.Scope.Options.Username = "username"; mo.Scope.Options.Password = "password"; mo.Scope.Options.EnablePrivileges = true; mo.InvokeMethod("Reboot", new Object [] ); }
This code binds to the Win32_OperatingSystem WMI class, enumerates its instances, and invokes the Reboot method for each operating system. Of course in most cases, there would be a single instance of the Win32_OperatingSystem class and it is probably easier to just locate such an instance directly based on its key. Unfortunately, the key of this class is the name of the OS, which is often very long and easy to mistype, therefore, I chose to use the enumeration approach. The thing you should notice here is the line of code that sets the EnablePrivileges property of the ConnectionOptions object to true . Unlike the AdjustTokenPrivileges function, which allows you to enable and disable individual privileges, setting the EnablePrivileges property to true enables all privileges granted to a given user or group . In effect, assigning this property to true is equivalent to obtaining the set of privileges associated with the access token via the GetTokenInformation function; iterating through the returned array of token privileges to set the enabled attribute for each of them; and then invoking AdjustTokenPrivileges . Here again the designers of the System.Management namespace chose to trade flexibility for simplicity by disallowing access to individual user and group privileges.
Lastly, there is still the question of figuring out which WMI operations are privileged and exactly what privileges are required to perform such operations. Although it is always possible to get the answer by digging through the mess of WMI security documentation, there is an easier way. It turns out that all WMI methods that require certain privileges are marked as such using the Privileges qualifier. For instance, take a look at the partial MOF definition for the Win32_OperatingSystem class:
class Win32_OperatingSystem : CIM_OperatingSystem { ... [Privileges{"SeShutdownPrivilege"}: ToSubClass] uint32 Reboot(); [Privileges{"SeShutdownPrivilege"}: ToSubClass] uint32 Shutdown(); [Privileges{"SeShutdownPrivilege"}: ToSubClass] uint32 Win32Shutdown(sint32 Flags, sint32 Reserved = 0); };
Here the Privileges qualifier is parameterized with the string value of the required privilege. In reality, the qualifier value is really an array of strings, so it is possible to account for methods that require multiple privileges to be enabled. Therefore, by examining the qualifiers associated with a given method, you can determine whether it is necessary to enable the privileges before attempting to execute the method. Note that it is not even necessary to check the qualifier value (since individual privileges cannot be controlled via the System.Management types anyway)—the mere presence of the Privileges qualifier is indicative of a privileged operation.
|
. NET System Management Services Authors: Golomshtok A. Published year: 2005 Pages: 52/79 |