Understanding Domain Trust Relationships


Domain trust relationships are a necessary part of Active Directory. These trust relationships essentially define a condition where one domain trusts the security mechanisms of another domain. The two domains are still separate; they simply trust the work performed by the other domain.

The following sections describe domain trust issues. For example, it’s important to consider the difference between authentication and permission when working with multiple domains. You’ll also see an example of how to work with the domain controller directly. Although you won’t have to perform this task very often, the need is usually critical when you do. The example concentrates on providing you with good sample code for the most commonly used directory services and Win32 API functions.

Defining the Domain Trust Issues

It’s easy to get confused about the relationship between authentication and permission when working with domain trust relationships. A caller trusted and authenticated by one domain is trusted and authenticated by the trusting domain. However, don’t assume that trust automatically equates to access. A caller is still subject to the security requirements of the trusting domain and doesn’t automatically receive access to resources. The only security right granted is authentication. Consequently, a domain could authenticate a caller on the trusting domain, but the caller still might not have any access to resources. Active Directory acts as the repository for all of the trust relationship information, so it plays a very active role in creating and maintaining trust relationships. This chapter won’t discuss the architectural details of trust relationships, but you can read about them in the article entitled “Trust Relationships” at ms-help://MS.MSDNQTR.2003FEB.1033/w2krk/html/dsbb_act_kxlx.htm.

Tip

The NetDiag tool can help you discover information about domains on your system. For example, you can use it to locate a specific domain or to enumerate domain accounts. You can also verify primary domain details such as the domain globally unique identifier (GUID) and security identifier (SID).

It’s important to note that there’s one architectural detail of significant importance for the security setup of your .NET application. When working with Windows NT, all trust relationships are one way. One domain trusts another, but the reverse might not be true. Even when Windows NT establishes two-way trust, it’s the result of two one-way trusts. Windows 2000 uses a domain tree setup where all trust relationships are two way. A parent that trusts a child domain will also have the child domain’s trust. Consequently, when you write applications that rely on domain trust relationships, you need to consider the target platform and ensure that the correct domain trust relationships are in place. Otherwise, you might have to solve odd security problems where User A can access a resource from Machine A, but not from Machine B. Unfortunately, the .NET Framework doesn’t make detection of such problems easy, so they normally look like an application bug or at least a setup error.

Note

You may run into situations where the domain trust security features of an application don’t work properly because the network administrator has used the incorrect procedure for working with the domains. For example, the Microsoft Knowledge Base article Q112372 (http://support.microsoft.com/default.aspx?scid=kb;[LN];112372) discusses what can happen when a network administrator uses the incorrect procedure to split a domain. Because the security identifier (SID) remains that same, trust relationships can fail. Unfortunately, the .NET Framework doesn’t provide good feedback on these issues, making it very difficult to locate the error. The best way to approach the problem is to verify that network administrators know the correct procedures to ensure that the error doesn’t occur in the first place.

Working Directly with the Domain Controller

The .NET Framework provides a number of tools to search Active Directory. For example, you can use a DirectoryEntry or a DirectorySearcher object to work with Active Directory entries. In fact, the example in the “Using Imperative Active Directory Security” section shows how to work with a DirectoryEntry object. However, if you want to work directly with the domain controller, you’ll need to resort to using Win32 API calls—those that help you work with directory services. This seeming omission is by design. For example, look at the DirectorySearcher.SearchRoot property documentation and you’ll notice that Microsoft mentions using the DsGetDcName() function to find the global catalog. Unfortunately, Microsoft doesn’t supply any example code that shows how to perform this task.

Tip

The list of directory service functions isn’t very long—you can normally perform any task you need to with a few calls. You’ll find a list of standard directory services functions at http://msdn.microsoft.com/library/en-us/netdir/ad/directory_service_functions.asp. Make sure you also check out the domain controller and replication management functions at http://msdn.microsoft.com/library/en-us/netdir/ad/dc_and_replication_management_functions.asp.These two lists don’t include some supplementary functions, but they do contain the main functions you’d call.

Listing 12.1 shows how to perform a number of domain controller–related tasks using directory service–related functions. This section relies on the PInvoke functionality provided by the .NET Framework to access the Win32 API. We’ll pursue the topic of Win32 API access for security needs in detail in Chapters 14 and 15. This chapter also tells you how you can get more information about using PInvoke in general. The example code shown in Listing 12.1 isn’t complete, but it does contain the essential code needed to perform the required tasks. You’ll find the complete source code for this example in the \Chapter 12\C#\DomainAccess and \Chapter 12\VB\DomainAccess folders of the source code located on the Sybex Web site.

Listing 12.1 Using Win32 API Calls for the Domain Controller

start example
// Define the method used to retrieve the computer name. // This first version accepts a domain GUID. [DllImport("NetAPI32.DLL", CharSet=CharSet.Ansi, SetLastError=true)] public static extern Int32 DsGetDcName(    String ComputerName,    String DomainName,    Guid DomainGuid,    String SiteName,    DsGetDcNameFlags Flags,    ref IntPtr DomainControllerInfo); // This second version doesn’t require a domain GUID. [DllImport("NetAPI32.DLL", CharSet=CharSet.Ansi, SetLastError=true)] public static extern Int32 DsGetDcName(    String ComputerName,    String DomainName,    IntPtr DomainGuid,    String SiteName,    DsGetDcNameFlags Flags,    ref IntPtr DomainControllerInfo); // This data structure contains the domain controller // information on return from the call. public struct DOMAIN_CONTROLLER_INFO {    public String              DomainControllerName;    public String              DomainControllerAddress;    public DCAddressType       DomainControllerAddressType;    public Guid                DomainGuid;    public String              DomainName;    public String              DnsForestName;    public DCInfoReturnFlags   Flags;    public String              DcSiteName;    public String              ClientSiteName; } // This function binds the current application to a directory services // session. In essence, it begins a remote procedure call to the domain // controller so your application can make queries. [DllImport("NTDSAPI.DLL", CharSet=CharSet.Ansi, SetLastError=true)] public static extern Int32 DsBind(String DomainController,                                   String DnsDomainName,                                   out IntPtr phDS); // This function releases the handle created by the DsBind() function. [DllImport("NTDSAPI.DLL", CharSet=CharSet.Ansi, SetLastError=true)] public static extern Int32 DsUnBind(IntPtr phDS); // This function obtains information about the domain controller. [DllImport("NTDSAPI.DLL", CharSet=CharSet.Ansi, SetLastError=true)] public static extern Int32 DsGetDomainControllerInfo(    IntPtr hDs,    String DomainName,    Int32 InfoLevel,    out Int32 pcOut,    out IntPtr ppInfo); // This data structure contains the information retrieved with // the DsGetDomainControllerInfo() function. You actually have a // choice of using two data structures; this is the more complex // of the two. public struct DS_DOMAIN_CONTROLLER_INFO_2 {    public String  NetbiosName;    public String  DnsHostName;    public String  SiteName;    public String  ComputerObjectName;    public String  ServerObjectName;    public Boolean fIsPdc;    public Boolean fDsEnabled;    public Boolean fIsGc;    public Guid    SiteObjectGuid;    public Guid    ComputerObjectGuid;    public Guid    ServerObjectGuid;    public Guid    NtdsDsaObjectGuid; } // This function frees the domain controller data structure created by // the DsGetDomainControllerInfo() function. [DllImport("NTDSAPI.DLL", CharSet=CharSet.Ansi, SetLastError=true)] public static extern void DsFreeDomainControllerInfo(Int32 InfoLevel,                                                      Int32 cInfo,                                                      IntPtr pInfo); private void btnQuery_Click(object sender, System.EventArgs e) {    DOMAIN_CONTROLLER_INFO  Output;     // Domain output structure.    IntPtr                  OutputRef;  // Domain Structure Reference.    IntPtr                  BindHandle; // Domain bind handle.    Int32                   DCDataLen;  // Domain data length.    IntPtr                  DCDataRef;  // Domain data reference.    DS_DOMAIN_CONTROLLER_INFO_2[] DCData;  // Domain Controller Data.    StringBuilder           DCDataOut;  // Final information output.    // Initialize the domain information data structure.    Output = new DOMAIN_CONTROLLER_INFO();    // Allocate memory from the global heap to hold the domain data    // structure reference.    OutputRef = Marshal.AllocHGlobal(Marshal.SizeOf(Output));    // Get the domain information for the default domain.    DsGetDcName(null,                "",                IntPtr.Zero,                "",                DsGetDcNameFlags.DS_NONE,                ref OutputRef);    // Convert the reference pointer to the associated data structure.    Output = (DOMAIN_CONTROLLER_INFO)Marshal.PtrToStructure(       OutputRef, typeof(DOMAIN_CONTROLLER_INFO));    // Free the memory used by the data structure reference.    Marshal.FreeHGlobal(OutputRef);    // Bind the application to the directory services instance.    DsBind(Output.DomainControllerName,           Output.DomainName,           out BindHandle);    // Get the domain controller information.    DsGetDomainControllerInfo(BindHandle,                              Output.DomainName,                              2,                              out DCDataLen,                              out DCDataRef);    // Size the domain controller information array.    DCData = new DS_DOMAIN_CONTROLLER_INFO_2[DCDataLen];    // Place the information in the array.    DCData[0] =       (DS_DOMAIN_CONTROLLER_INFO_2)Marshal.PtrToStructure(          DCDataRef,          typeof(DS_DOMAIN_CONTROLLER_INFO_2));    // Free the controller information pointer.    DsFreeDomainControllerInfo(2, DCDataLen, DCDataRef);    // Unbind the application from the directory services instance.    DsUnBind(BindHandle);    // Display the information on screen.    DCDataOut = new StringBuilder();    DCDataOut.Append("Computer Object Name: ");    DCDataOut.Append(DCData[0].ComputerObjectName); ... A lot of other data output. ...    DCDataOut.Append("\r\nSiteObject GUID: ");    DCDataOut.Append(DCData[0].SiteObjectGuid.ToString());    if (DCData[0].fDsEnabled)       DCDataOut.Append("\r\nDomain Services are Enabled");    if (DCData[0].fIsGc)       DCDataOut.Append("\r\nThis is the global catalog server.");    if (DCData[0].fIsPdc)       DCDataOut.Append("\r\nThis is the Primary Domain Controller.");    MessageBox.Show(DCDataOut.ToString(), "Domain Controller Data",       MessageBoxButtons.OK, MessageBoxIcon.Information); }
end example

The code begins by declaring all of the Win32 API functions used in the example. The [DllImport] attributes for this example always include three features. First, you must provide the name of the Win32 DLL that contains the function you want to use. Second, you must tell the CLR which character set to use: Unicode or ANSI. Third, you must set the SetLastError to true. Otherwise, you can’t use the Marshal.GetLastWin32Error() method to obtain extended error information.

The DsGetDcName() function presents a special problem because it requires a GUID as input. When working with C/C++, you can simply set this entry to null (Nothing in Visual Basic) and expect the code to work. However, .NET applications can’t set a Guid value to null because CLR returns an error. Consequently, the code creates an overload that accepts an IntPtr as input for the Guid value. The DsGetDcName() function returns a DOMAIN_CONTROLLER_INFO data structure. This data structure calls for the use of UInt32 values, which presents a problem for Visual Basic developers (Visual Basic doesn’t support unsigned integers directly). The code gets around this problem by using enumerated values for the two UInt32 values. Not only does this technique overcome the unsigned integer problem, but it also makes the resulting code easier to read.

Note

The code in Listing 12.1 lacks error trapping for the sake of clarity. You normally need to trap function return values, as well as use the Marshal.GetLastWin32Error() method to locate errors. In many cases, the .NET Framework won’t tell you what’s wrong because it doesn’t know—you have to rely on Win32 API error trapping. The sample code includes a full set of return errors as an enumeration that you can use in your own code.

The DsBind() function creates an RPC session between the requesting application and the domain controller. This function returns a handle to the RPC session that the code uses with other function calls. The handle identifies the RPC session. Notice that this function declaration uses an out IntPtr, rather than the ref IntPtr used by the DsGetDcName() function. The DsGetDcName() function uses memory allocated by CLR, while the DsBind() function uses memory allocated by the Win32 API. Consequently, the DsBind() function receives an un-initialized IntPtr that the code frees using the DsUnBind() function. In general, if the Win32 API function automatically allocates memory for you, then you must use the technique shown for the DsBind() function.

The DsGetDomainControllerInfo() function presents a few problems. First, you can’t call it until you call a number of the other functions because the code won’t have the required information. Almost every input argument derives from one of the other functions described in this section. Second, you must decide on how much information to request. This function can actually accept one of two data structures as input. The example shows how to create the more complicated of the two data structures, DS_DOMAIN_CONTROLLER_INFO_2. You indicate which data structure to use with the InfoLevel argument. Third, this function returns an array of data structures, which means the code has to perform additional handling to obtain the correct information. Because the DsGetDomainControllerInfo() function returns an array of data structures, you can’t allocate memory so you must rely on the DsFreeDomainControllerInfo() function to free the memory.

At this point, you can start looking at the btnQuery_Click() method. The code begins by initializing a DOMAIN_CONTROLLER_INFO data structure. This initialization process doesn’t initialize the internal variables—just the data structure itself. The initialization process is necessary so that you can use the Marshal.SizeOf(Output) method to determine the size of the data structure. The output from this method lets you allocate memory from the global heap for the data structure reference, OutputRef, using the Marshal.AllocHGlobal() method. Any memory you allocate manually won’t get garbage collected properly, so you must deallocate it using the Marshal.FreeHGlobal() method.

Now that the code has some memory to use, it calls the DsGetDcName() function to obtain preliminary information about the domain controller. The example shows how to obtain information for the default domain controller. Consequently, you don’t need to know anything about the host system. This technique always returns a domain controller if one exists (error trapping is essential if you use this on systems where the presence of a domain controller is in doubt). The DsGetDcName() function returns a pointer to a data structure, not the data structure itself. The code uses the Marshal.PtrToStructure() method to convert the pointer to a data structure you can use.

The code now has enough information to start a conversation with the domain controller, so it uses the DsBind() function to create an RPC session. Notice how this function relies on information retrieved from the Output data structure.

After the code begins a conversation with the domain controller, it uses the DsGetDomainControllerInfo() function to request information about the domain controller. This isn’t the standard server information, but the actual domain controller (the software server on the physical machine) information. The DsGetDomainControllerInfo() function, like many directory services functions, requires a bind handle as input so that it knows which RPC session to use. The DCDataLen output from this function states the number of array elements pointed at by the DCDataRef pointer.

The issue of how to handle arrays is important. Notice the first piece of the puzzle comes next. The code initializes DCData to hold the correct number of DS_DOMAIN_CONTROLLER_INFO_2 array elements. The code then uses the Marshal.PtrToStructure() method to place these array elements into DCData starting with the first array entry.

At this point, the program has the data needed to identify the server. The next two steps free the DS_DOMAIN_CONTROLLER_INFO_2 array pointer and close the RPC session with the server. You must perform these two steps or your program will have a significant data leak. The final lines of code display the information in the first element of the DS_DOMAIN_CONTROLLER_INFO_2 array. Figure 12.6 shows the output from this program.

click to expand
Figure 12.6: Using PInvoke can help you learn a lot about the domain controller.

As you can see from the figure, this application provides a wealth of information about the domain controller that you can’t obtain using strict .NET Framework functionality. However, you often need this information to invoke security on the domain controller and perform security-related tasks. For example, if you want to move security information from one machine to another, you need the DsAddSidHistory() function. Many of the .NET Framework methods also rely on information provided by the functions in this example. Finally, this example demonstrates that you can partially secure your server simply by denying the user access to unmanaged code.




.Net Development Security Solutions
.NET Development Security Solutions
ISBN: 0782142664
EAN: 2147483647
Year: 2003
Pages: 168

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net