[FREELDR] Implement SYSTEM hive recovery at bootloader level & use CmCheckRegistry for registry validation

Validate the SYSTEM hive with CmCheckRegistry and purge volatile data with the same function when initializing a hive descriptor for SYSTEM.
Also implement SYSTEM recovery code that takes use of SYSTEM log in case something is fishy with the hive. If hive repair doesn't have fully recovered the SYSTEM hive, FreeLdr will load the alternate variant of the SYSTEM hive, aka SYSTEM.ALT.

If FreeLdr repairs the hive with a LOG, it will mark it with HBOOT_BOOT_RECOVERED_BY_HIVE_LOG on BootRecover field of the header. All the recovered data that is present as dirty in memory will have to be flushed by the kernel once it is in charge of the system.
Otherwise if the system boot occurred by loading SYSTEM.ALT instead, FreeLdr will mark HBOOT_BOOT_RECOVERED_BY_ALTERNATE_HIVE, the kernel will start recovering the main hive as soon as it does any I/O activity into it.
This commit is contained in:
George Bișoc 2022-10-26 21:36:02 +02:00
parent cce399e772
commit 7983b65e10
No known key found for this signature in database
GPG key ID: 688C4FBE25D7DEF6
3 changed files with 486 additions and 23 deletions

View file

@ -2,6 +2,7 @@
* FreeLoader
*
* Copyright (C) 2014 Timo Kreuzer <timo.kreuzer@reactos.org>
* 2022 George Bișoc <george.bisoc@reactos.org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -41,6 +42,8 @@ HKEY CurrentControlSetKey = NULL;
#define GET_HHIVE_FROM_HKEY(hKey) GET_HHIVE(CmSystemHive)
#define GET_CM_KEY_NODE(hHive, hKey) ((PCM_KEY_NODE)HvGetCell(hHive, HKEY_TO_HCI(hKey)))
#define GET_HBASE_BLOCK(ChunkBase) ((PHBASE_BLOCK)ChunkBase)
PVOID
NTAPI
CmpAllocate(
@ -62,22 +65,47 @@ CmpFree(
FrLdrHeapFree(Ptr, 0);
}
/**
* @brief
* Initializes a flat hive descriptor for the
* hive and validates the registry hive.
* Volatile data is purged during this procedure
* for initialization.
*
* @param[in] CmHive
* A pointer to a CM (in-memory) hive descriptor
* containing the hive descriptor to be initialized.
*
* @param[in] ChunkBase
* An arbitrary pointer that points to the registry
* chunk base. This pointer serves as the base block
* containing the hive file header data.
*
* @param[in] LoadAlternate
* If set to TRUE, the function will initialize the
* hive as an alternate hive, otherwise FALSE to initialize
* it as primary.
*
* @return
* Returns TRUE if the hive has been initialized
* and registry data inside the hive is valid, FALSE
* otherwise.
*/
static
BOOLEAN
RegImportBinaryHive(
RegInitializeHive(
_In_ PCMHIVE CmHive,
_In_ PVOID ChunkBase,
_In_ ULONG ChunkSize)
_In_ BOOLEAN LoadAlternate)
{
NTSTATUS Status;
PCM_KEY_NODE KeyNode;
CM_CHECK_REGISTRY_STATUS CmStatusCode;
TRACE("RegImportBinaryHive(%p, 0x%lx)\n", ChunkBase, ChunkSize);
/* Allocate and initialize the hive */
CmSystemHive = FrLdrTempAlloc(sizeof(CMHIVE), 'eviH');
Status = HvInitialize(GET_HHIVE(CmSystemHive),
/* Initialize the hive */
Status = HvInitialize(GET_HHIVE(CmHive),
HINIT_FLAT, // HINIT_MEMORY_INPLACE
0,
0,
LoadAlternate ? HFILE_TYPE_ALTERNATE : HFILE_TYPE_PRIMARY,
ChunkBase,
CmpAllocate,
CmpFree,
@ -89,11 +117,374 @@ RegImportBinaryHive(
NULL);
if (!NT_SUCCESS(Status))
{
ERR("Corrupted hive %p!\n", ChunkBase);
FrLdrTempFree(CmSystemHive, 'eviH');
ERR("Failed to initialize the flat hive (Status 0x%lx)\n", Status);
return FALSE;
}
/* Now check the hive and purge volatile data */
CmStatusCode = CmCheckRegistry(CmHive, CM_CHECK_REGISTRY_BOOTLOADER_PURGE_VOLATILES | CM_CHECK_REGISTRY_VALIDATE_HIVE);
if (!CM_CHECK_REGISTRY_SUCCESS(CmStatusCode))
{
ERR("CmCheckRegistry detected problems with the loaded flat hive (check code %lu)\n", CmStatusCode);
return FALSE;
}
return TRUE;
}
/**
* @brief
* Loads and reads a hive log at specified
* file offset.
*
* @param[in] DirectoryPath
* A pointer to a string that denotes the directory
* path of the hives and logs location.
*
* @param[in] LogFileOffset
* The file offset of which this function uses to
* seek at specific position during read.
*
* @param[in] LogName
* A pointer to a string that denotes the name of
* the desired hive log (e.g. "SYSTEM").
*
* @param[out] LogData
* A pointer to the returned hive log data that was
* read. The following data varies depending on the
* specified offset set up by the caller, that is used
* to where to start reading from the hive log.
*
* @return
* Returns TRUE if the hive log was loaded and read
* successfully, FALSE otherwise.
*
* @remarks
* The returned log data pointer to the caller is a
* virtual address. You must use VaToPa that converts
* the address to a physical one in order to actually
* use it!
*/
static
BOOLEAN
RegLoadHiveLog(
_In_ PCSTR DirectoryPath,
_In_ ULONG LogFileOffset,
_In_ PCSTR LogName,
_Out_ PVOID *LogData)
{
ARC_STATUS Status;
ULONG LogId;
CHAR LogPath[MAX_PATH];
ULONG LogFileSize;
FILEINFORMATION FileInfo;
LARGE_INTEGER Position;
ULONG BytesRead;
PVOID LogDataVirtual;
PVOID LogDataPhysical;
/* Build the full path to the hive log */
RtlStringCbCopyA(LogPath, sizeof(LogPath), DirectoryPath);
RtlStringCbCatA(LogPath, sizeof(LogPath), LogName);
/* Open the file */
Status = ArcOpen(LogPath, OpenReadOnly, &LogId);
if (Status != ESUCCESS)
{
ERR("Failed to open %s (ARC code %lu)\n", LogName, Status);
return FALSE;
}
/* Get the file length */
Status = ArcGetFileInformation(LogId, &FileInfo);
if (Status != ESUCCESS)
{
ERR("Failed to get file information from %s (ARC code %lu)\n", LogName, Status);
ArcClose(LogId);
return FALSE;
}
/* Capture the size of the hive log file */
LogFileSize = FileInfo.EndingAddress.LowPart;
if (LogFileSize == 0)
{
ERR("LogFileSize is 0, %s is corrupt\n", LogName);
ArcClose(LogId);
return FALSE;
}
/* Allocate memory blocks for our log data */
LogDataPhysical = MmAllocateMemoryWithType(
MM_SIZE_TO_PAGES(LogFileSize + MM_PAGE_SIZE - 1) << MM_PAGE_SHIFT,
LoaderRegistryData);
if (LogDataPhysical == NULL)
{
ERR("Failed to allocate memory for log data\n");
ArcClose(LogId);
return FALSE;
}
/* Convert the address to virtual so that it can be useable */
LogDataVirtual = PaToVa(LogDataPhysical);
/* Seek within the log file at desired position */
Position.QuadPart = LogFileOffset;
Status = ArcSeek(LogId, &Position, SeekAbsolute);
if (Status != ESUCCESS)
{
ERR("Failed to seek at %s (ARC code %lu)\n", LogName, Status);
ArcClose(LogId);
return FALSE;
}
/* And read the actual data from the log */
Status = ArcRead(LogId, LogDataPhysical, LogFileSize, &BytesRead);
if (Status != ESUCCESS)
{
ERR("Failed to read %s (ARC code %lu)\n", LogName, Status);
ArcClose(LogId);
return FALSE;
}
*LogData = LogDataVirtual;
ArcClose(LogId);
return TRUE;
}
/**
* @brief
* Recovers the header base block of a flat
* registry hive.
*
* @param[in] ChunkBase
* A pointer to the registry hive chunk base of
* which the damaged header block is to be recovered.
*
* @param[in] DirectoryPath
* A pointer to a string that denotes the directory
* path of the hives and logs location.
*
* @param[in] LogName
* A pointer to a string that denotes the name of
* the desired hive log (e.g. "SYSTEM").
*
* @return
* Returns TRUE if the header base block was successfully
* recovered, FALSE otherwise.
*/
static
BOOLEAN
RegRecoverHeaderHive(
_Inout_ PVOID ChunkBase,
_In_ PCSTR DirectoryPath,
_In_ PCSTR LogName)
{
BOOLEAN Success;
CHAR FullLogFileName[MAX_PATH];
PVOID LogData;
PHBASE_BLOCK HiveBaseBlock;
PHBASE_BLOCK LogBaseBlock;
/* Build the complete path of the hive log */
RtlStringCbCopyA(FullLogFileName, sizeof(FullLogFileName), LogName);
RtlStringCbCatA(FullLogFileName, sizeof(FullLogFileName), ".LOG");
Success = RegLoadHiveLog(DirectoryPath, 0, FullLogFileName, &LogData);
if (!Success)
{
ERR("Failed to read the hive log\n");
return FALSE;
}
/* Make sure the header from the hive log is actually sane */
LogData = VaToPa(LogData);
LogBaseBlock = GET_HBASE_BLOCK(LogData);
if (!HvpVerifyHiveHeader(LogBaseBlock, HFILE_TYPE_LOG))
{
ERR("The hive log has corrupt base block\n");
return FALSE;
}
/* Copy the healthy header base block into the primary hive */
HiveBaseBlock = GET_HBASE_BLOCK(ChunkBase);
WARN("Recovering the hive base block...\n");
RtlCopyMemory(HiveBaseBlock,
LogBaseBlock,
LogBaseBlock->Cluster * HSECTOR_SIZE);
HiveBaseBlock->Type = HFILE_TYPE_PRIMARY;
return TRUE;
}
/**
* @brief
* Recovers the corrupt data of a primary flat
* registry hive.
*
* @param[in] ChunkBase
* A pointer to the registry hive chunk base of
* which the damaged hive data is to be replaced
* with healthy data from the corresponding hive log.
*
* @param[in] DirectoryPath
* A pointer to a string that denotes the directory
* path of the hives and logs location.
*
* @param[in] LogName
* A pointer to a string that denotes the name of
* the desired hive log (e.g. "SYSTEM").
*
* @return
* Returns TRUE if the hive data was successfully
* recovered, FALSE otherwise.
*
* @remarks
* Data recovery of the target hive does not always
* guarantee the primary hive is fully recovered.
* It could happen a block from a hive log is not
* marked dirty (pending to be written to disk) that
* has healthy data therefore the following bad block
* would still remain in corrupt state in the main primary
* hive. In such scenarios an alternate hive must be replayed.
*/
static
BOOLEAN
RegRecoverDataHive(
_Inout_ PVOID ChunkBase,
_In_ PCSTR DirectoryPath,
_In_ PCSTR LogName)
{
BOOLEAN Success;
ULONG StorageLength;
ULONG BlockIndex, LogIndex;
PUCHAR BlockPtr, BlockDest;
CHAR FullLogFileName[MAX_PATH];
PVOID LogData;
PUCHAR LogDataPhysical;
PHBASE_BLOCK HiveBaseBlock;
/* Build the complete path of the hive log */
RtlStringCbCopyA(FullLogFileName, sizeof(FullLogFileName), LogName);
RtlStringCbCatA(FullLogFileName, sizeof(FullLogFileName), ".LOG");
Success = RegLoadHiveLog(DirectoryPath, HV_LOG_HEADER_SIZE, FullLogFileName, &LogData);
if (!Success)
{
ERR("Failed to read the hive log\n");
return FALSE;
}
/* Make sure the dirty vector signature is there otherwise the hive log is corrupt */
LogDataPhysical = (PUCHAR)VaToPa(LogData);
if (*((PULONG)LogDataPhysical) != HV_LOG_DIRTY_SIGNATURE)
{
ERR("The hive log dirty signature could not be found\n");
return FALSE;
}
/* Copy the dirty data into the primary hive */
LogIndex = 0;
BlockIndex = 0;
HiveBaseBlock = GET_HBASE_BLOCK(ChunkBase);
StorageLength = HiveBaseBlock->Length / HBLOCK_SIZE;
for (; BlockIndex < StorageLength; ++BlockIndex)
{
/* Skip this block if it's not dirty and go to the next one */
if (LogDataPhysical[BlockIndex + sizeof(HV_LOG_DIRTY_SIGNATURE)] != HV_LOG_DIRTY_BLOCK)
{
continue;
}
/* Read the dirty block and copy it at right offsets */
BlockPtr = (PUCHAR)((ULONG_PTR)LogDataPhysical + 2 * HSECTOR_SIZE + LogIndex * HBLOCK_SIZE);
BlockDest = (PUCHAR)((ULONG_PTR)ChunkBase + (BlockIndex + 1) * HBLOCK_SIZE);
RtlCopyMemory(BlockDest, BlockPtr, HBLOCK_SIZE);
/* Increment the index in log as we continue further */
LogIndex++;
}
/* Fix the secondary sequence of the primary hive and compute a new checksum */
HiveBaseBlock->Sequence2 = HiveBaseBlock->Sequence1;
HiveBaseBlock->CheckSum = HvpHiveHeaderChecksum(HiveBaseBlock);
return TRUE;
}
/**
* @brief
* Imports the SYSTEM binary hive from
* the registry base chunk that's been
* provided by the loader block.
*
* @param[in] ChunkBase
* A pointer to the registry base chunk
* that serves for SYSTEM hive initialization.
*
* @param[in] ChunkSize
* The size of the registry base chunk. This
* parameter refers to the actual size of
* the SYSTEM hive. This parameter is currently
* unused.
*
* @param[in] LoadAlternate
* If set to TRUE, the function will initialize the
* hive as an alternate hive, otherwise FALSE to initialize
* it as primary.
*
* @return
* Returns TRUE if hive importing and initialization
* have succeeded, FALSE otherwise.
*/
BOOLEAN
RegImportBinaryHive(
_In_ PVOID ChunkBase,
_In_ ULONG ChunkSize,
_In_ PCSTR SearchPath,
_In_ BOOLEAN LoadAlternate)
{
BOOLEAN Success;
PCM_KEY_NODE KeyNode;
TRACE("RegImportBinaryHive(%p, 0x%lx)\n", ChunkBase, ChunkSize);
/* Assume that we don't need boot recover, unless we have to */
((PHBASE_BLOCK)ChunkBase)->BootRecover = HBOOT_NO_BOOT_RECOVER;
/* Allocate and initialize the hive */
CmSystemHive = FrLdrTempAlloc(sizeof(CMHIVE), 'eviH');
Success = RegInitializeHive(CmSystemHive, ChunkBase, LoadAlternate);
if (!Success)
{
/* Free the buffer and retry again */
FrLdrTempFree(CmSystemHive, 'eviH');
CmSystemHive = NULL;
if (!RegRecoverHeaderHive(ChunkBase, SearchPath, "SYSTEM"))
{
ERR("Failed to recover the hive header block\n");
return FALSE;
}
if (!RegRecoverDataHive(ChunkBase, SearchPath, "SYSTEM"))
{
ERR("Failed to recover the hive data\n");
return FALSE;
}
/* Now retry initializing the hive again */
CmSystemHive = FrLdrTempAlloc(sizeof(CMHIVE), 'eviH');
Success = RegInitializeHive(CmSystemHive, ChunkBase, LoadAlternate);
if (!Success)
{
ERR("Corrupted hive (despite recovery) %p\n", ChunkBase);
FrLdrTempFree(CmSystemHive, 'eviH');
return FALSE;
}
/*
* Acknowledge the kernel we recovered the SYSTEM hive
* on our side by applying log data.
*/
((PHBASE_BLOCK)ChunkBase)->BootRecover = HBOOT_BOOT_RECOVERED_BY_HIVE_LOG;
}
/* Save the root key node */
SystemHive = GET_HHIVE(CmSystemHive);
SystemRootCell = SystemHive->BaseBlock->RootCell;

View file

@ -30,7 +30,9 @@ typedef HANDLE HKEY, *PHKEY;
BOOLEAN
RegImportBinaryHive(
_In_ PVOID ChunkBase,
_In_ ULONG ChunkSize);
_In_ ULONG ChunkSize,
_In_ PCSTR SearchPath,
_In_ BOOLEAN LoadAlternate);
BOOLEAN
RegInitCurrentControlSet(

View file

@ -31,14 +31,22 @@ static BOOLEAN
WinLdrScanRegistry(
IN OUT PLIST_ENTRY BootDriverListHead);
typedef enum _BAD_HIVE_REASON
{
GoodHive = 1,
CorruptHive,
NoHive,
NoHiveAlloc
} BAD_HIVE_REASON, *PBAD_HIVE_REASON;
/* FUNCTIONS **************************************************************/
static BOOLEAN
WinLdrLoadSystemHive(
IN OUT PLOADER_PARAMETER_BLOCK LoaderBlock,
IN PCSTR DirectoryPath,
IN PCSTR HiveName)
_Inout_ PLOADER_PARAMETER_BLOCK LoaderBlock,
_In_ PCSTR DirectoryPath,
_In_ PCSTR HiveName,
_Out_ PBAD_HIVE_REASON Reason)
{
ULONG FileId;
CHAR FullHiveName[MAX_PATH];
@ -49,6 +57,9 @@ WinLdrLoadSystemHive(
PVOID HiveDataVirtual;
ULONG BytesRead;
/* Do not setup any bad reason for now */
*Reason = GoodHive;
/* Concatenate path and filename to get the full name */
RtlStringCbCopyA(FullHiveName, sizeof(FullHiveName), DirectoryPath);
RtlStringCbCatA(FullHiveName, sizeof(FullHiveName), HiveName);
@ -58,6 +69,7 @@ WinLdrLoadSystemHive(
if (Status != ESUCCESS)
{
WARN("Error while opening '%s', Status: %u\n", FullHiveName, Status);
*Reason = NoHive;
return FALSE;
}
@ -66,6 +78,7 @@ WinLdrLoadSystemHive(
if (Status != ESUCCESS)
{
WARN("Hive file has 0 size!\n");
*Reason = CorruptHive;
ArcClose(FileId);
return FALSE;
}
@ -79,6 +92,7 @@ WinLdrLoadSystemHive(
if (HiveDataPhysical == NULL)
{
WARN("Could not alloc memory for hive!\n");
*Reason = NoHiveAlloc;
ArcClose(FileId);
return FALSE;
}
@ -95,6 +109,7 @@ WinLdrLoadSystemHive(
if (Status != ESUCCESS)
{
WARN("Error while reading '%s', Status: %u\n", FullHiveName, Status);
*Reason = CorruptHive;
ArcClose(FileId);
return FALSE;
}
@ -113,9 +128,12 @@ WinLdrInitSystemHive(
IN BOOLEAN Setup)
{
CHAR SearchPath[1024];
PVOID ChunkBase;
PCSTR HiveName;
BOOLEAN Success;
BAD_HIVE_REASON Reason;
/* Load the corresponding text-mode setup system hive or the standard hive */
if (Setup)
{
RtlStringCbCopyA(SearchPath, sizeof(SearchPath), SystemRoot);
@ -123,31 +141,83 @@ WinLdrInitSystemHive(
}
else
{
// There is a simple logic here: try to load usual hive (system), if it
// fails, then give system.alt a try, and finally try a system.sav
// FIXME: For now we only try system
RtlStringCbCopyA(SearchPath, sizeof(SearchPath), SystemRoot);
RtlStringCbCatA(SearchPath, sizeof(SearchPath), "system32\\config\\");
HiveName = "SYSTEM";
}
TRACE("WinLdrInitSystemHive: loading hive %s%s\n", SearchPath, HiveName);
Success = WinLdrLoadSystemHive(LoaderBlock, SearchPath, HiveName);
Success = WinLdrLoadSystemHive(LoaderBlock, SearchPath, HiveName, &Reason);
if (!Success)
{
/* Check whether the SYSTEM hive does not exist or is too corrupt to be read */
if (Reason == CorruptHive || Reason == NoHive)
{
/* Try loading the alternate hive, the main hive should be recovered later */
goto LoadAlternateHive;
}
/* We are failing for other reason, bail out */
UiMessageBox("Could not load %s hive!", HiveName);
return FALSE;
}
/* Import what was loaded */
Success = RegImportBinaryHive(VaToPa(LoaderBlock->RegistryBase), LoaderBlock->RegistryLength);
Success = RegImportBinaryHive(VaToPa(LoaderBlock->RegistryBase), LoaderBlock->RegistryLength, SearchPath, FALSE);
if (!Success)
{
UiMessageBox("Importing binary hive failed!");
return FALSE;
/*
* Importing of the SYSTEM hive failed. The scenarios that would
* have made this possible are the following:
*
* 1. The primary hive is corrupt beyond repair (such as when
* core FS structures are total toast);
*
* 2. Repairing the hive could with a LOG could not recover it
* to the fullest. This is the case when the hive and LOG have
* diverged too much, or are mismatched, or the containing healthy
* data in the LOG was not marked as dirty that could be copied
* into the primary hive;
*
* 3. LOG is bad (e.g. corrupt dirty vector);
*
* 4. LOG does not physically exist on the backing storage.
*
* 5. SYSTEM hive does not physically exist or it is a 0 bytes file
* (the latter case still counts as corruption).
*
* With the hope to boot the system, load the mirror counterpart
* of the main hive, the alternate. The kernel should be able to recover
* the main hive later on as soon as it starts writing to it.
*/
LoadAlternateHive:
HiveName = "SYSTEM.ALT";
Success = WinLdrLoadSystemHive(LoaderBlock, SearchPath, HiveName, &Reason);
if (!Success)
{
UiMessageBox("Could not load %s hive!", HiveName);
return FALSE;
}
/* Retry importing it again */
Success = RegImportBinaryHive(VaToPa(LoaderBlock->RegistryBase), LoaderBlock->RegistryLength, SearchPath, TRUE);
if (!Success)
{
UiMessageBox("Importing binary hive failed!");
return FALSE;
}
/*
* Acknowledge the kernel we recovered the SYSTEM hive
* on our side by loading the alternate variant of the hive.
*/
WARN("SYSTEM hive does not exist or is corrupt and SYSTEM.ALT has been loaded!\n");
ChunkBase = VaToPa(LoaderBlock->RegistryBase);
((PHBASE_BLOCK)ChunkBase)->BootRecover = HBOOT_BOOT_RECOVERED_BY_ALTERNATE_HIVE;
}
// FIXME: Load SYSTEM.SAV if GUI setup installation is still in progress
/* Initialize the 'CurrentControlSet' link */
if (!RegInitCurrentControlSet(FALSE))
{