Repository navigation
Review fixes: installer lockout, LSA contracts, lsass-safe AP, tests for the lsass code - #1
Merged
Merged
Conversation
…heck install-doppio.ps1 imported register-filter.reg whenever the file sat next to it, so the filter went live with the first enrollment. Its closing advice - keep a second admin that is NOT enrolled - then locked exactly that admin out of the console and RDP: no secret for its 2FA tile, and every other tile hidden. Now the default run only installs the provider. -EnableFilter is a separate step and refuses while an enabled local account is not enrolled (-AllowUnenrolled names accounts that never sign in interactively). The closing message says to enroll the break-glass admin too. The CLSID check also looks at HKLM\SOFTWARE\Classes\CLSID, and -EnableFilter at the filter key, before anything is overwritten. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Q8WTBzhJ9rZXAXLe89ssH
Authentication package (ap.cpp): - No heap or CRT of its own on the logon path (Buf instead of std::wstring and std::to_wstring), SEH around the decision instead of catch (...), which does not catch an access violation under /EHsc. - AccountName is always returned from the LSA heap, as LSA_AP_LOGON_USER_EX2 requires, also for interactive logons. It used to stay null. - Only documented status codes: STATUS_ACCOUNT_RESTRICTION with SubStatus STATUS_INVALID_WORKSTATION to deny, STATUS_INVALID_LOGON_TYPE to decline, STATUS_NO_MEMORY on allocation failure. - The user name is read as a client address or as a packed offset, the form Winlogon and LogonUI use. An unreadable index denies (fail closed). Sub-authentication filter (subauth.cpp): - Authoritative and UserFlags are set on every path; the docs call them [out] and want Authoritative valid regardless of the result. - The veto is STATUS_INVALID_WORKSTATION, which the docs list; STATUS_ACCOUNT_RESTRICTION is not in that list. - MSV1_0_PASSTHRU logons get no objection: the index only knows local RIDs. - LogoffTime/KickoffTime stay untouched, and every call logs the incoming values to the debugger, so the VM test can settle what MSV1_0 passes. Store: - The index reads that run in lsass moved to store_index.cpp. The two LSA DLLs link only that file plus advapi32: no DPAPI, TOTP or bcrypt code in lsass anymore. Checked on the object files: the only unresolved symbols are Windows imports, __C_specific_handler and memset. - Buf and the SEH exception filter are shared in lsasafe.h. - Name normalization folds case with LCMapStringW(LOCALE_INVARIANT), so non-ASCII names match the name index; towlower only folded ASCII. - QueryAnyEnrollment counts REG_BINARY values under SID names only. - The DACL is set when a key is created; repairing old keys is the new EnsureStoreAcls, called by enroll.exe, not by every SaveState on the logon path. Build: /CETCOMPAT on every binary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Q8WTBzhJ9rZXAXLe89ssH
…<SID> - UpdateRemoteCredential leaves an RDP credential with its own provider while nothing is enrolled (Filter() hides nothing then), and leaves non-password logons such as smartcard alone instead of taking and then refusing them. - enroll /list shows every stored secret, its account and both index states, and marks secrets of deleted accounts, which still keep the filter active. enroll /remove accepts the SID from /list; it is canonicalized, so a lower-case SID still finds its entries. - enroll re-applies the store ACL once per run (EnsureStoreAcls), after argument checking. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Q8WTBzhJ9rZXAXLe89ssH
- Besides RunAsPPL/RunAsPPLBoot, both scripts look for WinInit event 12
("started as a protected process") since the last boot. Windows 11 22H2+
turns LSA protection on by default on new, enterprise-joined HVCI
devices, and that is not documented to show up in RunAsPPL.
- install-subauth.ps1 refuses on a domain controller, where the filter
would check domain RIDs against the local index.
- The texts say that MSV1_0 loads the filter on first use, so a broken one
can surface at the first network logon, not only at boot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Q8WTBzhJ9rZXAXLe89ssH
- tests/native/test_lsa compiles the real subauth.cpp, ap.cpp and store_index.cpp against a fake registry, event log and LSA heap, under ASan and UBSan: every veto and fail-closed path, the [out] parameters, hostile logon buffers (overruns, odd lengths, packed offsets, embedded NULs, overlong names), non-ASCII folding, and no leaked LSA heap block or registry handle. Re-introducing six of the old defects one at a time makes it fail each time. - tests/totp_check.py exits non-zero on a failed check and counts the Base32 checks; before, CI could not see a failure. - CI parses every PowerShell script, and runs /analyze /WX over the lsass code, so a finding there fails the build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Q8WTBzhJ9rZXAXLe89ssH
Brings the README in line with the previous commits: install-doppio.ps1 and -EnableFilter, enroll /list and /remove <SID>, the status codes and fail-closed rules of both LSA pieces, the open NetworkCleartext question, the runtime LSA-protection check, events 200-210 and test_lsa. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Q8WTBzhJ9rZXAXLe89ssH
The README told readers to keep the deny rights for NetworkCleartext logons as if they were known to cover type 8. That is not documented, so it now says to check it in the VM, like the filter's behaviour for those logons. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Q8WTBzhJ9rZXAXLe89ssH
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the findings of the code review of Part 1 and Part 2 (H1, M1–M6, N1–N10). Six commits, each of which builds and passes its tests on its own.
What changes
H1 – installer locked out the break-glass admin.
install-doppio.ps1importedregister-filter.regautomatically and advised keeping a second admin unenrolled, who then had no way in at the console or over RDP. The filter is now a separate-EnableFilterstep that refuses while an enabled local account is not enrolled (-AllowUnenrolledfor service accounts). The CLSID check also coversHKLM\SOFTWARE\Classes\CLSIDand the filter key.M1/M2 – authentication package (
ap.cpp). Rebuilt on the same lsass rules as the filter: no heap or CRT of its own, SEH around the decision, unreadable index = deny.AccountNameis always returned from the LSA heap, asLSA_AP_LOGON_USER_EX2requires. Status codes are now the documented ones (STATUS_ACCOUNT_RESTRICTIONwith SubStatusSTATUS_INVALID_WORKSTATION,STATUS_INVALID_LOGON_TYPE,STATUS_NO_MEMORY). The user name is read as a client address or a packed offset (N2).M3/M4/N5 – sub-auth filter (
subauth.cpp).AuthoritativeandUserFlagsare set on every path. The veto isSTATUS_INVALID_WORKSTATION, which is in the documented list.MSV1_0_PASSTHRUgets no objection.LogoffTime/KickoffTimestay untouched, and every call logs the incoming values, so the VM test can settle the open question.install-subauth.ps1refuses on a DC.Store. The index reads that run in lsass moved to
store_index.cpp. The two LSA DLLs now link only that file and advapi32, with no DPAPI, TOTP or bcrypt code in lsass. On the object files, the only unresolved symbols are Windows imports,__C_specific_handlerandmemset. Locale-invariant case folding (N8).QueryAnyEnrollmentcounts real secrets only (M6). The DACL repair moved from everySaveStatetoenroll.exe(N9).Filter/enroll (N4, M6).
UpdateRemoteCredentialleaves RDP credentials alone while nothing is enrolled, and leaves smartcard serializations with their own provider. Newenroll /listshows orphaned secrets of deleted accounts, andenroll /remove <SID>removes them.Scripts (N1, N7). LSA install scripts also check WinInit event 12 since the last boot, because the Win11 default-on case is not documented to appear in
RunAsPPL. The texts note thatAuth0loads on first use.Tests/CI (N10). New
tests/native/test_lsaruns the realsubauth.cpp,ap.cppandstore_index.cppagainst a fake registry, event log and LSA heap under ASan/UBSan.totp_check.pycan now fail CI. CI parses every.ps1, adds/CETCOMPAT, and runs/analyze /WXover the lsass code.Verified here
.cppfiles: clang syntax check against the mingw-w64 Windows headers, 0 errors, no new warnings.make -C tests/native: both suites pass. Re-introducing six of the old defects one at a time makestest_lsafail each time.totp_check.pyexits 1 on a wrong RFC vector and on a broken Base32 decoder./W4 /sdl /guard:cf /CETCOMPAT, CFG check,/analyze /WXover the lsass code, PowerShell parse of every script, both native suites, Python check.Not verified – please check
[Doppio-SubAuth]lines now show whatLogoffTime/KickoffTimehold and whichLogonLevela NetworkCleartext (type 8) logon arrives with.STATUS_ACCOUNT_RESTRICTIONtoSTATUS_INVALID_WORKSTATION(Microsoft's description: "The user is not authorized to log on to the specified workstation"), so a client shows a different error text. The article update in theadmincafe-nuxt explains it.2776655. When it moves to this branch,build-ristretto.bathas to add%DOPPIO%\store_index.cppnext to%DOPPIO%\store.cpp, orristretto.exewill not link.🤖 Generated with Claude Code
https://claude.ai/code/session_015Q8WTBzhJ9rZXAXLe89ssH
Generated by Claude Code