{
	"id": "01cd662b-67a7-4b11-9db0-f74a8ef8bd4a",
	"created_at": "2026-04-06T00:20:53.214875Z",
	"updated_at": "2026-04-10T03:20:23.742823Z",
	"deleted_at": null,
	"sha1_hash": "4638786f7da1dda4ea477a593d3589cff0eab57c",
	"title": "Too Log; Didn't Read — Unknown Actor Using CLFS Log Files for Stealth | Mandiant",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 2036549,
	"plain_text": "Too Log; Didn't Read — Unknown Actor Using CLFS Log Files\r\nfor Stealth | Mandiant\r\nBy Mandiant\r\nPublished: 2021-09-01 · Archived: 2026-04-05 23:38:11 UTC\r\nWritten by: Adrien Bataille, Blaine Stancill\r\nThe Mandiant Advanced Practices team recently discovered a new malware family we have named PRIVATELOG\r\nand its installer, STASHLOG. In this post, we will share a novel and especially interesting technique the samples\r\nuse to hide data, along with detailed analysis of both files that was performed with the support of FLARE analysts.\r\nWe will also share sample detection rules, and hunting recommendations to find similar activity in your\r\nenvironment.\r\nMandiant has yet to observe PRIVATELOG or STASHLOG in any customer environments or to recover any\r\nsecond-stage payloads launched by PRIVATELOG. This may indicate malware that is still in development, the\r\nwork of a researcher, or targeted activity.\r\nCLFS and Transaction Files\r\nPRIVATELOG and STASHLOG rely on the Common Log File System (CLFS) to hide a second stage payload in\r\nregistry transaction files.\r\nCLFS is a log framework that was introduced by Microsoft in Windows Vista and Windows Server 2003 R2 for\r\nhigh performance. It provides applications with API functions—available in clfsw32.dll—to create, store and read\r\nlog data.\r\nBecause the file format is not widely used or documented, there are no available tools that can parse CLFS log\r\nfiles. This provides attackers with an opportunity to hide their data as log records in a convenient way, because\r\nthese are accessible through API functions. This is similar in nature to malware which may rely, for example, on\r\nthe Windows Registry or NTFS Extended Attributes to hide their data, which also provide locations to store and\r\nretrieve binary data with the Windows API.\r\nIn Microsoft Windows, CLFS is notably used by the Kernel Transaction Manager (KTM) for both Transactional\r\nNTFS (TxF) and Transactional Registry (TxR) operations. These allow applications to perform a number of\r\nchanges on the filesystem or registry, all grouped in a single transaction that can be committed or rolled back. For\r\nexample, to open a registry key in a transaction, the functions RegCreateKeyTransacted(),\r\nRegOpenKeyTransacted(), and RegDeleteKeyTransacted() are available.\r\nRegistry transactions are stored in dedicated files with the following naming scheme: .TMContainer.regtrans-ms\r\nor .TxR..regtrans-ms. These are CLFS containers that are referenced in a master .blf file that only contains\r\nmetadata and can be found in various locations including user profile directories.\r\nhttps://www.mandiant.com/resources/unknown-actor-using-clfs-log-files-for-stealth\r\nPage 1 of 12\n\nRegistry transaction forensics were briefly explored in a previous blog post. The CLFS master and container file\r\nformats are mostly undocumented; however, previous research is available on GitHub.\r\nMalware Obfuscation\r\nAs with many malware families, most of the strings used by PRIVATELOG and STASHLOG are obfuscated. Yet\r\nthe technique observed here is uncommon and relies on XOR’ing each byte with a hard-coded byte inline, with no\r\nloops. Effectively, each string is therefore encrypted with a unique byte stream.\r\nFigure 1: Sample string deobfuscation for \"PrintNotify\"\r\nInterestingly, some of the deobfuscated strings from the installer are used for logging error messages and have\r\nspelling errors or typos such as:\r\nLog index=%d, data border exceed bounday.\\n\r\nInteral data hash mismatch.\\n\r\nLog buffer size=%u too small, expect aleast %u bytes.\\n\r\nIntroducing STASHLOG\r\nIn addition to containing obfuscated strings, the installer’s code is protected using various control flow\r\nobfuscation techniques that make static analysis cumbersome. Figure 2 is a graph overview of the installer’s\r\nmain() function demonstrating the effects of the control flow obfuscation.\r\nhttps://www.mandiant.com/resources/unknown-actor-using-clfs-log-files-for-stealth\r\nPage 2 of 12\n\nFigure 2: Graph view of main()\r\nSTASHLOG has two different modes of operation:\r\nWithout any arguments, during which it will prepare the environment\r\nWith a single argument, which is a file that should be hidden in a CLFS file\r\nPreparing the Environment\r\nExecuted without arguments, the installer prints two values to the console:\r\nThe GUID returned from the registry value of\r\nHKLM\\SOFTWARE\\Microsoft\\Cryptography\\MachineGUID\r\nhttps://www.mandiant.com/resources/unknown-actor-using-clfs-log-files-for-stealth\r\nPage 3 of 12\n\nA 56-byte value derived from a randomly generated GUID with CoCreateGUID()\r\nFigure 3: Sample console output\r\nThe 56-byte value is a concatenation of the random GUID, its SHA1 hash, and the SHA1 hash of the previous\r\nvalues. So: GUID+sha1(GUID)+sha1(GUID+sha1(GUID)).\r\nThe randomly generated GUID is stored as a string in the GlobalAtom table, prefixed with win::. This table\r\nresides in memory and contains strings with their identifiers available to all applications.\r\nIf a string prefixed with win:: already exists when the installer is executed, then the pre-existing GUID in the\r\nGlobalAtom table is reused.\r\nEffectively, when executed with no arguments, the installer generates and prints out encryption keys that the actor\r\nuses to pre-encrypt the payload before it is written to disk.\r\nStashing the Payload\r\nWhen launched with an argument, the installer opens and decrypts the contents of the file passed as an argument.\r\nIt verifies that the file is suffixed by its SHA1 hash, and then generates the same 56-byte value using the stored\r\nGlobalAtom GUID string in memory.\r\nThe 56-byte value is SHA1 hashed again and the first 16-bytes form the initialization vector (IV), while the key is\r\nthe 16-byte MachineGUID value from the host’s registry. The encryption algorithm is HC-128, which is rarely\r\nseen used in malware.\r\nThe expected decrypted file contents have a 40-byte header:\r\nstruct payloadHeader {\r\n DWORD magic;\r\n DWORD minWinVer;\r\n DWORD maxWinVer;\r\n DWORD totalSize;\r\n WORD numBlocks;\r\n WORD unknown;\r\nhttps://www.mandiant.com/resources/unknown-actor-using-clfs-log-files-for-stealth\r\nPage 4 of 12\n\nBYTE sha1sum[20];\r\n}\r\nIn the analyzed installer, the “magic” value is referred to as a checksum; however, STASHLOG verifies this value\r\nmatches the hard-coded value 0x00686365. The number of blocks, specified at offset 16, must be between 2 and\r\n5. The malware also checks that the operating system version is within a lower and upper boundary and that the\r\nSHA1 hash of the decrypted data matches the payload header value at offset 20.\r\nFollowing the payload header, the malware expects blocks of encrypted data with 8-byte headers. Each block\r\nheader has the following structure:\r\nstruct blockHeader {\r\n DWORD magic;\r\n DWORD blockSize;\r\n}\r\nOnce the malware has checked and validated the structure of the payload, it searches for .blf files in the default\r\nuser’s profile directory and uses the .blf file with the oldest creation date timestamp.\r\nIn practice, the malware should typically find the file used for registry transaction logs:\r\nC:\\Users\\Default\\NTUSER.DAT.TM.blf\r\nIf a matching .blf file is indeed found, it is opened with the CreateLogFile() API from clfsw32.dll. This function\r\nopens CLFS logs and expects a file name in the following format, without the .blf extension: log:\u003cLogName\u003e[::\r\n\u003cLogStreamName\u003e]\r\nThe log file is reset using the CloseAndResetLogFile() function and will be opened again to insert the data.\r\nBefore inserting data into the CLFS log file, the malware decrypts each block using HC-128. The key is the 16-\r\nbyte atom GUID and the IV is the first 16-bytes of the atom GUID SHA1 hash. Each block is then re-encrypted\r\nwith the new key material as follows:\r\nThe encryption key is the 16-byte GUID from GetVolumeNameForVolumeMountPointW().\r\nThe IV is the first 16-bytes of the SHA1 hash of the concatenated GUIDs from:\r\nGetVolumeNameForVolumeMountPointW()\r\nThe registry value HKLM\\SOFTWARE\\Microsoft\\Cryptography\\MachineGUID\r\nThe contents are written to the CLFS log file using the clfsw32.dll API function ReserveAndAppendLog(). The\r\npayload header is written to the log file as the first entry, followed by separate entries for each block.\r\nThe data is effectively stored in the first container file for the registry transaction\r\nlog: C:\\Users\\Default\\NTUSER.DAT\u003cGUID\u003e.TMContainer00000000000000000001.regtrans-ms.\r\nOnto PRIVATELOG\r\nhttps://www.mandiant.com/resources/unknown-actor-using-clfs-log-files-for-stealth\r\nPage 5 of 12\n\nThe PRIVATELOG sample recovered by Mandiant is an un-obfuscated 64-bit DLL named prntvpt.dll. It contains\r\nexports, which mimic those of legitimate prntvpt.dll files, although the exports have no functionality.\r\nPRIVATELOG expects to be loaded from PrintConfig.dll, which is the main DLL of a service named PrintNotify,\r\nvia DLL search order hijacking.\r\nThe malicious code is executed at the DLL’s entry point. It starts by verifying the command-line arguments of the\r\nprocess it is running in and expects to be running under svchost.exe -k print. If this matches, the malware resolves\r\nthe function address for the ServiceMain export function of PrintConfig.dll, which is the service entry point of the\r\nservice using this command line. This function is patched using Microsoft Detours—a publicly available library\r\nused for instrumenting Win32 functions—so that the execution flow appears to happen in the legitimate service\r\nDLL.\r\nThe patched ServiceMain function is where PRIVATELOG executes most of its functionality.\r\nSimilarly to STASHLOG, PRIVATELOG starts by enumerating *.blf files in the default user’s profile directory\r\nand uses the .blf file with the oldest creation date timestamp.\r\nIf a matching .blf file is found, PRIVATELOG opens it with the clfsw32.dll function CreateLogFile(). The log file\r\nis then marshalled and parsed using other functions specific to CLFS, such as CreateLogMarshallingArea(),\r\nReadLogFile() and ReadNextLogFile(). The malware expects to find specific entries which match our analysis of\r\nthe installer.\r\nPRIVATELOG expects the first log entry to have the following format:\r\nA size greater than 40 (payload header size)\r\nA WORD value of 2, 3, 4, or 5 at offset 16 (number of blocks)\r\nIf the first entry matches the aforementioned criteria, then subsequent records are read until one has an 8-byte\r\nheader with the following:\r\nIts first DWORD must equal 2 (assumed magic value)\r\nIts second DWORD must be less than the entry’s size minus the header. This value equals the size of the\r\npayload which will be decrypted.\r\nOnce the expected log entry is found, its contents are decrypted using the HC-128 encryption algorithm. The\r\ndecryption key and IV are generated using the same unique host properties that were used by STASHLOG.\r\nIt is worth noting that PRIVATELOG only decrypts the first matching block and that at least 2 to 5 blocks are\r\nexpected to be inserted by STASHLOG.\r\nPRIVATELOG finally uses a rarely seen technique to execute the DLL payload, which this time relies on NTFS\r\ntransactions. The injection process is similar to Phantom DLL hollowing and is described as follows:\r\nOpen a transacted handle to a copied file via the API CreateFileTransactedA()\r\nIn the sample analyzed by Mandiant, the file used for the transaction is a copy of the legitimate\r\nbinary C:\\Windows\\System32\\dbghelp.dll, which is copied to\r\nC:\\Windows\\system32\\WindowsPowerShell\\v1.0\\dbghelp.dll.\r\nhttps://www.mandiant.com/resources/unknown-actor-using-clfs-log-files-for-stealth\r\nPage 6 of 12\n\nOverwrite the transacted file with the decrypted payload contents\r\nCreate a section backed by the transacted file with SEC_IMAGE attributes via the API NtCreateSection()\r\nMap a view of the newly created section\r\nThis implicitly loads the transacted file data to some degree. The PE header is validated, and\r\nsections mapped into memory; however, it does not fix section permissions or resolve imports.\r\nFix the section permissions\r\nResolve the imports in the Import Table\r\nExecute the payload's entry point\r\nFind and execute the export function named SvcMain\r\nHunting for PRIVATELOG\r\nContainer Sample\r\nFigure 4 shows a fabricated container file representing a sample expected log file created by STASHLOG and\r\nloaded by PRIVATELOG.\r\nhttps://www.mandiant.com/resources/unknown-actor-using-clfs-log-files-for-stealth\r\nPage 7 of 12\n\nFigure 4: Example log file created by STASHLOG\r\nYARA Rules\r\nMandiant created YARA rules to hunt for PRIVATELOG and STASHLOG as well as possible variants based on\r\nvarious methodologies and unique strings that they use. Rules to detect CLFS containers matching PRIVATELOG\r\nstructures or containing encrypted data are also provided. These rules should be tested thoroughly before they are\r\nrun in a production environment.\r\nimport \"math\"\r\nimport \"pe\"\r\nrule HUNTING_Win_PRIVATELOG_CLFS {\r\n meta:\r\n author = \"adrien.bataille@mandiant.com\"\r\n description = \"This rule looks for CLFS containers containing possible data used by PRIVATELOG. As this\r\n condition:\r\n filesize \u003c 100MB and filesize \u003e= 512KB\r\n and uint16(0) == 0x0015 // signature\r\n and uint8(2) != 0 // fixup value upper byte\r\n and uint8(3) == 0 // always 0\r\n and uint16(4) == uint16(6) and uint16(4) != 0 // num sectors\r\n and uint32(8) == 0 // always 0\r\n and uint32(16) == 1 // always 1\r\n and uint32(20) == 0 // always 0\r\n and uint32(40) == 0x70 // size of record header\r\n // size of data at least 0x28 for first record\r\n and uint32(0x70+0x18) - 0x28 \u003e= 0x28\r\n // payloadHeader.numblocks (payloadHeader at 0x70+uint16(0x70+0x22))\r\n and (uint16(0x70+uint16(0x70+0x22)+0x10) == 0x2 or uint16(0x70+uint16(0x70+0x22)+0x10) == 0x3 or uint16(\r\n // this is a size, assume it is less than our filesize\r\n and uint32(0x70+uint16(0x70+0x22)+0xC) \u003c filesize\r\n // confirm malware using 2 different methods\r\n and (\r\n // look for hardcoded magic in first log record\r\n uint32(0x70+uint16(0x70+0x22)) == 0x00686365 or\r\n // loop through each possible sector to look for a blockheader struct\r\n for any i in (0 .. (filesize \\ 512) - 1):\r\n (\r\n // look for record header, num sectors and size of record\r\n uint16(i*512)==0x0015 and uint16(i*512+4) == uint16(i*512+6) and uint16(i*512+4) != 0 and uint32(i*5\r\n // look for magic and blockheader.blocksize in payload\r\n uint32(i*512+0x70+uint16(i*512+0x70+0x22)) == 2 and uint32(i*512+0x70+uint16(i*512+0x70+0x22)+4) ==\r\n )\r\n )\r\n}\r\nhttps://www.mandiant.com/resources/unknown-actor-using-clfs-log-files-for-stealth\r\nPage 8 of 12\n\nrule HUNTING_Win_CLFS_Entropy {\r\n meta:\r\n author = \"adrien.bataille@mandiant.com\"\r\n description = \"This rule looks for CLFS containers with records containing high entropy. As this rule ma\r\n condition:\r\n filesize \u003c 100MB and filesize \u003e= 512KB\r\n and uint16(0) == 0x0015 // signature\r\n and uint8(2) != 0 // fixup value upper byte\r\n and uint8(3) == 0 // always 0\r\n and uint16(4) == uint16(6) and uint16(4) != 0 // num sectors\r\n and uint32(8) == 0 // always 0\r\n and uint32(16) == 1 // always 1\r\n and uint32(20) == 0 // always 0\r\n and uint32(40) == 0x70 // size of record header\r\n and for any i in (0 .. (filesize \\ 512) - 1) :\r\n (\r\n // look for record header, num sectors and size of record\r\n uint16(i*512)==0x0015 and uint16(i*512+4) == uint16(i*512+6) and uint16(i*512+4) != 0 and uint32(i*512+4\r\n // look for high entropy in the record[8:] to account for possible block header\r\n and math.entropy(i*512+0x70+uint16(i*512+0x70+0x22)+8, i*512+0x70+uint16(i*512+0x70+0x22)+uint32(i*512+0\r\n )\r\n}\r\nrule HUNTING_Win_PRIVATELOG_1_strict {\r\n meta:\r\n author = \"adrien.bataille@mandiant.com\"\r\n description = \"Detects PRIVATELOG and STASHLOG variants based on strings and imports\"\r\n md5 = \"91b08896fbda9edb8b6f93a6bc811ec6\"\r\n strings:\r\n $hvid = \"Global\\\\HVID_\" ascii\r\n $apci = \"Global\\\\APCI#\" wide\r\n condition:\r\n uint16(0) == 0x5A4D and uint32(uint32(0x3C)) == 0x00004550 and\r\n (\r\n all of them and\r\n (\r\n pe.imports(\"clfsw32.dll\",\"CreateLogMarshallingArea\") and\r\n pe.imports(\"kernel32.dll\", \"VirtualProtect\") and\r\n pe.imports(\"ktmw32.dll\", \"CreateTransaction\") and\r\n pe.imports(\"kernel32.dll\", \"CreateFileTransactedA\")\r\n )\r\n )\r\n}\r\nhttps://www.mandiant.com/resources/unknown-actor-using-clfs-log-files-for-stealth\r\nPage 9 of 12\n\nrule HUNTING_Win_PRIVATELOG_2_notstrict {\r\n meta:\r\n author = \"adrien.bataille@mandiant.com\"\r\n description = \"Detects possible PRIVATELOG and STASHLOG variants based on strings or imports. This rule\r\n md5 = \"91b08896fbda9edb8b6f93a6bc811ec6\"\r\n strings:\r\n $hvid = \"Global\\\\HVID_\" ascii\r\n $apci = \"Global\\\\APCI#\" wide\r\n condition:\r\n uint16(0) == 0x5A4D and uint32(uint32(0x3C)) == 0x00004550 and\r\n (\r\n any of them or\r\n (\r\n pe.imports(\"clfsw32.dll\",\"CreateLogMarshallingArea\") and\r\n pe.imports(\"kernel32.dll\", \"VirtualProtect\") and\r\n pe.imports(\"ktmw32.dll\", \"CreateTransaction\") and\r\n pe.imports(\"kernel32.dll\", \"CreateFileTransactedA\")\r\n )\r\n )\r\n}\r\nrule HUNTING_Win_hijack_prntvpt {\r\n meta:\r\n author = \"adrien.bataille@mandiant.com\"\r\n description = \"Detects possible hijack of legitimate prntvpt.dll based on missing export\"\r\n md5 = \"91b08896fbda9edb8b6f93a6bc811ec6\"\r\n condition:\r\n uint16(0) == 0x5A4D and uint32(uint32(0x3C)) == 0x00004550 and\r\n pe.exports(\"PTOpenProviderEx\")\r\n and not pe.exports(\"MergeAndValidatePrintTicketThunk\")\r\n}\r\nEDR / SIEM\r\nTo complement static hunting with Yara, Mandiant also recommends hunting for similar indicators of compromise\r\nin “process”, “imageload” or “filewrite” events of typical EDR logs. These would cover cases where\r\nPRIVATELOG may resolve imports dynamically with LoadLibrary() and GetProcAddress(), versus static imports\r\nin currently known samples.\r\nFigure 5 identifies key modules loaded by PRIVATELOG that may be used to create hunting queries: ktmw32.dll,\r\ndbghelp.dll and clfsw32.dll.\r\nhttps://www.mandiant.com/resources/unknown-actor-using-clfs-log-files-for-stealth\r\nPage 10 of 12\n\nFigure 5: Memory view of a running PRIVATELOG process\r\nExample hunting queries include:\r\nAny process writing or loading C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\dbghelp.dll\r\nAny process loading both clfsw32.dll and ktmw32.dll\r\nsvchost.exe -k print loading clfsw32.dll or ktmw32.dll\r\nAny svchost.exe process loading clfsw32.dll\r\nConcerning svchost.exe, although we have observed many cases of other svchost.exe processes loading\r\nktmw32.dll, we have only rarely observed svchost.exe processes loading clfsw32.dll.\r\nFile writes to .regtrans-ms or .blf files are fairly common, however stacking the process name and file paths may\r\nalso provide good results. For example, file writes to the registry transaction file for the default user are likely to\r\nbe uncommon.\r\nHashes\r\nPRIVATELOG\r\nPrntvpt.dll:\r\n1e53559e6be1f941df1a1508bba5bb9763aedba23f946294ce5d92646877b40c\r\nSTASHLOG\r\nShiver.exe:\r\n720610b9067c8afe857819a098a44cab24e9da5cf6a086351d01b73714afd397\r\nMITRE ATT\u0026ACK Techniques\r\nID Technique\r\nT1012 Query Registry\r\nT1564 Hide Artifacts\r\nT1574 Hijack Execution Flow\r\nhttps://www.mandiant.com/resources/unknown-actor-using-clfs-log-files-for-stealth\r\nPage 11 of 12\n\nT1574.002 DLL Side-Loading\r\nT1055.013 Process Injection: Process Doppelgänging\r\nFireEye Product Detections\r\nPlatform(s) Detection Name\r\nNetwork Security\r\nEmail Security\r\nDetection On Demand\r\nMalware Analysis\r\nFile Protect\r\nFE_APT_Loader_Win_PRIVATELOG\r\nFE_APT_Installer_Win_STASHLOG\r\nHX Security Generic.mg.0c605276ff21b515\r\nPosted in\r\nThreat Intelligence\r\nSecurity \u0026 Identity\r\nSource: https://www.mandiant.com/resources/unknown-actor-using-clfs-log-files-for-stealth\r\nhttps://www.mandiant.com/resources/unknown-actor-using-clfs-log-files-for-stealth\r\nPage 12 of 12",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"ETDA"
	],
	"references": [
		"https://www.mandiant.com/resources/unknown-actor-using-clfs-log-files-for-stealth"
	],
	"report_names": [
		"unknown-actor-using-clfs-log-files-for-stealth"
	],
	"threat_actors": [],
	"ts_created_at": 1775434853,
	"ts_updated_at": 1775791223,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/4638786f7da1dda4ea477a593d3589cff0eab57c.pdf",
		"text": "https://archive.orkl.eu/4638786f7da1dda4ea477a593d3589cff0eab57c.txt",
		"img": "https://archive.orkl.eu/4638786f7da1dda4ea477a593d3589cff0eab57c.jpg"
	}
}