Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

ScanX - A Registry Cleaner

4.95/5 (92 votes)
29 Jan 2012CPOL11 min read 194.9K   12.5K  
C#/WPF - ScanX: Creating a commercial quality Registry cleaner.

main.jpg

..and then he deleted something

..and his computer froze and never booted back up again. I love those stories about the Registry; the critical box gets hosed because Billy decides to start editing the Registry on the fly, network scripts that delete some vital key and take down an entire department ..ignorance is bliss I suppose - but not always.. and certainly not where it concerns the systems Registry.

The Registry though, is just a kind of hierarchal database, storing varied information on the Operating System and installed components. In the beginning, it was intended only to store configuration information for COM components, but later became a vehicle for peripheral application storage in an attempt to reduce reliance on .ini files, and now.. it seems we are moving in yet another direction, back towards modular design principles, with application data stored within the application assemblies. That not withstanding, the Registry will continue to be a core element in the Windows Operating System for some time to come.

So what does a Registry cleaner do, and do I really need one? Primarily, what it does is path testing, whether it be paths in the file system, or links between elements within the Registry itself. I realized this a few years ago, when it so happened that I needed a Registry cleaner.. A friend had a particularly nasty virus on her computer, one that disabled the anti-virus, and set itself up with system level access. I managed to remove it, but after deleting its components, the networking no longer worked. It had inserted itself into the networking stack, and without knowing what or where these entries were, I needed a Registry cleaner to fix it. The one I used cleared it right up (though without purchasing it, I had to manually delete the entries.. which in turn led me to writing the first version of this application). It was then that I began examining the match entries to try and figure out how these applications worked, and started understanding the complex relationship between various Registry entries.

I'm not going to bore you with some long essay on how the Registry works, there are plenty of tutorials on the internet, Wikipedia has a good one. What I will do, is give you a mile high view of how this application works, if you want to understand it better, or expand on this application; some serious debugging and reading of the class notes will be required..

Accessing the Registry using API

At the heart of this application is the Registry class cLightning, something I wrote in VB6 many years ago, and translated to C# as one of my first projects in this language. Most of the data types and Registry API in advapi.dll are accessible with this class. There are also a number of useful methods like RunAs, ShellOpen, Rot13, and access escalation routines.

Enumerating through hundreds of thousands of Registry keys requires a 'need for speed' approach, ergo the inbuilt Registry methods are simply out of the question (at least a 2:1 speed advantage using PInvoke). There are individual methods for each data type:

C#
 /// <summary>
/// Read Resource Descriptor data type
/// </summary>
/// <param name="RootKey">enum: root key</param>
/// <param name="SubKey">string: named subkey</param>
/// <param name="Value">string: named value</param>
/// <returns>string: value / empty on fail</returns>
public string ReadResourceDescriptor(ROOT_KEY RootKey, string SubKey, string Value)
{
    UIntPtr hKey = UIntPtr.Zero;
    uint pvSize = 1024;
    uint pdwType = (uint)VALUE_TYPE.REG_FULL_RESOURCE_DESCRIPTOR;
    byte[] bBuffer = new byte[1024];
    string sRet = String.Empty;

    try
    {
        if (RegOpenKeyEx(RootKey, SubKey, 0, KEY_READ, ref hKey) == ERROR_NONE)
        {
            if (RegQueryValueEx(hKey, Value, 0, ref pdwType, 
                                ref bBuffer[0], ref pvSize) == ERROR_NONE)
                for (int i = 0; i < (pvSize); i++)
                {
                    sRet += bBuffer[i].ToString();
                }
        }
        return sRet;
    }
    finally
    {
        if (hKey != UIntPtr.Zero)
            RegCloseKey(hKey);
    }
}

/// <summary>
/// Write a Resource Descriptor value
/// </summary>
/// <param name="RootKey">enum: root key</param>
/// <param name="SubKey">string: named subkey</param>
/// <param name="Value">string: named value</param>
/// <param name="Data">byte array: data</param>
/// <returns>bool</returns>
public bool WriteResourceDescriptor(ROOT_KEY RootKey, 
            string SubKey, string Value, byte[] Data)
{
    UIntPtr hKey = UIntPtr.Zero;
    uint pdwType = (uint)VALUE_TYPE.REG_FULL_RESOURCE_DESCRIPTOR;

    try
    {
        if (RegOpenKeyEx(RootKey, SubKey, 0, KEY_WRITE, ref hKey) == ERROR_NONE)
        {
            if (RegSetValueEx(hKey, Value, 0, pdwType, ref Data[0], 
                       (Data.GetUpperBound(0) + 1)) == ERROR_NONE)
                return true;
        }
        return false;
    }
    finally
    {
        if (hKey != UIntPtr.Zero)
            RegCloseKey(hKey);
    }
}

..and methods for collecting keys and values into an array:

C#
/// <summary>
/// Enumerate and collect keys
/// </summary>
/// <param name="RootKey">enum: root key</param>
/// <param name="SubKey">string: named subkey</param>
/// <returns>ArrayList</returns>
public ArrayList EnumKeys(ROOT_KEY RootKey, string SubKey)
{
    uint keyLen = 255;
    uint index = 0;
    int ret = 0;
    long lastWrite = 0;
    UIntPtr hKey = UIntPtr.Zero;
    StringBuilder keyName = new StringBuilder(255);
    ArrayList keyList = new ArrayList();

    try
    {
        if (RegOpenKeyEx(RootKey, SubKey, 0, KEY_ENUMERATE_SUB_KEYS, ref hKey) != ERROR_NONE)
            return keyList;
        do
        {
            keyLen = 255;
            ret = RegEnumKeyEx(hKey, index, keyName, ref keyLen, 
                      IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, out lastWrite);
            if (ret == ERROR_NONE)
            {
                keyList.Add(keyName.ToString());
            }
            index += 1;
        }
        while (ret == 0);
        return keyList;
    }
    finally
    {
        if (hKey != UIntPtr.Zero)
            RegCloseKey(hKey);
    }
}

There are also routines that delete keys and values, test key existence, test for empty values, etc.

Beating a path..

By far the most challenging portion of this project was file and directory path testing. This is because there are literally half a dozen different ways to list the same path. Unicode, psuedo paths, truncated, prefixed, postfixed.. an absolute nightmare. As you can imagine, it took some tweaking to eliminate false positives, first by testing the path with a Regular Expression, then if the file could not be found, going through a number of path conversions and extraction methods:

C#
private static Regex _regPath = new Regex(
    @"([?a-z A-Z]:.*\\)([?\w.]+)", RegexOptions.IgnoreCase |
        RegexOptions.CultureInvariant |
        RegexOptions.IgnorePatternWhitespace |
        RegexOptions.Compiled
    );

private string CleanPath(string Path)
{
    Match mc = _regPath.Match(Path);

    // test fast way first
    if (mc.Success && FileExists(mc.Groups[0].Value))
    {
        return mc.Groups[0].Value;
    }
    else
    {
        // extract path upon failure of regexp
        return ExtractPath(Path);
    }
}

private string ExtractPath(string Path)
{
    string sp = Path.ToUpper();

    // test path first
    if (!FileExists(sp) && IsFileCandidate(sp))
    {
        // trim to drive root
        if (sp.Substring(1, 1) != CHR_COLAN)
        {
            sp = sp.Substring(sp.IndexOf(CHR_COLAN) - 1);
        }
        // truncate leading path
        if (sp.Substring(3).Contains(STR_PATH))
        {
            sp = sp.Substring(sp.IndexOf(STR_PATH, 3) - 1);//PathFilter(path);
        }
        // find and trim to extension
        foreach (string s in _aExtensions)
        {
            if (sp.Contains(s))
            {
                sp = sp.Substring(0, sp.IndexOf(s) + s.Length);
                break;
            }
        }
        // get the long path
        if (sp.Contains(CHR_TILDE))
        {
            sp = GetLongName(sp);
        }
    }
    return sp;
}

There's actually a number of other methods not shown, used as safety valves to ensure the correct path is tested, something you should step through if interested..

Scan steps

There are 12 phases, and a number of subscans in the scanning process, as taken from the class notes in cRegScan:

Custom controls

Purpose: Test for valid software class registration and command paths (CLSID).

Scan 1 - CLSID

  1. Valid registration - HKEY_CLASSES_ROOT\CLSID\..\InProcSvr32(+)
  2. Typelib paths - HKEY_CLASSES_ROOT\..
  3. Appid paths - HKEY_CLASSES_ROOT\CLSID\... Val-AppID <-> HKEY_CLASSES_ROOT\AppID

Scan 2 - Interface

  1. Type lib paths - HKEY_CLASSES_ROOT\Interface\TypeLib <-> CLSID\TypeLib
  2. Interface paths - HKEY_CLASSES_ROOT\Interface\...\ProxyStubClsid32 <-> CLSID

Scan 3 - TypeLib

  1. Empty keys - HKEY_CLASSES_ROOT\TypeLib\..\HELPDIR
  2. Typelib paths - HKEY_CLASSES_ROOT\TypeLib\..\..\win32

Scan 4 - File extensions

  1. Empty ext keys - HKEY_CLASSES_ROOT\...

User software

  • Purpose: Test for valid software paths
  • Location: HKEY_CURRENT_USER\Software.
  • Method: Collect all software keys from \Software branch, and scan for valid path entries.

System software

Purpose: Scan 1-5 -System Software Test for valid ID registrations.

  1. Test class names - HKEY_CLASSES_ROOT\.xxx DefVal <-> HKEY_CLASSES_ROOT\..
  2. Test clsid - HKEY_CLASSES_ROOT\..\CLSID DefVal <-> HKEY_CLASSES_ROOT\CLSID
  3. Object menu path - HKEY_CLASSES_ROOT\..\shell\edit\command
  4. Object open path - HKEY_CLASSES_ROOT\..\shell\open\command
  5. Object icon path - HKEY_CLASSES_ROOT\..\DefaultIcon

Fonts

  • Location: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts.
  • References: From HKLM -> fonts folder.
  • Method: Path testing for valid occurrence.

Help files

  • Location: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Help.
  • Reference: From HKLM -> Help registration.
  • Method: Path testing for valid occurrence.

Shared DLLs

  • Location: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\SharedDLLs.
  • Reference: From HKLM -> path test.
  • Method: Path testing for valid occurrence.

Startup entries

  • Location: 1) HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
  • Location: 2) HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce
  • Location: 3) HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnceEx
  • References: From HKLM -> path test.
  • Method: Path testing for valid occurrence.

Installed software

  • Location: 1) HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall
  • References: From HKLM -> path test.
  • Method: Path testing for valid occurrence of uninstall strings.

Virtual devices

  • Location: 1) HKEY_LOCAL_MACHINE\SYSTEM\ControlSet\Control\VirtualDeviceDrivers
  • References: From HKLM -> fix for 16bit VDM value type mismatch.
  • Method: Value type testing for VDM bug.

History and Start menu

  • References: From HKLM -> scan for valid link paths.
  • Method: Value type testing for valid entry
  • Location: HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\MenuOrder\Start Menu\Programs
  • Location: HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\...\OpenWithList | OpenWithProgids
  • Location: HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\ComDlg32\OpenSaveMRU
  • Location: HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\UserAssist\...\Count

Deep scan

  • References: From HKLM -> scan for valid link paths
  • Method: Value type testing for valid entry
  • Location: HKEY_LOCAL_MACHINE\SOFTWARE
  • Location: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\...\Products

MRU lists

  • References: From HKCU -> scan for valid link paths
  • Method: Value type searchLocations: HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\RecentDocs
  • Locations: HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\TypedURLs
  • Locations: HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\RunMRU
  • Locations: HKEY_CURRENT_USER\Software\Microsoft\Search Assistant\ACMru\5603
  • Locations: HKEY_CURRENT_USER\Software\Microsoft\Search Assistant\ACMru\5001
  • Locations: HKEY_CURRENT_USER\Software\Microsoft\Search Assistant\ACMru\5647
  • Locations: HKEY_CURRENT_USER\"Software\Microsoft\Windows\CurrentVersion\Applets\Wordpad\Recent File List"
  • Locations: HKEY_CURRENT_USER\"Software\Microsoft\Windows\CurrentVersion\Applets\Regedit\Favorites"
  • Locations: HKEY_CURRENT_USER\"Software\Microsoft\Windows\CurrentVersion\Applets\Regedit"
  • Locations: HKEY_CURRENT_USER\"Software\Microsoft\Windows\CurrentVersion\Applets\Paint\Recent File List"
  • Locations: HKEY_CURRENT_USER\"Software\Microsoft\Windows\CurrentVersion\Explorer\ComDlg32\LastVisitedMRU"
  • Locations: HKEY_CURRENT_USER\"Software\Microsoft\Windows\CurrentVersion\Explorer\Wallpaper\MRU"
  • Locations: HKEY_CURRENT_USER\"Software\Microsoft\MediaPlayer\Player\RecentFileList"
  • Locations: HKEY_CURRENT_USER\"Software\Microsoft\MediaPlayer\Player\RecentURLList"

Yes, a little cryptic I know.. but basically all routines (except MRU, which is kind of a heuristic scan for MRU lists) are doing path testing in various areas of the Registry. The control scan is the most complicated, it tests for paths to shell/edit/default/icon paths and IDs under a CLSID subkey, it also tests for valid type, proxy, and interface registration UIDs associated with that class.

As an example, one of the control scan segments tests for registered type libraries under the HKEY_CLASSES_ROOT\Clsid{xxxx}\Typelib subkey:

clsid.jpg

The scanning engine compares this to the list of registered type libraries under the HKEY_CLASSES_ROOT\Typelib branch. If the type library UID is absent, the scan reports this as an invalid entry. The same method is used for AppId, ProxyStub, etc.

typelib.jpg

The best way to get a handle on what all this does, is just spending time both stepping through the methods and examining the paths in Regedit to try and make the logical connections.

UAC and Virtualization

In yet another patch meant to address 30 years of security complacency, we now have file and Registry Virtualization. Essentially, it just redirects write requests to more secure areas of the Registry or file system. This shouldn't affect the application scan though, because we are running under administrator credentials. This is just one of those apps that has to run as administrator, because many areas of the Registry are now write protected under Vista/W7. This is accomplished by changing the app.manifest requestedExecutionLevel to:

XML
<requestedExecutionLevel  level="requireAdministrator" uiAccess="false" />

The application will also need to be published with an external compiler, as the VS Publisher will throw an error with this setting (apparently by design).

Breaks like the wind..

There used to be a website that had a repository of trojans, virii, worms, etc., available for download (long gone, no doubt sued into oblivion), some exploits even had original source code. While writing a spyware scanner, I took the time to go through some of the more nefarious examples, learning what I could about how they worked. What I took away from that experience was that some of the original authors were real hard-core programmers, and that it took a great deal of knowledge of the low level Windows internals to pull off some of these exploits. So, I noticed while testing *this* application that some of the entries were not being deleted. Checking those entries permissions via Regedit, I saw something new, the 'Trusted Installer' [service] has full control of the key, with all other users, even System, having only read access. My first thought was.. did someone in Redmond finally figure out how to keep us from modifying selected NTFS objects, even the Administrator? So, I clicked on the owner tab, changed the ownership to the Administrators group, and gave myself back full control (and if you can do it in Regedit, you can do it in code). So, it makes me wonder.. who do they create these security controls for, I mean, if I (a humble amateur) can subvert this easily, what is one of those hard-core cracker dudes going to think?

Anyways.. the first thing you need to do, is change the object's ownership:

C#
public bool ChangeObjectOwnership(string ObjectName, SE_OBJECT_TYPE ObjectType)
{
    bool success = false;
    IntPtr pSidAdmin = IntPtr.Zero;
    IntPtr pAcl = IntPtr.Zero;
    string name = ObjectName;
    SID_IDENTIFIER_AUTHORITY sidNTAuthority = 
      new SID_IDENTIFIER_AUTHORITY() { Value = new byte[] { 0, 0, 0, 0, 0, 5 } };

    success = AllocateAndInitializeSid(ref sidNTAuthority, 
      (byte)2, SECURITY_BUILTIN_DOMAIN_RID, 
      DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, ref pSidAdmin);

    if (ObjectName.StartsWith("HKEY_CLASSES_ROOT"))
    {
        name = ObjectName.Replace("HKEY_CLASSES_ROOT", "CLASSES_ROOT");
    }
    else if (ObjectName.StartsWith("HKEY_CURRENT_USER"))
    {
        name = ObjectName.Replace("HKEY_CURRENT_USER", "CURRENT_USER");
    }
    else if (ObjectName.StartsWith("HKEY_LOCAL_MACHINE"))
    {
        name = ObjectName.Replace("HKEY_LOCAL_MACHINE", "MACHINE");
    }
    else if (ObjectName.StartsWith("HKEY_USERS"))
    {
        name = ObjectName.Replace("HKEY_USERS", "USERS");
    }

    if (success)
    {
        EXPLICIT_ACCESS[] explicitAccesss = new EXPLICIT_ACCESS[1];
        explicitAccesss[0].grfAccessPermissions = ACCESS_MASK.GENERIC_ALL;
        explicitAccesss[0].grfAccessMode = ACCESS_MODE.SET_ACCESS;
        explicitAccesss[0].grfInheritance = NO_INHERITANCE;
        explicitAccesss[0].Trustee.TrusteeForm = TRUSTEE_FORM.TRUSTEE_IS_SID;
        explicitAccesss[0].Trustee.TrusteeType = TRUSTEE_TYPE.TRUSTEE_IS_GROUP;
        explicitAccesss[0].Trustee.ptstrName = pSidAdmin;
        //modify dacl
        SetEntriesInAcl(1, ref explicitAccesss[0], IntPtr.Zero, out pAcl);

        success = SetPrivilege(SE_TAKE_OWNERSHIP_NAME, true);
        if (success)
        {
            // set admin as owner
            int p = SetNamedSecurityInfo(name, ObjectType, 
              SECURITY_INFORMATION.OWNER_SECURITY_INFORMATION, 
              pSidAdmin, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
            success = SetPrivilege(SE_TAKE_OWNERSHIP_NAME, false);
            if (success)
            {
                SetNamedSecurityInfo(name, ObjectType, 
                  SECURITY_INFORMATION.DACL_SECURITY_INFORMATION, 
                  IntPtr.Zero, IntPtr.Zero, pAcl, IntPtr.Zero);
            }
        }
    }

    if (pSidAdmin != IntPtr.Zero)
    {
        FreeSid(pSidAdmin);
    }
    if (pAcl != IntPtr.Zero)
    {
        LocalFree(pAcl);
    }
    return success;
}

private bool SetPrivilege(string privilege, bool allow)
{
    bool success = false;
    IntPtr token = IntPtr.Zero;
    TOKEN_PRIVILEGES tokenPrivileges = new TOKEN_PRIVILEGES();

    success = OpenProcessToken(GetCurrentProcess(), 
      (uint)TOKEN_PRIVILEGES_ENUM.TOKEN_ADJUST_PRIVILEGES | 
      (uint)TOKEN_PRIVILEGES_ENUM.TOKEN_QUERY, out token);
    if (success)
    {
        if (allow)
        {
            LUID luid;
            LookupPrivilegeValueA(null, privilege, out luid);
            tokenPrivileges.PrivilegeCount = 1;
            tokenPrivileges.Privileges = new LUID_AND_ATTRIBUTES[1];
            tokenPrivileges.Privileges[0].Luid = luid;
            tokenPrivileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
        }
        success = AdjustTokenPrivileges(token, false, ref tokenPrivileges, 0, 
                                        IntPtr.Zero, IntPtr.Zero);
    }
    if (token != IntPtr.Zero)
    {
        CloseHandle(token);
    }
    return success;
}

What we are doing here is modifying the access token of the current process to include the rights required to take ownership of an object, then changing the object's owner. This happens if a value or key fails to be deleted.

Now it's not just enough to own the object, you also require the rights to modify or delete the object. This is accomplished by rebuilding the object's DACL, or access control lists, a series of bitmasks compared to the users access token that determine the rights available to the process. The object's DACL is composed of ACE entries, or Access Control Entries, individual bitmasks for read/write/execute etc. So what we need to do is rebuild the DACL and apply the new list to the object.

First the setup, get the account information including the user's SID, and the handle to the subkey to be altered:

C#
public bool ChangeKeyPermissions(ROOT_KEY RootKey, string SubKey, string AccountName, 
       eRegistryAccess AccessMask, eAccessType AccessType, eInheritenceFlags Inheritence)
{
    // set key permissions (gate)
    IntPtr lKey = IntPtr.Zero;
    ACCOUNT_PERM tAccount = new ACCOUNT_PERM();
    SID_IDENTIFIER_AUTHORITY tAuthority = new SID_IDENTIFIER_AUTHORITY();

    try
    {
        // default account
        tAccount.AccountName = "";
        tAccount.AccessMask = (uint)ACCESS_MASK.GENERIC_READ;
        tAccount.AceFlags = (byte)CONTAINER_INHERIT_ACE;
        tAccount.AceType = (byte)ACCESS_ALLOWED_ACE_TYPE;
        tAccount.pSid = IntPtr.Zero;
        tAuthority.Value = new byte[] { 0, 0, 0, 0, 0, (byte)SECURITY_WORLD_SID_AUTHORITY };

        // test access
        if (AllocateAndInitializeSid(ref tAuthority, (byte)1, 
           (int)SECURITY_WORLD_RID, 0, 0, 0, 0, 0, 0, 0, ref tAccount.pSid) == true)
        {
            // set up account
            tAccount.AccountName = AccountName;
            tAccount.AccessMask = (uint)AccessMask;
            tAccount.AceFlags = (byte)Inheritence;
            tAccount.AceType = (byte)AccessType;
            tAccount.pSid = IntPtr.Zero;
            tAccount.SidPassedByCaller = false;
            // apply change to key
            if ((RegOpenKeyEx(RootKey, SubKey, 0, (int)(READ_CONTROL | WRITE_DAC), ref lKey) == 0))
            {
                return SetKeyPermissions(lKey, tAccount);
            }
        }
        return false;
    }
    finally
    {
        // cleanup
        if (lKey != IntPtr.Zero)
        {
            RegCloseKey(lKey);
        }
        if ((tAccount.pSid != IntPtr.Zero) && (tAccount.SidPassedByCaller == true))
        {
            FreeSid(tAccount.pSid);
            tAccount.pSid = IntPtr.Zero;
        }
    }
}

Then create the security descriptor, and apply it to the Registry key:

C#
private bool CreateKeyDescriptor(IntPtr pOldSD, ACCOUNT_PERM Account, ref MEM_DATA MemData)
{
    // reconstruct security descriptor
    bool bDefault = false;
    bool bPresent = false;
    int iControlBits = 0;
    int iControlSet = 0;
    int iDomain = 0;
    int iFlag = 0;
    int iLength = 0;
    int iRevision = 0;
    int iSidLen = 0;
    int iTotal = 0;
    int iUse = 0;
    StringBuilder sDomain = new StringBuilder(256);
    IntPtr pNewACL = IntPtr.Zero;
    IntPtr pAcl = IntPtr.Zero;
    IntPtr pSid = IntPtr.Zero;
    IntPtr pPnt = IntPtr.Zero;
    ACL_SIZE_INFORMATION tAclSize = new ACL_SIZE_INFORMATION();
    ACL tTempACL = new ACL();
    ACE tTempAce = new ACE();

    try
    {
        MemData.pAcl = IntPtr.Zero;
        MemData.pSd = IntPtr.Zero;
        // get size
        pSid = LocalAlloc(LMEM_INITIALIZED, SECURITY_DESCRIPTOR_MIN_LENGTH);
        if (pSid == IntPtr.Zero)
        {
            return false;
        }
        // store pointer
        MemData.pSd = pSid;

        // init descriptor
        if (InitializeSecurityDescriptor(pSid, SECURITY_DESCRIPTOR_REVISION) == 0)
        {
            return false;
        }

        // check for existing sd
        if (pOldSD != IntPtr.Zero)
        {
            if (GetSecurityDescriptorDacl(pOldSD, out bPresent, ref pAcl, out bDefault) == true)
            {
                // extract dacl
                if ((bPresent == true) && (pAcl != IntPtr.Zero))
                {
                    if (GetAclInformation(pAcl, ref tAclSize, Marshal.SizeOf(tAclSize), 
                          ACL_INFORMATION_CLASS.AclSizeInformation) == false)
                    {
                        return false;
                    }
                    else
                    {
                        iTotal = tAclSize.AclBytesInUse;
                    }
                }
                else
                {
                    iTotal = Marshal.SizeOf(tTempACL);
                }
            }
            else
            {
                return false;
            }
        }

        // allocate sid //
        // get callers sid
        if (Account.pSid == IntPtr.Zero)
        {
            iDomain = 256;
            // get size
            LookupAccountName(null, Account.AccountName, IntPtr.Zero, 
                                    ref iSidLen, sDomain, ref iDomain, out iUse);
            Account.pSid = LocalAlloc(LMEM_INITIALIZED, iSidLen);
            if (Account.pSid == IntPtr.Zero)
            {
                return false;
            }
            // get the sid
            if (LookupAccountName(null, Account.AccountName, Account.pSid, 
                        ref iSidLen, sDomain, ref iDomain, out iUse) == false)
            {
                return false;
            }
        }

        // ace buffer
        iLength = (Marshal.SizeOf(tTempAce) + GetLengthSid(Account.pSid)) - 4;
        iTotal += iLength;
        pNewACL = LocalAlloc(LMEM_INITIALIZED, iTotal);
        if (pNewACL == IntPtr.Zero)
        {
            return false;
        }
        // store pointer
        MemData.pAcl = pNewACL;

        // init acl
        if (InitializeAcl(pNewACL, iTotal, ACL_REVISION) == false)
        {
            return false;
        }

        // build dacl in sequence
        if (Account.AceType == ACCESS_DENIED_ACE_TYPE)
        {
            if (BuildACE(pNewACL, Account.AceType, Account.AceFlags, 
                            Account.AccessMask, Account.pSid) == false)
            {
                return false;
            }
        }

        // copy non-inherited ace
        if ((bPresent == true) && (pAcl != IntPtr.Zero) && (tAclSize.AceCount > 0))
        {
            // combine old and new ACE entries
            for (int count = 0; count < tAclSize.AceCount; count++)
            {
                // next ace
                GetAce(pAcl, count, out pPnt);
                if (pPnt == IntPtr.Zero)
                {
                    return false;
                }
                RtlMoveMemory(ref tTempAce, pPnt, Marshal.SizeOf(tTempAce));
                // exit on inherited ace
                if (((int)tTempAce.Header.AceFlags & INHERITED_ACE) == INHERITED_ACE)
                {
                    break;
                }
                int x = (int)pPnt + 8;
                IntPtr pPt = new IntPtr(x);
                // check ace value
                if (!SidIsEqual(Account.pSid, pPt))
                {
                    // add ace
                    AddAce(pNewACL, ACL_REVISION, MAXDWORD, pPnt, tTempAce.Header.AceSize);
                }
            }
        }

        // add explicit permit
        if (Account.AceType == ACCESS_ALLOWED_ACE_TYPE)
        {
            BuildACE(pNewACL, Account.AceType, Account.AceFlags, Account.AccessMask, Account.pSid);
        }

        // enties with inheritence flag
        if ((bPresent == true) && (pAcl != IntPtr.Zero) && (tAclSize.AceCount > 0))
        {
            for (int count = 0; count < tAclSize.AceCount; count++)
            {
                GetAce(pAcl, count, out pPnt);
                RtlMoveMemory(ref tTempAce, pPnt, Marshal.SizeOf(tTempAce));
                AddAce(pNewACL, ACL_REVISION, MAXDWORD, pPnt, tTempAce.Header.AceSize);
            }
        }

        // descriptor flags
        if (pOldSD != IntPtr.Zero)
        {
            if (GetSecurityDescriptorControl(pOldSD, out iFlag, out iRevision) != 0)
            {
                if ((iFlag & SE_DACL_AUTO_INHERITED) == SE_DACL_AUTO_INHERITED)
                {
                    iControlBits = SE_DACL_AUTO_INHERIT_REQ | SE_DACL_AUTO_INHERITED;
                    iControlSet = iControlBits;
                }
                else if ((iFlag & SE_DACL_PROTECTED) == SE_DACL_PROTECTED)
                {
                    iControlBits = SE_DACL_PROTECTED;
                    iControlSet = iControlBits;
                }
                if (iControlSet != 0)
                {
                    SetSecurityDescriptorControl(pSid, iControlBits, iControlSet);
                }
            }
        }
        // add dacl
        return SetSecurityDescriptorDacl(pSid, 1, pNewACL, 0);
    }

    finally
    {
        if (Account.pSid != IntPtr.Zero)
        {
            LocalFree(Account.pSid);
            Account.pSid = IntPtr.Zero;
        }
    }
}

private bool BuildACE(IntPtr lAclId, byte bType, byte bFlags, uint lMask, IntPtr lPointer)
{
    // build an ace entry
    int iAceLen = 0;
    int iSidLen = 0;
    IntPtr pAce = IntPtr.Zero;
    ACE tTempAce = new ACE();

    try
    {
        // get len
        iSidLen = GetLengthSid(lPointer);
        iAceLen = Marshal.SizeOf(tTempAce) + iSidLen - 4;
        // allocate space
        pAce = LocalAlloc(LMEM_INITIALIZED, iAceLen);

        if (pAce != IntPtr.Zero)
        {
            // ace struct
            tTempAce.Header = new ACE_HEADER();
            tTempAce.Header.AceType = bType;
            tTempAce.Header.AceFlags = bFlags;
            tTempAce.Header.AceSize = (short)iAceLen;
            tTempAce.Mask = (int)lMask;

            // copy to buffer
            int dDt = (int)pAce + 8;
            IntPtr pDt = new IntPtr(dDt);
            RtlMoveMemory(pAce, ref tTempAce, Marshal.SizeOf(tTempAce));
            RtlMoveMemory(pDt, lPointer, iSidLen);

            // add to acl
            return AddAce(lAclId, ACL_REVISION, MAXDWORD, pAce, iAceLen);
        }
        return false;
    }
    finally
    {
        if (pAce != IntPtr.Zero)
        {
            LocalFree(pAce);
        }
    }
}
private bool SidIsEqual(IntPtr pSid1, IntPtr pSid2)
{
    return (EqualSid(pSid1, pSid2));
}

Yes.. long spaghetti code is the order of the day.. but that's just how it's done, no simplified .NET methods for this kind of access, at least none that I am aware of.. With just a few changes, most NTFS objects can have their security descriptors modified, including files, named pipes, etc. If you want a hint at how this can be done, it's all in a VB6 project I wrote some years back, called NTMasterClass.

System restore

restore.jpg

Creating a restore point is probably a good idea for any application that makes serious modifications to the Registry, and included in the scan library is a class that can do just that. It's also a good idea to launch this on a background worker thread, and provide a visual indicator for the user:

C#
private void RestoreProgressStart()
{
    _dTime = DateTime.Now;
    _bRestoreComplete = false;
    _aRestoreTimer.IsEnabled = true;
    grdRestore.Visibility = Visibility.Visible;
    _pnlScanResults.lstResults.IsEnabled = false;
    UnLockControls(false);
    // launch restore on a new thread
    _oProcessAsyncBackgroundWorker = new BackgroundWorker();
    _oProcessAsyncBackgroundWorker.WorkerSupportsCancellation = true;
    _oProcessAsyncBackgroundWorker.DoWork += 
             new DoWorkEventHandler(_oProcessAsyncBackgroundWorker_DoWork);
    _oProcessAsyncBackgroundWorker.RunWorkerCompleted += 
      new RunWorkerCompletedEventHandler(_oProcessAsyncBackgroundWorker_RunWorkerCompleted);
    _oProcessAsyncBackgroundWorker.RunWorkerAsync();

    double safe = 0;
    do
    {
        DoEvents();
        _tTimeElapsed = DateTime.Now.Subtract(_dTime);
        safe = _tTimeElapsed.TotalSeconds;
        // break at 5 minutes, something has gone wrong
        if (safe > 300)
        {
            break;
        }
    } while (_bRestoreComplete != true);
}

The DoEvents method allows UI processing to continue while the system restore is in progress, the loop is a form of wait timer that exits when the StartRestore method returns.

C#
private static void DoEvents()
{
    DispatcherFrame frame = new DispatcherFrame(true);
    Dispatcher.CurrentDispatcher.BeginInvoke(
    DispatcherPriority.Background, (SendOrPostCallback)delegate(object arg)
    {
        var f = arg as DispatcherFrame;
        f.Continue = false;
    },
    frame
    );
    Dispatcher.PushFrame(frame);
}

The restore class itself is completely portable, and can be dropped into other applications.

C#
public class cRestore
{
    #region Constants
    private const int APPLICATION_INSTALL = 0;
    private const int APPLICATION_UNINSTALL = 1;
    private const int DEVICE_DRIVER_INSTALL = 10;
    private const int MODIFY_SETTINGS = 12;
    private const int CANCELLED_OPERATION = 13;
    private const int FO_DELETE = 0x3;
    private const int FOF_ALLOWUNDO = 0x40;
    private const int FOF_NOCONFIRMATION = 0x10;
    private const int BEGIN_SYSTEM_CHANGE = 100;
    private const int END_SYSTEM_CHANGE = 101;
    private const int BEGIN_NESTED_SYSTEM_CHANGE = 102;
    private const int END_NESTED_SYSTEM_CHANGE = 103;
    private const int DESKTOP_SETTING = 2;
    private const int ACCESSIBILITY_SETTING = 3;
    private const int OE_SETTING = 4;
    private const int APPLICATION_RUN = 5;
    private const int WINDOWS_SHUTDOWN = 8;
    private const int WINDOWS_BOOT = 9;
    private const int MAX_DESC = 64;
    private const int MAX_DESC_W = 256;
    private const string RESTORE_KEY = 
      @"Software\Microsoft\Windows NT\CurrentVersion\SystemRestore";
    private const string RESTORE_VALUE = 
      @"SystemRestorePointCreationFrequency";
    #endregion

    #region Enum
    private enum RESTORE_TYPE
    {
        APPLICATION_INSTALL = 0,
        APPLICATION_UNINSTALL = 1,
        MODIFY_SETTINGS = 12,
        CANCELLED_OPERATION = 13,
        RESTORE = 6,

        CHECKPOINT = 7,
        DEVICE_DRIVER_INSTALL = 10,
        FIRSTRUN = 11,
        BACKUP_RECOVERY = 14,
    }
    #endregion

    #region Structs
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    internal struct RESTOREPTINFO 
    {
        public int dwEventType;
        public int dwRestorePtType;
        public Int64 llSequenceNumber;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAX_DESC_W + 1)]
        public string szDescription;
    }

    [StructLayout(LayoutKind.Sequential)]
    internal struct SMGRSTATUS 
    {
        public int nStatus;
        public Int64 llSequenceNumber;
    }
    #endregion

    #region API
    [DllImport("srclient.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool SRSetRestorePointW(ref RESTOREPTINFO pRestorePtSpec, 
                   out SMGRSTATUS pSMgrStatus);
    #endregion

    #region Fields
    private long _lSeqNum = 0;
    private int _iRestInt = 0;
    #endregion

    #region Methods
    public bool StartRestore(string Description)
    {
        int maj = Environment.OSVersion.Version.Major;
        int min = Environment.OSVersion.Version.Minor;
        RESTOREPTINFO tRPI = new RESTOREPTINFO();
        SMGRSTATUS tStatus = new SMGRSTATUS();
        
        // compatability
        if (!(maj == 4 && min == 90 || maj > 4))
        {
            return false;
        }

        tRPI.dwEventType = BEGIN_SYSTEM_CHANGE;
        tRPI.dwRestorePtType = (int)RESTORE_TYPE.MODIFY_SETTINGS;
        tRPI.llSequenceNumber = 0;
        tRPI.szDescription = Description;

        // test for key that defines multiple restores per cycle
        cLightning cl = new cLightning();
        if (cl.ValueExists(cLightning.ROOT_KEY.HKEY_LOCAL_MACHINE, 
                           RESTORE_KEY, RESTORE_VALUE))
        {
            _iRestInt = cl.ReadDword(cLightning.ROOT_KEY.HKEY_LOCAL_MACHINE, 
                                     RESTORE_KEY, RESTORE_VALUE);
        }
        // set to 2 minutes
        cl.WriteDword(cLightning.ROOT_KEY.HKEY_LOCAL_MACHINE, RESTORE_KEY, RESTORE_VALUE, 2);
        if (SRSetRestorePointW(ref tRPI, out tStatus))
        {
            _lSeqNum = tStatus.llSequenceNumber;
            return true;
        }
        return false;
    }

    public bool EndRestore(bool Cancel)
    {
        RESTOREPTINFO tRPI = new RESTOREPTINFO();
        SMGRSTATUS tStatus = new SMGRSTATUS();
        bool success = false;

        tRPI.dwEventType = END_SYSTEM_CHANGE;
        tRPI.llSequenceNumber = _lSeqNum;

        if (Cancel == true)
        {
            tRPI.dwRestorePtType = CANCELLED_OPERATION;
        }

        try
        {
            success = (SRSetRestorePointW(ref tRPI, out tStatus));
        }
        finally 
        {
            // reset
            cLightning cl = new cLightning();
            cl.WriteDword(cLightning.ROOT_KEY.HKEY_LOCAL_MACHINE, 
                          RESTORE_KEY, RESTORE_VALUE, _iRestInt);
        }
        return success;
    }
    #endregion
}

Visuals

progress.jpg

The circular progress bar was adapted from a Silverlight example by Colin Eberhardt and made into a nifty little user control.

items.jpg

The progressbar in the list tems was fashioned by overlapping image brushes:

XML
 <ProgressBar Maximum="10" Minimum="1" Value="{Binding Scope}" Width="72" Height="14" Margin="2">
    <ProgressBar.Template>
        <ControlTemplate>
            <Grid>
                <Rectangle Name="PART_Track" HorizontalAlignment="Left" 
                        VerticalAlignment="Top" Margin="1" Height="14" Width="66">
                    <Rectangle.Fill>
                        <ImageBrush ImageSource="..\Images\starsbg.png" 
                             Stretch="None" AlignmentX="Left" AlignmentY="Top" />
                    </Rectangle.Fill>
                </Rectangle>
                <Rectangle Name="PART_Indicator" HorizontalAlignment="Left" 
                        VerticalAlignment="Top"  Margin="1" Height="14">
                    <Rectangle.Fill>
                        <ImageBrush ImageSource="..\Images\stars.png" 
                              Stretch="None" AlignmentX="Left" AlignmentY="Top" />
                    </Rectangle.Fill>
                </Rectangle>
            </Grid>
        </ControlTemplate>
    </ProgressBar.Template>
</ProgressBar>

The rest is just style and brushes..

Final thoughts..

Though I have tested this on several different machines (Vista/W7) with no problems or surprises, remember, it is beta.. though I think even in a worst case scenario, you might have to run system restore.. just don't let Billy play with your Regedit ;0)

Updates

  • Dec 20, 2011 -Added a visual to system restore process, which is now launching as a background thread.
  • Dec 31, 2011 -Added updated code and expanded article.
  • Jan 1, 2012 - Did a heap of bug fixing, so it's pretty much done now (perhaps a couple of cosmetic todo's), If you find a bug, shout out (and if you're nice, I might even fix it.. ;o).
  • Jan 4, 2012 - Tested on Vista64. Changes made to support 64 bit OS scan. Bug found in API declaration in cSecurity caused memory error in 64. Fixed parse error in CircularProgressBar::GridUtils.
  • Jan 29, 2011 - Some small bugs fixed, changed iconography slightly.

License

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