Enhance registry access logging and COM server resolution - #770
Enhance registry access logging and COM server resolution#770Giulia Stocco (gfs) wants to merge 4 commits into
Conversation
RegistryKeyToRegistryObject discarded RegistryAccessRule.AccessControlType, so Allow and Deny ACEs were indistinguishable in collected data and any analysis of registry permissions was unsound. Split the comma joined RegistryRights mask into individual rights, mirroring how FileSystemCollector handles FileSystemRights, and prefix each with the access control type. Entries are now "Allow:SetValue" / "Deny:SetValue", which makes ContainsKey and Contains with DictData usable. The Dictionary<string, List<string>> shape is unchanged, and unprefixed rights in existing databases still hydrate. Also populate RegistryObject.PermissionsString, which was declared but never assigned, with the key's SDDL. It is a flat string so rules can match ACE patterns against it with Regex; the Permissions dictionary cannot be matched that way because OAT's regex operation discards dictionary valued fields.
ComObjectCollector never expanded environment variables. It explicitly skipped the System32 prefixing branch when a path contained '%' and then passed the raw string to FilePathToFileSystemObject, so every COM server registered under %SystemRoot%, %ProgramData% or similar failed to resolve and silently produced a FileSystemObject with no permissions. Expand environment variables before resolving, and also resolve native object manager prefixes (\??\, \SystemRoot\). Handle LocalServer32 and LocalServer in addition to InprocServer32. Their values are command lines rather than bare paths, so the executable is extracted from the front, preferring a quoted path and then the first token ending in .exe. Remove the InprocServer64 branch. There is no such registry key, so it was dead code. Bitness is selected by the registry view, not the key name, so the resolved binary is now assigned to x86_Binary in the 32-bit view and to x64_Binary in the 64-bit view. Both views are already parsed, so both fields are populated across a run; previously x64_Binary was never set at all. The three near identical inline blocks are replaced by a single ResolveServerBinary helper. Existing quote, whitespace and System32 handling is preserved in RegistryReferenceParser.NormalizePath.
Analysis rules are evaluated against the before and after states of a single
object and cannot reach a second one, so a rule over a RegistryObject can never
ask about the file that key points at. Detecting a load point that unprivileged
users can repoint, or that resolves to a binary they can supply, therefore
requires the join to happen during collection.
Add LoadPointObject, which carries for one collected unit the source registry
key and its ACL, the CLSID it resolved through, the resolved target path, the
target's ACL, and flat boolean and string summaries a rule can test directly:
SourceKeyUserWritable, TargetUserWritable, TargetExists, TargetAclSource and
TargetAclUnavailable. Flat fields are deliberate; dotted field navigation is
shallow and regex cannot match dictionary valued fields.
Add LoadPointCollector, driven by a list of LoadPointDefinition rather than a
chain of ifs. It covers COM servers, StaticPluginMap under
InstallService\State, AppInit_DLLs and services; adding coverage means adding an
entry. A load point naming a CLSID is followed through
Classes\CLSID\{guid}\InprocServer32 to the binary, which is the indirection the
whole feature exists to resolve.
A missing target is represented explicitly and kept distinct from a failed ACL
read, because a nonexistent DLL at a path an unprivileged user can write is the
exploitable condition. Where the target is missing the ACL of the nearest
existing parent directory is captured instead, and TargetAclSource says which
was used.
PermissionUtils holds the single definition of user writable, evaluated against
Everyone, INTERACTIVE, Authenticated Users and BUILTIN\Users by both SID and
account name, with Deny taking precedence over Allow.
RegistryObject gains ReferencedPaths and ReferencedClsids, cracked out of the
key's values at collection time and environment expanded. They are List<string>
so Regex, Contains, StartsWith and EndsWith all work on them. Extraction is pure
string manipulation over compiled expressions with substring pre-filters, a
match timeout and length and count caps, since it runs for every value of every
key of every hive in both views.
Ship four rules over the new type covering a writable source key, a writable
target, a missing target in a writable directory, and both ends under
unprivileged control. Severities match the existing rules, which top out at
WARNING.
Tests cover reference extraction, Deny precedence, nearest existing parent
resolution, serialization round trips, and rule matching against a synthetic
object in the shape of CVE-2026-50343 plus benign and unreadable-ACL negatives.
False positives are the main risk to this feature, so the negative cases are
asserted explicitly. Registry and COM collection remain Windows only and are
gated accordingly.
|
Note The following is output from an automated COMPASS SDL Security review run against this PR's diff. It is machine-generated and does not represent my personal review or opinion. Findings should be triaged on their merits. SDL Security ReviewScope: application source under 🔴 Blocking
|
A path read out of the registry does not have to name this machine, and whoever can write the value picks which machine it names. Resolving one was not a passive read: File.Exists, the ACL lookup and FilePathToFileSystemObject each open a session to that host and authenticate as the account running the collection, which for these collectors is usually an administrator, and then read content that host controls. The StaticPluginMap key the load point collector was written for grants INTERACTIVE SetValue, so an unprivileged user could choose the host. Load point targets and COM server binaries that resolve through a UNC path or a mapped network drive are now reported by path and left alone. The path is still recorded, and LoadPointObject.TargetIsNetworkPath says why nothing else was collected, with TargetAclUnavailable set so the existing rules do not read the absence of an ACL as a verdict. --follow-network-paths opts back in, in the same way --download-cloud opts into hydrating cloud placeholders. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d4d92e29-a96d-4ec9-91f7-fe7d4a350534
|
Addressed the blocking finding in 020d19d. Load point targets and COM server binaries that resolve through a UNC path or a mapped network drive are no longer touched. The path is still recorded so a rule can see where the load point points, New The three warnings are not addressed in this commit. |
This pull request introduces a new "Load Point Collector" feature, which identifies and analyzes registry-based load points and the binaries they reference. It also improves the handling of COM object server binary resolution and enhances the
RegistryObjectto expose referenced CLSIDs and paths for better rule analysis. Additionally, the UI and command-line options are updated to support the new collector.Load Point Collector Feature:
LoadPointCollector(with corresponding UI, CLI, and object model support) that collects registry load points and resolves them to the binaries they reference, enabling more comprehensive analysis of potential attack surfaces. (LoadPointObject.cs,CommandOptions.cs,AttackSurfaceAnalyzerClient.cs,CollectorOptionsRazor.razor,LoadPointCollectorOptions.razor,Types.cs,AsaRule.cs,JsonUtils.cs) [1] [2] [3] [4] [5] [6] [7] [8] [9] [10]COM Object Collector Improvements:
ComObjectCollector.cs) [1] [2]Registry Object Enhancements:
RegistryObjectto include a flat SDDL permissions string, a list of referenced CLSIDs, and a list of referenced file paths. This supports more powerful and regex-friendly rule analysis. (RegistryObject.cs)Command-line and Serialization Support:
LoadPointCollectorandLoadPointObject. (CommandOptions.cs,JsonUtils.cs) [1] [2] [3]UI Integration:
CollectorOptionsRazor.razor,LoadPointCollectorOptions.razor) [1] [2]