diff --git a/Functions.h b/Functions.h index c492452..7e33172 100644 --- a/Functions.h +++ b/Functions.h @@ -13,8 +13,20 @@ std::wstring GetDomainNameFromSid(const PSID tSid); std::wstring GenerateAccessMask(DWORD iCurrentMask); std::wstring GenerateInheritanceFlags(DWORD iCurrentFlags); HANDLE RegisterFileHandle(HANDLE hFile, std::wstring sOperation); -bool CheckIfAntivirusIsActive(); +std::wstring GetAntivirusStateDescription(); std::wstring FileTimeToString(LPFILETIME tFileTime); BOOL WriteToFile(std::wstring & sStringToWrite, HANDLE hFile); +// helper typedefs +typedef struct SidCompare +{ + inline bool operator()(PSID p1, PSID p2) const + { + const DWORD iLength1 = GetLengthSid(p1); + const DWORD iLength2 = GetLengthSid(p2); + if (iLength1 != iLength2) return iLength1 < iLength2; + return memcmp(p1, p2, iLength1) > 0; + } +} +SidCompare; diff --git a/Helpers.cpp b/Helpers.cpp index 1a0aee2..a50af17 100644 --- a/Helpers.cpp +++ b/Helpers.cpp @@ -18,18 +18,6 @@ #include "Operation.h" #include "Functions.h" -typedef struct SidCompare -{ - inline bool operator()(PSID p1, PSID p2) const - { - const DWORD iLength1 = GetLengthSid(p1); - const DWORD iLength2 = GetLengthSid(p2); - if (iLength1 != iLength2) return iLength1 < iLength2; - return memcmp(p1, p2, iLength1) > 0; - } -} -SidCompare; - const PSID GetSidFromName(std::wstring & sAccountName) { // for caching @@ -46,7 +34,7 @@ const PSID GetSidFromName(std::wstring & sAccountName) } } - // first see if the name look like a sid + // first see if the name looks like a sid PSID tSidFromSid; if (ConvertStringSidToSid(sAccountName.c_str(), &tSidFromSid) != 0) { @@ -95,7 +83,7 @@ std::wstring GetNameFromSid(const PSID tSid, bool * bMarkAsOrphan) // scope lock for thread safety { std::shared_lock oLock(oMutex); - std::map::iterator oInteractor = oSidToNameLookup.find(tSid); + std::map::iterator oInteractor = oSidToNameLookup.find(tSid); if (oInteractor != oSidToNameLookup.end()) { // if blank that means the account has no associated same @@ -276,7 +264,8 @@ VOID EnablePrivs() return; } - WCHAR * sPrivsToSet[] = { SE_RESTORE_NAME, SE_BACKUP_NAME, SE_TAKE_OWNERSHIP_NAME, SE_SECURITY_NAME }; + WCHAR * sPrivsToSet[] = { SE_RESTORE_NAME, SE_BACKUP_NAME, + SE_TAKE_OWNERSHIP_NAME, SE_CHANGE_NOTIFY_NAME }; for (int i = 0; i < sizeof(sPrivsToSet) / sizeof(WCHAR *); i++) { // populate the privilege adjustment structure @@ -397,7 +386,7 @@ HANDLE RegisterFileHandle(HANDLE hFile, std::wstring sOperation) } } -bool CheckIfAntivirusIsActive() +std::wstring GetAntivirusStateDescription() { // initialize COM for checking the antivirus status HRESULT hResult = CoInitializeEx(0, COINIT_APARTMENTTHREADED); @@ -407,21 +396,21 @@ bool CheckIfAntivirusIsActive() } // assume not installed by default - bool bIsInstalled = false; + bool bIsEnabled = false; // query the product list IWSCProductList * PtrProductList = nullptr; if (FAILED(CoCreateInstance(__uuidof(WSCProductList), NULL, CLSCTX_INPROC_SERVER, __uuidof(IWSCProductList), reinterpret_cast (&PtrProductList)))) { - return false; + return L"Unknown"; } // initialize the antivirus provider list if (FAILED(PtrProductList->Initialize(WSC_SECURITY_PROVIDER_ANTIVIRUS))) { PtrProductList->Release(); - return false; + return L"Unknown"; } // get the current product count @@ -429,7 +418,7 @@ bool CheckIfAntivirusIsActive() if (FAILED(PtrProductList->get_Count(&ProductCount))) { PtrProductList->Release(); - return false; + return L"Unknown"; } for (LONG i = 0; i < ProductCount; i++) @@ -439,7 +428,7 @@ bool CheckIfAntivirusIsActive() if (FAILED(PtrProductList->get_Item(i, &PtrProduct))) { PtrProductList->Release(); - return false; + return L"Unknown"; } // fetch the product state @@ -448,16 +437,16 @@ bool CheckIfAntivirusIsActive() { PtrProduct->Release(); PtrProductList->Release(); - return false; + return L"Unknown"; } - bIsInstalled |= (ProductState == WSC_SECURITY_PRODUCT_STATE_ON); + bIsEnabled |= (ProductState == WSC_SECURITY_PRODUCT_STATE_ON); PtrProduct->Release(); } // return status PtrProductList->Release(); - return bIsInstalled; + return (bIsEnabled) ? L"On" : L"Off"; } std::wstring FileTimeToString(LPFILETIME tFileTime) @@ -468,9 +457,9 @@ std::wstring FileTimeToString(LPFILETIME tFileTime) // convert the date to a string and return WCHAR sTime[24]; - GetDateFormatEx(LOCALE_NAME_INVARIANT, LOCALE_USE_CP_ACP, NULL, + GetDateFormatEx(LOCALE_NAME_INVARIANT, LOCALE_USE_CP_ACP, &tTime, L"yyyy'-'MM'-'dd ", sTime, _countof(sTime), NULL); - GetTimeFormatEx(LOCALE_NAME_INVARIANT, LOCALE_USE_CP_ACP, NULL, + GetTimeFormatEx(LOCALE_NAME_INVARIANT, LOCALE_USE_CP_ACP, &tTime, L"HH':'mm':'ss", sTime + wcslen(sTime), (int) (_countof(sTime) - wcslen(sTime))); return std::wstring(sTime); diff --git a/Main.cpp b/Main.cpp index 6d3e675..8d5a983 100644 --- a/Main.cpp +++ b/Main.cpp @@ -516,7 +516,7 @@ int wmain(int iArgs, WCHAR * aArgs[]) wprintf(L"= Scan Path(s): %s\n", (*sScanPath).c_str()); wprintf(L"= Maximum Threads: %d\n", (int)InputOutput::MaxThreads()); wprintf(L"= What If Mode: %s\n", InputOutput::InWhatIfMode() ? L"Yes" : L"No"); - wprintf(L"= Antivirus Active: %s\n", CheckIfAntivirusIsActive() ? L"Yes" : L"No"); + wprintf(L"= Antivirus Active: %s\n", GetAntivirusStateDescription().c_str()); wprintf(L"===============================================================================\n"); // do the scan diff --git a/OperationHelp.cpp b/OperationHelp.cpp index 69ddce3..10136dc 100644 --- a/OperationHelp.cpp +++ b/OperationHelp.cpp @@ -15,17 +15,17 @@ OperationHelp::OperationHelp(std::queue & oArgList) : Operation(oA LR"( repacls.exe /Path ... other options .... -Repacls was developed to address large scale migrations, transitions, health -checks, and access control optimizations. Repacls is multi-threaded and -employs account name caching to accelerate operation on large file servers -with millions of files. It was developed to address a variety of platform -limitations, resource consumption concerns, and bugs within xcacls, icacls, -setacl, and subinacl. +Repacls was developed to address large scale migrations, transitions, health +checks, and access control optimizations. Repacls is multi-threaded and +employs account name caching to accelerate operation on large file servers +with millions of files. It was developed to address a variety of platform +limitations, resource consumption concerns, and bugs within xcacls, icacls, +setacl, and subinacl. -Important: Unless otherwise specified, all repacls commands are recursive. +Important: Unless otherwise specified, all repacls commands are recursive. -Repacls must be executed with administrator permissions and will attempt to -acquire the backup, restore, and take ownership privileges during its +Repacls must be executed with administrator permissions and will attempt to +acquire the backup, restore, and take ownership privileges during its execution. Any command line parameter that accepts an account or domain name can also use @@ -40,7 +40,7 @@ or end of your command as to not confuse them with ordered parameters. /Path Specifies the file or directory to process. If a directory, the directory - is processed recursively; all operations specified affect the directory + is processed recursively; all operations specified affect the directory and all files and folders under the directory (unless otherwise specified). This parameter is mandatory. This command can be specified multiple times. @@ -48,69 +48,69 @@ or end of your command as to not confuse them with ordered parameters. Specifies a server that has one or more shares to process. This command is equivalent to specifying /Path for every share on a particular file server. By default, only non-administrative, non-hidden shares are scanned. - To only scan administrative shares (e.g. C$), append :AdminOnly to the - computer name. To include hidden, non-administrative shares, append - :IncludeHidden to the computer name. By appending :Match= or :NoMatch= - with a literal string or regular expression, any share name that does not + To only scan administrative shares (e.g. C$), append :AdminOnly to the + computer name. To include hidden, non-administrative shares, append + :IncludeHidden to the computer name. By appending :Match= or :NoMatch= + with a literal string or regular expression, any share name that does not match or mismatch the specified string, respectively, will be excluded. /DomainPaths [:StopOnError|] - Specifies a domain to scan for member servers that should be processed. + Specifies a domain to scan for member servers that should be processed. For each server that is found, a /SharePaths command is processed for that particular server. This takes the same extra parameters as - /SharePaths including another option StopOnError to stop processing if - the shares of any particular computer can not be read; if not specified + /SharePaths including another option StopOnError to stop processing if + the shares of any particular computer cannot be read; if not specified an error will be shown on the screen but processing will continue. /Quiet Hides all non-error output. This option will greatly enhance performance if - a large number of changes are being processed. Alternatively, it is - advisable to redirect console output to a file (using the > redirector) if + a large number of changes are being processed. Alternatively, it is + advisable to redirect console output to a file (using the > redirector) if /Quiet cannot be specified. /Threads Specifies the number of threads to use while processing. The default value - of '5' is usually adequate, but can be increased if performing changes - over a higher-latency connection. Since changes to a parent directory + of '5' is usually adequate, but can be increased if performing changes + over a higher-latency connection. Since changes to a parent directory often affect the inherited security on children, the security of children objects are always processed after the security on their parent objects - are fully processed. + are fully processed. /WhatIf - This option will analyze security and report on any potential changes - without actually committing the data. Use of /WhatIf is recommended for + This option will analyze security and report on any potential changes + without actually committing the data. Use of /WhatIf is recommended for those first using the tool. /NoHiddenSystem Use this option to avoid processing any file marked as both 'hidden' and - 'system'. These are what Windows refers to 'operating system protected + 'system'. These are what Windows refers to 'operating system protected files' in Windows Explorer. Ordered Options =============== Ordered Options are executed on each SID encountered in the security descriptor -in the order they are specified on the command line. Executing commands in -this way is preferable to multiple commands because the security descriptor is -only read and written once for the entire command which is especially helpful +in the order they are specified on the command line. Executing commands in +this way is preferable to multiple commands because the security descriptor is +only read and written once for the entire command which is especially helpful for large volumes. Commands That Do Not Alter Security ----------------------------------- /PrintDescriptor Prints out the security descriptor to the screen. This is somewhat useful - for seeing the under-the-cover changes to the security descriptor before + for seeing the under-the-covers changes to the security descriptor before and after a particular command. /CheckCanonical - This command inspects the DACL and SACL for canonical order compliance - (i.e., the rules in the ACL are ordered as explicitly deny, explicitly allow, + This command inspects the DACL and SACL for canonical order compliance + (i.e., the rules in the ACL are ordered as explicitly deny, explicitly allow, inherited deny, inherited allow). If non-canonical entries are detected, it is recommended to inspect the ACL with icacls.exe or Windows Explorer to ensure the ACL is not corrupted in a more significant way. /BackupSecurity - Export the security descriptor to the file specified. The file is - outputted in the format of file|descriptor on each line. The security + Export the security descriptor to the file specified. The file is + outputted in the format of file|descriptor on each line. The security descriptor is formatted as specified in the documentation for ConvertDescriptorToStringSecurityDescriptor(). This command does not print informational messages other than errors. @@ -119,143 +119,147 @@ Commands That Do Not Alter Security Reports any instance of an account specified. /FindDomain - Reports any instance of an account matching the specified domain. + Reports any instance of an account matching the specified domain. /FindNullAcl Reports any instance of a null ACL. A null ACL, unlike an empty ACL, allows - all access (i.e., similar to an ACE with 'Everyone' with 'Full Control') + all access (i.e., similar to an ACE with 'Everyone' with 'Full Control') /Locate This command will write a comma separated value file with the fields of filename, creation time, file modified time, file size and file attributes. - The regular expression will perform a case insensitive regular expression - search against file name or directory name. To report all data, pass .* + The regular expression will perform a case insensitive regular expression + search against file name or directory name. To report all data, pass .* as the regular expression. /Report This command will write a comma separated value file with the fields of filename, security descriptor part (e.g., DACL), account name, permissions, and inheritance flags. The regular expression will perform a case - insensitive regular expression search against the account name in + insensitive regular expression search against the account name in DOMAIN\user format. To report all data, pass .* as the regular expression. An optional qualifier after regular expression can be specified after the regular expression to refine what part of the security descriptor to scan. See Other Notes & Limitations section for more information. -Commands That Can Alter Security (When /WhatIf Is Not Present) +Commands That Can Alter Security (When /WhatIf Is Not Present) -------------------------------- /AddAccountIfMissing - This command will ensure the account specified has full control access to + This command will ensure the account specified has full control access to path and all directories/files under the path either via explicit or inherited permissions. This command does not take into account any - permissions that would be granted to the specified user via group + permissions that would be granted to the specified user via group membership. If the account does not have access, access is - granted. This command is useful to correct issues where a user or + granted. This command is useful to correct issues where a user or administrator has mistakenly removed an administrative group from some directories. /Compact - This command will look for mergeable entires in the security descriptor and + This command will look for mergeable entries in the security descriptor and merge them. For example, running icacls.exe /grant Everyone:R followed by icacls.exe /grant Everyone:(CI)(OI)(R) will produce - two entries even those the second command supersedes the first one. - Windows Explorer automatically merges these entries when display security - information so you have to use other utilities to detect these - inefficiencies. While these's nothing inherently wrong with these - entires, it possible for them to result file system is performance - degradation. + two entries even those the second command supersedes the first one. + Windows Explorer automatically merges these entries when display security + information so you have to use other utilities to detect these + inefficiencies. While there's nothing inherently wrong with these + entries, it possible for them to result file system is performance + degradation. /CopyDomain : - This command is identical to /MoveDomain except that the original - entry referring the SourceDomainName is retained instead of replaced. + This command is identical to /MoveDomain except that the original + entry referring the SourceDomainName is retained instead of replaced. This command only applies to the SACL and the DACL. If this command is used multiple times, it is recommended to use /Compact to ensure there are not any redundant access control entries. )"; -std::wcout << -LR"( + std::wcout << + LR"( /MoveDomain : This command will look to see whether any account in has an identically-named account in . If so, any entires are converted to use the new domain. For example, 'OldDomain\Domain Admins' would become 'NewDomain\Domain Admins'. Since - this operation relies on the names being resolvable, specifying a SID - instead of domain name for this command does not work. + this operation relies on the names being resolvable, specifying a SID + instead of domain name for this command does not work. /RemoveAccount Will remove from the security descriptor. If the specified name - is found as the file owner, the owner is replaced by the builtin + is found as the file owner, the owner is replaced by the built-in Administrators group. If the specified name is found as the group owner - (a defunct attribute that has no function in terms of security), it is + (a defunct attribute that has no function in terms of security), it is also replace with the built-in Administrators group. /RemoveOrphans Remove any account whose SID is derived from the specified - and can no longer be resolved to a valid name + and can no longer be resolved to a valid name. /RemoveRedundant This command will remove any explicit permission that is redundant of of the permissions its already given through inheritance. This option helps recovered from the many individual explicit permissions that may - have been littered from the old cacls.exe command that didn't understand + have been littered from the old cacls.exe command that didn't understand how to set up inheritance. -/ReplaceAccount : +/ReplaceAccount : Search for an account and replace it with another account. +/ReplaceMap + This command will read in the specified file that contains a list of + account mappings that are specified in the same format as /ReplaceAccount. + /RestoreSecurity The reverse operation of /BackupSecurity. Takes the file name and security descriptors specified in the file specified and applies them to the file - system. This command does not print informational messages other than + system. This command does not print informational messages other than errors. /SetOwner Will set the owner of the file to the name specified. /UpdateHistoricalSids - Will update any SIDs that present in the security descriptor and are part - of a SID history with the primary SID that is associated an account. This + Will update any SIDs that present in the security descriptor and are part + of a SID history with the primary SID that is associated an account. This is especially useful after a domain migration and prior to removing - excess SID history on accounts. + excess SID history on accounts. Exclusive Options ================= Exclusive options cannot be combined with any other security operations. -/Help or /? or /H +/Help or /? or /H Shows this information. /ResetChildren This will reset all children of path to the to inherit from the parent. It - will not affect the security of the parent. This command does not affect + will not affect the security of the parent. This command does not affect the security the root directory as specified by the /Path argument. /InheritChildren This will cause any parent that is currently set to block inheritance to start allowing inheritance. Any explicit entries on the children are - preserved. This command does not will not affect the security the root + preserved. This command does not will not affect the security the root directory as specified by the /Path argument. Other Notes & Limitations ========================= -- To only affect a particular part of a security descriptor, you can add on an +- To only affect a particular part of a security descriptor, you can add on an optional ':X' parameter after the end of the account name where X is a comma separated list of DACL,SACL, OWNER, or GROUP. For example, '/RemoveAccount "DOM\joe:DACL,OWNER"' will only cause the designated account - to be removed from the DACL and OWNER parts of the security descriptor. + to be removed from the DACL and OWNER parts of the security descriptor. -- Since repacls is multi-threaded, any file output shown on the screen or - written to an output file may appear differently between executions. In this - is problematic for your needs, you can turn off multi-threading by setting - /Threads to '1' or, in the case of comparing files between runs, sort the +- Since repacls is multi-threaded, any file output shown on the screen or + written to an output file may appear differently between executions. If this + is problematic for your needs, you can turn off multi-threading by setting + /Threads to '1' or, in the case of comparing files between runs, sort the output before comparing with your favorite text editor. - Antivirus applications can degrade performance tremendously if active while running repacls. If performance is a concern and you are processing a large - volume, you may want to consider temporarily disabling realtime virus - scanning. Antivirus status is noted when executing repacls. - + volume, you may want to consider temporarily disabling real-time virus + scanning. + Examples ======== - Replace all instances of DOM\jack to DOM\jill in C:\test: @@ -265,7 +269,7 @@ Examples names in DOMA with DOMB: repacls.exe /Path C:\Test /MoveDomain DOMA:DOMB -- Update old SID references, remove any explicit permissions that are already +- Update old SID references, remove any explicit permissions that are already granted by inherited permissions, and compact all ACLs if not compacted: repacls.exe /Path C:\Test /UpdateHistoricalSids /RemoveRedundant /Compact diff --git a/OperationLocate.cpp b/OperationLocate.cpp index d7b9dbd..592c735 100644 --- a/OperationLocate.cpp +++ b/OperationLocate.cpp @@ -74,7 +74,10 @@ void OperationLocate::ProcessObjectAction(ObjectEntry & tObjectEntry) // fetch file attribute data WIN32_FILE_ATTRIBUTE_DATA tData; - GetFileAttributesExW(tObjectEntry.Name.c_str(), GetFileExInfoStandard, &tData); + if (GetFileAttributesExW(tObjectEntry.Name.c_str(), GetFileExInfoStandard, &tData) == 0) + { + InputOutput::AddError(L"ERROR: Unable to read file attributes."); + } // convert the file size to a string WCHAR sSize[32]; diff --git a/OperationReplaceMap.cpp b/OperationReplaceMap.cpp new file mode 100644 index 0000000..2e93613 --- /dev/null +++ b/OperationReplaceMap.cpp @@ -0,0 +1,83 @@ +#include "OperationReplaceMap.h" +#include "InputOutput.h" +#include "Functions.h" + +#include +#include +#include +#include + +ClassFactory * OperationReplaceMap::RegisteredFactory = +new ClassFactory(GetCommand()); + +OperationReplaceMap::OperationReplaceMap(std::queue & oArgList) : Operation(oArgList) +{ + // exit if there are not enough arguments to part + std::vector sSubArgs = ProcessAndCheckArgs(1, oArgList, L"\\0"); + + // open the file + std::wifstream fFile(sSubArgs[0].c_str()); + + // adapt the stream to read windows unicode files + fFile.imbue(std::locale(fFile.getloc(), new std::codecvt_utf8)); + + // read the file line-by-line + std::wstring sLine; + while (std::getline(fFile, sLine)) + { + // parse the search and replace account which are separated by a '=' character + // also, sometimes a carriage return appears in the input stream so adding + // it here ensures it is stripped from the very end + std::vector oLineItems = SplitArgs(sLine, L":|\r"); + + // verify the line contains at least two elements + if (oLineItems.size() != 2) + { + wprintf(L"ERROR: The replacement map line '%s' is invalid.", sLine.c_str()); + exit(-1); + } + + // verify search sid + const PSID tSearchSid = GetSidFromName(oLineItems.at(0)); + if (tSearchSid == nullptr) + { + wprintf(L"ERROR: The map search value '%s' is invalid.", oLineItems.at(0).c_str()); + exit(-1); + } + + // verify replace sid + const PSID tReplaceSid = GetSidFromName(oLineItems.at(1)); + if (tReplaceSid == nullptr) + { + wprintf(L"ERROR: The map replace value '%s' is invalid.", oLineItems.at(1).c_str()); + exit(-1); + } + + // update the map + oReplaceMap[tSearchSid] = tReplaceSid; + } + + // cleanup + fFile.close(); + + // flag this as being an ace-level action + AppliesToDacl = true; + AppliesToSacl = true; + AppliesToGroup = true; + AppliesToOwner = true; +} + +SidActionResult OperationReplaceMap::DetermineSid(WCHAR * const sSdPart, ObjectEntry & tObjectEntry, PSID const tCurrentSid, PSID & tResultantSid) +{ + // check if the sid matches the ace + std::map::iterator oInteractor = oReplaceMap.find(tCurrentSid); + if (oInteractor == oReplaceMap.end()) return SidActionResult::Nothing; + + // return the replacement sid + std::wstring sSearchAccount = GetNameFromSidEx(oInteractor->first); + std::wstring sReplaceAccount = GetNameFromSidEx(oInteractor->second); + InputOutput::AddInfo(L"Replacing '" + sSearchAccount + L"' with '" + sReplaceAccount + L"'", sSdPart); + tResultantSid = oInteractor->second; + return SidActionResult::Replace; +} \ No newline at end of file diff --git a/OperationReplaceMap.h b/OperationReplaceMap.h new file mode 100644 index 0000000..f8c0cd8 --- /dev/null +++ b/OperationReplaceMap.h @@ -0,0 +1,23 @@ +#pragma once + +#include "Operation.h" + +class OperationReplaceMap : public Operation +{ +private: + + // statics used by command registration utility + static std::wstring GetCommand() { return L"ReplaceMap"; } + static ClassFactory * RegisteredFactory; + + // operation specific + std::map oReplaceMap; + +public: + + // overrides + SidActionResult DetermineSid(WCHAR * const sSdPart, ObjectEntry & tObjectEntry, PSID const tCurrentSid, PSID & tResultantSid) override; + + // constructors + OperationReplaceMap(std::queue & oArgList); +}; \ No newline at end of file diff --git a/repacls.vcxproj b/repacls.vcxproj index e52dac6..3878713 100644 --- a/repacls.vcxproj +++ b/repacls.vcxproj @@ -141,6 +141,10 @@ false true None + true + + true + false Console @@ -150,6 +154,7 @@ false RequireAdministrator true + NoErrorReport @@ -168,6 +173,9 @@ 4100 true None + true + false + true Console @@ -177,6 +185,7 @@ RequireAdministrator true true + NoErrorReport @@ -194,6 +203,7 @@ + @@ -221,6 +231,7 @@ + diff --git a/repacls.vcxproj.filters b/repacls.vcxproj.filters index 73ce92c..b4485b9 100644 --- a/repacls.vcxproj.filters +++ b/repacls.vcxproj.filters @@ -97,6 +97,9 @@ Source\Operations + + Source\Operations + @@ -210,6 +213,9 @@ Includes\Operations + + Includes\Operations +