Click here to Skip to main content
15,867,686 members
Articles / Programming Languages / C#
Article

WMI Namespace Security

Rate me:
Please Sign up or sign in to vote.
4.70/5 (13 votes)
4 Dec 2006CPOL15 min read 131.4K   3.5K   35   35
Describes WMI namespace security, and introduces code to review and modify WMI Namespace security.

Sample Image

Introduction

Few would challenge the assertion that Windows Management Instrumentation (WMI) has significantly enhanced manageability of the Windows OS desktop. Derived from the Distributed Management Task Force (DMTF) and Web Based Enterprise Management (WBEM) initiatives intended to standardize management of the desktop, WMI allows for standard methods of access to Windows OS components such as registry settings, OS information, PC hardware data, event logs, etc. WMI leverages the Common Information Model (CIM), a general schema describing computer resources and components. The idea of DMTF/WBEM was to standardize access to data and methods across OS platforms previously available only to those who understood the intricacies of the native API set. If you (or a group you are a member of) are not a trustee of the local or domain Administrators group, the default WMI security settings for Windows 2000/XP/2003/Vista (Beta 2) do not allow the ability to remotely connect to the WMI service. This could be problematic since you cannot read what you cannot access. Because organizations may prefer to delegate tasks such as reading remote WMI counters to selected employees without elevating them to Administrator status, the default WMI security may have to be modified.

Using the code

WMI security is implemented by granting trustees permissions to WMI namespaces. A WMI namespace is a collection of logically grouped data and methods describing computer components or services. You can view WMI namespace security on the Windows platform by opening the WMI Control in the Computer Management console, and then expand "Services and Applications." Click "WMI Control" to highlight, then right click and choose "Properties."

width=155

You should now see all available items for WMI management. Notice you can back up/restore the WMi repository here (Backup/Restore tab), set the default namespace for scripting (under the Advanced tab), or even set the logging level for errors in this applet.

width=227

Go to the Security tab and then expand the Root node of the namespace tree. If you click on a namespace, and then select Security, you should be able to observe the security applied to that particular namespace.

width=227

If your machine has retained default security, you should notice that the Administrators group in Windows XP and the Domain Administrators group on a Windows 2003 domain controller are the only two groups with the Remote Enable permissions.

Technically, WMI is a DCOM application, thus any change to DCOM security (via dcomconfig.exe) may also affect WMI. However, it is much safer to implement any necessary changes through the WMI applet shown above, because changes to DCOM security can potentially affect all DCOM applications, something you probably don’t want to do.

Because the group I work for require personnel who are not members of the Domain Administrators group to read CIMv2 data from remote servers, and since I didn’t particularly relish spending valuable time executing terminal service to implement the necessary security modifications, I created the utility described herein. Essentially, I required a tool to read WMI namespace security (possibly to database?) and implement any necessary security changes. Additionally, I wanted a command line utility to script the necessary changes or monitor future WMI namespace settings.

Below are possible security settings for WMI namespaces in Windows 2000/XP/ 2003/Vista (Beta 2).

Permissions    Explanation

Execute Methods:    Execute Read/Write/Delete objects from CIM
Partial Write  :    Write to static WMI objects
Provide Write  :    Write to dynamic WMI objects
Enable Account :    Read  properties of WMI objects
Remote Enable  :    Access WMI from remote systems
Read Security  :    Read security settings    
Edit Security  :    Change security settings

Since these security settings are hosted by the same security model as other objects in a Microsoft OS, you should know something about how access control in the Windows NT/2000/XP/2003 platforms work. Objects in Windows 32-bit platforms are secured by a security descriptor (SD) which contains, among other things, two types of Access Control Lists (ACLs): a Discretionary Access Control List (DACL) and a System Access Control List (SACL). For this utility, we’re only concerned with the DACL, which specifies what a trustee can or cannot do to the object. The DACL is itself comprised of trustee Security IDs (SIDs) and their requisite Access Control Entries (ACEs). Trustees are identified in the DACL by their SID which is mapped to a trustee by the OS upon account creation. When you log into a Windows OS, the OS grants you a token with you personal SID and any SIDs of groups you are a member of. These SIDs are checked by the OS when you try to access an object secured by the OS, and matched against the SIDs in the DACL. You will get the culmination of access permissions granted to the SIDs in your token unless an explicit access denied is encountered.

Now that you have a concept of WMI namespace security, let’s delve into the code to modify it. I wrote the utility for the 1.1 version of the .NET framework, using the C# language. For simplicity, I created two classes, ChangeNameSpaceSecurity and ViewNameSpaceSecurity which operate as their name implies, inheriting some common connection methods and data from class NameSpaceSecurity. The utility makes use of Interop services for high-level unmanaged APIs, WMI security methods, and ACE strings, which I’ll explain later. To aid in the ACE string creation, I employed the AceStringManager class, which derives from AceString, the actual workhorse.

The main program (WmiSecurity.cs) determines whether the user wants to view existing WMI security or modify existing WMI security by the value of the unsigned integer, uiCommandLine. Let’s assume the user wants to modify existing security (CL_MODIFY or CL_MODIFYRECURSE), so a computer name, root namespace, trustee with desired access rights, and recursion switch have been supplied by the caller. Take a look at the method Help() in WmiSecurity.cs module to determine the exact syntax required. WmiSecurity uses the CreateAccessMask() method of the WmiSecurity class shown below to convert the access request from the command line into WMI specific access rights.

C#
public static uint CreateAccessMask(string sAccess)
{
    if(sAccess.ToUpper()=="READ")
    {
        return AceStringManager.WBEM_ENABLE;
    }
    else if (sAccess.ToUpper()=="REMOTEACCESS")
    {
        return AceStringManager.WBEM_ENABLE|
               AceStringManager.WBEM_REMOTE_ACCESS;
    }
    else if (sAccess.ToUpper()=="PROVIDERWRITE")
    {
        return AceStringManager.WBEM_ENABLE|
               AceStringManager.WBEM_WRITE_PROVIDER;
    }
    else if (sAccess.ToUpper()=="PARTIALWRITE")
    {
        return AceStringManager.WBEM_ENABLE|
               AceStringManager.WBEM_PARTIAL_WRITE_REP;
    }
    else if (sAccess.ToUpper()=="FULLWRITE")
    {
        return AceStringManager.WBEM_ENABLE|
               AceStringManager.WBEM_FULL_WRITE_REP;
    }
    else if (sAccess.ToUpper()=="FULL")
    {
        return AceStringManager.WBEM_REMOTE_ACCESS|
               AceStringManager.WBEM_METHOD_EXECUTE|
               AceStringManager.WBEM_FULL_WRITE_REP|
               AceStringManager.WBEM_ENABLE|
               AceStringManager.READ_CONTROL|
               AceStringManager.WRITE_DAC;
    }
    else
        return 0;
}

Be aware that the security settings available were consolidated considerably to match common security choices deemed appropriate. For instance, looking at the CreateAccessMask() method of WmiSecurity reveals the assignment of WBEM_ENABLE access to “Read” and additional rights to each consecutive access level, ending with “Full.” Be familiar with what each access level provides and alter this to suit your particular situation.

The main program (WmiSecurity.cs) calls the ChangeNameSpaceSecurity(), constructor, passing in the namespace to modify and whether the resulting action will recurse through child namespaces as well. The next line sets the Ace flags, which determines whether an access allowed or denied Ace is created, and sends in the unsigned integer representation from CreateAccessMask(). The main program must now determine whether a backslash is contained in the sTrustee string passed in by the caller which will determine if the trustee is in domain\trustee format or is in a well known SID format represented by a 2 letter constant.

C#
if(sTrustee.IndexOf("\\")==npos)
    changesec.Modify(sComputer, sTrustee);
else
{
    changesec.Modify(sComputer,
       changesec.getSidStringFromName(sTrustee));
}

The 2 letter constants are defined in Sddl.h as well know SIDs, i.e. Domain Users, Domain Guests, Domain Admins, etc. Look at the Platform SDK for detailed information, but suffice to say that ‘DA’ represents domain admins, ‘DG’ represents domain guests, and so on. The important thing to understand is that the OS will take care of converting these constants into useable SIDs for us. We are not so lucky for users and groups the OS has not predefined - for these we must convert the name to a SID. ChangeNameSpaceSecurity’s getSidStringFrom Name() method will do this for us, but we must use some unmanaged APIs to get there. Looking at this method, you can see it is relatively straightforward, employing LookupAccountName() and ConvertSidToStringSid() unmanaged API’s through Interop services.

C#
public string getSidStringFromName(string sAccount)
{
    StringBuilder sbDomain;        // Domain name.
    Int32 iDomainSize;            // Domain name size.
    Int32 iSidSize;                // Size of the returned SID.
    IntPtr pAccountSid = IntPtr.Zero;    // Sid of account
    IntPtr pSid = IntPtr.Zero;    // Set ptr to zero.
    int iError;                    // API error            
    SID_NAME_USE snu;  
    string sSid = "";
    iSidSize=0;
    sbDomain = new StringBuilder();
    iDomainSize=0;
            

    // The 1st time this will fail, but we can
    // then alloc necessary sid buffer size
    LookupAccountName(null, sAccount, pAccountSid, 
          ref iSidSize, sbDomain, ref iDomainSize, out snu);

    pAccountSid=Marshal.AllocHGlobal(iSidSize);

    // Try a 2nd time with proper buffer size...
    if(!LookupAccountName(null, sAccount, pAccountSid, 
        ref iSidSize, sbDomain, ref iDomainSize, out snu))
    {
        iError=Marshal.GetLastWin32Error();
        // free up allocation
        Marshal.FreeHGlobal(pAccountSid);
        throw new Exception("LookupAccountName error: " + 
                            iError.ToString());
    }
    else
    {
        if(!ConvertSidToStringSid(pAccountSid, ref sSid))
        {
            iError=Marshal.GetLastWin32Error();
            // free up allocation
            Marshal.FreeHGlobal(pAccountSid);
            throw new Exception("ConvertSidToStringSid error: " + 
                                iError.ToString());
        }
    }

    Marshal.FreeHGlobal(pAccountSid);
    return sSid;
}

Figure 1: getSidStringFromName

I use LookupAccountName in the same manner I would if this were written in the venerable C/C++ language. Like many API’s where the required buffer is unknown, the strategy is to pass in a null pointer (pAccountSid), telling the API our buffer size (iSidSize) is zero. The call will fail of course, but it will return (via iSidSize) the size of the buffer needed for a successful call. We use this to allocate a pointer to a buffer (pAccountSid), call the API again, and we’re set. If anything goes wrong, we throw an exception with the error code and bug out. Returned to the main program should be a textual SID representation of the trustee in question.

ChangeNameSpaceSecurity uses the Modify() method to implement security modifications. ChangeNamespaceSecurity first ensures that the object has been properly initialized, i.e. a namespace has been chosen, the bAccessAllowed sentinel has been set, the actual access control flags have been set (i.e. Read or Write), and whether or not security modifications on the namespace will be applied to all objects under the namespace recursively. Assuming these tasks have been done, we attempt to connect to the remote computer’s WMI service. If all is well then we should see the “Connected to \\xyz” on the command line, and if not, we throw an exception.

Before anything else is done, Modify() attempts to create an Ace string object for the pending WMI method calls. Ace strings are a subset of Security Descriptor Definition Language (SDDL), which is itself a string representation of a security descriptor. SDDL and the corresponding high level unmanaged API’s make working with access security in Microsoft systems considerably easier than it used to be. Having worked previously with the low level access control API’s, I can tell you this format saves a lot of wrangling that was synonymous with access control code. The Ace string component of SDDL is structured as shown below:

ace_type;ace_flags;rights;object_guid;inherit_object_guid;account_sid

Notice that each component is separated by a semi-colon and each component must be in order. With this in mind, the AceStringManager object’s constructor is called as shown below:

C#
AceStringManager asm = new AceStringManager(sTrusteeSid, 
                       this.m_bAccessAllowed, this.m_bRecursive);

which constructs the new object with the trustee SID (more on this), ace type, and whether or not the modifications will be recursive to other objects in the namespace. AceStringManager inherits from the AceString class, which actually does the grunt work of constructing a workable Ace string. When the Modify() invokes AceStringManager’s only method, ReturnSddlString(), AceStringManager in turn calls AceString’s CreateAceStringFromWmiRight() method.

To clarify, let’s assume you want to allow the HelpDesk group the remote connect and read ability on your network servers. CreateAccessMask() would convert the REMOTEACCESS request from the command line into the access mask WBEM_ENABLE|WBEM_REMOTE_ACCESS. AceStringManager’s ReturnSddlString() method would consecutively call AceString’s CreateAceStringFromWmiRight() for WBEM_ENABLE and then WBEM_REMOTE_ACCESS. This in turn adds the “WP” and “CC” to the m_sbRights Stringbuilder of AceString, which ultimately comprises part of the Ace string created. To ensure that each right added to the Ace string is unique, AceString searches for a previous instance via the IndexOf () method of the m_sbRights Stringbuilder.

Back in AceStringManager, we now call CreateFinalAceString(), passing in the SID string of a trustee we obtained earlier or the well known SID string constant. We first start by indicating whether the ACE is access allowed or denied by adding an ‘A’ or a ‘D’ to the m_SbAceString StringBuilder object followed by a semicolon. Depending on whether or not we want to apply the modification recursively, we next either append ‘CI’ (container-inherit) or nothing, followed again by a semicolon. We next append m_sbRights string (created previously by CreateAceStringFromWmiRight()) to m_SbAceString, followed again by a semicolon. Since this utility will not define an object_guid or an inherit_object_guid, we leave these empty, as shown by appending 2 semicolons to m_SbAceString. Finally, CreateFinalAceString() appends the SID string to m_sbAceString. This Ace string is now passed back through AceStringManager to the original caller, ChangeNameSpaceSecurity. If you want to learn more about Ace strings or SDDL syntax, you can do a search on “Ace Strings” or “SDDL” in the platform SDK.

Now having obtained the necessary Ace string, we make use of System.Management’s ManagementPath class, whose job it is to build a path to the intended WMI object, in this case our selected namespace. If all is OK, we now call on System.Management.ManagementClass, which takes our ManagementScope, m_ms, (defined at connection), the ManagementPath m_mp, and finally any connection options and allows us to invoke methods on the management object (the namespace) we desire. If you wonder where WMI methods or data can be viewed, you should investigate the WMI Object Browser, part of the WMI Tools available from Microsoft’s web site. A detailed explanation could be the subject for another article, but suffice to say that some investigation on your part is definitely worthwhile. This utility reveals all namespaces available, as well as data and methods available for each object in the namespace.

If you look up ManagementBaseObject in the Framework 1.1 SDK you’ll see something similar to this:

C#
public ManagementBaseObject InvokeMethod, string methodName,
   ManagementBaseObject inParameters, InvokeMethodOptions options);

And since we’re looking to get the existing security on the namespace, we use “GetSD” as the method, we have no inParameters, and no options either. Since each method call is unique, it’s a good idea to see what success is defined as – in this case 0.

Now to view the resulting data, we have to understand a little more about Security Descriptors (SDs). Security Descriptors are defined as an opaque structure of pointers to DACLs and SACLs, Primary Owner SIDs, etc. Opaque means you should never attempt to directly modify the contents of a SD directly – instead you should defer to existing APIs specifically created for this purpose. I alluded to the fact that we’ll be using the high level access control APIs, and here’s where the payoff is. We can take the output of the WMI method GetSD, namely a pointer to the SD, and use this as input to the ConvertSecurityDescriptorToStringSecurityDescriptor() unmanaged API. Of course, if something unexpected happens during the WMI GetSd() method, we throw an exception and bug out.

You’ll notice the use of Platform Invocation (PInvoke) for calling unmanaged API’s in this utility. For the 1.1 framework, there is no way around it for some of the security API’s, although that may change in the future. A comprehensive look at InteropServices would fill a book, so I’ll just touch on a few things. Notice the error code after the call to ConvertSecuritydDescriptorToStringSecurityDescriptor() call, namely that which obtains the error via Marshall.GetLastWin32Error(). Marshall is a framework class that, according to the documentation:

“Provides a collection of methods for allocating unmanaged memory, copying unmanaged memory blocks, and converting managed to unmanaged types, as well as other miscellaneous methods used when interacting with unmanaged code.”

The GetLastWin32Error() call grabs the Win32 return from the unmanaged API call executed. You also need this class to free memory allocated from unmanaged API calls. To do this, use the FreeHGlobal() method, as this will free the memory pointing to the security descriptor structure we asked for. So having obtained the unmanaged string representing a SD, we call PtrToString Auto() from the Marshall class, which allocates a managed string and copies characters from the unmanaged string to it. After this, we simply display the resulting string representing the old namespace security to the screen. Now remember the Ace string created to modify the target computer’s WMI namespace? It is still in string form, so it needs to be converted into a structure format the system can understand. That’s exactly what the next line:

C#
ConvertStringSecurityDescriptorToSecurityDescriptor(
     stringSecurityDescriptor, 1, out pSystemSD,  out sdSize) 

does. The variable pSystemSD is essentially a pointer to the variable that receives the converted security descriptor. The next line two lines:

C#
byte[] securityDescriptor = new byte[sdSize];
Marshal.Copy(pSystemSD,securityDescriptor, 0, sdSize);

creates a byte array with size defined by sdSize, and then copies the contents of the unmanaged memory pointer (pSystemSD, our SD “blob”) to a managed 8-bit unsigned integer array, in this case securityDescriptor. Note the 0 is the stating index of the copy, and we copy exactly the size of sdSize. Note the next line:

C#
ManagementBaseObject inParams = systemSecurity.GetMethodParameters("SetSD");

Which returns a ManagementBaseObject representing the input parameters for our final call, SetSD(). The next few lines accomplish this:

C#
inParams["SD"] = securityDescriptor;
outParams = systemSecurity.InvokeMethod("SetSD", inParams, null);

Essentially, we pass in a SD formatted the way the system can use, and invoke the SetSD() method call. If the return value isn’t what we like, we’ll throw an exception alerting the caller to this. In the finally section, we should free unmanaged memory we have allocated, namely pSystemSD and pStringSD. To view existing security on WMI Namespaces, the main program (WmiSecurity.cs) needs only the computer to run on, the namespace to target, and whether we want to recursively look at subsequent namespace objects. Because the value of uiCommandLine would be CL_VIEW or CL_VIEWRECURSE, the main program will use the ViewNameSpaceSecurity class versus the ChangeNameSpaceSecurity class used earlier. ViewNameSpaceSecurity uses the ViewSecurity() method, which in turn calls the ViewSecurityDescriptor() method to obtain and display the textual SID. I won’t bother to explain this method as it is conceptually very similar to ChangeNameSpaceSecurity’s Modify() method. The only change here is that ViewSecurity() calls the EnumNameSpaces() method to recurse through subsequent namespace objects when this option is indicated.

So at this point, some examples should suffice to illustrate the use of the utility. Suppose you wanted to add the domain HelpDesk group to the CIMV2 and subsequent namespaces on the S223001 computer. Further, suppose this group should have the ability to remotely connect and read WMI objects. To do this:

WmiSecurity /C=S223001 /A /N=Root\CimV2 /M=MyDomain\HelpDesk:REMOTEACCESS /R

Of course, the user executing the utility requires the Edit Security right on the target namespace or the ubiquitous “Access Denied” error will appear. Likewise, to view the existing security on a WMI namespace:

WmiSecurity /C=S223001 /N=Root\CimV2 /R

Which will present the security on the Root\Cimv2 namespace and all child namespaces. Where /C = Computer (without UNC specification) to view. Where /N = Namespace to target Where /R (optional)= view all subsequent WMI namespaces as well as present one

One final thought: please be aware that any modification to WMI namespace security could potentially weaken system security. Thus, extensive testing should preclude any WMI namespace security modification, particularly in a real world environment. Properly planned, WMI security modifications will enhance employee productivity without introducing possible security breaches.

Points of Interest

This is my first foray into the managed world code with respect to systems programming. In short, there is a fair amount of research to be done if the appropriate method is not available within the .NET framework to accomplish what you want. On the other hand, the actual coding and testing of the project is much easier, and with built in exception handling, dependable. WMI is a part of the Windows platform I would recommend all systems engineers and programmers look into.

History

This project was developed about 11 months ago, and I have since moved onto other things. However, I plan to recode this in C++ so that WMI namespace security fits into my larger overall object security code I have written.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
United States United States
Software developer for the past 10 years in the Windows environment. Married, with two teenagers, and no money!

Comments and Discussions

 
Questioncannot start service named wampmysld thru wmi Pin
Member 102495104-Sep-13 4:32
Member 102495104-Sep-13 4:32 
GeneralMy vote of 5 Pin
venek31-Oct-11 3:45
venek31-Oct-11 3:45 
Generalproblem in geting sid Pin
parmjeet51516-Jun-11 23:58
parmjeet51516-Jun-11 23:58 
Generalremove user from wmi namespace Pin
olegiz26-Dec-10 23:18
olegiz26-Dec-10 23:18 
Generalexecute methods... Pin
Lee Dunsmoor3-Dec-10 4:05
Lee Dunsmoor3-Dec-10 4:05 
AnswerRe: execute methods... Pin
Luiz Pena23-Sep-15 3:09
Luiz Pena23-Sep-15 3:09 
QuestionPrevious permissions taken into account? Pin
alzoo7-Sep-09 1:58
alzoo7-Sep-09 1:58 
QuestionMultiple computers Pin
alzoo1-Sep-09 4:20
alzoo1-Sep-09 4:20 
AnswerRe: Multiple computers Pin
J_Madden2-Sep-09 3:34
J_Madden2-Sep-09 3:34 
GeneralRe: Multiple computers Pin
alzoo2-Sep-09 5:54
alzoo2-Sep-09 5:54 
GeneralError in converting ManagementBaseObject to byte[] Pin
shashankkadge29-Jul-09 3:52
shashankkadge29-Jul-09 3:52 
hi,
Thanks for this article and the source code.
I am trying to read Share Permissions on a windows XP machine. Following is the part of my code. The code works well expect when i use the ConvertSecurityDescriptorToStringSecurityDescriptor API.

ManagementScope scope = new ManagementScope(@"\\" + "mymachin" + @"\root\cimv2");

// Works when fileName is local directory, but not UNC path.
ManagementPath path = new ManagementPath();
path.RelativePath = @"win32_logicalsharesecuritysetting.name=" + "'" + @"TestHomeFolder" + "'";

ManagementObject fileSecurity = new ManagementObject(scope, path, null);

// When used with UNC path, exception with "Not Found" is thrown.
ManagementBaseObject outParams = (ManagementBaseObject)fileSecurity.InvokeMethod("GetSecurityDescriptor", null, null);

// Get security descriptor and DACL for specified file.
ManagementBaseObject descriptor = (ManagementBaseObject)outParams.Properties["Descriptor"].Value;
ManagementBaseObject[] dacl = (ManagementBaseObject[])descriptor.Properties["Dacl"].Value;

//part of code copied from J-Madden's article - start.

IntPtr pStringSD = IntPtr.Zero;
int iStringSDSize = 0;
string stringSD;

int iError = 0;bool bRes;

// following line gives error. "Unable to cast object of type 'System.Management.ManagementBaseObject' to type 'System.Byte[]'."

bRes = ConvertSecurityDescriptorToStringSecurityDescriptor((byte[])outParams["Descriptor"], 1, (int)SECURITY_INFORMATION.DACL_SECURITY_INFORMATION | (int)SECURITY_INFORMATION.OWNER_SECURITY_INFORMATION | (int)SECURITY_INFORMATION.GROUP_SECURITY_INFORMATION | (int)SECURITY_INFORMATION.SACL_SECURITY_INFORMATION, out pStringSD, out iStringSDSize);

if (!bRes)
{
iError = Marshal.GetLastWin32Error();
throw new Exception("ConvertSecurityDescriptorToStringSecurityDescriptor API Error: " + iError);
}

stringSD = Marshal.PtrToStringAuto(pStringSD);
MessageBox.Show(stringSD);

//part of code copied from J-Madden's article - end.

I would appreciate any help in getting the SDDL text string back.
I had tried the NetShareGetInfo API as well, but was not able to get the correct SDDL.
e.g int errCode = NetShareGetInfo(serverName, netName, 502, ref pBuffer);

thanks.
QuestionError Messages Pin
VinceJB25-Mar-09 4:39
VinceJB25-Mar-09 4:39 
AnswerRe: Error Messages Pin
J_Madden25-Mar-09 16:54
J_Madden25-Mar-09 16:54 
GeneralRe: Error Messages Pin
VinceJB25-Mar-09 23:20
VinceJB25-Mar-09 23:20 
GeneralRe: Error Messages Pin
J_Madden26-Mar-09 11:13
J_Madden26-Mar-09 11:13 
GeneralDownloads broken Pin
Ben Lye6-Jan-09 23:32
Ben Lye6-Jan-09 23:32 
GeneralRe: Downloads broken Pin
J_Madden7-Jan-09 5:12
J_Madden7-Jan-09 5:12 
GeneralRe: Downloads broken Pin
Ben Lye7-Jan-09 5:17
Ben Lye7-Jan-09 5:17 
GeneralTerms of Usage Pin
CeeDubbVA12-Sep-08 4:41
CeeDubbVA12-Sep-08 4:41 
GeneralRe: Terms of Usage Pin
J_Madden12-Sep-08 6:13
J_Madden12-Sep-08 6:13 
GeneralRe: Terms of Usage Pin
raghunath_b12-Sep-08 8:31
raghunath_b12-Sep-08 8:31 
GeneralRe: Terms of Usage Pin
coolshad7-Apr-09 2:49
coolshad7-Apr-09 2:49 
GeneralRe: Terms of Usage Pin
J_Madden7-Apr-09 4:26
J_Madden7-Apr-09 4:26 
GeneralRe: Terms of Usage Pin
coolshad7-Apr-09 4:30
coolshad7-Apr-09 4:30 
GeneralRe: Terms of Usage Pin
J_Madden7-Apr-09 5:05
J_Madden7-Apr-09 5:05 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.