diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..79fcfa6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.vs +*.vcxproj.user diff --git a/Binx64/HiveSwarming.exe b/Binx64/HiveSwarming.exe new file mode 100644 index 0000000..4986fec Binary files /dev/null and b/Binx64/HiveSwarming.exe differ diff --git a/Binx64/HiveSwarming.pdb b/Binx64/HiveSwarming.pdb new file mode 100644 index 0000000..64fe64f Binary files /dev/null and b/Binx64/HiveSwarming.pdb differ diff --git a/CommonFunctions.cpp b/CommonFunctions.cpp new file mode 100644 index 0000000..52076dd --- /dev/null +++ b/CommonFunctions.cpp @@ -0,0 +1,60 @@ +// (C) Stormshield 2020 +// Licensed under the Apache license, version 2.0 +// See LICENSE.txt for details + +#include +#include "Constants.h" +#include "CommonFunctions.h" +#include +#include +#include + +void DeleteHiveLogFiles +( + _In_ const std::wstring &HiveFilePath +) +{ + for (auto& Ext : Constants::Hives::LogFileExtensions) + { + std::wstring LogFilePath = HiveFilePath + Ext; + DWORD Attributes = GetFileAttributesW(LogFilePath.c_str()); + if (Attributes != INVALID_FILE_ATTRIBUTES) + { + Attributes &= ~FILE_ATTRIBUTE_HIDDEN; + Attributes &= ~FILE_ATTRIBUTE_SYSTEM; + SetFileAttributesW(LogFilePath.c_str(), Attributes); + DeleteFileW(LogFilePath.c_str()); + } + } +} + +void ReportError +( + _In_ const HRESULT ErrorCode, + _In_ const std::wstring& Context +) +{ + LPWSTR MessageBuffer = NULL; + DWORD FmtResult = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_MAX_WIDTH_MASK, + NULL, ErrorCode, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPWSTR)&MessageBuffer, 0, NULL); + + if (!Context.empty()) + { + std::wcerr << Context << L":" << std::endl; + } + + if (FmtResult == 0) + { + std::wcerr << L"ERROR " << std::hex << std::setw(8) << ErrorCode << L" (could not format message)"; + } + else + { + std::wcerr << L"ERROR " << std::hex << std::setw(8) << ErrorCode << L" (" << MessageBuffer << L")"; + LocalFree((PVOID)MessageBuffer); + MessageBuffer = NULL; + } + + std::wcerr << std::endl << std::endl; +} \ No newline at end of file diff --git a/CommonFunctions.h b/CommonFunctions.h new file mode 100644 index 0000000..c22aff6 --- /dev/null +++ b/CommonFunctions.h @@ -0,0 +1,22 @@ +// (C) Stormshield 2020 +// Licensed under the Apache license, version 2.0 +// See LICENSE.txt for details + +#pragma once +#include + +/// @brief Delete .LOG1 and .LOG2 system files that were created when loading an application hive +/// @param[in] HiveFilePath Path to the hive file +void DeleteHiveLogFiles +( + _In_ const std::wstring &HiveFilePath +); + +/// @brief Report an error +/// @param[in] ErrorCode HRESULT value +/// @param[in] Context Optional context +void ReportError +( + _In_ const HRESULT ErrorCode, + _In_ const std::wstring& Context = std::wstring{} +); \ No newline at end of file diff --git a/Constants.h b/Constants.h new file mode 100644 index 0000000..8a9a459 --- /dev/null +++ b/Constants.h @@ -0,0 +1,87 @@ +// (C) Stormshield 2020 +// Licensed under the Apache license, version 2.0 +// See LICENSE.txt for details + +#pragma once +#include +#include +#include + +/// Program constants +namespace Constants { + + /// Command-line constants + namespace Program { + /// Switch for converting a hive to a .reg file + static const std::wstring HiveToRegFileSwitch { L"--hive-to-reg-file" }; + + /// Switch for converting a .reg file to a hive + static const std::wstring RegFileToHiveSwitch { L"--reg-file-to-hive" }; + }; + + /// Program defaults + namespace Defaults { + /// Default Root registry path in generated .reg files + static const std::wstring ExportKeyPath { L"(HiveRoot)" }; + }; + + /// Hive-specific constants + namespace Hives { + /// Extensions of extra files that are generated when manipulating registry hives + static const std::vector LogFileExtensions{ L".LOG1", L".LOG2" }; + }; + + /// .reg file-specific constants + namespace RegFiles { + /// New lines used in .reg files + static const std::wstring NewLines { L"\r\n" }; + + /// Preamble found in .reg files + static const std::wstring Preamble { L"\ufeff" // Byte Order Mark + L"Windows Registry Editor Version 5.00\r\n" + L"\r\n" }; + + /// Character used at start of line beginning a registry key path + static const WCHAR KeyOpening { L'[' }; + + /// Character delimiting parent key from its child in a registry path + static const WCHAR PathSeparator { L'\\' }; + + /// Character used at end of line closing a registry key path + static const WCHAR KeyClosing { L']' }; + + /// Character used for default registry values + static const WCHAR DefaultValue { L'@' }; + + /// Character used for separating value declaration from its content + static const WCHAR ValueNameSeparator { L'=' }; + + /// Sequence used for describing a DWORD rendition + static const std::wstring DwordPrefix { L"dword:" }; + + /// Sequence used for describing a hexadecimal rendition + static const std::wstring HexPrefix { L"hex" }; + + /// Character used when beginning the value type for a hexadecimal rendition + static const WCHAR HexTypeSpecOpening { L'(' }; + + /// Character used when ending the value type for a hexadecimal rendition + static const WCHAR HexTypeSpecClosing { L')' }; + + /// Character at the beginning of the hexadecimal rendition + static const WCHAR HexSuffix { L':' }; + + /// Byte separator in hexadecimal renditions + static const WCHAR HexByteSeparator { L',' }; + + /// Desired maximal line length when dumping hexadecimal rendition of registry values + static const SIZE_T HexWrappingLimit = 80u; + + /// Count of leading spaces when wrapping a hexadecimal rendition + static const SIZE_T HexNewLineLeadingSpaces = 2u; + + /// Sequence used when continuing a hexadecimal rendition on a new line + static const std::wstring HexByteNewLine { L"\\\r\n\u0020\u0020" }; + }; +}; + diff --git a/Conversions.h b/Conversions.h new file mode 100644 index 0000000..71b6c82 --- /dev/null +++ b/Conversions.h @@ -0,0 +1,82 @@ +// (C) Stormshield 2020 +// Licensed under the Apache license, version 2.0 +// See LICENSE.txt for details + +#pragma once + +#include +#include +#include + +/// Internal representation of a registry value. +struct RegistryValue { + /// Name of the registry value. May not contain null character + std::wstring Name; + + /// Type of the registry value. Usually a small number. + DWORD Type; + + /// Binary representation of the underlying data as a byte buffer. + std::vector BinaryValue; +}; + +/// Internal representation of a registry key. +struct RegistryKey { + /// Name of the registry key. May contain any character except backslash + std::wstring Name; + + /// Container of subkeys + std::vector Subkeys; + + /// Container of values + std::vector Values; +}; + +/// @brief Create an internal representation of a registry key from a registry hive (binary) file +/// @param[in] HiveFilePath Path to the registry hive +/// @param[in] RootName Path to the root key for export +/// @param[out] RegKey Internal structure +/// @return HRESULT semantics +/// @note For the moment, system files that are created (.LOG1, .LOG2) are not cleaned up. +_Must_inspect_result_ +HRESULT HiveToInternal +( + _In_ const std::wstring &HiveFilePath, + _In_ const std::wstring &RootName, + _Out_ RegistryKey& RegKey +); + +/// @brief Create a .reg file from the internal representation of a registry key +/// @param[in] RegKey Representation of the registry key +/// @param[in] OutputFilePath Path of the desired output file +/// @return HRESULT semantics +/// @note #OutputFilePath is overwritten if it already exists +_Must_inspect_result_ +HRESULT InternalToRegfile +( + _In_ const RegistryKey& RegKey, + _In_ const std::wstring &OutputFilePath +); + +/// @brief Create a hive file from the internal representation of a registry key +/// @param[in] RegKey Representation of the registry key +/// @param[in] OutputFilePath Path of the desired output file +/// @return HRESULT semantics +/// @note #OutputFilePath is overwritten if it already exists +_Must_inspect_result_ +HRESULT InternalToHive +( + _In_ const RegistryKey& RegKey, + _In_ const std::wstring &OutputFilePath +); + +/// @brief Create an internal representation of a registry key from a registry .reg (text) file +/// @param[in] RegFilePath Path to the registry .reg file +/// @param[out] RegKey Internal structure +/// @return HRESULT semantics +_Must_inspect_result_ +HRESULT RegfileToInternal +( + _In_ const std::wstring& RegFilePath, + _Out_ RegistryKey& RegKey +); \ No newline at end of file diff --git a/HiveSwarming.cpp b/HiveSwarming.cpp new file mode 100644 index 0000000..50a82c3 --- /dev/null +++ b/HiveSwarming.cpp @@ -0,0 +1,101 @@ +// (C) Stormshield 2020 +// Licensed under the Apache license, version 2.0 +// See LICENSE.txt for details + +#include +#include +#include "Conversions.h" +#include "CommonFunctions.h" +#include "Constants.h" + +/// @brief Program entry point +/// @param[in] Argc Command line token count, including program name +/// @param[in] Argv Tokens of the command line (#Argc valid entries) +/// @retval EXIT_SUCCESS Program execution successful +/// @retval EXIT_FAILURE Program execution failed +int wmain( + INT CONST Argc, + LPCWSTR CONST* CONST Argv +) +{ + HRESULT Result = E_FAIL; + + RegistryKey InternalStruct; + + auto Usage = [&]() + { + std::wcerr << L"Usage: " << std::endl << + L"\t" << Argv[0] << L" " << Constants::Program::HiveToRegFileSwitch << L" " << std::endl << + L"\t" << Argv[0] << L" " << Constants::Program::RegFileToHiveSwitch << L" " << std::endl << + std::endl; + }; + + if (Argc <= 1) + { + Usage(); + Result = S_OK; + goto Cleanup; + } + + if (Constants::Program::HiveToRegFileSwitch == Argv[1]) + { + if (Argc != 4) + { + Usage(); + Result = E_INVALIDARG; + goto Cleanup; + } + + const std::wstring HivePath { Argv[2] }; + const std::wstring RegPath { Argv[3] }; + + Result = HiveToInternal(HivePath, Constants::Defaults::ExportKeyPath, InternalStruct); + if (FAILED(Result)) + { + goto Cleanup; + } + + Result = InternalToRegfile(InternalStruct, RegPath); + if (FAILED(Result)) + { + goto Cleanup; + } + } + else if (Constants::Program::RegFileToHiveSwitch == Argv[1]) + { + if (Argc != 4) + { + Usage(); + Result = E_INVALIDARG; + goto Cleanup; + } + + const std::wstring RegPath { Argv[2] }; + const std::wstring HivePath { Argv[3] }; + + Result = RegfileToInternal(RegPath, InternalStruct); + if (FAILED(Result)) + { + ReportError(Result, L"Serializing registry file" + RegPath); + goto Cleanup; + } + + Result = InternalToHive(InternalStruct, HivePath); + if (FAILED(Result)) + { + ReportError(Result, L"Writing hive file " + HivePath); + goto Cleanup; + } + } + else + { + Usage(); + Result = E_INVALIDARG; + goto Cleanup; + } + + Result = S_OK; + +Cleanup: + return FAILED(Result) ? EXIT_FAILURE : EXIT_SUCCESS; +} \ No newline at end of file diff --git a/HiveSwarming.sln b/HiveSwarming.sln new file mode 100644 index 0000000..a30bf64 --- /dev/null +++ b/HiveSwarming.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29609.76 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "HiveSwarming", "HiveSwarming.vcxproj", "{00DC3B6B-A592-4736-9CA9-130E4DF06CD4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {00DC3B6B-A592-4736-9CA9-130E4DF06CD4}.Debug|x64.ActiveCfg = Debug|x64 + {00DC3B6B-A592-4736-9CA9-130E4DF06CD4}.Debug|x64.Build.0 = Debug|x64 + {00DC3B6B-A592-4736-9CA9-130E4DF06CD4}.Debug|x86.ActiveCfg = Debug|Win32 + {00DC3B6B-A592-4736-9CA9-130E4DF06CD4}.Debug|x86.Build.0 = Debug|Win32 + {00DC3B6B-A592-4736-9CA9-130E4DF06CD4}.Release|x64.ActiveCfg = Release|x64 + {00DC3B6B-A592-4736-9CA9-130E4DF06CD4}.Release|x64.Build.0 = Release|x64 + {00DC3B6B-A592-4736-9CA9-130E4DF06CD4}.Release|x86.ActiveCfg = Release|Win32 + {00DC3B6B-A592-4736-9CA9-130E4DF06CD4}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {FD1888F9-D331-4CD9-A58D-0C0F01F6735B} + EndGlobalSection +EndGlobal diff --git a/HiveSwarming.vcxproj b/HiveSwarming.vcxproj new file mode 100644 index 0000000..b52c88f --- /dev/null +++ b/HiveSwarming.vcxproj @@ -0,0 +1,149 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + + + + + + + + + + + + + + 16.0 + {00DC3B6B-A592-4736-9CA9-130E4DF06CD4} + HiveSwarming + 10.0 + + + + Application + true + v142 + Unicode + + + Application + false + v142 + true + Unicode + + + Application + true + v142 + Unicode + + + Application + false + v142 + true + Unicode + + + + + + + + + + + + true + + + false + + + + Level3 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpplatest + MultiThreadedDebug + + + Console + true + + + + + Level3 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpplatest + MultiThreadedDebug + + + Console + true + + + + + Level3 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpplatest + MultiThreaded + + + Console + true + true + true + + + + + Level3 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpplatest + MultiThreaded + + + Console + true + true + true + + + + + + \ No newline at end of file diff --git a/HiveSwarming.vcxproj.filters b/HiveSwarming.vcxproj.filters new file mode 100644 index 0000000..13721f8 --- /dev/null +++ b/HiveSwarming.vcxproj.filters @@ -0,0 +1,48 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + \ No newline at end of file diff --git a/HiveToInternal.cpp b/HiveToInternal.cpp new file mode 100644 index 0000000..c54b619 --- /dev/null +++ b/HiveToInternal.cpp @@ -0,0 +1,186 @@ +// (C) Stormshield 2020 +// Licensed under the Apache license, version 2.0 +// See LICENSE.txt for details + +#include "Conversions.h" +#include "CommonFunctions.h" +#include + +/// @brief Create an internal representation of a registry key from the handle to a registry key +/// @param[in] Hkey the registry key +/// @param[in] KeyName Name of the key #Hkey is a handle to +/// @param[out] RegKey Representation of the key +/// @return HRESULT semantics +_Must_inspect_result_ +static HRESULT HkeyToInternal +( + _In_ const HKEY Hkey, + _In_ const std::wstring& KeyName, + _Out_ RegistryKey& RegKey +) +{ + HRESULT Result = E_FAIL; + DWORD SubkeyCount = 0; + DWORD MaximalSubKeyLength = 0; + DWORD ValueCount = 0; + DWORD MaximalValueNameLength = 0; + DWORD MaximalValueLength = 0; + LPWSTR SubKeyNameBuffer = NULL; + LPWSTR ValueNameBuffer = NULL; + PBYTE ValueBuffer = NULL; + + if (Hkey == NULL) + { + ReportError(E_HANDLE, L"Null HKEY"); + return E_HANDLE; + } + + Result = HRESULT_FROM_WIN32(RegQueryInfoKeyW(Hkey, NULL, 0, NULL, &SubkeyCount, &MaximalSubKeyLength, + NULL, &ValueCount, &MaximalValueNameLength, &MaximalValueLength, NULL, NULL)); + if (FAILED(Result)) + { + ReportError(Result, L"Getting information on HKEY - Current key name: " + KeyName); + goto Cleanup; + } + + SubKeyNameBuffer = (LPWSTR)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (1 + MaximalSubKeyLength) * sizeof(WCHAR)); + if (SubKeyNameBuffer == NULL) + { + Result = HRESULT_FROM_WIN32(GetLastError()); + ReportError(Result, L"Allocating space for subkey names - Current key name: " + KeyName); + goto Cleanup; + } + + ValueNameBuffer = (LPWSTR)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (1 + MaximalValueNameLength) * sizeof(WCHAR)); + if (ValueNameBuffer == NULL) + { + Result = HRESULT_FROM_WIN32(GetLastError()); + ReportError(Result, L"Allocating space for value names - Current key name: " + KeyName); + goto Cleanup; + } + + ValueBuffer = (PBYTE)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, MaximalValueLength); + if (ValueBuffer == NULL) + { + Result = HRESULT_FROM_WIN32(GetLastError()); + ReportError(Result, L"Allocating space for values - Current key name: " + KeyName); + goto Cleanup; + } + + RegKey.Name = KeyName; + + for (DWORD ValueIndex = 0; ValueIndex < ValueCount; ++ValueIndex) + { + DWORD ValueNameLength = MaximalValueNameLength + 1; + DWORD ValueType = 0; + DWORD DataLength = MaximalValueLength + 1; + Result = HRESULT_FROM_WIN32(RegEnumValueW(Hkey, ValueIndex, ValueNameBuffer, &ValueNameLength, + NULL, &ValueType, ValueBuffer, &DataLength)); + if (FAILED(Result)) + { + std::wostringstream ErrorMessageStream; + ErrorMessageStream << L"Getting value at index " << ValueIndex << L" - Current key name: " << KeyName; + ReportError(Result, ErrorMessageStream.str()); + goto Cleanup; + } + + RegistryValue NewValue; + NewValue.Name.assign(ValueNameBuffer, ValueNameLength); + NewValue.Type = ValueType; + NewValue.BinaryValue.assign(ValueBuffer, ValueBuffer + DataLength); + + RegKey.Values.emplace_back(std::move(NewValue)); + } + + for (DWORD SubkeyIndex = 0; SubkeyIndex < SubkeyCount; ++SubkeyIndex) + { + HKEY HSubkey = NULL; + DWORD SubkeyNameLength = MaximalSubKeyLength + 1; + + Result = HRESULT_FROM_WIN32(RegEnumKeyExW(Hkey, SubkeyIndex, SubKeyNameBuffer, &SubkeyNameLength, NULL, NULL, NULL, NULL)); + if (FAILED(Result)) + { + std::wostringstream ErrorMessageStream; + ErrorMessageStream << L"Getting subkey name at index " << SubkeyIndex << L" - Current key name: " << KeyName; + ReportError(Result, ErrorMessageStream.str()); + goto Cleanup; + } + + Result = HRESULT_FROM_WIN32(RegOpenKeyExW(Hkey, SubKeyNameBuffer, 0, KEY_ALL_ACCESS, &HSubkey)); + if (FAILED(Result)) + { + std::wostringstream ErrorMessageStream; + ErrorMessageStream << L"Opening subkey named " << SubKeyNameBuffer << L" at index " << SubkeyIndex << L" - Current key name: " << KeyName; + ReportError(Result, ErrorMessageStream.str()); + goto Cleanup; + } + + std::wstring SubkeyName; + SubkeyName.assign(SubKeyNameBuffer, SubkeyNameLength); + + RegistryKey NewKey; + Result = HkeyToInternal(HSubkey, SubkeyName, NewKey); + if (FAILED(Result)) + { + std::wostringstream ErrorMessageStream; + ErrorMessageStream << L"Getting contents of subkey named " << SubkeyName << L" - Current key name: " << KeyName; + ReportError(Result, ErrorMessageStream.str()); + goto Cleanup; + } + RegKey.Subkeys.emplace_back(std::move(NewKey)); + + RegCloseKey(HSubkey); + } +Cleanup: + if (SubKeyNameBuffer) + { + HeapFree(GetProcessHeap(), 0, (LPVOID)SubKeyNameBuffer); + SubKeyNameBuffer = NULL; + } + if (ValueNameBuffer) + { + HeapFree(GetProcessHeap(), 0, (LPVOID)ValueNameBuffer); + ValueNameBuffer = NULL; + } + if (ValueBuffer) + { + HeapFree(GetProcessHeap(), 0, (LPVOID)ValueBuffer); + ValueBuffer = NULL; + } + + return Result; +} + +// non-static function: documented in header. +_Must_inspect_result_ +HRESULT HiveToInternal +( + _In_ const std::wstring& HiveFilePath, + _In_ const std::wstring& RootName, + _Out_ RegistryKey& RegKey +) +{ + HRESULT Result = E_FAIL; + HKEY HiveKey = NULL; + + Result = HRESULT_FROM_WIN32(RegLoadAppKeyW(HiveFilePath.c_str(), &HiveKey, KEY_ALL_ACCESS, REG_PROCESS_APPKEY, 0)); + if (FAILED(Result)) + { + ReportError(Result, L"Loading hive file " + HiveFilePath); + goto Cleanup; + } + + Result = HkeyToInternal(HiveKey, RootName, RegKey); + +Cleanup: + if (HiveKey != NULL) + { + RegCloseKey(HiveKey); + HiveKey = NULL; + + DeleteHiveLogFiles(HiveFilePath); + } + + + return Result; +} \ No newline at end of file diff --git a/InternalToHive.cpp b/InternalToHive.cpp new file mode 100644 index 0000000..ddad8e4 --- /dev/null +++ b/InternalToHive.cpp @@ -0,0 +1,130 @@ +// (C) Stormshield 2020 +// Licensed under the Apache license, version 2.0 +// See LICENSE.txt for details + +#include "Conversions.h" +#include "CommonFunctions.h" +#include +#include + +/// @brief Fill an empty HKEY with internal structures representing a registry key and dependencies +/// @param[in] RegKey Registry key to dump into the HKEY +/// @param[in] KeyHandle Handle to the key +/// @return HRESULT semantics +_Must_inspect_result_ +static HRESULT InternalToHkey( + _In_ const RegistryKey& RegKey, + _In_ const HKEY KeyHandle +) +{ + HRESULT Result = E_FAIL; + HKEY SubkeyHandle = NULL; + + if (KeyHandle == NULL) + { + ReportError(E_HANDLE, L"Invalid Parameter"); + return E_HANDLE; + } + + for (auto &Value : RegKey.Values) + { + if (Value.BinaryValue.size() > MAXDWORD) + { + ReportError(E_UNEXPECTED, L"Binary value is too long (name: " + Value.Name + L")"); + Result = E_UNEXPECTED; + goto Cleanup; + } + Result = HRESULT_FROM_WIN32(RegSetValueExW(KeyHandle, Value.Name.c_str(), 0, Value.Type, + Value.BinaryValue.data(), static_cast(Value.BinaryValue.size()))); + if (FAILED(Result)) + { + ReportError(Result, L"Could not set value " + Value.Name + L" of key " + RegKey.Name); + goto Cleanup; + } + } + + for (auto &Key : RegKey.Subkeys) + { + if (SubkeyHandle != NULL) + { + RegCloseKey(SubkeyHandle); + SubkeyHandle = NULL; + } + + Result = HRESULT_FROM_WIN32(RegCreateKeyExW(KeyHandle, Key.Name.c_str(), 0, NULL, REG_OPTION_NON_VOLATILE, + KEY_ALL_ACCESS, NULL, &SubkeyHandle, NULL)); + + if (FAILED(Result)) + { + ReportError(Result, L"Could not create subkey " + Key.Name + L" of key " + RegKey.Name); + goto Cleanup; + } + + Result = InternalToHkey(Key, SubkeyHandle); + + if (FAILED(Result)) + { + ReportError(Result, L"Could not render subkey " + Key.Name + L" of key " + RegKey.Name); + goto Cleanup; + } + } + + Result = S_OK; +Cleanup: + if (SubkeyHandle != NULL) + { + RegCloseKey(SubkeyHandle); + SubkeyHandle = NULL; + } + + return Result; +} + + +// non-static function: documented in header. +_Must_inspect_result_ +HRESULT InternalToHive +( + _In_ const RegistryKey& RegKey, + _In_ const std::wstring& OutputFilePath +) +{ + HRESULT Result = E_FAIL; + HKEY HiveKey = NULL; + + if (!DeleteFileW(OutputFilePath.c_str())) + { + if (GetLastError() != ERROR_FILE_NOT_FOUND) + { + Result = HRESULT_FROM_WIN32(GetLastError()); + ReportError(Result, L"Could not delete hive file " + OutputFilePath); + goto Cleanup; + } + } + + Result = HRESULT_FROM_WIN32(RegLoadAppKeyW(OutputFilePath.c_str(), &HiveKey, KEY_ALL_ACCESS, REG_PROCESS_APPKEY, 0)); + if (FAILED(Result)) + { + ReportError(Result, L"Could not create empty hive file " + OutputFilePath); + goto Cleanup; + } + + Result = InternalToHkey(RegKey, HiveKey); + if (FAILED(Result)) + { + ReportError(Result, L"Could not render internal structure to hive"); + } + + Result = S_OK; + +Cleanup: + if (HiveKey != NULL) + { + RegCloseKey(HiveKey); + HiveKey = NULL; + + DeleteHiveLogFiles(OutputFilePath); + } + + return Result; +} \ No newline at end of file diff --git a/InternalToRegfile.cpp b/InternalToRegfile.cpp new file mode 100644 index 0000000..7012f47 --- /dev/null +++ b/InternalToRegfile.cpp @@ -0,0 +1,408 @@ +// (C) Stormshield 2020 +// Licensed under the Apache license, version 2.0 +// See LICENSE.txt for details + +#include "Constants.h" +#include "Conversions.h" +#include "CommonFunctions.h" +#include +#include + +/// @brief Perform global replacement of substring in a std::wstring +/// @param[in,out] String String on which to perform substitutions +/// @param[in] Pattern Substring that will be replaced by #Replacement in #String +/// @param[in] Replacement What is inserted in place of #Substring in #String +static VOID GlobalStringSubstitute( + _Inout_ std::wstring& String, + _In_ const std::wstring& Pattern, + _In_ const std::wstring& Replacement +) +{ + size_t Position = String.find(Pattern); + while (Position != String.npos) + { + String.replace(Position, Pattern.length(), Replacement); + Position = String.find(Pattern, Position + Replacement.length()); + } +} + +/// @brief Append binary contents of a std::wstring to an opened file +/// @param[in] OutFileHandle Handle to the file +/// @param[in] String Input string +/// @note The string buffer is dumped to the file “as is”. +/// Null characters may be appended to the file if the string buffer includes some of them. +_Must_inspect_result_ +static HRESULT WriteStringBufferToFile +( + _In_ const HANDLE OutFileHandle, + _In_ const std::wstring String +) +{ + HRESULT Result = E_FAIL; + + if (String.length() >= MAXDWORD / sizeof(decltype(String)::traits_type::char_type)) + { + Result = HRESULT_FROM_WIN32(ERROR_ARITHMETIC_OVERFLOW); + ReportError(Result, L"Writing data that is too large"); + return Result; + } + + if (OutFileHandle == NULL || OutFileHandle == INVALID_HANDLE_VALUE) + { + ReportError(E_HANDLE, L"Invalid parameter"); + return E_HANDLE; + } + + { + const DWORD BytesToWrite = static_cast(String.length() * sizeof(decltype(String)::traits_type::char_type)); + DWORD BytesWritten = 0; + + if (!WriteFile(OutFileHandle, (LPCVOID)String.c_str(), BytesToWrite, &BytesWritten, NULL)) + { + Result = HRESULT_FROM_WIN32(GetLastError()); + ReportError(Result, L"Could not write to output file"); + return Result; + } + if (BytesWritten != BytesToWrite) + { + Result = E_UNEXPECTED; + ReportError(Result, L"Bytes not fully written to file"); + return Result; + } + } + return S_OK; +} + +/// @brief Render the contents of a registry value in a .reg file +/// @param[in] OutFileHandle Handle to the file +/// @param[in] FirstLineSizeSoFar How many characters have already been written on the line when dumping +/// the name of the value and the equal sign. +/// This is used for mimicking .reg format line breaks before lines over 80 characters +/// @param[in] RegValue Representation of the registry value +/// @return HRESULT semantics +_Must_inspect_result_ +static HRESULT RenderBinaryValue +( + _In_ const HANDLE OutFileHandle, + _In_ const SIZE_T FirstLineSizeSoFar, + _In_ const RegistryValue& RegValue +) +{ + HRESULT Result = E_FAIL; + SIZE_T CurLineSizeSoFar = FirstLineSizeSoFar; + if (OutFileHandle == NULL || OutFileHandle == INVALID_HANDLE_VALUE) + { + return E_HANDLE; + } + + std::wostringstream BinaryRenditionStream; + BinaryRenditionStream << Constants::RegFiles::HexPrefix; + if (RegValue.Type != REG_BINARY) + { + BinaryRenditionStream << Constants::RegFiles::HexTypeSpecOpening << std::hex << RegValue.Type << Constants::RegFiles::HexTypeSpecClosing; + } + BinaryRenditionStream << Constants::RegFiles::HexSuffix; + + CurLineSizeSoFar += BinaryRenditionStream.str().length(); + + for (std::vector::const_iterator CurByteIt = RegValue.BinaryValue.cbegin(); CurByteIt != RegValue.BinaryValue.cend(); ++CurByteIt) + { + BinaryRenditionStream << std::hex << std::setfill(L'0') << std::setw(2) << *CurByteIt; + CurLineSizeSoFar += 2; + if (CurByteIt + 1 != RegValue.BinaryValue.cend()) + { + BinaryRenditionStream << Constants::RegFiles::HexByteSeparator; + CurLineSizeSoFar += 1; + if (CurLineSizeSoFar > Constants::RegFiles::HexWrappingLimit - 4) + { + // adding "xx,\" would go over 80 characters, reg export typically breaks line here. + BinaryRenditionStream << Constants::RegFiles::HexByteNewLine; + CurLineSizeSoFar = Constants::RegFiles::HexNewLineLeadingSpaces; + } + } + } + + BinaryRenditionStream << Constants::RegFiles::NewLines; + + Result = WriteStringBufferToFile(OutFileHandle, BinaryRenditionStream.str()); + if (FAILED(Result)) + { + ReportError(Result, L"Could not write to output file"); + return Result; + } + + return S_OK; +} + +/// @brief Render the contents of a REG_DWORD registry value in a .reg file +/// @param[in] OutFileHandle Handle to the file +/// @param[in] FirstLineSizeSoFar Use for fallback to #RenderBinaryValue +/// @param[in] RegValue Representation of the registry value +/// @return HRESULT semantics +_Must_inspect_result_ +static HRESULT RenderDwordValue +( + _In_ const HANDLE OutFileHandle, + _In_ const SIZE_T FirstLineSizeSoFar, + _In_ const RegistryValue& RegValue +) +{ + if (OutFileHandle == NULL || OutFileHandle == INVALID_HANDLE_VALUE) + { + ReportError(E_HANDLE, L"Invalid parameter"); + return E_HANDLE; + } + + if (RegValue.Type != REG_DWORD) + { + ReportError(E_HANDLE, L"Invalid parameter"); + return E_INVALIDARG; + } + + if (RegValue.BinaryValue.size() != sizeof(DWORD)) + { + return RenderBinaryValue(OutFileHandle, FirstLineSizeSoFar, RegValue); + } + + std::wostringstream DwordRenditionStream; + DwordRenditionStream << L"dword:"; + DwordRenditionStream << std::hex << std::setw(8) << std::setfill(L'0') << *(DWORD*)(RegValue.BinaryValue.data()); + DwordRenditionStream << Constants::RegFiles::NewLines; + + return WriteStringBufferToFile(OutFileHandle, DwordRenditionStream.str()); + +} + +/// @brief Render the contents of a REG_SZ registry value in a .reg file +/// @param[in] OutFileHandle Handle to the file +/// @param[in] FirstLineSizeSoFar Use for fallback to #RenderBinaryValue +/// @param[in] RegValue Representation of the registry value +/// @return HRESULT semantics +/// @note This falls back to binary rendition if the REG_SZ does not meet requirements such as: +/// - REG_SZ values should be terminated by a null character +/// - REG_SZ values may not contain other null characters +/// - REG_SZ value sizes should be a multiple of 2 as they store WCHAR-based values. +_Must_inspect_result_ +static HRESULT RenderStringValue +( + _In_ const HANDLE OutFileHandle, + _In_ const SIZE_T FirstLineSizeSoFar, + _In_ const RegistryValue& RegValue +) +{ + std::wstring WstringValue; + if (OutFileHandle == NULL || OutFileHandle == INVALID_HANDLE_VALUE) + { + ReportError(E_HANDLE, L"Invalid parameter"); + return E_HANDLE; + } + + if (RegValue.Type != REG_SZ) + { + ReportError(E_HANDLE, L"Invalid parameter"); + return E_INVALIDARG; + } + + if (RegValue.BinaryValue.size() % sizeof(WCHAR) != 0) + { + return RenderBinaryValue(OutFileHandle, FirstLineSizeSoFar, RegValue); + } + + WstringValue.assign((PWCHAR) RegValue.BinaryValue.data(), RegValue.BinaryValue.size() / sizeof(WCHAR)); + if (WstringValue.empty()) + { + return RenderBinaryValue(OutFileHandle, FirstLineSizeSoFar, RegValue); + } + + if ( WstringValue.empty() + || WstringValue.back() != L'\0' + || std::count(WstringValue.cbegin(), WstringValue.cend() - 1, L'\0') != 0 + ) + { + return RenderBinaryValue(OutFileHandle, FirstLineSizeSoFar, RegValue); + } + + // Now we can render the value as REG_SZ: Remove null character + WstringValue.pop_back(); + + GlobalStringSubstitute(WstringValue, L"\\", L"\\\\"); + GlobalStringSubstitute(WstringValue, L"\"", L"\\\""); + GlobalStringSubstitute(WstringValue, L"\n", L"\r\n"); + + std::wostringstream StringRenditionStream; + StringRenditionStream << L"\"" << WstringValue << L"\"" << Constants::RegFiles::NewLines; + return WriteStringBufferToFile(OutFileHandle, StringRenditionStream.str()); +} + +/// @brief Render a registry value and its contents in a .reg file +/// @param[in] OutFileHandle Handle to the file +/// @param[in] RegValue Representation of the registry value +/// @return HRESULT semantics +_Must_inspect_result_ +HRESULT RenderRegistryValue +( + _In_ const HANDLE OutFileHandle, + _In_ const RegistryValue& RegValue +) +{ + HRESULT Result = E_FAIL; + SIZE_T FirstLineSizeSoFar = 0; + if (OutFileHandle == NULL || OutFileHandle == INVALID_HANDLE_VALUE) + { + ReportError(E_HANDLE, L"Invalid parameter"); + return E_HANDLE; + } + + std::wstring EscapedName = RegValue.Name; + GlobalStringSubstitute(EscapedName, L"\\", L"\\\\"); + GlobalStringSubstitute(EscapedName, L"\"", L"\\\""); + GlobalStringSubstitute(EscapedName, L"\n", L"\r\n"); + std::wostringstream Str; + if (EscapedName.empty()) + { + Str << Constants::RegFiles::DefaultValue << Constants::RegFiles::ValueNameSeparator; + } + else + { + Str << L"\"" << EscapedName << L"\"="; + } + Result = WriteStringBufferToFile(OutFileHandle, Str.str()); + if (FAILED(Result)) + { + ReportError(Result, L"Could not write data to file"); + return Result; + } + + FirstLineSizeSoFar += Str.str().length(); + + if (RegValue.Type == REG_DWORD) + { + return RenderDwordValue(OutFileHandle, FirstLineSizeSoFar, RegValue); + } + if (RegValue.Type == REG_SZ) + { + return RenderStringValue(OutFileHandle, FirstLineSizeSoFar, RegValue); + } + return RenderBinaryValue(OutFileHandle, FirstLineSizeSoFar, RegValue); +} + +/// @brief Render a registry key and its values and subkeys in a .reg file +/// @param[in] OutFileHandle Handle to the file +/// @param[in] RegKey Representation of the registry key +/// @param[in] PathSoFar Path to this key +/// @return HRESULT semantics +_Must_inspect_result_ +HRESULT RenderRegistryKey +( + _In_ const HANDLE OutFileHandle, + _In_ const RegistryKey& RegKey, + _In_ const std::wstring &PathSoFar +) +{ + HRESULT Result = E_FAIL; + if (OutFileHandle == NULL || OutFileHandle == INVALID_HANDLE_VALUE) + { + ReportError(E_HANDLE, L"Invalid parameter"); + return E_HANDLE; + } + + std::wstring NewPath; + if (PathSoFar.empty()) + { + NewPath = RegKey.Name; + } + else + { + NewPath = PathSoFar + Constants::RegFiles::PathSeparator + RegKey.Name; + } + + { + std::wostringstream KeySpecStream; + KeySpecStream << Constants::RegFiles::KeyOpening << NewPath << Constants::RegFiles::KeyClosing << Constants::RegFiles::NewLines; + + Result = WriteStringBufferToFile(OutFileHandle, KeySpecStream.str()); + if (FAILED(Result)) + { + ReportError(E_HANDLE, L"Could not write to output file"); + return Result; + } + } + + for (const RegistryValue &Value : RegKey.Values) + { + Result = RenderRegistryValue(OutFileHandle, Value); + if (FAILED(Result)) + { + ReportError(E_HANDLE, L"Could not render registry value" + Value.Name); + return Result; + } + } + + { + Result = WriteStringBufferToFile(OutFileHandle, Constants::RegFiles::NewLines ); + if (FAILED(Result)) + { + ReportError(E_HANDLE, L"Could not write to output file"); + return Result; + } + } + + for (const RegistryKey &Key : RegKey.Subkeys) + { + Result = RenderRegistryKey(OutFileHandle, Key, NewPath); + if (FAILED(Result)) + { + ReportError(E_HANDLE, L"Could not render registry key" + Key.Name); + return Result; + } + } + + return S_OK; +} + +// non-static function: documented in header. +_Must_inspect_result_ +HRESULT InternalToRegfile +( + _In_ const RegistryKey& RegKey, + _In_ const std::wstring& OutputFilePath +) +{ + HRESULT Result = E_FAIL; + HANDLE OutFileHandle = INVALID_HANDLE_VALUE; + + OutFileHandle = CreateFileW(OutputFilePath.c_str(), GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (OutFileHandle == INVALID_HANDLE_VALUE) + { + Result = HRESULT_FROM_WIN32(GetLastError()); + ReportError(Result, L"Could not open file " + OutputFilePath + L" for writing"); + goto Cleanup; + } + + { + Result = WriteStringBufferToFile(OutFileHandle, Constants::RegFiles::Preamble); + if (FAILED(Result)) + { + ReportError(Result, L"Could not write to output file"); + goto Cleanup; + } + } + + Result = RenderRegistryKey(OutFileHandle, RegKey, std::wstring{}); + if (FAILED(Result)) + { + ReportError(Result, L"Could not render registry key"); + goto Cleanup; + } + + Result = S_OK; + +Cleanup: + if (OutFileHandle != INVALID_HANDLE_VALUE) + { + CloseHandle(OutFileHandle); + OutFileHandle = INVALID_HANDLE_VALUE; + } + + return Result; +} \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..fe43cf6 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,13 @@ + Copyright 2020 Stormshield + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..9b9895f --- /dev/null +++ b/README.txt @@ -0,0 +1,64 @@ +(C) 2020 Stormshield + +HiveSwarming - Conversions between registry hive and registry export formats + without a need for special privileges. + +USAGE +----- + +HiveSwarming.exe --reg-file-to-hive +HiveSwarming.exe --hive-to-reg-file + +EXIT CODE +--------- +0 means success, other values mean failure. +Failure will be accompanied by some output on standard error. + +FAQ +--- + +Q. Why this name? +A. Registry vocabulary tends to follow beekeeping terms. Swarming is a term + that perfectly applies to a migration of your data. + + See also: https://devblogs.microsoft.com/oldnewthing/20030808-00/?p=42943 + +Q. Any limitations? +A. Yes. First, when you load a hive file using the API RegLoadAppKeyW, all + security descriptors for all keys inside the hive should be identical. + Otherwise it will fail. + Second, if a key name contains a closing bracket followed by a newline + character, your .reg file is not parseable. This limitation is also valid + for standard .reg files + +Q. Is the .reg file compatible with reg.exe import? +A. Yes. However the generated .reg file has [(HiveRoot)] as root key. You will + have to substitute it globally to make it importable at any desired location. + When converting back, it is not necessary to keep [(HiveRoot)] as the root + key, but the only requirement is that all keys descend of the first one. + +Q. What are requirements for .reg files? +A. .reg files must: + - be encoded as UTF-16, Little-Endian, with a Byte Order Mark + - Use \r\n for line endings + - Start with "Windows Registry Editor Version 5.00" and one blank line + only, followed by the first key + - Have all keys be descendants of the first key + - Have no trailing or leading spaces on lines + - Have no blank lines between a key and its last value + - Have a blank line after the last value of a key (including last key) + - Be importable to the registry + Some third party software, like RegView, will generate invalid files. For + example double quotes inside strings will be left unescaped; export file + encoding will use a multi-byte character set, and Unicode data will be lost. + Those files are not supported. + +Q. Some random system, hidden files appeared in the process. +A. This is a consequence of the internals of registry hives. We try to delete + these files once the conversions are done, but it is possible that + something failed (HANDLE still opened for example). You may probably delete + these files themselves if your hive is not mounted anywhere and not in use + by any process. + +Q. Do you accept pull requests? +A. They are welcome and will be reviewed. diff --git a/RegfileToInternal.cpp b/RegfileToInternal.cpp new file mode 100644 index 0000000..e01ac0d --- /dev/null +++ b/RegfileToInternal.cpp @@ -0,0 +1,522 @@ +// (C) Stormshield 2020 +// Licensed under the Apache license, version 2.0 +// See LICENSE.txt for details + +#include "Constants.h" +#include "Conversions.h" +#include "CommonFunctions.h" +#include +#include +#include + +/// @brief Consume a list of registry values in a .reg file +/// @param[in,out] ValueList String view to the beginning of a list of values. +/// Updated to the next token after the list of values, newlines having been consumed. +/// @param[in] Values Container for the read values +/// @return HRESULT semantics +_Must_inspect_result_ +static HRESULT ValueListToInternal +( + _Inout_ std::wstring_view& ValueList, + _Inout_ std::vector& Values +) +{ + auto ConsumeChars = [&ValueList](SIZE_T CharCount) + { + ValueList.remove_prefix(CharCount); + }; + + auto HasChar = [&ValueList](CONST WCHAR ExpectedChar) -> bool + { + bool ReturnValue = !ValueList.empty() && ValueList[0] == ExpectedChar; + if (ReturnValue) + { + ValueList.remove_prefix(1); + } + return ReturnValue; + }; + + auto HasString = [&ValueList](const std::wstring& ExpectedString) -> bool + { + bool ReturnValue = ValueList.length() >= ExpectedString.length() && std::equal(ExpectedString.cbegin(), ExpectedString.cend(), ValueList.cbegin()); + if (ReturnValue) + { + ValueList.remove_prefix(ExpectedString.length()); + } + return ReturnValue; + }; + + auto EndOfStream = [&ValueList]() -> bool { return ValueList.empty(); }; + + do + { + if (ValueList.empty()) + { + return S_OK; + } + + // Values are all consumed when getting a new line. + if (HasString(Constants::RegFiles::NewLines)) + { + while (HasString(Constants::RegFiles::NewLines)) + { + // + } + + return S_OK; + } + + + RegistryValue Value; + + if (HasChar(Constants::RegFiles::DefaultValue)) + { + Value.Name.clear(); + } + else if (HasChar(L'"')) + { + // We must have a value name here + size_t ClosingQuotePos = 0; + std::wostringstream ValueNameStream; + while (true) + { + if (ClosingQuotePos >= ValueList.length()) + { + ReportError(E_UNEXPECTED, L"Looking for closing quotation mark"); + return E_UNEXPECTED; + } + if (ValueList[ClosingQuotePos] == L'"') + { + break; + } + if (ValueList[ClosingQuotePos] == L'\\') + { + if (ClosingQuotePos + 1 >= ValueList.length()) + { + ReportError(E_UNEXPECTED, L"Backslash at end of buffer"); + return E_UNEXPECTED; + } + ValueNameStream << ValueList[ClosingQuotePos + 1]; + ClosingQuotePos += 2; + } + else + { + ValueNameStream << ValueList[ClosingQuotePos]; + ClosingQuotePos += 1; + } + } + Value.Name = ValueNameStream.str(); + ConsumeChars(ClosingQuotePos + 1); + } + else + { + ReportError(E_UNEXPECTED, L"Value name should be literal @ or begin with double quote"); + return E_UNEXPECTED; + } + + if (EndOfStream()) + { + std::wostringstream ErrorMessageStream; + ErrorMessageStream << L"Value " << Value.Name << L" - Missing data"; + ReportError(E_UNEXPECTED, ErrorMessageStream.str()); + return E_UNEXPECTED; + } + if (!HasChar(Constants::RegFiles::ValueNameSeparator)) + { + std::wostringstream ErrorMessageStream; + ErrorMessageStream << L"Value name " << Value.Name << L" - Missing = sign"; + ReportError(E_UNEXPECTED, ErrorMessageStream.str()); + return E_UNEXPECTED; + } + + if (EndOfStream()) + { + std::wostringstream ErrorMessageStream; + ErrorMessageStream << L"Value name " << Value.Name << L" - No data after = sign"; + ReportError(E_UNEXPECTED, ErrorMessageStream.str()); + return E_UNEXPECTED; + } + + if (HasString(Constants::RegFiles::DwordPrefix)) + { + Value.Type = REG_DWORD; + DWORD DwordValue; + if (ValueList.length() <= 8) + { + std::wostringstream ErrorMessageStream; + ErrorMessageStream << L"Value name " << Value.Name << L" - Buffer less than 8 characters after dword declaration"; + ReportError(E_UNEXPECTED, ErrorMessageStream.str()); + return E_UNEXPECTED; + } + + { + std::wstring ReadValue{ &ValueList[0], 8 }; + std::wistringstream DwordReadingStream{ ReadValue, std::ios_base::in }; + DwordReadingStream >> std::hex >> std::setw(8) >> std::setfill(L'0') >> DwordValue; + + std::wostringstream DwordVerificationStream; + DwordVerificationStream << std::hex << std::setw(8) << std::setfill(L'0') << DwordValue; + + std::wstring VerificationString{ DwordVerificationStream.str() }; + if (_wcsnicmp(VerificationString.c_str(), ReadValue.c_str(), 8) != 0) + { + std::wostringstream ErrorMessageStream; + ErrorMessageStream << L"Value name " << Value.Name << L" - Could not parse dword from string " << ReadValue; + ReportError(E_UNEXPECTED, ErrorMessageStream.str()); + return E_UNEXPECTED; + } + } + + Value.BinaryValue.resize(sizeof(DWORD)); + CopyMemory(Value.BinaryValue.data(), &DwordValue, sizeof(DwordValue)); + + ConsumeChars(8); + + if (!HasString(Constants::RegFiles::NewLines)) + { + std::wostringstream ErrorMessageStream; + ErrorMessageStream << L"Value name " << Value.Name << L" - Dword value not followed by \\r\\n"; + ReportError(E_UNEXPECTED, ErrorMessageStream.str()); + return E_UNEXPECTED; + } + + } + else if (HasString(Constants::RegFiles::HexPrefix)) + { + // general case + if (ValueList.empty()) + { + std::wostringstream ErrorMessageStream; + ErrorMessageStream << L"Value name " << Value.Name << L" - End of buffer after hex declaration"; + ReportError(E_UNEXPECTED, ErrorMessageStream.str()); + return E_UNEXPECTED; + } + + // in the general case, (xx) indicates the value type: + // "myvalue"=hex(xx):... + // for REG_BINARY, (xx) is omitted: + // "myvalue"=hex:... + // If we find the opening parenthese, we parse the registry type. + if (HasChar(Constants::RegFiles::HexTypeSpecOpening)) + { + size_t ClosingParenPos = ValueList.find_first_not_of(L"0123456789abcdefABCDEF"); + if (ClosingParenPos == ValueList.npos || ClosingParenPos == 0 || ValueList[ClosingParenPos] != Constants::RegFiles::HexTypeSpecClosing) + { + std::wostringstream ErrorMessageStream; + ErrorMessageStream << L"Value name " << Value.Name << L" - Could not find closing parenthesis"; + ReportError(E_UNEXPECTED, ErrorMessageStream.str()); + return E_UNEXPECTED; + } + + { + std::wistringstream ValueTypeInStream{ std::wstring{ &ValueList[0], ClosingParenPos }, std::ios_base::in }; + ValueTypeInStream >> std::hex >> Value.Type; + } + ConsumeChars(ClosingParenPos + 1); + } + else + { + Value.Type = REG_BINARY; + } + + if (!HasChar(Constants::RegFiles::HexSuffix)) + { + std::wostringstream ErrorMessageStream; + ErrorMessageStream << L"Value name " << Value.Name << L" - Missing : sign after hex declaration"; + ReportError(E_UNEXPECTED, ErrorMessageStream.str()); + return E_UNEXPECTED; + } + + while (true) + { + if (ValueList.empty()) + { + std::wostringstream ErrorMessageStream; + ErrorMessageStream << L"Value name " << Value.Name << L" - End of data while reading binary value"; + ReportError(E_UNEXPECTED, ErrorMessageStream.str()); + return E_UNEXPECTED; + } + if (HasString(Constants::RegFiles::NewLines)) + { + // end of binary data + break; + } + else if (HasChar(Constants::RegFiles::HexByteSeparator)) + { + continue; + } + else if (HasString(Constants::RegFiles::HexByteNewLine)) + { + continue; + } + else if (ValueList.length() >= 2 && isxdigit(ValueList[0]) && isxdigit(ValueList[1])) + { + { + int ByteVal; + std::wistringstream HexValueInStream{ std::wstring{ &ValueList[0], 2 }, std::ios_base::in }; + HexValueInStream >> std::hex >> ByteVal; + Value.BinaryValue.push_back(static_cast(ByteVal)); + } + ConsumeChars(2); + continue; + } + else + { + std::wostringstream ErrorMessageStream; + ErrorMessageStream << L"Value name " << Value.Name << L" - Expecting two hexadecimal digits"; + ReportError(E_UNEXPECTED, ErrorMessageStream.str()); + return E_UNEXPECTED; + } + } + } + else if (HasChar(L'"')) + { + Value.Type = REG_SZ; + size_t ClosingQuotePos = 0; + std::wostringstream ValueString; + while (true) + { + if (ClosingQuotePos >= ValueList.length()) + { + std::wostringstream ErrorMessageStream; + ErrorMessageStream << L"Value name " << Value.Name << L" - Could not find end of string value"; + ReportError(E_UNEXPECTED, ErrorMessageStream.str()); + return E_UNEXPECTED; + } + if (ValueList[ClosingQuotePos] == L'"') + { + break; + } + if (ValueList[ClosingQuotePos] == L'\\') + { + if (ClosingQuotePos + 1 >= ValueList.length()) + { + std::wostringstream ErrorMessageStream; + ErrorMessageStream << L"Value name " << Value.Name << L" - End of data after escape character"; + ReportError(E_UNEXPECTED, ErrorMessageStream.str()); + return E_UNEXPECTED; + } + ValueString << ValueList[ClosingQuotePos + 1]; + ClosingQuotePos += 2; + } + else + { + ValueString << ValueList[ClosingQuotePos]; + ClosingQuotePos += 1; + } + } + ConsumeChars(ClosingQuotePos + 1); + + // we must store a terminator in the registry value + std::wstring SzVal = ValueString.str() + L'\0'; + Value.BinaryValue.resize(SzVal.length() * sizeof(WCHAR)); + CopyMemory(Value.BinaryValue.data(), SzVal.c_str(), Value.BinaryValue.size()); + + if (!HasString(Constants::RegFiles::NewLines)) + { + std::wostringstream ErrorMessageStream; + ErrorMessageStream << L"Value name " << Value.Name << L" - Value not followed by new line"; + ReportError(E_UNEXPECTED, ErrorMessageStream.str()); + return E_UNEXPECTED; + } + } + + Values.emplace_back(std::move(Value)); + } while (TRUE); +} + +/// @brief Serializes the keys from a .reg file to internal representation +/// @param[in,out] RegList Remainder of the .reg file before and after serialization +/// Updated to the next token after all keys (and their values) that +/// begin with #PathPrefix, newlines having been consumed. +/// @param[in] PathPrefix Prefix to all keys that are to be read +/// @param[in,out] RegKeys Container for all read keys +_Must_inspect_result_ +static HRESULT RegListToInternal +( + _Inout_ std::wstring_view& RegList, + _In_ const std::wstring& PathPrefix, + _Inout_ std::vector& RegKeys +) +{ + HRESULT Result = E_FAIL; + static const std::wstring KeyClosingAtEOL = Constants::RegFiles::KeyClosing + Constants::RegFiles::NewLines; + + if (RegList.empty()) + { + std::wostringstream ErrorMessageStream; + ErrorMessageStream << L"Reading key beginning with prefix " << (PathPrefix.empty() ? L"" : PathPrefix.c_str()) << L" - Expecting content"; + ReportError(E_UNEXPECTED, ErrorMessageStream.str()); + return E_UNEXPECTED; + } + + do + { + if (RegList[0] != Constants::RegFiles::KeyOpening) + { + std::wostringstream ErrorMessageStream; + ErrorMessageStream << L"Reading key beginning with prefix " << (PathPrefix.empty() ? L"" : PathPrefix.c_str()) << L" - Line does not begin with opening bracket"; + ReportError(E_UNEXPECTED, ErrorMessageStream.str()); + return E_UNEXPECTED; + } + auto EndKeyPos = RegList.find(KeyClosingAtEOL, 1); + if (EndKeyPos == RegList.npos) + { + std::wostringstream ErrorMessageStream; + ErrorMessageStream << L"Reading key beginning with prefix " << (PathPrefix.empty() ? L"" : PathPrefix.c_str()) << L" - Could not find closing bracket followed by new line"; + ReportError(E_UNEXPECTED, ErrorMessageStream.str()); + return E_UNEXPECTED; + } + + const std::wstring_view KeyPath{ &RegList[1], EndKeyPos - 1 }; + + if (KeyPath.length() <= PathPrefix.length() || !std::equal(PathPrefix.cbegin(), PathPrefix.cend(), KeyPath.cbegin())) + { + // not a sub-key for our caller. maybe a parent caller could handle this. + break; + } + const std::wstring_view KeyName{ &KeyPath[PathPrefix.length()], KeyPath.length() - PathPrefix.length() }; + RegistryKey NewKey; + NewKey.Name = KeyName; + + RegList.remove_prefix(EndKeyPos + 1); + RegList.remove_prefix(Constants::RegFiles::NewLines.length()); + Result = ValueListToInternal(RegList, NewKey.Values); + if (FAILED(Result)) + { + std::wostringstream ErrorMessageStream; + ErrorMessageStream << L"Reading key " << PathPrefix << KeyName << L" - Could not read values"; + ReportError(E_UNEXPECTED, ErrorMessageStream.str()); + return Result; + } + + if (!RegList.empty()) + { + std::wstring NewPrefix = std::wstring{ KeyPath } + Constants::RegFiles::PathSeparator; + Result = RegListToInternal(RegList, NewPrefix, NewKey.Subkeys); + if (FAILED(Result)) + { + std::wostringstream ErrorMessageStream; + ErrorMessageStream << L"Checking if there are keys beginning with " << NewPrefix; + ReportError(E_UNEXPECTED, ErrorMessageStream.str()); + return Result; + } + } + + RegKeys.emplace_back(std::move(NewKey)); + } while (!RegList.empty()); + + return S_OK; +} + +// non-static function: documented in header. +_Must_inspect_result_ +HRESULT RegfileToInternal +( + _In_ const std::wstring& RegFilePath, + _Out_ RegistryKey& RegKey +) +{ + HRESULT Result = E_FAIL; + HANDLE InFileHandle = INVALID_HANDLE_VALUE; + LARGE_INTEGER FileSize; + std::wstring FileContents; + std::vector KeysInFile; + + InFileHandle = CreateFileW(RegFilePath.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (InFileHandle == INVALID_HANDLE_VALUE) + { + Result = HRESULT_FROM_WIN32(GetLastError()); + ReportError(Result, L"Opening file " + RegFilePath); + goto Cleanup; + } + + if (!GetFileSizeEx(InFileHandle, &FileSize)) + { + Result = HRESULT_FROM_WIN32(GetLastError()); + ReportError(Result, L"Getting file size of " + RegFilePath); + goto Cleanup; + } + + if (FileSize.HighPart > 0) + { + Result = E_OUTOFMEMORY; + ReportError(Result, L"File " + RegFilePath + L" is too large (larger than 4GB)"); + goto Cleanup; + } + + if (FileSize.LowPart % sizeof(WCHAR) != 0) + { + Result = E_UNEXPECTED; + ReportError(Result, L"File " + RegFilePath + L" should have an even size because it is expected to hold WCHAR code units only"); + goto Cleanup; + } + + // resize buffer + FileContents.resize(FileSize.LowPart / sizeof(WCHAR), L'\0'); + + { + DWORD BytesRead = 0; + if (!ReadFile(InFileHandle, (PVOID)(&FileContents[0]), FileSize.LowPart, &BytesRead, NULL)) + { + Result = HRESULT_FROM_WIN32(GetLastError()); + goto Cleanup; + } + if (BytesRead != FileSize.LowPart) + { + Result = E_UNEXPECTED; + ReportError(Result, L"File " + RegFilePath + L" could not be read to buffer"); + goto Cleanup; + } + } + + { + std::wstring_view Remainder { FileContents }; + + if (Remainder.length() < Constants::RegFiles::Preamble.length() || !std::equal(Constants::RegFiles::Preamble.cbegin(), Constants::RegFiles::Preamble.cend(), FileContents.cbegin())) + { + Result = E_UNEXPECTED; + ReportError(Result, L"File " + RegFilePath + L" preamble not found"); + goto Cleanup; + } + + Remainder.remove_prefix(Constants::RegFiles::Preamble.length()); + + Result = RegListToInternal(Remainder, std::wstring{}, KeysInFile); + if (FAILED(Result)) + { + goto Cleanup; + } + + // at first level, should have exactly one registry key + if (KeysInFile.size() != 1) + { + Result = E_UNEXPECTED; + ReportError(Result, L"Multiple root keys were found in the registry file"); + goto Cleanup; + } + + // whole file should have been slurped. + if (Remainder.length() != 0) + { + Result = E_UNEXPECTED; + std::wostringstream ErrorMessageStream; + ErrorMessageStream << L"Conversion to internal structure left " << Remainder.length() << L" code units in the file unparsed"; + ReportError(Result, ErrorMessageStream.str()); + goto Cleanup; + } + + RegKey = KeysInFile[0]; + } + + Result = S_OK; + +Cleanup: + if (InFileHandle != INVALID_HANDLE_VALUE) + { + CloseHandle(InFileHandle); + InFileHandle = INVALID_HANDLE_VALUE; + } + + return Result; +}